From 732b67d8cb644a00baea7accfa76e2a3c1b448d0 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 11 Mar 2022 14:27:07 -0400 Subject: [PATCH] Allow injectable typefaces in image text editor. Co-authored-by: Rashad Sookram --- .../securesms/fonts/FontTypefaceProvider.kt | 31 +++++++++++++++++++ .../mediasend/AvatarSelectionActivity.java | 2 +- .../ImageEditorModelRenderMediaTransform.java | 3 +- .../securesms/mediasend/MediaTransform.java | 2 ++ .../mediasend/v2/MediaSelectionViewModel.kt | 2 +- .../scribbles/ImageEditorFragment.java | 10 ++++-- .../wallpaper/crop/WallpaperCropActivity.java | 6 ++-- .../crop/WallpaperCropViewModel.java | 4 ++- .../signal/imageeditor/app/MainActivity.java | 18 ++++++++++- .../imageeditor/core/ImageEditorView.java | 13 +++++--- .../imageeditor/core/RendererContext.java | 25 +++++++++++---- .../imageeditor/core/model/EditorModel.java | 8 ++--- .../core/renderers/MultiLineTextRenderer.java | 20 ++---------- 13 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/fonts/FontTypefaceProvider.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/fonts/FontTypefaceProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/fonts/FontTypefaceProvider.kt new file mode 100644 index 0000000000..d90bb5484a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/fonts/FontTypefaceProvider.kt @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.fonts + +import android.content.Context +import android.graphics.Typeface +import org.signal.imageeditor.core.Renderer +import org.signal.imageeditor.core.RendererContext +import org.thoughtcrime.securesms.util.FutureTaskListener +import java.util.Locale +import java.util.concurrent.ExecutionException + +/** + * RenderContext TypefaceProvider that provides typefaces using TextFont. + */ +object FontTypefaceProvider : RendererContext.TypefaceProvider { + override fun getSelectedTypeface(context: Context, renderer: Renderer, invalidate: RendererContext.Invalidate): Typeface { + return when (val fontResult = Fonts.resolveFont(context, Locale.getDefault(), TextFont.BOLD)) { + is Fonts.FontResult.Immediate -> fontResult.typeface + is Fonts.FontResult.Async -> { + fontResult.future.addListener(object : FutureTaskListener { + override fun onSuccess(result: Typeface?) { + invalidate.onInvalidate(renderer) + } + + override fun onFailure(exception: ExecutionException?) = Unit + }) + + fontResult.placeholder + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index 4dece8d264..e7fffbb99c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -15,8 +15,8 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.LiveData; -import org.thoughtcrime.securesms.R; import org.signal.imageeditor.core.model.EditorModel; +import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.profiles.AvatarHelper; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java index f2a738ec0c..f401c3adce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/ImageEditorModelRenderMediaTransform.java @@ -12,6 +12,7 @@ import androidx.annotation.WorkerThread; import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; import org.signal.imageeditor.core.model.EditorModel; +import org.thoughtcrime.securesms.fonts.FontTypefaceProvider; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; @@ -40,7 +41,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor public @NonNull Media transform(@NonNull Context context, @NonNull Media media) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Bitmap bitmap = modelToRender.render(context, size); + Bitmap bitmap = modelToRender.render(context, size, FontTypefaceProvider.INSTANCE); try { bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaTransform.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaTransform.java index 34a3e67ab9..4d3712af73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaTransform.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaTransform.java @@ -5,6 +5,8 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; +import org.signal.imageeditor.core.RendererContext; + public interface MediaTransform { @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt index df1f6ca4e9..b16b882815 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt @@ -278,7 +278,7 @@ class MediaSelectionViewModel( } fun send( - selectedContacts: List = emptyList(), + selectedContacts: List = emptyList() ): Maybe { return repository.send( store.state.selectedMedia, 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 bb1caf0a9e..eb0500766c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -8,7 +8,9 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; +import android.graphics.Typeface; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; @@ -31,6 +33,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.signal.imageeditor.core.RendererContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.ResizeAnimation; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -44,6 +47,7 @@ import org.signal.imageeditor.core.model.EditorModel; import org.signal.imageeditor.core.renderers.BezierDrawingRenderer; import org.signal.imageeditor.core.renderers.FaceBlurRenderer; import org.signal.imageeditor.core.renderers.MultiLineTextRenderer; +import org.thoughtcrime.securesms.fonts.FontTypefaceProvider; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations; @@ -218,6 +222,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu imageEditorHud = view.findViewById(R.id.scribble_hud); imageEditorView = view.findViewById(R.id.image_editor_view); + imageEditorView.setTypefaceProvider(FontTypefaceProvider.INSTANCE); + int width = getResources().getDisplayMetrics().widthPixels; int height = (int) ((16 / 9f) * width); imageEditorView.setMinimumHeight(height); @@ -552,7 +558,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu FaceDetector detector = new AndroidFaceDetector(); Point size = model.getOutputSizeMaxWidth(1000); - Bitmap render = model.render(ApplicationDependencies.getApplication(), size); + Bitmap render = model.render(ApplicationDependencies.getApplication(), size, FontTypefaceProvider.INSTANCE); try { return new FaceDetectionResult(detector.detect(render), new Point(render.getWidth(), render.getHeight()), inverseCropPosition); } finally { @@ -766,7 +772,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu @WorkerThread public @NonNull Uri renderToSingleUseBlob() { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Bitmap image = imageEditorView.getModel().render(requireContext()); + Bitmap image = imageEditorView.getModel().render(requireContext(), FontTypefaceProvider.INSTANCE); image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); image.recycle(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java index 2e5e13ff2d..2b839f9312 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java @@ -23,13 +23,13 @@ import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProviders; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.BaseActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.conversation.colors.ColorizerView; import org.signal.imageeditor.core.ImageEditorView; import org.signal.imageeditor.core.model.EditorElement; import org.signal.imageeditor.core.model.EditorModel; import org.signal.imageeditor.core.renderers.FaceBlurRenderer; +import org.thoughtcrime.securesms.BaseActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.conversation.colors.ColorizerView; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.scribbles.UriGlideRenderer; import org.thoughtcrime.securesms.util.AsynchronousCallback; diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java index 785dd2fcb3..0c186b948f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java @@ -15,7 +15,9 @@ import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; +import org.signal.imageeditor.core.RendererContext; import org.signal.imageeditor.core.model.EditorModel; +import org.thoughtcrime.securesms.fonts.FontTypefaceProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.AsynchronousCallback; @@ -49,7 +51,7 @@ final class WallpaperCropViewModel extends ViewModel { { SignalExecutors.BOUNDED.execute( () -> { - Bitmap bitmap = model.render(context, size); + Bitmap bitmap = model.render(context, size, FontTypefaceProvider.INSTANCE); try { ChatWallpaper chatWallpaper = repository.setWallPaper(BitmapUtil.toWebPByteArray(bitmap)); callback.onComplete(chatWallpaper); diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java index e94c3796c4..5bd54df022 100644 --- a/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java @@ -1,12 +1,15 @@ package org.signal.imageeditor.app; import android.Manifest; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Typeface; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; @@ -24,6 +27,8 @@ import androidx.core.content.ContextCompat; import org.signal.imageeditor.app.renderers.UriRenderer; import org.signal.imageeditor.app.renderers.UrlRenderer; import org.signal.imageeditor.core.ImageEditorView; +import org.signal.imageeditor.core.Renderer; +import org.signal.imageeditor.core.RendererContext; import org.signal.imageeditor.core.UndoRedoStackListener; import org.signal.imageeditor.core.model.EditorElement; import org.signal.imageeditor.core.model.EditorModel; @@ -43,6 +48,17 @@ public final class MainActivity extends AppCompatActivity { private ImageEditorView imageEditorView; private Menu menu; + private final RendererContext.TypefaceProvider typefaceProvider = (context, renderer, invalidate) -> { + if (Build.VERSION.SDK_INT < 26) { + return Typeface.create(Typeface.DEFAULT, Typeface.BOLD); + } else { + return new Typeface.Builder("") + .setFallback("sans-serif") + .setWeight(900) + .build(); + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -206,7 +222,7 @@ public final class MainActivity extends AppCompatActivity { new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0); } else { - Bitmap bitmap = imageEditorView.getModel().render(this); + Bitmap bitmap = imageEditorView.getModel().render(this, typefaceProvider); try { Uri uri = saveBmp(bitmap); diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java index 54201d1ad5..0bd0065e42 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java @@ -76,8 +76,9 @@ public final class ImageEditorView extends FrameLayout { private final RectF visibleViewPort = Bounds.newFullBounds(); private final RectF screen = new RectF(); - private TapListener tapListener; - private RendererContext rendererContext; + private TapListener tapListener; + private RendererContext rendererContext; + private RendererContext.TypefaceProvider typefaceProvider; @Nullable private EditSession editSession; @@ -145,10 +146,14 @@ public final class ImageEditorView extends FrameLayout { } } + public void setTypefaceProvider(@NonNull RendererContext.TypefaceProvider typefaceProvider) { + this.typefaceProvider = typefaceProvider; + } + @Override protected void onDraw(Canvas canvas) { - if (rendererContext == null || rendererContext.canvas != canvas) { - rendererContext = new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate); + if (rendererContext == null || rendererContext.canvas != canvas || rendererContext.typefaceProvider != typefaceProvider) { + rendererContext = new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate, typefaceProvider); } rendererContext.save(); try { diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/RendererContext.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/RendererContext.java index c7430f7b9b..d48a613b42 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/RendererContext.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/RendererContext.java @@ -6,6 +6,8 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -39,6 +41,9 @@ public final class RendererContext { @NonNull public final Invalidate invalidate; + @NonNull + public final TypefaceProvider typefaceProvider; + private boolean blockingLoad; private float fade = 1f; @@ -48,12 +53,13 @@ public final class RendererContext { 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; - this.canvasMatrix = new CanvasMatrix(canvas); - this.rendererReady = rendererReady; - this.invalidate = invalidate; + public RendererContext(@NonNull Context context, @NonNull Canvas canvas, @NonNull Ready rendererReady, @NonNull Invalidate invalidate, @NonNull TypefaceProvider typefaceProvider) { + this.context = context; + this.canvas = canvas; + this.canvasMatrix = new CanvasMatrix(canvas); + this.rendererReady = rendererReady; + this.invalidate = invalidate; + this.typefaceProvider = typefaceProvider; } public void setBlockingLoad(boolean blockingLoad) { @@ -126,6 +132,13 @@ public final class RendererContext { return maskPaint; } + /** + * Allows a RenderContext creator to specify which font to use for text on the fly. + */ + public interface TypefaceProvider { + @NonNull Typeface getSelectedTypeface(@NonNull Context context, @NonNull Renderer renderer, @NonNull Invalidate invalidate); + } + public interface Ready { Ready NULL = (renderer, cropMatrix, size) -> { diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java index af5c221941..976a81d66d 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java @@ -716,15 +716,15 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { * Blocking render of the model. */ @WorkerThread - public @NonNull Bitmap render(@NonNull Context context) { - return render(context, null); + public @NonNull Bitmap render(@NonNull Context context, @NonNull RendererContext.TypefaceProvider typefaceProvider) { + return render(context, null, typefaceProvider); } /** * Blocking render of the model. */ @WorkerThread - public @NonNull Bitmap render(@NonNull Context context, @Nullable Point size) { + public @NonNull Bitmap render(@NonNull Context context, @Nullable Point size, @NonNull RendererContext.TypefaceProvider typefaceProvider) { EditorElement image = editorElementHierarchy.getFlipRotate(); RectF cropRect = editorElementHierarchy.getCropRect(); Point outputSize = size != null ? size : getOutputSize(); @@ -732,7 +732,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { Bitmap bitmap = Bitmap.createBitmap(outputSize.x, outputSize.y, Bitmap.Config.ARGB_8888); try { Canvas canvas = new Canvas(bitmap); - RendererContext rendererContext = new RendererContext(context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL); + RendererContext rendererContext = new RendererContext(context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL, typefaceProvider); RectF bitmapArea = new RectF(); bitmapArea.right = bitmap.getWidth(); diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java index 2122b19929..077f92a9a0 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java @@ -6,8 +6,6 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Typeface; -import android.os.Build; import android.os.Parcel; import android.view.animation.Interpolator; @@ -70,11 +68,8 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen public MultiLineTextRenderer(@Nullable String text, @ColorInt int color, @NonNull Mode mode) { this.mode = mode; - Typeface typeface = getTypeface(); - modePaint.setAntiAlias(true); modePaint.setTextSize(100); - modePaint.setTypeface(typeface); setColorInternal(color); @@ -82,7 +77,6 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen paint.setAntiAlias(true); paint.setTextSize(100); - paint.setTypeface(typeface); textScale = paint.getTextSize() / regularTextSize; @@ -96,6 +90,9 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen public void render(@NonNull RendererContext rendererContext) { super.render(rendererContext); + paint.setTypeface(rendererContext.typefaceProvider.getSelectedTypeface(rendererContext.context, this, rendererContext.invalidate)); + modePaint.setTypeface(rendererContext.typefaceProvider.getSelectedTypeface(rendererContext.context, this, rendererContext.invalidate)); + float height = 0; float width = 0; for (Line line : lines) { @@ -507,17 +504,6 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen }; } - private static @NonNull Typeface getTypeface() { - if (Build.VERSION.SDK_INT < 26) { - return Typeface.create(Typeface.DEFAULT, Typeface.BOLD); - } else { - return new Typeface.Builder("") - .setFallback("sans-serif") - .setWeight(900) - .build(); - } - } - public enum Mode { REGULAR(0), HIGHLIGHT(1),