From 1dfac120dde7b279879c03a6efdff52939b4e80a Mon Sep 17 00:00:00 2001 From: M66B Date: Thu, 7 Apr 2022 12:17:34 +0200 Subject: [PATCH] Replaced AnnotatedSource by ImageSpanEx --- .../eu/faircode/email/ActivitySignature.java | 13 +- .../eu/faircode/email/AdapterMessage.java | 38 ++--- .../eu/faircode/email/EditTextCompose.java | 7 +- .../eu/faircode/email/FragmentAnswer.java | 13 +- .../eu/faircode/email/FragmentCompose.java | 40 ++--- .../main/java/eu/faircode/email/HtmlEx.java | 14 +- .../java/eu/faircode/email/HtmlHelper.java | 45 +----- .../java/eu/faircode/email/ImageHelper.java | 137 +++++++----------- .../java/eu/faircode/email/ImageSpanEx.java | 77 ++++++++++ 9 files changed, 211 insertions(+), 173 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/ImageSpanEx.java diff --git a/app/src/main/java/eu/faircode/email/ActivitySignature.java b/app/src/main/java/eu/faircode/email/ActivitySignature.java index 672161d0cf..3f90052f20 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySignature.java +++ b/app/src/main/java/eu/faircode/email/ActivitySignature.java @@ -54,6 +54,7 @@ import androidx.preference.PreferenceManager; import com.google.android.material.bottomnavigation.BottomNavigationView; import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; import java.util.Objects; @@ -272,12 +273,14 @@ public class ActivitySignature extends ActivityBase { else if (etText.isRaw()) etText.setText(html); else - etText.setText(HtmlHelper.fromHtml(html, new Html.ImageGetter() { + etText.setText(HtmlHelper.fromHtml(html, new HtmlHelper.ImageGetterEx() { @Override - public Drawable getDrawable(String source) { - if (source != null && source.startsWith("cid:")) - source = null; - return ImageHelper.decodeImage(ActivitySignature.this, -1, source, true, 0, 1.0f, etText); + public Drawable getDrawable(Element element) { + String source = element.attr("src"); + if (source.startsWith("cid:")) + element.attr("src", "cid:"); + return ImageHelper.decodeImage(ActivitySignature.this, + -1, element, true, 0, 1.0f, etText); } }, null, this)); loaded = true; diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 49f0de4bce..153c44958a 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -528,8 +528,13 @@ public class AdapterMessage extends RecyclerView.Adapter 0 && image[0].getSource() != null) { - ImageHelper.AnnotatedSource a = new ImageHelper.AnnotatedSource(image[0].getSource()); - Uri uri = Uri.parse(a.getSource()); + Uri uri = Uri.parse(image[0].getSource()); if ("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) { ripple(event); if (onOpenLink(uri, null, false)) @@ -613,15 +617,13 @@ public class AdapterMessage extends RecyclerView.Adapter 0) { - ImageHelper.AnnotatedSource a = new ImageHelper.AnnotatedSource(image[0].getSource()); - String source = a.getSource(); - if (!TextUtils.isEmpty(source)) { - if (!a.isTracking()) { - ripple(event); - onOpenImage(message.id, source); - } + if (image[0] instanceof ImageSpanEx && + ((ImageSpanEx) image[0]).getTracking()) return true; - } + + ripple(event); + onOpenImage(message.id, image[0].getSource()); + return true; } DynamicDrawableSpan[] ddss = buffer.getSpans(off, off, DynamicDrawableSpan.class); @@ -2815,10 +2817,11 @@ public class AdapterMessage extends RecyclerView.Adapter= Build.VERSION_CODES.P) { if (drawable instanceof AnimatedImageDrawable) @@ -5664,13 +5667,12 @@ public class AdapterMessage extends RecyclerView.Adapter"); + out.append("\""); + + if (style[j] instanceof ImageSpanEx) { + ImageSpanEx img = (ImageSpanEx) style[j]; + int w = img.getWidth(); + if (w > 0) + out.append(" width=\"").append(w).append("\""); + int h = img.getHeight(); + if (h > 0) + out.append(" height=\"").append(h).append("\""); + } + + out.append(">"); // Don't output the dummy character underlying the image. i = next; diff --git a/app/src/main/java/eu/faircode/email/HtmlHelper.java b/app/src/main/java/eu/faircode/email/HtmlHelper.java index 38a8962adb..d02b47cc33 100644 --- a/app/src/main/java/eu/faircode/email/HtmlHelper.java +++ b/app/src/main/java/eu/faircode/email/HtmlHelper.java @@ -110,7 +110,6 @@ import java.net.URI; import java.text.DateFormat; import java.text.ParseException; import java.text.ParsePosition; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1371,38 +1370,6 @@ public class HtmlHelper { .attr("x-font-size-abs", Integer.toString(textSizeSmall)); img.appendChild(a); } - - // Annotate source with width and height - if (!TextUtils.isEmpty(src)) { - int width = 0; - int height = 0; - - // Relative sizes (%) = use image size - - String awidth = img.attr("width").replace(" ", ""); - for (int i = 0; i < awidth.length(); i++) - if (Character.isDigit(awidth.charAt(i))) - width = width * 10 + (byte) awidth.charAt(i) - (byte) '0'; - else { - width = 0; - break; - } - - String aheight = img.attr("height").replace(" ", ""); - for (int i = 0; i < aheight.length(); i++) - if (Character.isDigit(aheight.charAt(i))) - height = height * 10 + (byte) aheight.charAt(i) - (byte) '0'; - else { - height = 0; - break; - } - - if (width != 0 || height != 0) { - ImageHelper.AnnotatedSource a = new ImageHelper.AnnotatedSource( - src, width, height, !TextUtils.isEmpty(tracking)); - img.attr("src", a.getAnnotated()); - } - } } // Selective new lines @@ -2928,7 +2895,7 @@ public class HtmlHelper { static SpannableStringBuilder fromDocument( Context context, @NonNull Document document, - @Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) { + @Nullable ImageGetterEx imageGetter, @Nullable Html.TagHandler tagHandler) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean debug = prefs.getBoolean("debug", false); boolean monospaced_pre = prefs.getBoolean("monospaced_pre", false); @@ -3350,9 +3317,9 @@ public class HtmlHelper { if (!TextUtils.isEmpty(src)) { Drawable d = (imageGetter == null ? context.getDrawable(R.drawable.twotone_broken_image_24) - : imageGetter.getDrawable(src)); + : imageGetter.getDrawable(element)); ssb.insert(start, "\uFFFC"); // Object replacement character - setSpan(ssb, new ImageSpan(d, src), start, start + 1); + setSpan(ssb, new ImageSpanEx(d, element), start, start + 1); } break; case "li": @@ -3608,7 +3575,7 @@ public class HtmlHelper { return fromHtml(html, null, null, context); } - static Spanned fromHtml(@NonNull String html, @Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler, Context context) { + static Spanned fromHtml(@NonNull String html, @Nullable ImageGetterEx imageGetter, @Nullable Html.TagHandler tagHandler, Context context) { Document document = JsoupEx.parse(html); return fromDocument(context, document, imageGetter, tagHandler); } @@ -3716,4 +3683,8 @@ public class HtmlHelper { spanned.getSpanFlags(spans[i])); return reverse; } + + interface ImageGetterEx { + Drawable getDrawable(Element element); + } } diff --git a/app/src/main/java/eu/faircode/email/ImageHelper.java b/app/src/main/java/eu/faircode/email/ImageHelper.java index db38b8f0f0..d6cb750e2a 100644 --- a/app/src/main/java/eu/faircode/email/ImageHelper.java +++ b/app/src/main/java/eu/faircode/email/ImageHelper.java @@ -59,6 +59,8 @@ import androidx.preference.PreferenceManager; import com.caverock.androidsvg.SVG; +import org.jsoup.nodes.Element; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -302,6 +304,20 @@ class ImageHelper { } static Drawable decodeImage(final Context context, final long id, String source, boolean show, int zoom, final float scale, final TextView view) { + return decodeImage(context, id, source, 0, 0, false, show, zoom, scale, view); + } + + static Drawable decodeImage(final Context context, final long id, Element img, boolean show, int zoom, final float scale, final TextView view) { + String source = img.attr("src"); + Integer w = Helper.parseInt(img.attr("width")); + Integer h = Helper.parseInt(img.attr("height")); + boolean tracking = !TextUtils.isEmpty(img.attr("x-tracking")); + return decodeImage(context, id, source, w == null ? 0 : w, h == null ? 0 : h, tracking, show, zoom, scale, view); + } + + private static Drawable decodeImage(final Context context, final long id, + String source, final int aw, final int ah, boolean tracking, + boolean show, int zoom, final float scale, final TextView view) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean inline = prefs.getBoolean("inline_images", false); @@ -309,24 +325,22 @@ class ImageHelper { final Resources res = context.getResources(); try { - final AnnotatedSource a = new AnnotatedSource(source); - - if (TextUtils.isEmpty(a.source)) { + if (TextUtils.isEmpty(source)) { Drawable d = context.getDrawable(R.drawable.twotone_broken_image_24); d.setBounds(0, 0, px, px); return d; } - boolean embedded = a.source.startsWith("cid:"); - boolean data = a.source.startsWith("data:"); - boolean content = a.source.startsWith("content:"); + boolean embedded = source.startsWith("cid:"); + boolean data = source.startsWith("data:"); + boolean content = source.startsWith("content:"); - Log.d("Image show=" + show + " inline=" + inline + " source=" + a.source); + Log.d("Image show=" + show + " inline=" + inline + " source=" + source); // Embedded images if (embedded && (show || inline)) { DB db = DB.getInstance(context); - String cid = "<" + a.source.substring(4) + ">"; + String cid = "<" + source.substring(4) + ">"; EntityAttachment attachment = db.attachment().getAttachment(id, cid); if (attachment == null) { Log.i("Image not found CID=" + cid); @@ -347,7 +361,7 @@ class ImageHelper { attachment.getMimeType(), scaleToPixels); if (view != null) - fitDrawable(d, a, scale, view); + fitDrawable(d, aw, ah, scale, view); return d; } catch (IOException ex) { Log.w(ex); @@ -369,7 +383,7 @@ class ImageHelper { Drawable d = new BitmapDrawable(res, bm); d.setBounds(0, 0, bm.getWidth(), bm.getHeight()); if (view != null) - fitDrawable(d, a, scale, view); + fitDrawable(d, aw, ah, scale, view); return d; } } @@ -377,11 +391,11 @@ class ImageHelper { } // Data URI - if (data && (show || inline || a.tracking)) + if (data && (show || inline || tracking)) try { int scaleToPixels = res.getDisplayMetrics().widthPixels; - String mimeType = getDataUriType(a.source); - ByteArrayInputStream bis = getDataUriStream(a.source); + String mimeType = getDataUriType(source); + ByteArrayInputStream bis = getDataUriStream(source); Bitmap bm = getScaledBitmap(bis, "data:" + mimeType, mimeType, scaleToPixels); if (bm == null) throw new IllegalArgumentException("decode byte array failed"); @@ -390,7 +404,7 @@ class ImageHelper { d.setBounds(0, 0, bm.getWidth(), bm.getHeight()); if (view != null) - fitDrawable(d, a, scale, view); + fitDrawable(d, aw, ah, scale, view); return d; } catch (IllegalArgumentException ex) { Log.i(ex); @@ -401,7 +415,7 @@ class ImageHelper { if (content && (show || inline)) try { - Uri uri = Uri.parse(a.source); + Uri uri = Uri.parse(source); Log.i("Loading image source=" + uri); Bitmap bm; @@ -409,16 +423,16 @@ class ImageHelper { try (InputStream is = context.getContentResolver().openInputStream(uri)) { if (is == null) throw new FileNotFoundException(uri.toString()); - bm = getScaledBitmap(is, a.source, null, scaleToPixels); + bm = getScaledBitmap(is, source, null, scaleToPixels); if (bm == null) - throw new FileNotFoundException(a.source); + throw new FileNotFoundException(source); } Drawable d = new BitmapDrawable(res, bm); d.setBounds(0, 0, bm.getWidth(), bm.getHeight()); if (view != null) - fitDrawable(d, a, scale, view); + fitDrawable(d, aw, ah, scale, view); return d; } catch (Throwable ex) { // FileNotFound, Security @@ -437,7 +451,7 @@ class ImageHelper { } // Check cache - Drawable cached = getCachedImage(context, id, a.source); + Drawable cached = getCachedImage(context, id, source); if (cached != null || view == null) { if (view == null) if (cached == null) { @@ -447,7 +461,7 @@ class ImageHelper { } else return cached; else - fitDrawable(cached, a, scale, view); + fitDrawable(cached, aw, ah, scale, view); return cached; } @@ -486,17 +500,17 @@ class ImageHelper { public void run() { try { // Check cache again - Drawable cached = getCachedImage(context, id, a.source); + Drawable cached = getCachedImage(context, id, source); if (cached != null) { - fitDrawable(cached, a, scale, view); - post(cached, a.source); + fitDrawable(cached, aw, ah, scale, view); + post(cached, source); return; } // Download image - Drawable d = downloadImage(context, id, a.source, null); - fitDrawable(d, a, scale, view); - post(d, a.source); + Drawable d = downloadImage(context, id, source, null); + fitDrawable(d, aw, ah, scale, view); + post(d, source); } catch (Throwable ex) { // Show broken icon Log.i(ex); @@ -505,7 +519,7 @@ class ImageHelper { : R.drawable.twotone_broken_image_24); Drawable d = context.getDrawable(resid); d.setBounds(0, 0, px, px); - post(d, a.source); + post(d, source); } } @@ -547,7 +561,7 @@ class ImageHelper { private static Map drawableBounds = new WeakHashMap<>(); - static void fitDrawable(final Drawable d, final AnnotatedSource a, float scale, final View view) { + static void fitDrawable(final Drawable d, int aw, int ah, float scale, final View view) { synchronized (drawableBounds) { if (drawableBounds.containsKey(d)) d.setBounds(drawableBounds.get(d)); @@ -559,15 +573,15 @@ class ImageHelper { int w = Math.round(Helper.dp2pixels(view.getContext(), bounds.width()) * scale); int h = Math.round(Helper.dp2pixels(view.getContext(), bounds.height()) * scale); - if (a.width == 0 && a.height != 0) - a.width = Math.round(a.height * w / (float) h); - if (a.height == 0 && a.width != 0) - a.height = Math.round(a.width * h / (float) w); + if (aw == 0 && ah != 0) + aw = Math.round(ah * w / (float) h); + if (ah == 0 && aw != 0) + ah = Math.round(aw * h / (float) w); - if (a.width != 0 && a.height != 0) { - boolean swap = ((w > h) != (a.width > a.height)) && false; - w = Math.round(Helper.dp2pixels(view.getContext(), swap ? a.height : a.width) * scale); - h = Math.round(Helper.dp2pixels(view.getContext(), swap ? a.width : a.height) * scale); + if (aw != 0 && ah != 0) { + boolean swap = ((w > h) != (aw > ah)) && false; + w = Math.round(Helper.dp2pixels(view.getContext(), swap ? ah : aw) * scale); + h = Math.round(Helper.dp2pixels(view.getContext(), swap ? aw : ah) * scale); } float width = view.getContext().getResources().getDisplayMetrics().widthPixels; @@ -931,55 +945,4 @@ class ImageHelper { return (lum / n); } - - static class AnnotatedSource { - private String source; - private int width = 0; - private int height = 0; - private boolean tracking = false; - - // Encapsulate some ugliness - - AnnotatedSource(String source) { - this.source = source; - - if (source != null && source.endsWith("###")) { - int pos = source.substring(0, source.length() - 3).lastIndexOf("###"); - if (pos > 0) { - int x = source.indexOf("x", pos + 3); - int s = source.indexOf(":", pos + 3); - if (x > 0 && s > x) - try { - this.width = Integer.parseInt(source.substring(pos + 3, x)); - this.height = Integer.parseInt(source.substring(x + 1, s)); - this.tracking = Boolean.parseBoolean(source.substring(s + 1, source.length() - 3)); - this.source = source.substring(0, pos); - } catch (NumberFormatException ex) { - Log.e(ex); - } - } - } - } - - AnnotatedSource(String source, int width, int height, boolean tracking) { - this.source = source; - this.width = width; - this.height = height; - this.tracking = tracking; - } - - public String getSource() { - return this.source; - } - - public boolean isTracking() { - return this.tracking; - } - - String getAnnotated() { - return (width == 0 && height == 0 - ? source - : source + "###" + width + "x" + height + ":" + tracking + "###"); - } - } } diff --git a/app/src/main/java/eu/faircode/email/ImageSpanEx.java b/app/src/main/java/eu/faircode/email/ImageSpanEx.java new file mode 100644 index 0000000000..3333c1e746 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/ImageSpanEx.java @@ -0,0 +1,77 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2022 by Marcel Bokhorst (M66B) +*/ + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.text.style.ImageSpan; + +import androidx.annotation.NonNull; + +import org.jsoup.nodes.Element; + +public class ImageSpanEx extends ImageSpan { + private final int width; + private final int height; + private final boolean tracking; + + public ImageSpanEx(@NonNull Drawable drawable, @NonNull Element img) { + super(drawable, img.attr("src")); + + int _width = 0; + int _height = 0; + + // Relative sizes (%) = use image size + + String awidth = img.attr("width").replace(" ", ""); + for (int i = 0; i < awidth.length(); i++) + if (Character.isDigit(awidth.charAt(i))) + _width = _width * 10 + (byte) awidth.charAt(i) - (byte) '0'; + else { + _width = 0; + break; + } + + String aheight = img.attr("height").replace(" ", ""); + for (int i = 0; i < aheight.length(); i++) + if (Character.isDigit(aheight.charAt(i))) + _height = _height * 10 + (byte) aheight.charAt(i) - (byte) '0'; + else { + _height = 0; + break; + } + + this.width = _width; + this.height = _height; + this.tracking = !TextUtils.isEmpty(img.attr("x-tracking")); + } + + public int getWidth() { + return this.width; + } + + public int getHeight() { + return this.height; + } + + public boolean getTracking() { + return this.tracking; + } +}