diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java index 61802bd96b1..77e4dfedf72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java @@ -109,7 +109,7 @@ public void persist() { persistedFlags = flags; } - void reset() { + public void reset() { restoreState(persistedFlags); } 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 96272b624a6..8433dce10e6 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 @@ -604,6 +604,21 @@ private Point getOutputSize() { return new Point(width, height); } + @NonNull + public Point getOutputSizeMaxWidth(int maxDimension) { + PointF outputSize = editorElementHierarchy.getOutputSize(size); + + int width = Math.min(maxDimension, (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x)); + int height = (int) (width * outputSize.y / outputSize.x); + + if (height > maxDimension) { + height = maxDimension; + width = (int) (height * outputSize.x / outputSize.y); + } + + return new Point(width, height); + } + @Override public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) { @@ -819,4 +834,16 @@ public boolean isCropping() { return editorElementHierarchy.getCropEditorElement().getFlags().isVisible(); } + /** + * Returns a matrix that maps bounds to the crop area. + */ + public Matrix getInverseCropPosition() { + Matrix matrix = new Matrix(); + matrix.set(findRelativeMatrix(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement())); + matrix.postConcat(editorElementHierarchy.getFlipRotate().getLocalMatrix()); + + Matrix positionRelativeToCrop = new Matrix(); + matrix.invert(positionRelativeToCrop); + return positionRelativeToCrop; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java index bf491ddd064..aad9eed7efc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java @@ -1,28 +1,50 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.graphics.Matrix; +import android.graphics.RectF; import android.os.Parcel; import androidx.annotation.NonNull; import java.util.UUID; -final class ParcelUtils { +public final class ParcelUtils { private ParcelUtils() { } - static void writeMatrix(@NonNull Parcel dest, @NonNull Matrix matrix) { + public static void writeMatrix(@NonNull Parcel dest, @NonNull Matrix matrix) { float[] values = new float[9]; matrix.getValues(values); dest.writeFloatArray(values); } - static void readMatrix(@NonNull Matrix matrix, @NonNull Parcel in) { + public static void readMatrix(@NonNull Matrix matrix, @NonNull Parcel in) { float[] values = new float[9]; in.readFloatArray(values); matrix.setValues(values); } + public static @NonNull Matrix readMatrix(@NonNull Parcel in) { + Matrix matrix = new Matrix(); + readMatrix(matrix, in); + return matrix; + } + + public static void writeRect(@NonNull Parcel dest, @NonNull RectF rect) { + dest.writeFloat(rect.left); + dest.writeFloat(rect.top); + dest.writeFloat(rect.right); + dest.writeFloat(rect.bottom); + } + + public static @NonNull RectF readRectF(@NonNull Parcel in) { + float left = in.readFloat(); + float top = in.readFloat(); + float right = in.readFloat(); + float bottom = in.readFloat(); + return new RectF(left, top, right, bottom); + } + static UUID readUUID(@NonNull Parcel in) { return new UUID(in.readLong(), in.readLong()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java index 101c8edf96a..38ace8648d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java @@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.imageeditor.model.ParcelUtils; /** * A rectangle that will be rendered on the blur mask layer. Intended for blurring faces. @@ -20,27 +20,24 @@ public class FaceBlurRenderer implements Renderer { private static final int CORNER_RADIUS = 0; private final RectF faceRect; - private final Point imageDimensions; - private final Matrix scaleMatrix; + private final Matrix imageProjectionMatrix; - public FaceBlurRenderer(@NonNull RectF faceRect, @NonNull Matrix matrix) { - this.faceRect = faceRect; - this.imageDimensions = new Point(0, 0); - this.scaleMatrix = matrix; + private FaceBlurRenderer(@NonNull RectF faceRect, @NonNull Matrix imageProjectionMatrix) { + this.faceRect = faceRect; + this.imageProjectionMatrix = imageProjectionMatrix; } public FaceBlurRenderer(@NonNull RectF faceRect, @NonNull Point imageDimensions) { - this.faceRect = faceRect; - this.imageDimensions = imageDimensions; - this.scaleMatrix = new Matrix(); + this.faceRect = faceRect; + this.imageProjectionMatrix = new Matrix(); - scaleMatrix.setRectToRect(new RectF(0, 0, this.imageDimensions.x, this.imageDimensions.y), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + this.imageProjectionMatrix.setRectToRect(new RectF(0, 0, imageDimensions.x, imageDimensions.y), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.FILL); } @Override public void render(@NonNull RendererContext rendererContext) { rendererContext.canvas.save(); - rendererContext.canvas.concat(scaleMatrix); + rendererContext.canvas.concat(imageProjectionMatrix); rendererContext.canvas.drawRoundRect(faceRect, CORNER_RADIUS, CORNER_RADIUS, rendererContext.getMaskPaint()); rendererContext.canvas.restore(); } @@ -57,25 +54,17 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeFloat(faceRect.left); - dest.writeFloat(faceRect.top); - dest.writeFloat(faceRect.right); - dest.writeFloat(faceRect.bottom); - dest.writeInt(imageDimensions.x); - dest.writeInt(imageDimensions.y); + ParcelUtils.writeMatrix(dest, imageProjectionMatrix); + ParcelUtils.writeRect(dest, faceRect); } public static final Creator CREATOR = new Creator() { @Override public FaceBlurRenderer createFromParcel(Parcel in) { - float left = in.readFloat(); - float top = in.readFloat(); - float right = in.readFloat(); - float bottom = in.readFloat(); - int x = in.readInt(); - int y = in.readInt(); - - return new FaceBlurRenderer(new RectF(left, top, right, bottom), new Point(x, y)); + Matrix imageProjection = ParcelUtils.readMatrix(in); + RectF faceRect = ParcelUtils.readRectF (in); + + return new FaceBlurRenderer(faceRect, imageProjection); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index c47ebc6cad6..4c193bf9d85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -3,6 +3,7 @@ import android.Manifest; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; @@ -19,14 +20,14 @@ import androidx.fragment.app.Fragment; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.TooltipPopup; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.ImageEditorView; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; -import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; import org.thoughtcrime.securesms.imageeditor.renderers.FaceBlurRenderer; +import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; @@ -344,30 +345,51 @@ public void onColorChange(int color) { @Override public void onBlurFacesToggled(boolean enabled) { - if (!enabled) { - imageEditorView.getModel().clearFaceRenderers(); + EditorModel model = imageEditorView.getModel(); + EditorElement mainImage = model.getMainImage(); + if (mainImage == null) { return; } - if (cachedFaceDetection != null && cachedFaceDetection.first().equals(getUri())) { - renderFaceBlurs(cachedFaceDetection.second()); + if (!enabled) { + model.clearFaceRenderers(); return; - } else if (cachedFaceDetection != null && !cachedFaceDetection.first().equals(getUri())) { - cachedFaceDetection = null; } - AlertDialog progress = SimpleProgressDialog.show(requireContext()); - - SimpleTask.run(() -> { - Bitmap bitmap = ((UriGlideRenderer) imageEditorView.getModel().getMainImage().getRenderer()).getBitmap(); + Matrix inverseCropPosition = model.getInverseCropPosition(); - if (bitmap != null) { - FaceDetector detector = new FirebaseFaceDetector(); - return new FaceDetectionResult(detector.detect(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight())); + if (cachedFaceDetection != null) { + if (cachedFaceDetection.first().equals(getUri()) && cachedFaceDetection.second().position.equals(inverseCropPosition)) { + renderFaceBlurs(cachedFaceDetection.second()); + return; } else { - return new FaceDetectionResult(Collections.emptyList(), new Point(0, 0)); + cachedFaceDetection = null; } + } + + AlertDialog progress = SimpleProgressDialog.show(requireContext()); + mainImage.getFlags().setChildrenVisible(false); + + SimpleTask.run(getLifecycle(), () -> { + if (mainImage.getRenderer() != null) { + Bitmap bitmap = ((UriGlideRenderer) mainImage.getRenderer()).getBitmap(); + if (bitmap != null) { + FaceDetector detector = new FirebaseFaceDetector(); + + Point size = model.getOutputSizeMaxWidth(1000); + Bitmap render = model.render(ApplicationDependencies.getApplication(), size); + try { + return new FaceDetectionResult(detector.detect(render), new Point(render.getWidth(), render.getHeight()), inverseCropPosition); + } finally { + render.recycle(); + mainImage.getFlags().reset(); + } + } + } + + return new FaceDetectionResult(Collections.emptyList(), new Point(0, 0), new Matrix()); }, result -> { + mainImage.getFlags().reset(); renderFaceBlurs(result); progress.dismiss(); }); @@ -469,7 +491,9 @@ private void renderFaceBlurs(@NonNull FaceDetectionResult result) { for (RectF face : faces) { FaceBlurRenderer faceBlurRenderer = new FaceBlurRenderer(face, size); - imageEditorView.getModel().addElementWithoutPushUndo(new EditorElement(faceBlurRenderer, EditorModel.Z_MASK)); + EditorElement element = new EditorElement(faceBlurRenderer, EditorModel.Z_MASK); + element.getLocalMatrix().set(result.position); + imageEditorView.getModel().addElementWithoutPushUndo(element); } imageEditorView.invalidate(); @@ -534,10 +558,12 @@ public interface Controller { private static class FaceDetectionResult { private final List rects; private final Point imageSize; + private final Matrix position; - private FaceDetectionResult(@NonNull List rects, @NonNull Point imageSize) { + private FaceDetectionResult(@NonNull List rects, @NonNull Point imageSize, @NonNull Matrix position) { this.rects = rects; this.imageSize = imageSize; + this.position = new Matrix(position); } } }