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.
This commit is contained in:
parent
6515db2a18
commit
1d1229b623
4 changed files with 30 additions and 149 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
string id = content_id;
|
||||
if (id == null) {
|
||||
id = REPLACED_CID_TEMPLATE.printf(this.next_replaced_buffer_number++);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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(
|
||||
|
||||
this.web_view.add_cid_resource(id, buffer);
|
||||
|
||||
return "<img alt=\"%s\" class=\"%s\" src=\"%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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ pre {
|
|||
.geary_replaced_inline_image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
/* Inline collapsable quote blocks */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue