[geary/wip/728002-webkit2: 59/105] Use CID resources to display images for multipart/mixed messages.



commit c58088d54f166bebcde661a2e582bd62544d3c24
Author: Michael James Gratton <mike vee net>
Date:   Tue Oct 11 00:58:52 2016 +1100

    Use CID resources to display images for multipart/mixed messages.
    
    * src/client/conversation-viewer/conversation-message.vala
      (ConversationMessage): Remove ReplacedImage and related code.
      (ConversationMessage::inline_image_replacer): Don't bother loading,
      scaling, rotating and serialising the images, just add them as CID
      resources.
    
    * src/client/components/client-web-view.vala (ClientWebView): Modify the
      cid_resources map contain memory buffers, not files, and update call
      sites.

 src/client/components/client-web-view.vala         |   22 ++--
 src/client/composer/composer-widget.vala           |    5 +-
 .../conversation-viewer/conversation-message.vala  |  148 ++------------------
 ui/conversation-web-view.css                       |    2 +-
 4 files changed, 29 insertions(+), 148 deletions(-)
---
diff --git a/src/client/components/client-web-view.vala b/src/client/components/client-web-view.vala
index e0dfae0..e31c73f 100644
--- a/src/client/components/client-web-view.vala
+++ b/src/client/components/client-web-view.vala
@@ -8,6 +8,8 @@
 public class ClientWebView : WebKit.WebView {
 
 
+    public const string CID_PREFIX = "cid:";
+
     private const double ZOOM_DEFAULT = 1.0;
     private const double ZOOM_FACTOR = 0.1;
 
@@ -52,7 +54,8 @@ public class ClientWebView : WebKit.WebView {
         set { if (zoom_level != (float)value) zoom_level = (float)value; }
     }
 
-    private Gee.Map<string,File> cid_resources = new Gee.HashMap<string,File>();
+    private Gee.Map<string,Geary.Memory.Buffer> cid_resources =
+        new Gee.HashMap<string,Geary.Memory.Buffer>();
 
 
     /** Emitted when a user clicks a link in this web view. */
@@ -90,8 +93,8 @@ public class ClientWebView : WebKit.WebView {
     /**
      * Adds a resource that may be accessed via a cid:id url.
      */
-    public void add_cid_resource(string id, File file) {
-        this.cid_resources[id] = file;
+    public void add_cid_resource(string id, Geary.Memory.Buffer buf) {
+        this.cid_resources[id] = buf;
     }
 
     /**
@@ -131,16 +134,11 @@ public class ClientWebView : WebKit.WebView {
     }
 
     internal void handle_cid_request(WebKit.URISchemeRequest request) {
-        const string CID_PREFIX = "cid:";
-
         string cid = request.get_uri().substring(CID_PREFIX.length);
-        File? file = this.cid_resources[cid];
-        if (file != null) {
-            try {
-                request.finish(file.read(), -1, null);
-            } catch (Error err) {
-                request.finish_error(err);
-            }
+        Geary.Memory.Buffer? buf = this.cid_resources[cid];
+        if (buf != null) {
+            request.finish(buf.get_input_stream(), buf.size, null);
+            attachment_loaded(cid);
         } else {
             request.finish_error(
                 new FileError.NOENT("Unknown CID: %s".printf(cid))
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 6444da2..c39ffcb 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -1533,7 +1533,10 @@ public class ComposerWidget : Gtk.EventBox {
                         // attachment instead.
                         if (part.content_id != null) {
                             this.cid_files[part.content_id] = file;
-                            this.editor.add_cid_resource(part.content_id, file);
+                            this.editor.add_cid_resource(
+                                part.content_id,
+                                new Geary.Memory.FileBuffer(file, true)
+                            );
                         } else {
                             type = Geary.Mime.DispositionType.ATTACHMENT;
                         }
diff --git a/src/client/conversation-viewer/conversation-message.vala 
b/src/client/conversation-viewer/conversation-message.vala
index 5be9e50..f5a3936 100644
--- a/src/client/conversation-viewer/conversation-message.vala
+++ b/src/client/conversation-viewer/conversation-message.vala
@@ -19,7 +19,7 @@ public class ConversationMessage : Gtk.Grid {
 
 
     private const string FROM_CLASS = "geary-from";
-    private const string DATA_IMAGE_CLASS = "geary_data_inline_image";
+    private const string REPLACED_CID_TEMPLATE = "replaced_%02u@geary";
     private const string REPLACED_IMAGE_CLASS = "geary_replaced_inline_image";
 
     private const int MAX_PREVIEW_BYTES = Geary.Email.MAX_PREVIEW_BYTES;
@@ -126,21 +126,6 @@ public class ConversationMessage : Gtk.Grid {
 
     }
 
-    // Internal class to associate inline image buffers (replaced by
-    // rotated scaled versions of them) so they can be saved intact if
-    // the user requires it
-    private class ReplacedImage : Geary.BaseObject {
-        public string id;
-        public string filename;
-        public Geary.Memory.Buffer buffer;
-
-        public ReplacedImage(int replaced_number, string filename, Geary.Memory.Buffer buffer) {
-            id = "%X".printf(replaced_number);
-            this.filename = filename;
-            this.buffer = buffer;
-        }
-    }
-
     private const string[] INLINE_MIME_TYPES = {
         "image/png",
         "image/gif",
@@ -252,10 +237,6 @@ public class ConversationMessage : Gtk.Grid {
     // Should any remote messages be always loaded and displayed?
     private bool always_load_remote_images;
 
-    private int next_replaced_buffer_number = 0;
-    private Gee.HashMap<string, ReplacedImage> replaced_images = new Gee.HashMap<string, ReplacedImage>();
-    private Gee.HashSet<string> replaced_content_ids = new Gee.HashSet<string>();
-
     // Resource that have been loaded by the web view
     private Gee.Map<string,WebKit.WebResource> resources =
         new Gee.HashMap<string,WebKit.WebResource>();
@@ -265,6 +246,8 @@ public class ConversationMessage : Gtk.Grid {
 
     /** Fired when an attachment is added for inline display. */
     public signal void attachment_displayed_inline(string id);
+    private int next_replaced_buffer_number = 0;
+
 
     /** Fired when the user requests remote images be loaded. */
     public signal void flag_remote_images();
@@ -742,111 +725,20 @@ public class ConversationMessage : Gtk.Grid {
             
             return null;
         }
-        
-        // Even if the image doesn't need to be rotated, there's a win here: by reducing the size
-        // of the image at load time, it reduces the amount of work that has to be done to insert
-        // it into the HTML and then decoded and displayed for the user ... note that we currently
-        // have the doucment set up to reduce the size of the image to fit in the viewport, and a
-        // scaled load-and-deode is always faster than load followed by scale.
-        Geary.Memory.Buffer rotated_image = buffer;
-        string mime_type = content_type.get_mime_type();
-        try {
-            Gdk.PixbufLoader loader = new Gdk.PixbufLoader();
-            loader.size_prepared.connect(on_inline_image_size_prepared);
-            
-            Geary.Memory.UnownedBytesBuffer? unowned_buffer = buffer as Geary.Memory.UnownedBytesBuffer;
-            if (unowned_buffer != null)
-                loader.write(unowned_buffer.to_unowned_uint8_array());
-            else
-                loader.write(buffer.get_uint8_array());
-            loader.close();
-            
-            Gdk.Pixbuf? pixbuf = loader.get_pixbuf();
-            if (pixbuf != null) {
-                pixbuf = pixbuf.apply_embedded_orientation();
-                
-                // trade-off here between how long it takes to compress the data and how long it
-                // takes to turn it into Base-64 (coupled with how long it takes WebKit to then
-                // Base-64 decode and uncompress it)
-                uint8[] image_data;
-                pixbuf.save_to_buffer(out image_data, "png", "compression", "5");
-                
-                // Save length before transferring ownership (which frees the array)
-                int image_length = image_data.length;
-                rotated_image = new Geary.Memory.ByteBuffer.take((owned) image_data, image_length);
-                mime_type = "image/png";
-            }
-        } catch (Error err) {
-            debug("Unable to load and rotate image %s for display: %s", filename, err.message);
-        }
-        
-        // store so later processing of the message doesn't replace this element with the original
-        // MIME part
-        string? escaped_content_id = null;
-        if (!Geary.String.is_empty(content_id)) {
-            replaced_content_ids.add(content_id);
-            escaped_content_id = Geary.HTML.escape_markup(content_id);
+
+        string id = content_id;
+        if (id == null) {
+            id = REPLACED_CID_TEMPLATE.printf(this.next_replaced_buffer_number++);
         }
-        
-        // Store the original buffer and its filename in a local map so they can be recalled later
-        // (if the user wants to save it) ... note that Content-ID is optional and there's no
-        // guarantee that filename will be unique, even in the same message, so need to generate
-        // a unique identifier for each object
-        ReplacedImage replaced_image = new ReplacedImage(next_replaced_buffer_number++, filename,
-            buffer);
-        replaced_images.set(replaced_image.id, replaced_image);
-        
-        return "<img alt=\"%s\" class=\"%s %s\" src=\"%s\" replaced-id=\"%s\" %s />".printf(
-            Geary.HTML.escape_markup(filename),
-            DATA_IMAGE_CLASS, REPLACED_IMAGE_CLASS,
-            assemble_data_uri(mime_type, rotated_image),
-            Geary.HTML.escape_markup(replaced_image.id),
-            escaped_content_id != null ? @"cid=\"$escaped_content_id\"" : "");
-    }
 
-    // Returns a URI suitable for an IMG SRC attribute (or elsewhere, potentially) that is the
-    // memory buffer unpacked into a Base-64 encoded data: URI
-    private string assemble_data_uri(string mimetype, Geary.Memory.Buffer buffer) {
-        // attempt to use UnownedBytesBuffer to avoid memcpying a potentially huge buffer only to
-        // free it when the encoding operation is completed
-        string base64;
-        Geary.Memory.UnownedBytesBuffer? unowned_bytes = buffer as Geary.Memory.UnownedBytesBuffer;
-        if (unowned_bytes != null)
-            base64 = Base64.encode(unowned_bytes.to_unowned_uint8_array());
-        else
-            base64 = Base64.encode(buffer.get_uint8_array());
-
-        return "data:%s;base64,%s".printf(mimetype, base64);
-    }
+        this.web_view.add_cid_resource(id, buffer);
 
-    // Called by Gdk.PixbufLoader when the image's size has been determined but not loaded yet ...
-    // this allows us to load the image scaled down, for better performance when manipulating and
-    // writing the data URI for WebKit
-    private static void on_inline_image_size_prepared(Gdk.PixbufLoader loader, int width, int height) {
-        // easier to use as local variable than have the const listed everywhere in the code
-        // IN ALL SCREAMING CAPS
-        int scale = MAX_INLINE_IMAGE_MAJOR_DIM;
-        
-        // Borrowed liberally from Shotwell's Dimensions.get_scaled() method
-        
-        // check for existing fit
-        if (width <= scale && height <= scale)
-            return;
-        
-        int adj_width, adj_height;
-        if ((width - scale) > (height - scale)) {
-            double aspect = (double) scale / (double) width;
-            
-            adj_width = scale;
-            adj_height = (int) Math.round((double) height * aspect);
-        } else {
-            double aspect = (double) scale / (double) height;
-            
-            adj_width = (int) Math.round((double) width * aspect);
-            adj_height = scale;
-        }
-        
-        loader.set_size(adj_width, adj_height);
+        return "<img alt=\"%s\" class=\"%s\" src=\"%s%s\" />".printf(
+            Geary.HTML.escape_markup(filename),
+            REPLACED_IMAGE_CLASS,
+            ClientWebView.CID_PREFIX,
+            Geary.HTML.escape_markup(id)
+        );
     }
 
     private void show_images(bool remember) {
@@ -938,18 +830,6 @@ public class ConversationMessage : Gtk.Grid {
     //     }
     // }
 
-    private ReplacedImage? get_replaced_image() {
-        ReplacedImage? image = null;
-        // string? replaced_id = this.context_menu_element.get_attribute(
-        //     "replaced-id"
-        // );
-        // this.context_menu_element = null;
-        // if (!Geary.String.is_empty(replaced_id)) {
-        //     image = replaced_images.get(replaced_id);
-        // }
-        return image;
-    }
-
     private inline void set_revealer(Gtk.Revealer revealer,
                                      bool expand,
                                      bool use_transition) {
diff --git a/ui/conversation-web-view.css b/ui/conversation-web-view.css
index 438b85e..5405c46 100644
--- a/ui/conversation-web-view.css
+++ b/ui/conversation-web-view.css
@@ -85,7 +85,7 @@ pre {
   .geary_replaced_inline_image {
     display: block;
     max-width: 100%;
-    margin-top: 1em;
+    margin: 1em 0;
   }
 
   /* Inline collapsable quote blocks */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]