[simple-scan] Make saving asyncronous, multithreaded, cancellable



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]