From 514048171bf6a3530fbdf48e9d51b3cc135f2783 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Wed, 3 Jun 2020 03:30:24 -0300 Subject: [PATCH] Add Image Editor support for blur mask layer. --- .../imageeditor/RendererContext.java | 25 +++++++ .../imageeditor/model/EditorElement.java | 11 ++- .../imageeditor/model/EditorModel.java | 1 + .../renderers/BezierDrawingRenderer.java | 2 + .../securesms/scribbles/UriGlideRenderer.java | 67 ++++++++++++++++++- 5 files changed, 101 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java index 0883d6fcd1..4b510e6d03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java @@ -3,11 +3,17 @@ package org.thoughtcrime.securesms.imageeditor; import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.imageeditor.model.EditorElement; + +import java.util.Collections; +import java.util.List; + /** * Contains all of the information required for a {@link Renderer} to do its job. *

@@ -38,6 +44,9 @@ public final class RendererContext { private boolean isEditing = true; + private List children = Collections.emptyList(); + private Paint maskPaint; + public RendererContext(@NonNull Context context, @NonNull Canvas canvas, @NonNull Ready rendererReady, @NonNull Invalidate invalidate) { this.context = context; this.canvas = canvas; @@ -100,6 +109,22 @@ public final class RendererContext { canvasMatrix.getCurrent(into); } + public void setChildren(@NonNull List children) { + this.children = children; + } + + public @NonNull List getChildren() { + return children; + } + + public void setMaskPaint(@Nullable Paint maskPaint) { + this.maskPaint = maskPaint; + } + + public @Nullable Paint getMaskPaint() { + return maskPaint; + } + public interface Ready { Ready NULL = (renderer, cropMatrix, size) -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java index cb30c3106f..26d10d8556 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java @@ -97,7 +97,7 @@ public final class EditorElement implements Parcelable { * * @param rendererContext Canvas to draw on to. */ - void draw(@NonNull RendererContext rendererContext) { + public void draw(@NonNull RendererContext rendererContext) { if (!flags.isVisible() && !flags.isChildrenVisible()) return; rendererContext.save(); @@ -113,6 +113,7 @@ public final class EditorElement implements Parcelable { float alpha = alphaAnimation.getValue(); if (alpha > 0) { rendererContext.setFade(alpha); + rendererContext.setChildren(children); drawSelf(rendererContext); rendererContext.setFade(1f); } @@ -133,7 +134,9 @@ public final class EditorElement implements Parcelable { private static void drawChildren(@NonNull List children, @NonNull RendererContext rendererContext) { for (EditorElement element : children) { - element.draw(rendererContext); + if (element.zOrder >= 0) { + element.draw(rendererContext); + } } } @@ -252,6 +255,10 @@ public final class EditorElement implements Parcelable { animationMatrix = AnimationMatrix.singlePulse(scale, invalidate); } + public int getZOrder() { + return zOrder; + } + public interface PerElementFunction { void apply(EditorElement element); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index 79cca5babc..7a95f9831f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -35,6 +35,7 @@ import java.util.UUID; */ public final class EditorModel implements Parcelable, RendererContext.Ready { + public static final int Z_MASK = -1; public static final int Z_DRAWING = 0; public static final int Z_STICKERS = 0; public static final int Z_TEXT = 1; diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java index c69cb452e1..6617508b5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java @@ -98,6 +98,8 @@ public final class BezierDrawingRenderer extends InvalidateableRenderer implemen int alpha = paint.getAlpha(); paint.setAlpha(rendererContext.getAlpha(alpha)); + paint.setXfermode(rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint().getXfermode() : null); + bezierLine.draw(canvas, paint); paint.setAlpha(alpha); diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java index 37f1a206c7..faf51e34b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java @@ -2,13 +2,20 @@ package org.thoughtcrime.securesms.scribbles; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Parcel; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -20,6 +27,8 @@ import com.bumptech.glide.request.transition.Transition; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; +import org.thoughtcrime.securesms.imageeditor.model.EditorElement; +import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequest; @@ -43,8 +52,10 @@ final class UriGlideRenderer implements Renderer { private final int maxWidth; private final int maxHeight; - @Nullable - private Bitmap bitmap; + @Nullable private Bitmap bitmap; + @Nullable private Bitmap blurredBitmap; + @Nullable private Paint blurPaint; + @Nullable private BlurMaskFilter blurMaskFilter; UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) { this.imageUri = imageUri; @@ -94,17 +105,52 @@ final class UriGlideRenderer implements Renderer { int alpha = paint.getAlpha(); paint.setAlpha(rendererContext.getAlpha(alpha)); - rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint); + rendererContext.canvas.drawBitmap(bitmap, 0, 0, rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint() : paint); paint.setAlpha(alpha); rendererContext.restore(); + + renderBlurOverlay(rendererContext); } else if (rendererContext.isBlockingLoad()) { // If failed to load, we draw a black out, in case image was sticker positioned to cover private info. rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint); } } + private void renderBlurOverlay(RendererContext rendererContext) { + boolean renderMask = false; + + for (EditorElement child : rendererContext.getChildren()) { + if (child.getZOrder() == EditorModel.Z_MASK) { + renderMask = true; + if (blurMaskFilter == null) { + blurMaskFilter = new BlurMaskFilter(4, BlurMaskFilter.Blur.NORMAL); // This blurs edges of the mask shapes + } + if (blurPaint == null) { + blurPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + blurPaint.setMaskFilter(blurMaskFilter); + } + blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + rendererContext.setMaskPaint(blurPaint); + child.draw(rendererContext); + } + } + + if (renderMask) { + rendererContext.save(); + rendererContext.canvasMatrix.concat(imageProjectionMatrix); + + blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); + blurPaint.setMaskFilter(null); + if (blurredBitmap == null) blurredBitmap = blur(bitmap, rendererContext.context); + rendererContext.canvas.drawBitmap(blurredBitmap, 0, 0, blurPaint); + blurPaint.setXfermode(null); + + rendererContext.restore(); + } + } + private GlideRequest getBitmapGlideRequest(@NonNull Context context, boolean preview) { int width = this.maxWidth; int height = this.maxHeight; @@ -177,6 +223,21 @@ final class UriGlideRenderer implements Renderer { return matrix; } + private static @NonNull Bitmap blur(@NonNull Bitmap bitmap, @NonNull Context context) { + RenderScript rs = RenderScript.create(context); + Allocation input = Allocation.createFromBitmap(rs, bitmap); + Allocation output = Allocation.createTyped (rs, input.getType()); + ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + + script.setRadius(25f); + script.setInput(input); + script.forEach(output); + + Bitmap blurred = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + output.copyTo(blurred); + return blurred; + } + public static final Creator CREATOR = new Creator() { @Override public UriGlideRenderer createFromParcel(Parcel in) {