[simple-scan] Make saving asyncronous, multithreaded, cancellable
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [simple-scan] Make saving asyncronous, multithreaded, cancellable
- Date: Mon, 29 May 2017 05:04:23 +0000 (UTC)
commit 9610f19f277c25fa0945991ce5b703a8302534f4
Author: Stéphane Fillion <stphanef3724 gmail com>
Date: Tue May 2 00:57:29 2017 -0400
Make saving asyncronous, multithreaded, cancellable
src/app-window.vala | 150 +++++----
src/book.vala | 758 +++++++++++++++++++++++++++++++------------
src/page.vala | 102 +-----
src/preferences-dialog.vala | 44 ---
4 files changed, 653 insertions(+), 401 deletions(-)
---
diff --git a/src/app-window.vala b/src/app-window.vala
index 16b2241..4ef5b22 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -117,7 +117,6 @@ public class AppWindow : Gtk.ApplicationWindow
private string? missing_driver = null;
private Gtk.FileChooserDialog? save_dialog;
- private ProgressBarDialog progress_dialog;
public Book book { get; private set; }
private bool book_needs_saving;
@@ -497,7 +496,7 @@ public class AppWindow : Gtk.ApplicationWindow
return "jpeg";
}
- private bool save_document ()
+ private async bool save_document_async ()
{
var uri = choose_file_location ();
if (uri == null)
@@ -509,27 +508,34 @@ public class AppWindow : Gtk.ApplicationWindow
var format = uri_to_format (uri);
- show_progress_dialog ();
+ var cancellable = new Cancellable ();
+ var progress_bar = new CancellableProgressBar ("Saving", cancellable);
+ action_bar.pack_end (progress_bar);
+ progress_bar.visible = true;
try
{
- book.save (format, settings.get_int ("jpeg-quality"), file);
+ yield book.save_async (format, settings.get_int ("jpeg-quality"), file, (fraction) =>
+ {
+ progress_bar.set_fraction (fraction);
+ }, cancellable);
}
catch (Error e)
{
- hide_progress_dialog ();
+ progress_bar.destroy ();
warning ("Error saving file: %s", e.message);
show_error_dialog (/* Title of error dialog when save failed */
_("Failed to save file"),
e.message);
return false;
}
+ progress_bar.destroy_with_delay (500);
book_needs_saving = false;
book_uri = uri;
return true;
}
- private bool prompt_to_save (string title, string discard_label)
+ private async bool prompt_to_save_async (string title, string discard_label)
{
if (!book_needs_saving)
return true;
@@ -552,7 +558,7 @@ public class AppWindow : Gtk.ApplicationWindow
switch (response)
{
case Gtk.ResponseType.YES:
- if (save_document ())
+ if (yield save_document_async ())
return true;
else
return false;
@@ -581,15 +587,19 @@ public class AppWindow : Gtk.ApplicationWindow
private void new_document ()
{
- if (!prompt_to_save (/* Text in dialog warning when a document is about to be lost */
- _("Save current document?"),
- /* Button in dialog to create new document and discard unsaved document */
- _("Discard Changes")))
- return;
+ prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
+ _("Save current document?"),
+ /* Button in dialog to create new document and discard unsaved document
*/
+ _("Discard Changes"), (obj, res) =>
+ {
+ if (!prompt_to_save_async.end(res))
+ return;
- if (scanning)
- stop_scan ();
- clear_document ();
+ if (scanning)
+ stop_scan ();
+
+ clear_document ();
+ });
}
[GtkCallback]
@@ -782,7 +792,7 @@ public class AppWindow : Gtk.ApplicationWindow
{
var dir = DirUtils.make_tmp ("simple-scan-XXXXXX");
file = File.new_for_path (Path.build_filename (dir, "scan.png"));
- page.save ("png", 0, file);
+ page.save_png (file);
}
catch (Error e)
{
@@ -1121,12 +1131,12 @@ public class AppWindow : Gtk.ApplicationWindow
[GtkCallback]
private void save_file_button_clicked_cb (Gtk.Widget widget)
{
- save_document ();
+ save_document_async.begin ();
}
public void save_document_activate_cb ()
{
- save_document ();
+ save_document_async.begin ();
}
[GtkCallback]
@@ -1165,24 +1175,22 @@ public class AppWindow : Gtk.ApplicationWindow
[GtkCallback]
private void email_button_clicked_cb (Gtk.Widget widget)
{
- email_document ();
+ email_document_async.begin ();
}
public void email_document_activate_cb ()
{
- email_document ();
+ email_document_async.begin ();
}
- private void email_document ()
+ private async void email_document_async ()
{
- show_progress_dialog ();
-
try
{
var dir = DirUtils.make_tmp ("simple-scan-XXXXXX");
var type = document_hint == "text" ? "pdf" : "jpeg";
var file = File.new_for_path (Path.build_filename (dir, "scan." + type));
- book.save (type, settings.get_int ("jpeg-quality"), file);
+ yield book.save_async (type, settings.get_int ("jpeg-quality"), file, null, null);
var command_line = "xdg-email";
if (type == "pdf")
command_line += "--attach %s".printf (file.get_path ());
@@ -1199,8 +1207,6 @@ public class AppWindow : Gtk.ApplicationWindow
{
warning ("Unable to email document: %s", e.message);
}
-
- hide_progress_dialog ();
}
private void print_document ()
@@ -1296,22 +1302,23 @@ public class AppWindow : Gtk.ApplicationWindow
show_about ();
}
- private bool on_quit ()
+ private void on_quit ()
{
- if (!prompt_to_save (/* Text in dialog warning when a document is about to be lost */
- _("Save document before quitting?"),
- /* Button in dialog to quit and discard unsaved document */
- _("Quit without Saving")))
- return false;
-
- destroy ();
+ prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
+ _("Save document before quitting?"),
+ /* Text in dialog warning when a document is about to be lost */
+ _("Quit without Saving"), (obj, res) =>
+ {
+ if (!prompt_to_save_async.end(res))
+ return;
- if (save_state_timeout != 0)
- save_state (true);
+ destroy ();
- autosave_manager.cleanup ();
+ if (save_state_timeout != 0)
+ save_state (true);
- return true;
+ autosave_manager.cleanup ();
+ });
}
[GtkCallback]
@@ -1495,7 +1502,8 @@ public class AppWindow : Gtk.ApplicationWindow
[GtkCallback]
private bool window_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event)
{
- return !on_quit ();
+ on_quit ();
+ return true; /* Let us quit on our own terms */
}
private void page_added_cb (Book book, Page page)
@@ -1670,9 +1678,6 @@ public class AppWindow : Gtk.ApplicationWindow
debug ("Restoring window to fullscreen");
fullscreen ();
}
-
- progress_dialog = new ProgressBarDialog (this, _("Saving document…"));
- book.saving.connect (book_saving_cb);
}
private bool is_desktop (string name)
@@ -1782,38 +1787,53 @@ public class AppWindow : Gtk.ApplicationWindow
}
}
- private void book_saving_cb (int page_number)
+ public void start ()
{
- /* Prevent GUI from freezing */
- while (Gtk.events_pending ())
- Gtk.main_iteration ();
-
- var total = (int) book.n_pages;
- var fraction = (page_number + 1.0) / total;
- var complete = fraction == 1.0;
- if (complete)
- Timeout.add (500, () => {
- progress_dialog.visible = false;
- return false;
- });
- var message = _("Saving page %d out of %d").printf (page_number + 1, total);
-
- progress_dialog.fraction = fraction;
- progress_dialog.message = message;
+ visible = true;
}
+}
- public void show_progress_dialog ()
+private class CancellableProgressBar : Gtk.HBox
+{
+ private Gtk.ProgressBar bar;
+ private Gtk.Button? button;
+
+ public CancellableProgressBar (string? text, Cancellable? cancellable)
{
- progress_dialog.visible = true;
+ bar = new Gtk.ProgressBar ();
+ bar.visible = true;
+ bar.set_text (text);
+ bar.set_show_text (true);
+ pack_start (bar);
+
+ if (cancellable != null)
+ {
+ button = new Gtk.Button.with_label (/* Text of button for cancelling save */
+ _("Cancel"));
+ button.visible = true;
+ button.clicked.connect (() =>
+ {
+ cancellable.cancel ();
+ });
+ pack_start (button);
+ }
}
- public void hide_progress_dialog ()
+ public void set_fraction (double fraction)
{
- progress_dialog.visible = false;
+ bar.set_fraction (fraction);
}
- public void start ()
+ public void destroy_with_delay (uint delay)
{
- visible = true;
+ button.set_sensitive (false);
+
+ Timeout.add (delay, () =>
+ {
+ this.destroy ();
+ return false;
+ });
}
+
+
}
diff --git a/src/book.vala b/src/book.vala
index 1abc0b1..eaffc41 100644
--- a/src/book.vala
+++ b/src/book.vala
@@ -9,6 +9,8 @@
* license.
*/
+public delegate void ProgressionCallback (double fraction);
+
public class Book
{
private List<Page> pages;
@@ -20,7 +22,6 @@ public class Book
public signal void reordered ();
public signal void cleared ();
public signal void changed ();
- public signal void saving (int i);
public Book ()
{
@@ -155,77 +156,445 @@ public class Book
return File.new_for_uri (filename);
}
- private void save_multi_file (string type, int quality, File file) throws Error
+ public async void save_async (string t, int q, File f, ProgressionCallback? p, Cancellable? c) throws
Error
{
+ var book_saver = new BookSaver ();
+ yield book_saver.save_async (this, t, q, f, p, c);
+ }
+}
+
+private class BookSaver
+{
+ private uint n_pages;
+ private int quality;
+ private File file;
+ private unowned ProgressionCallback progression_callback;
+ private double progression;
+ private Mutex progression_mutex;
+ private Cancellable? cancellable;
+ private AsyncQueue<WriteTask> write_queue;
+ private SourceFunc save_async_callback;
+
+ /* save_async get called in the main thread to start saving. It
+ * distributes all encode tasks to other threads then yield so
+ * the ui can continue operating. The method then return once saving
+ * is completed, cancelled, or failed */
+ public async void save_async (Book book, string type, int quality, File file, ProgressionCallback?
progression_callback, Cancellable? cancellable) throws Error
+ {
+ var timer = new Timer ();
+
+ this.n_pages = book.n_pages;
+ this.quality = quality;
+ this.file = file;
+ this.cancellable = cancellable;
+ this.save_async_callback = save_async.callback;
+ this.write_queue = new AsyncQueue<WriteTask> ();
+ this.progression = 0;
+ this.progression_mutex = Mutex ();
+
+ /* Configure a callback that monitor saving progression */
+ if (progression_callback == null)
+ this.progression_callback = (fraction) =>
+ {
+ debug ("Save progression: %f%%", fraction*100.0);
+ };
+ else
+ this.progression_callback = progression_callback;
+
+ /* Configure an encoder */
+ ThreadPoolFunc<EncodeTask>? encode_delegate = null;
+ switch (type)
+ {
+ case "jpeg":
+ encode_delegate = encode_jpeg;
+ break;
+ case "png":
+ encode_delegate = encode_png;
+ break;
+#if HAVE_WEBP
+ case "webp":
+ encode_delegate = encode_webp;
+ break;
+#endif
+ case "pdf":
+ encode_delegate = encode_pdf;
+ break;
+ }
+ var encoder = new ThreadPool<EncodeTask>.with_owned_data (encode_delegate, (int) get_num_processors
(), false);
+
+ /* Configure a writer */
+ ThreadFunc<Error?>? write_delegate = null;
+ switch (type)
+ {
+ case "jpeg":
+ case "png":
+#if HAVE_WEBP
+ case "webp":
+#endif
+ write_delegate = write_multifile;
+ break;
+ case "pdf":
+ write_delegate = write_pdf;
+ break;
+ }
+ var writer = new Thread<Error?> (null, write_delegate);
+
+ /* Issue encode tasks */
for (var i = 0; i < n_pages; i++)
{
- var page = get_page (i);
- page.save (type, quality, make_indexed_file (file.get_uri (), i));
- saving (i);
+ var encode_task = new EncodeTask ();
+ encode_task.number = i;
+ encode_task.page = book.get_page(i);
+ encoder.add ((owned) encode_task);
}
+
+ /* Waiting for saving to finish */
+ yield;
+
+ /* At this point, any remaining encode_task ought to remain unprocessed */
+ ThreadPool.free ((owned) encoder, true, true);
+
+ /* Any error from any thread ends up here */
+ var error = writer.join ();
+ if (error != null)
+ throw error;
+
+ timer.stop ();
+ debug ("Save time: %f seconds", timer.elapsed (null));
}
- private uint8[]? compress_zlib (uint8[] data, uint max_size)
+ /* Those methods are run in the encoder threads pool. It process
+ * one encode_task issued by save_async and reissue the result with
+ * a write_task */
+
+ private void encode_png (owned EncodeTask encode_task)
{
- var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION);
- var out_data = new uint8[max_size];
+ if (cancellable.is_cancelled ())
+ return;
- stream.next_in = data;
- stream.next_out = out_data;
- while (true)
- {
- /* Compression complete */
- if (stream.avail_in == 0)
- break;
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true);
- /* Out of space */
- if (stream.avail_out == 0)
- return null;
+ string[] keys = { "x-dpi", "y-dpi", "icc-profile", null };
+ string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), icc_data, null };
+ if (icc_data == null)
+ keys[2] = null;
- if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR)
- return null;
+ try
+ {
+ image.save_to_bufferv (out write_task.data, "png", keys, values);
+ }
+ catch (Error error)
+ {
+ write_task.error = error;
}
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
- var n_written = out_data.length - stream.avail_out;
- out_data.resize ((int) n_written);
+ update_progression ();
+ }
- return out_data;
+ private void encode_jpeg (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true);
+
+ string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null };
+ string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), "%d".printf (quality), icc_data,
null };
+ if (icc_data == null)
+ keys[3] = null;
+
+ try
+ {
+ image.save_to_bufferv (out write_task.data, "jpeg", keys, values);
+ }
+ catch (Error error)
+ {
+ write_task.error = error;
+ }
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
+
+ update_progression ();
}
- private ByteArray jpeg_data;
+#if HAVE_WEBP
+ private void encode_webp (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true);
+ var webp_data = WebP.encode_rgb (image.get_pixels (),
+ image.get_width (),
+ image.get_height (),
+ image.get_rowstride (),
+ (float) quality);
+#if HAVE_COLORD
+ WebP.MuxError mux_error;
+ var mux = WebP.Mux.new_mux ();
+ uint8[] output;
+
+ mux_error = mux.set_image (webp_data, false);
+ debug ("mux.set_image: %s", mux_error.to_string ());
+
+ if (icc_data != null)
+ {
+ mux_error = mux.set_chunk ("ICCP", icc_data.data, false);
+ debug ("mux.set_chunk: %s", mux_error.to_string ());
+ if (mux_error != WebP.MuxError.OK)
+ warning ("icc profile data not saved with page %i", encode_task.number);
+ }
+
+ mux_error = mux.assemble (out output);
+ debug ("mux.assemble: %s", mux_error.to_string ());
+ if (mux_error != WebP.MuxError.OK)
+ write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf
(encode_task.number));
+
+ write_task.data = (owned) output;
+#else
+
+ if (webp_data.length == 0)
+ write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf
(encode_task.number));
+
+ write_task.data = (owned) webp_data;
+#endif
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
+
+ update_progression ();
+ }
+#endif
- private uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi)
+ private void encode_pdf (owned EncodeTask encode_task)
{
- jpeg_data = new ByteArray ();
- string[] keys = { "quality", "x-dpi", "y-dpi", null };
- string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null };
+ var page = encode_task.page;
+ var image = page.get_image (true);
+ var width = image.width;
+ var height = image.height;
+ unowned uint8[] pixels = image.get_pixels ();
+ int depth = 8;
+ string color_space = "DeviceRGB";
+ string? filter = null;
+ uint8[] data;
+
+ if (page.is_color)
+ {
+ depth = 8;
+ color_space = "DeviceRGB";
+ var data_length = height * width * 3;
+ data = new uint8[data_length];
+ for (var row = 0; row < height; row++)
+ {
+ var in_offset = row * image.rowstride;
+ var out_offset = row * width * 3;
+ for (var x = 0; x < width; x++)
+ {
+ var in_o = in_offset + x*3;
+ var out_o = out_offset + x*3;
+
+ data[out_o] = pixels[in_o];
+ data[out_o+1] = pixels[in_o+1];
+ data[out_o+2] = pixels[in_o+2];
+ }
+ }
+ }
+ else if (page.depth == 2)
+ {
+ int shift_count = 6;
+ depth = 2;
+ color_space = "DeviceGray";
+ var data_length = height * ((width * 2 + 7) / 8);
+ data = new uint8[data_length];
+ var offset = 0;
+ for (var row = 0; row < height; row++)
+ {
+ /* Pad to the next line */
+ if (shift_count != 6)
+ {
+ offset++;
+ shift_count = 6;
+ }
+
+ var in_offset = row * image.rowstride;
+ for (var x = 0; x < width; x++)
+ {
+ /* Clear byte */
+ if (shift_count == 6)
+ data[offset] = 0;
+
+ /* Set bits */
+ var p = pixels[in_offset + x*3];
+ if (p >= 192)
+ data[offset] |= 3 << shift_count;
+ else if (p >= 128)
+ data[offset] |= 2 << shift_count;
+ else if (p >= 64)
+ data[offset] |= 1 << shift_count;
+
+ /* Move to the next position */
+ if (shift_count == 0)
+ {
+ offset++;
+ shift_count = 6;
+ }
+ else
+ shift_count -= 2;
+ }
+ }
+ }
+ else if (page.depth == 1)
+ {
+ int mask = 0x80;
+
+ depth = 1;
+ color_space = "DeviceGray";
+ var data_length = height * ((width + 7) / 8);
+ data = new uint8[data_length];
+ var offset = 0;
+ for (var row = 0; row < height; row++)
+ {
+ /* Pad to the next line */
+ if (mask != 0x80)
+ {
+ offset++;
+ mask = 0x80;
+ }
+
+ var in_offset = row * image.rowstride;
+ for (var x = 0; x < width; x++)
+ {
+ /* Clear byte */
+ if (mask == 0x80)
+ data[offset] = 0;
+
+ /* Set bit */
+ if (pixels[in_offset+x*3] != 0)
+ data[offset] |= (uint8) mask;
+
+ /* Move to the next bit */
+ mask >>= 1;
+ if (mask == 0)
+ {
+ offset++;
+ mask = 0x80;
+ }
+ }
+ }
+ }
+ else
+ {
+ depth = 8;
+ color_space = "DeviceGray";
+ var data_length = height * width;
+ data = new uint8 [data_length];
+ for (var row = 0; row < height; row++)
+ {
+ var in_offset = row * image.rowstride;
+ var out_offset = row * width;
+ for (var x = 0; x < width; x++)
+ data[out_offset+x] = pixels[in_offset+x*3];
+ }
+ }
+
+ /* Compress data and use zlib compression if it is smaller than JPEG.
+ * zlib compression is slower in the worst case, so do JPEG first
+ * and stop zlib if it exceeds the JPEG size */
+ var write_task = new WriteTaskPDF ();
+ uint8[]? jpeg_data = null;
try
{
- image.save_to_callbackv (write_pixbuf_data, "jpeg", keys, values);
+ jpeg_data = compress_jpeg (image, quality, page.dpi);
}
- catch (Error e)
+ catch (Error error)
+ {
+ write_task.error = error;
+ }
+ var zlib_data = compress_zlib (data, jpeg_data.length);
+ if (zlib_data != null)
+ {
+ filter = "FlateDecode";
+ data = zlib_data;
+ }
+ else
{
+ filter = "DCTDecode";
+ data = jpeg_data;
}
- var data = (owned) jpeg_data.data;
- jpeg_data = null;
- return data;
+ write_task.number = encode_task.number;
+ write_task.data = data;
+ write_task.width = width;
+ write_task.height = height;
+ write_task.color_space = color_space;
+ write_task.depth = depth;
+ write_task.filter = filter;
+ write_task.dpi = page.dpi;
+ write_queue.push (write_task);
+
+ update_progression ();
}
- private bool write_pixbuf_data (uint8[] buf) throws Error
+ private Error? write_multifile ()
{
- jpeg_data.append (buf);
- return true;
+ for (var i=0; i < n_pages; i++)
+ {
+ if (cancellable.is_cancelled ())
+ {
+ finished_saving ();
+ return null;
+ }
+
+ var write_task = write_queue.pop ();
+ if (write_task.error != null)
+ {
+ finished_saving ();
+ return write_task.error;
+ }
+
+ var indexed_file = make_indexed_file (file.get_uri (), write_task.number);
+ try
+ {
+ var stream = indexed_file.replace (null, false, FileCreateFlags.NONE);
+ stream.write_all (write_task.data, null);
+ }
+ catch (Error error)
+ {
+ finished_saving ();
+ return error;
+ }
+ }
+
+ update_progression ();
+ finished_saving ();
+ return null;
}
- private void save_pdf (File file, int quality) throws Error
+ /* Those methods are run in the writer thread. It receive all
+ * write_tasks sent to it by the encoder threads and write those to
+ * disk. */
+
+ private Error? write_pdf ()
{
/* Generate a random ID for this file */
var id = "";
for (var i = 0; i < 4; i++)
id += "%08x".printf (Random.next_int ());
- var stream = file.replace (null, false, FileCreateFlags.NONE, null);
+ FileOutputStream? stream = null;
+ try
+ {
+ stream = file.replace (null, false, FileCreateFlags.NONE, null);
+ }
+ catch (Error error)
+ {
+ finished_saving ();
+ return error;
+ }
var writer = new PDFWriter (stream);
/* Choose object numbers */
@@ -309,156 +678,40 @@ public class Book
writer.write_string (">>\n");
writer.write_string ("endobj\n");
- for (var i = 0; i < n_pages; i++)
+ /* Process each page in order */
+ var tasks_in_standby = new Queue<WriteTaskPDF> ();
+ for (int i = 0; i < n_pages; i++)
{
- var page = get_page (i);
- var image = page.get_image (true);
- var width = image.width;
- var height = image.height;
- unowned uint8[] pixels = image.get_pixels ();
- var page_width = width * 72.0 / page.dpi;
- var page_height = height * 72.0 / page.dpi;
-
- int depth = 8;
- string color_space = "DeviceRGB";
- string? filter = null;
- char[] width_buffer = new char[double.DTOSTR_BUF_SIZE];
- char[] height_buffer = new char[double.DTOSTR_BUF_SIZE];
- uint8[] data;
- if (page.is_color)
+ if (cancellable.is_cancelled ())
{
- depth = 8;
- color_space = "DeviceRGB";
- var data_length = height * width * 3;
- data = new uint8[data_length];
- for (var row = 0; row < height; row++)
- {
- var in_offset = row * image.rowstride;
- var out_offset = row * width * 3;
- for (var x = 0; x < width; x++)
- {
- var in_o = in_offset + x*3;
- var out_o = out_offset + x*3;
-
- data[out_o] = pixels[in_o];
- data[out_o+1] = pixels[in_o+1];
- data[out_o+2] = pixels[in_o+2];
- }
- }
+ finished_saving ();
+ return null;
}
- else if (page.depth == 2)
- {
- int shift_count = 6;
- depth = 2;
- color_space = "DeviceGray";
- var data_length = height * ((width * 2 + 7) / 8);
- data = new uint8[data_length];
- var offset = 0;
- for (var row = 0; row < height; row++)
- {
- /* Pad to the next line */
- if (shift_count != 6)
- {
- offset++;
- shift_count = 6;
- }
- var in_offset = row * image.rowstride;
- for (var x = 0; x < width; x++)
- {
- /* Clear byte */
- if (shift_count == 6)
- data[offset] = 0;
-
- /* Set bits */
- var p = pixels[in_offset + x*3];
- if (p >= 192)
- data[offset] |= 3 << shift_count;
- else if (p >= 128)
- data[offset] |= 2 << shift_count;
- else if (p >= 64)
- data[offset] |= 1 << shift_count;
-
- /* Move to the next position */
- if (shift_count == 0)
- {
- offset++;
- shift_count = 6;
- }
- else
- shift_count -= 2;
- }
- }
- }
- else if (page.depth == 1)
+ var write_task = tasks_in_standby.peek_head ();
+ if (write_task != null && write_task.number == i)
+ tasks_in_standby.pop_head ();
+ else
{
- int mask = 0x80;
-
- depth = 1;
- color_space = "DeviceGray";
- var data_length = height * ((width + 7) / 8);
- data = new uint8[data_length];
- var offset = 0;
- for (var row = 0; row < height; row++)
+ while (true)
{
- /* Pad to the next line */
- if (mask != 0x80)
+ write_task = (WriteTaskPDF) write_queue.pop ();
+ if (write_task.error != null)
{
- offset++;
- mask = 0x80;
+ finished_saving ();
+ return write_task.error;
}
+ if (write_task.number == i)
+ break;
- var in_offset = row * image.rowstride;
- for (var x = 0; x < width; x++)
- {
- /* Clear byte */
- if (mask == 0x80)
- data[offset] = 0;
-
- /* Set bit */
- if (pixels[in_offset+x*3] != 0)
- data[offset] |= (uint8) mask;
-
- /* Move to the next bit */
- mask >>= 1;
- if (mask == 0)
- {
- offset++;
- mask = 0x80;
- }
- }
- }
- }
- else
- {
- depth = 8;
- color_space = "DeviceGray";
- var data_length = height * width;
- data = new uint8 [data_length];
- for (var row = 0; row < height; row++)
- {
- var in_offset = row * image.rowstride;
- var out_offset = row * width;
- for (var x = 0; x < width; x++)
- data[out_offset+x] = pixels[in_offset+x*3];
+ tasks_in_standby.insert_sorted (write_task, (a, b) => {return a.number - b.number;});
}
}
- /* Compress data and use zlib compression if it is smaller than JPEG.
- * zlib compression is slower in the worst case, so do JPEG first
- * and stop zlib if it exceeds the JPEG size */
- var jpeg_data = compress_jpeg (image, quality, page.dpi);
- var zlib_data = compress_zlib (data, jpeg_data.length);
- if (zlib_data != null)
- {
- filter = "FlateDecode";
- data = zlib_data;
- }
- else
- {
- filter = "DCTDecode";
- data = jpeg_data;
- }
+ var page_width = write_task.width * 72.0 / write_task.dpi;
+ var page_height = write_task.height * 72.0 / write_task.dpi;
+ var width_buffer = new char[double.DTOSTR_BUF_SIZE];
+ var height_buffer = new char[double.DTOSTR_BUF_SIZE];
/* Page */
writer.write_string ("\n");
@@ -480,16 +733,16 @@ public class Book
writer.write_string ("<<\n");
writer.write_string ("/Type /XObject\n");
writer.write_string ("/Subtype /Image\n");
- writer.write_string ("/Width %d\n".printf (width));
- writer.write_string ("/Height %d\n".printf (height));
- writer.write_string ("/ColorSpace /%s\n".printf (color_space));
- writer.write_string ("/BitsPerComponent %d\n".printf (depth));
- writer.write_string ("/Length %d\n".printf (data.length));
- if (filter != null)
- writer.write_string ("/Filter /%s\n".printf (filter));
+ writer.write_string ("/Width %d\n".printf (write_task.width));
+ writer.write_string ("/Height %d\n".printf (write_task.height));
+ writer.write_string ("/ColorSpace /%s\n".printf (write_task.color_space));
+ writer.write_string ("/BitsPerComponent %d\n".printf (write_task.depth));
+ writer.write_string ("/Length %d\n".printf (write_task.data.length));
+ if (write_task.filter != null)
+ writer.write_string ("/Filter /%s\n".printf (write_task.filter));
writer.write_string (">>\n");
writer.write_string ("stream\n");
- writer.write (data);
+ writer.write (write_task.data);
writer.write_string ("\n");
writer.write_string ("endstream\n");
writer.write_string ("endobj\n");
@@ -499,7 +752,7 @@ public class Book
writer.start_object (struct_tree_root_number);
writer.write_string ("%u 0 obj\n".printf (struct_tree_root_number));
writer.write_string ("<<\n");
- writer.write_string ("/Type /StructTreeRoot\n");
+ writer.write_string ("/Type /StructTreeRoot\n");
writer.write_string (">>\n");
writer.write_string ("endobj\n");
@@ -516,8 +769,6 @@ public class Book
writer.write_string ("\n");
writer.write_string ("endstream\n");
writer.write_string ("endobj\n");
-
- saving (i);
}
/* Info */
@@ -534,10 +785,10 @@ public class Book
var xref_offset = writer.offset;
writer.write_string ("xref\n");
writer.write_string ("0 %zu\n".printf (writer.object_offsets.length + 1));
- writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, 0)));
+ writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (0)));
for (var i = 0; i < writer.object_offsets.length; i++)
if (writer.object_offsets[i] == 0)
- writer.write_string ("%010zu 65535 f \n".printf (next_empty_object (writer, i + 1)));
+ writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (i + 1)));
else
writer.write_string ("%010zu 00000 n \n".printf (writer.object_offsets[i]));
@@ -553,34 +804,117 @@ public class Book
writer.write_string ("startxref\n");
writer.write_string ("%zu\n".printf (xref_offset));
writer.write_string ("%%EOF\n");
+
+ update_progression ();
+ finished_saving ();
+ return null;
}
- static int next_empty_object (PDFWriter writer, int start)
+ /* update_progression is called once by page by encoder threads and
+ * once at the end by writer thread. */
+ private void update_progression ()
{
- for (var i = start; i < writer.object_offsets.length; i++)
- if (writer.object_offsets[i] == 0)
- return i + 1;
- return 0;
+ double step = 1.0 / (double)(n_pages+1);
+ progression_mutex.lock ();
+ progression += step;
+ progression_mutex.unlock ();
+ Idle.add (() =>
+ {
+ progression_callback (progression);
+ return false;
+ });
}
- public void save (string type, int quality, File file) throws Error
+ /* finished_saving is called by the writer thread when it's done,
+ * meaning there is nothing left to do or saving has been
+ * cancelled */
+ private void finished_saving ()
{
- switch (type)
+ /* Wake-up save_async method in main thread */
+ Idle.add ((owned)save_async_callback);
+ }
+
+ /* Utility methods */
+
+ private File make_indexed_file (string uri, int i)
+ {
+ if (n_pages == 1)
+ return File.new_for_uri (uri);
+
+ /* Insert index before extension */
+ var basename = Path.get_basename (uri);
+ string prefix = uri, suffix = "";
+ var extension_index = basename.last_index_of_char ('.');
+ if (extension_index >= 0)
{
- case "jpeg":
- case "png":
-#if HAVE_WEBP
- case "webp":
-#endif
- save_multi_file (type, quality, file);
- break;
- case "pdf":
- save_pdf (file, quality);
- break;
- default:
- throw new FileError.INVAL ("Unknown file type: %s".printf (type));
+ suffix = basename.slice (extension_index, basename.length);
+ prefix = uri.slice (0, uri.length - suffix.length);
}
+ var width = n_pages.to_string().length;
+ var number_format = "%%0%dd".printf (width);
+ var filename = prefix + "-" + number_format.printf (i + 1) + suffix;
+ return File.new_for_uri (filename);
}
+
+ private static uint8[]? compress_zlib (uint8[] data, uint max_size)
+ {
+ var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION);
+ var out_data = new uint8[max_size];
+
+ stream.next_in = data;
+ stream.next_out = out_data;
+ while (true)
+ {
+ /* Compression complete */
+ if (stream.avail_in == 0)
+ break;
+
+ /* Out of space */
+ if (stream.avail_out == 0)
+ return null;
+
+ if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR)
+ return null;
+ }
+
+ var n_written = out_data.length - stream.avail_out;
+ out_data.resize ((int) n_written);
+
+ return out_data;
+ }
+
+ private static uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) throws Error
+ {
+ uint8[] jpeg_data;
+ string[] keys = { "quality", "x-dpi", "y-dpi", null };
+ string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null };
+
+ image.save_to_bufferv (out jpeg_data, "jpeg", keys, values);
+ return jpeg_data;
+ }
+}
+
+private class EncodeTask
+{
+ public int number;
+ public Page page;
+}
+
+private class WriteTask
+{
+ public int number;
+ public uint8[] data;
+ public Error error;
+}
+
+private class WriteTaskPDF : WriteTask
+{
+ public int width;
+ public int height;
+ public string color_space;
+ public int depth;
+ public string? filter;
+ public int dpi;
}
private class PDFWriter
@@ -625,4 +959,12 @@ private class PDFWriter
{
object_offsets[index - 1] = (uint)offset;
}
+
+ public int next_empty_object (int start)
+ {
+ for (var i = start; i < object_offsets.length; i++)
+ if (object_offsets[i] == 0)
+ return i + 1;
+ return 0;
+ }
}
diff --git a/src/page.vala b/src/page.vala
index 47c142f..582aef8 100644
--- a/src/page.vala
+++ b/src/page.vala
@@ -624,13 +624,16 @@ public class Page
return image;
}
- private string? get_icc_data_encoded (string icc_profile_filename)
+ public string? get_icc_data_encoded ()
{
+ if (color_profile == null)
+ return null;
+
/* Get binary data */
string contents;
try
{
- FileUtils.get_contents (icc_profile_filename, out contents);
+ FileUtils.get_contents (color_profile, out contents);
}
catch (Error e)
{
@@ -641,102 +644,33 @@ public class Page
/* Encode into base64 */
return Base64.encode ((uchar[]) contents.to_utf8 ());
}
-
+
public void copy_to_clipboard (Gtk.Window window)
- {
+ {
var display = window.get_display ();
var clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD);
var image = get_image (true);
clipboard.set_image (image);
}
- public void save (string type, int quality, File file) throws Error
+ public void save_png (File file) throws Error
{
var stream = file.replace (null, false, FileCreateFlags.NONE, null);
- var writer = new PixbufWriter (stream);
var image = get_image (true);
string? icc_profile_data = null;
if (color_profile != null)
- icc_profile_data = get_icc_data_encoded (color_profile);
+ icc_profile_data = get_icc_data_encoded ();
- if (strcmp (type, "jpeg") == 0)
- {
- string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null };
- string[] values = { "%d".printf (dpi), "%d".printf (dpi), "%d".printf (quality),
icc_profile_data, null };
- if (icc_profile_data == null)
- keys[3] = null;
- writer.save (image, "jpeg", keys, values);
- }
- else if (strcmp (type, "png") == 0)
- {
- string[] keys = { "x-dpi", "y-dpi", "icc-profile", null };
- string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null };
- if (icc_profile_data == null)
- keys[2] = null;
- writer.save (image, "png", keys, values);
- }
-#if HAVE_WEBP
- else if (strcmp (type, "webp") == 0)
- {
- var webp_data = WebP.encode_rgb (image.get_pixels (),
- image.get_width (),
- image.get_height (),
- image.get_rowstride (),
- (float) quality);
-#if HAVE_COLORD
- WebP.MuxError mux_error;
- var mux = WebP.Mux.new_mux ();
- uint8[] output;
-
- mux_error = mux.set_image (webp_data, false);
- debug ("mux.set_image: %s", mux_error.to_string ());
-
- if (icc_profile_data != null)
- {
- mux_error = mux.set_chunk ("ICCP", icc_profile_data.data, false);
- debug ("mux.set_chunk: %s", mux_error.to_string ());
- if (mux_error != WebP.MuxError.OK)
- warning ("icc profile data not saved in %s", file.get_basename ());
- }
-
- mux_error = mux.assemble (out output);
- debug ("mux.assemble: %s", mux_error.to_string ());
- if (mux_error != WebP.MuxError.OK)
- throw new FileError.FAILED (_("Unable to encode %s").printf (file.get_basename ()));
+ string[] keys = { "x-dpi", "y-dpi", "icc-profile", null };
+ string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null };
+ if (icc_profile_data == null)
+ keys[2] = null;
- stream.write_all (output, null);
-#else
-
- if (webp_data.length == 0)
- throw new FileError.FAILED (_("Unable to encode %s").printf (file.get_basename ()));
-
- stream.write_all (webp_data, null);
-#endif
- }
-#endif
- else
- throw new FileError.INVAL ("Unknown file type: %s".printf (type));
- }
-}
-
-public class PixbufWriter
-{
- public FileOutputStream stream;
-
- public PixbufWriter (FileOutputStream stream)
- {
- this.stream = stream;
- }
-
- public void save (Gdk.Pixbuf image, string type, string[] option_keys, string[] option_values) throws
Error
- {
- image.save_to_callbackv (write_pixbuf_data, type, option_keys, option_values);
- }
-
- private bool write_pixbuf_data (uint8[] buf) throws Error
- {
- stream.write_all (buf, null, null);
- return true;
+ image.save_to_callbackv ((buf) =>
+ {
+ stream.write_all (buf, null, null);
+ return true;
+ }, "png", keys, values);
}
}
diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala
index d67c111..bf213fb 100644
--- a/src/preferences-dialog.vala
+++ b/src/preferences-dialog.vala
@@ -468,50 +468,6 @@ private class PreferencesDialog : Gtk.Dialog
}
}
-private class ProgressBarDialog : Gtk.Window
-{
- private Gtk.ProgressBar bar;
-
- public double fraction
- {
- get { return bar.fraction; }
- set { bar.fraction = value; }
- }
-
- public string message
- {
- get { return bar.text; }
- set { bar.text = value; }
- }
-
- public ProgressBarDialog (Gtk.ApplicationWindow parent, string title)
- {
- bar = new Gtk.ProgressBar ();
- var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5);
- var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5);
- hbox.hexpand = true;
-
- bar.text = "";
- bar.show_text = true;
- bar.set_size_request (225, 25);
- set_size_request (250, 50);
-
- vbox.pack_start (bar, true, false, 0);
- hbox.pack_start (vbox, true, false, 0);
- add (hbox);
- this.title = title;
-
- transient_for = parent;
- set_position (Gtk.WindowPosition.CENTER_ON_PARENT);
- modal = true;
- resizable = false;
-
- hbox.visible = true;
- vbox.visible = true;
- bar.visible = true;
- }
-}
-
private class PageIcon : Gtk.DrawingArea
{
private string text;
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]