/* * Copyright (C) 2009 Canonical Ltd. * Author: Robert Ancell * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. See http://www.gnu.org/copyleft/gpl.html the full text of the * license. */ #include #include #include #include #include #include #include #include #include #include "book.h" enum { PROP_0, PROP_NEEDS_SAVING }; enum { PAGE_ADDED, PAGE_REMOVED, REORDERED, CLEARED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0, }; struct BookPrivate { GList *pages; gboolean needs_saving; }; G_DEFINE_TYPE (Book, book, G_TYPE_OBJECT); Book * book_new () { return g_object_new (BOOK_TYPE, NULL); } void book_clear (Book *book) { GList *iter; for (iter = book->priv->pages; iter; iter = iter->next) { Page *page = iter->data; g_object_unref (page); } g_list_free (book->priv->pages); book->priv->pages = NULL; g_signal_emit (book, signals[CLEARED], 0); } static void page_changed_cb (Page *page, Book *book) { book_set_needs_saving (book, TRUE); } Page * book_append_page (Book *book, gint width, gint height, gint dpi, ScanDirection scan_direction) { Page *page; page = page_new (width, height, dpi, scan_direction); g_signal_connect (page, "pixels-changed", G_CALLBACK (page_changed_cb), book); g_signal_connect (page, "crop-changed", G_CALLBACK (page_changed_cb), book); book->priv->pages = g_list_append (book->priv->pages, page); g_signal_emit (book, signals[PAGE_ADDED], 0, page); book_set_needs_saving (book, TRUE); return page; } void book_move_page (Book *book, Page *page, gint location) { book->priv->pages = g_list_remove (book->priv->pages, page); book->priv->pages = g_list_insert (book->priv->pages, page, location); g_signal_emit (book, signals[REORDERED], 0, page); book_set_needs_saving (book, TRUE); } void book_delete_page (Book *book, Page *page) { g_signal_handlers_disconnect_by_func (page, page_changed_cb, book); g_signal_emit (book, signals[PAGE_REMOVED], 0, page); book->priv->pages = g_list_remove (book->priv->pages, page); g_object_unref (page); book_set_needs_saving (book, TRUE); } gint book_get_n_pages (Book *book) { return g_list_length (book->priv->pages); } Page * book_get_page (Book *book, gint page_number) { if (page_number < 0) page_number = g_list_length (book->priv->pages) + page_number; return g_list_nth_data (book->priv->pages, page_number); } gint book_get_page_index (Book *book, Page *page) { return g_list_index (book->priv->pages, page); } static GFile * make_indexed_file (const gchar *uri, gint i) { gchar *basename, *suffix, *indexed_uri; GFile *file; if (i == 0) return g_file_new_for_uri (uri); basename = g_path_get_basename (uri); suffix = g_strrstr (basename, "."); if (suffix) indexed_uri = g_strdup_printf ("%.*s-%d%s", (int) (strlen (uri) - strlen (suffix)), uri, i, suffix); else indexed_uri = g_strdup_printf ("%s-%d", uri, i); g_free (basename); file = g_file_new_for_uri (indexed_uri); g_free (indexed_uri); return file; } static gboolean book_save_multi_file (Book *book, const gchar *type, GFile *file, GError **error) { GList *iter; gboolean result = TRUE; gint i; gchar *uri; uri = g_file_get_uri (file); for (iter = book->priv->pages, i = 0; iter && result; iter = iter->next, i++) { Page *page = iter->data; GFile *file; file = make_indexed_file (uri, i); result = page_save (page, type, file, error); g_object_unref (file); } g_free (uri); return result; } static void save_ps_pdf_surface (cairo_surface_t *surface, GdkPixbuf *image, gdouble dpi) { cairo_t *context; context = cairo_create (surface); cairo_scale (context, 72.0 / dpi, 72.0 / dpi); gdk_cairo_set_source_pixbuf (context, image, 0, 0); cairo_pattern_set_filter (cairo_get_source (context), CAIRO_FILTER_BEST); cairo_paint (context); cairo_destroy (context); } static cairo_status_t write_cairo_data (GFileOutputStream *stream, unsigned char *data, unsigned int length) { gboolean result; GError *error = NULL; result = g_output_stream_write_all (G_OUTPUT_STREAM (stream), data, length, NULL, NULL, &error); if (error) { g_warning ("Error writing data: %s", error->message); g_error_free (error); } return result ? CAIRO_STATUS_SUCCESS : CAIRO_STATUS_WRITE_ERROR; } static gboolean book_save_ps (Book *book, GFile *file, GError **error) { GFileOutputStream *stream; GList *iter; cairo_surface_t *surface; stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error); if (!stream) return FALSE; surface = cairo_ps_surface_create_for_stream ((cairo_write_func_t) write_cairo_data, stream, 0, 0); for (iter = book->priv->pages; iter; iter = iter->next) { Page *page = iter->data; double width, height; GdkPixbuf *image; image = page_get_image (page, TRUE); width = gdk_pixbuf_get_width (image) * 72.0 / page_get_dpi (page); height = gdk_pixbuf_get_height (image) * 72.0 / page_get_dpi (page); cairo_ps_surface_set_size (surface, width, height); save_ps_pdf_surface (surface, image, page_get_dpi (page)); cairo_surface_show_page (surface); g_object_unref (image); } cairo_surface_destroy (surface); g_object_unref (stream); return TRUE; } typedef struct { int offset; int n_objects; GList *object_offsets; GFileOutputStream *stream; } PDFWriter; static PDFWriter * pdf_writer_new (GFileOutputStream *stream) { PDFWriter *writer; writer = g_malloc0 (sizeof (PDFWriter)); writer->stream = g_object_ref (stream); return writer; } static void pdf_writer_free (PDFWriter *writer) { g_object_unref (writer->stream); g_list_free (writer->object_offsets); g_free (writer); } static void pdf_write (PDFWriter *writer, const unsigned char *data, size_t length) { g_output_stream_write_all (G_OUTPUT_STREAM (writer->stream), data, length, NULL, NULL, NULL); writer->offset += length; } static void pdf_printf (PDFWriter *writer, const char *format, ...) { va_list args; gchar *string; va_start (args, format); string = g_strdup_vprintf (format, args); va_end (args); pdf_write (writer, (unsigned char *)string, strlen (string)); g_free (string); } static int pdf_start_object (PDFWriter *writer) { writer->n_objects++; writer->object_offsets = g_list_append (writer->object_offsets, GINT_TO_POINTER (writer->offset)); return writer->n_objects; } static guchar * compress_zlib (guchar *data, size_t length, size_t *n_written) { z_stream stream; guchar *out_data; out_data = g_malloc (sizeof (guchar) * length); stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; if (deflateInit (&stream, Z_BEST_COMPRESSION) != Z_OK) return NULL; stream.next_in = data; stream.avail_in = length; stream.next_out = out_data; stream.avail_out = length; while (stream.avail_in > 0) { if (deflate (&stream, Z_FINISH) == Z_STREAM_ERROR) break; } deflateEnd (&stream); if (stream.avail_in > 0) { g_free (out_data); return NULL; } *n_written = length - stream.avail_out; return out_data; } static void jpeg_init_cb (struct jpeg_compress_struct *info) {} static boolean jpeg_empty_cb (struct jpeg_compress_struct *info) { return TRUE; } static void jpeg_term_cb (struct jpeg_compress_struct *info) {} static guchar * compress_jpeg (GdkPixbuf *image, size_t *n_written) { struct jpeg_compress_struct info; struct jpeg_error_mgr jerr; struct jpeg_destination_mgr dest_mgr; int r; guchar *pixels; guchar *data; size_t max_length; info.err = jpeg_std_error (&jerr); jpeg_create_compress (&info); pixels = gdk_pixbuf_get_pixels (image); info.image_width = gdk_pixbuf_get_width (image); info.image_height = gdk_pixbuf_get_height (image); info.input_components = 3; info.in_color_space = JCS_RGB; /* TODO: JCS_GRAYSCALE? */ jpeg_set_defaults (&info); max_length = info.image_width * info.image_height * info.input_components; data = g_malloc (sizeof (guchar) * max_length); dest_mgr.next_output_byte = data; dest_mgr.free_in_buffer = max_length; dest_mgr.init_destination = jpeg_init_cb; dest_mgr.empty_output_buffer = jpeg_empty_cb; dest_mgr.term_destination = jpeg_term_cb; info.dest = &dest_mgr; jpeg_start_compress (&info, TRUE); for (r = 0; r < info.image_height; r++) { JSAMPROW row[1]; row[0] = pixels + r * gdk_pixbuf_get_rowstride (image); jpeg_write_scanlines (&info, row, 1); } jpeg_finish_compress (&info); *n_written = max_length - dest_mgr.free_in_buffer; jpeg_destroy_compress (&info); return data; } static gboolean book_save_pdf (Book *book, GFile *file, GError **error) { GFileOutputStream *stream; PDFWriter *writer; int catalog_number, pages_number, info_number; int xref_offset; int i; stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error); if (!stream) return FALSE; writer = pdf_writer_new (stream); g_object_unref (stream); /* Header */ pdf_printf (writer, "%%PDF-1.3\n"); /* Catalog */ catalog_number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", catalog_number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Type /Catalog\n"); pdf_printf (writer, "/Pages %d 0 R\n", catalog_number + 1); pdf_printf (writer, ">>\n"); pdf_printf (writer, "endobj\n"); /* Pages */ pdf_printf (writer, "\n"); pages_number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", pages_number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Type /Pages\n"); pdf_printf (writer, "/Kids ["); for (i = 0; i < book_get_n_pages (book); i++) { pdf_printf (writer, " %d 0 R", pages_number + 1 + (i*3)); } pdf_printf (writer, " ]\n"); pdf_printf (writer, "/Count %d\n", book_get_n_pages (book)); pdf_printf (writer, ">>\n"); pdf_printf (writer, "endobj\n"); for (i = 0; i < book_get_n_pages (book); i++) { int number, width, height, depth; size_t data_length, compressed_length; Page *page; GdkPixbuf *image; guchar *pixels, *data, *compressed_data; gchar *command; const gchar *color_space, *filter = NULL; float page_width, page_height; page = book_get_page (book, i); image = page_get_image (page, TRUE); width = gdk_pixbuf_get_width (image); height = gdk_pixbuf_get_height (image); pixels = gdk_pixbuf_get_pixels (image); page_width = width * 72. / page_get_dpi (page); page_height = height * 72. / page_get_dpi (page); if (page_is_color (page)) { int row; depth = 8; color_space = "DeviceRGB"; data_length = height * width * 3 + 1; data = g_malloc (sizeof (guchar) * data_length); for (row = 0; row < height; row++) { int x; guchar *in_line, *out_line; in_line = pixels + row * gdk_pixbuf_get_rowstride (image); out_line = data + row * width * 3; for (x = 0; x < width; x++) { guchar *in_p = in_line + x*3; guchar *out_p = out_line + x*3; out_p[0] = in_p[0]; out_p[1] = in_p[1]; out_p[2] = in_p[2]; } } } else if (page_get_depth (page) == 2) { int row, shift_count = 6; guchar *write_ptr; depth = 2; color_space = "DeviceGray"; data_length = height * ((width * 2 + 7) / 8); data = g_malloc (sizeof (guchar) * data_length); write_ptr = data; write_ptr[0] = 0; for (row = 0; row < height; row++) { int x; guchar *in_line; /* Pad to the next line */ if (shift_count != 6) { write_ptr++; write_ptr[0] = 0; shift_count = 6; } in_line = pixels + row * gdk_pixbuf_get_rowstride (image); for (x = 0; x < width; x++) { guchar *in_p = in_line + x*3; if (in_p[0] >= 192) write_ptr[0] |= 3 << shift_count; else if (in_p[0] >= 128) write_ptr[0] |= 2 << shift_count; else if (in_p[0] >= 64) write_ptr[0] |= 1 << shift_count; if (shift_count == 0) { write_ptr++; write_ptr[0] = 0; shift_count = 6; } else shift_count -= 2; } } } else if (page_get_depth (page) == 1) { int row, mask = 0x80; guchar *write_ptr; depth = 1; color_space = "DeviceGray"; data_length = height * ((width + 7) / 8); data = g_malloc (sizeof (guchar) * data_length); write_ptr = data; write_ptr[0] = 0; for (row = 0; row < height; row++) { int x; guchar *in_line; /* Pad to the next line */ if (mask != 0x80) { write_ptr++; write_ptr[0] = 0; mask = 0x80; } in_line = pixels + row * gdk_pixbuf_get_rowstride (image); for (x = 0; x < width; x++) { guchar *in_p = in_line + x*3; if (in_p[0] != 0) write_ptr[0] |= mask; mask >>= 1; if (mask == 0) { write_ptr++; write_ptr[0] = 0; mask = 0x80; } } } } else { int row; depth = 8; color_space = "DeviceGray"; data_length = height * width + 1; data = g_malloc (sizeof (guchar) * data_length); for (row = 0; row < height; row++) { int x; guchar *in_line, *out_line; in_line = pixels + row * gdk_pixbuf_get_rowstride (image); out_line = data + row * width; for (x = 0; x < width; x++) { guchar *in_p = in_line + x*3; guchar *out_p = out_line + x; out_p[0] = in_p[0]; } } } /* Compress data */ compressed_data = compress_zlib (data, data_length, &compressed_length); if (compressed_data) { /* Try if JPEG compression is better */ if (depth > 1) { guchar *jpeg_data; size_t jpeg_length; jpeg_data = compress_jpeg (image, &jpeg_length); if (jpeg_length < compressed_length) { filter = "DCTDecode"; g_free (data); g_free (compressed_data); data = jpeg_data; data_length = jpeg_length; } } if (!filter) { filter = "FlateDecode"; g_free (data); data = compressed_data; data_length = compressed_length; } } /* Page */ pdf_printf (writer, "\n"); number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Type /Page\n"); pdf_printf (writer, "/Parent %d 0 R\n", pages_number); pdf_printf (writer, "/Resources << /XObject << /Im%d %d 0 R >> >>\n", i, number+1); pdf_printf (writer, "/MediaBox [ 0 0 %.2f %.2f ]\n", page_width, page_height); pdf_printf (writer, "/Contents %d 0 R\n", number+2); pdf_printf (writer, ">>\n"); pdf_printf (writer, "endobj\n"); /* Page image */ pdf_printf (writer, "\n"); number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Type /XObject\n"); pdf_printf (writer, "/Subtype /Image\n"); pdf_printf (writer, "/Width %d\n", width); pdf_printf (writer, "/Height %d\n", height); pdf_printf (writer, "/ColorSpace /%s\n", color_space); pdf_printf (writer, "/BitsPerComponent %d\n", depth); pdf_printf (writer, "/Length %d\n", data_length); if (filter) pdf_printf (writer, "/Filter /%s\n", filter); pdf_printf (writer, ">>\n"); pdf_printf (writer, "stream\n"); pdf_write (writer, data, data_length); g_free (data); pdf_printf (writer, "\n"); pdf_printf (writer, "endstream\n"); pdf_printf (writer, "endobj\n"); /* Page contents */ command = g_strdup_printf ("q\n" "%f 0 0 %f 0 0 cm\n" "/Im%d Do\n" "Q", page_width, page_height, i); pdf_printf (writer, "\n"); number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Length %d\n", strlen (command) + 1); pdf_printf (writer, ">>\n"); pdf_printf (writer, "stream\n"); pdf_write (writer, (unsigned char *)command, strlen (command)); pdf_printf (writer, "\n"); pdf_printf (writer, "endstream\n"); pdf_printf (writer, "endobj\n"); g_free (command); g_object_unref (image); } /* Info */ pdf_printf (writer, "\n"); info_number = pdf_start_object (writer); pdf_printf (writer, "%d 0 obj\n", info_number); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Creator (Simple Scan " VERSION ")\n"); pdf_printf (writer, ">>\n"); pdf_printf (writer, "endobj\n"); /* Cross-reference table */ xref_offset = writer->offset; pdf_printf (writer, "xref\n"); pdf_printf (writer, "1 %d\n", writer->n_objects); GList *link; for (link = writer->object_offsets; link != NULL; link = link->next) { int offset = GPOINTER_TO_INT (link->data); pdf_printf (writer, "%010d 0000 n\n", offset); } /* Trailer */ pdf_printf (writer, "trailer\n"); pdf_printf (writer, "<<\n"); pdf_printf (writer, "/Size %d\n", writer->n_objects); pdf_printf (writer, "/Info %d 0 R\n", info_number); pdf_printf (writer, "/Root %d 0 R\n", catalog_number); pdf_printf (writer, ">>\n"); pdf_printf (writer, "startxref\n"); pdf_printf (writer, "%d\n", xref_offset); pdf_printf (writer, "%%%%EOF\n"); pdf_writer_free (writer); return TRUE; } gboolean book_save (Book *book, const gchar *type, GFile *file, GError **error) { gboolean result = FALSE; if (strcmp (type, "jpeg") == 0) result = book_save_multi_file (book, "jpeg", file, error); else if (strcmp (type, "png") == 0) result = book_save_multi_file (book, "png", file, error); else if (strcmp (type, "tiff") == 0) result = book_save_multi_file (book, "tiff", file, error); else if (strcmp (type, "ps") == 0) result = book_save_ps (book, file, error); else if (strcmp (type, "pdf") == 0) result = book_save_pdf (book, file, error); return result; } void book_set_needs_saving (Book *book, gboolean needs_saving) { gboolean needed_saving = book->priv->needs_saving; book->priv->needs_saving = needs_saving; if (needed_saving != needs_saving) g_object_notify (G_OBJECT (book), "needs-saving"); } gboolean book_get_needs_saving (Book *book) { return book->priv->needs_saving; } static void book_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { Book *self; self = BOOK (object); switch (prop_id) { case PROP_NEEDS_SAVING: book_set_needs_saving (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void book_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { Book *self; self = BOOK (object); switch (prop_id) { case PROP_NEEDS_SAVING: g_value_set_boolean (value, book_get_needs_saving (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void book_finalize (GObject *object) { Book *book = BOOK (object); book_clear (book); G_OBJECT_CLASS (book_parent_class)->finalize (object); } static void book_class_init (BookClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = book_get_property; object_class->set_property = book_set_property; object_class->finalize = book_finalize; g_object_class_install_property (object_class, PROP_NEEDS_SAVING, g_param_spec_boolean ("needs-saving", "needs-saving", "TRUE if this book needs saving", FALSE, G_PARAM_READWRITE)); signals[PAGE_ADDED] = g_signal_new ("page-added", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (BookClass, page_added), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, page_get_type ()); signals[PAGE_REMOVED] = g_signal_new ("page-removed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (BookClass, page_removed), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, page_get_type ()); signals[REORDERED] = g_signal_new ("reordered", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (BookClass, reordered), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[CLEARED] = g_signal_new ("cleared", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (BookClass, cleared), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); g_type_class_add_private (klass, sizeof (BookPrivate)); } static void book_init (Book *book) { book->priv = G_TYPE_INSTANCE_GET_PRIVATE (book, BOOK_TYPE, BookPrivate); }