Do not allow emoji in image editing if device doesn't support it.
This commit is contained in:
parent
3328e43a40
commit
d409278dd5
10 changed files with 136 additions and 1 deletions
|
@ -18,6 +18,7 @@ import java.util.regex.Pattern;
|
|||
|
||||
public final class EmojiUtil {
|
||||
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
|
||||
private static final String EMOJI_REGEX = "[^\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\p{Cf}\\p{Cs}\\s]";
|
||||
|
||||
private EmojiUtil() {}
|
||||
|
||||
|
@ -84,4 +85,12 @@ public final class EmojiUtil {
|
|||
|
||||
return (candidates != null && candidates.size() > 0) || EMOJI_PATTERN.matcher(text).matches();
|
||||
}
|
||||
|
||||
public static String stripEmoji(@Nullable String text) {
|
||||
if (text == null) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.replaceAll(EMOJI_REGEX, "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,12 @@ import com.bumptech.glide.request.RequestListener;
|
|||
import com.bumptech.glide.request.target.Target;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.FontUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.imageeditor.core.Bounds;
|
||||
import org.signal.imageeditor.core.ColorableRenderer;
|
||||
import org.signal.imageeditor.core.HiddenEditText;
|
||||
import org.signal.imageeditor.core.ImageEditorView;
|
||||
import org.signal.imageeditor.core.Renderer;
|
||||
import org.signal.imageeditor.core.SelectableRenderer;
|
||||
|
@ -44,6 +46,7 @@ import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
|
|||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.fonts.FontTypefaceProvider;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
@ -78,6 +81,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||
|
||||
private static final String TAG = Log.tag(ImageEditorFragment.class);
|
||||
|
||||
public static final boolean CAN_RENDER_EMOJI = FontUtil.canRenderEmojiAtFontSize(1024);
|
||||
|
||||
private static final float PORTRAIT_ASPECT_RATIO = 9 / 16f;
|
||||
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
|
@ -220,6 +225,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||
imageEditorView = view.findViewById(R.id.image_editor_view);
|
||||
|
||||
imageEditorView.setTypefaceProvider(new FontTypefaceProvider());
|
||||
if (!CAN_RENDER_EMOJI) {
|
||||
imageEditorView.addTextInputFilter(new RemoveEmojiTextFilter());
|
||||
}
|
||||
|
||||
int width = getResources().getDisplayMetrics().widthPixels;
|
||||
int height = (int) ((16 / 9f) * width);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.imageeditor.core.HiddenEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
|
||||
class RemoveEmojiTextFilter implements HiddenEditText.TextFilter {
|
||||
@Override
|
||||
public String filter(@NonNull String text) {
|
||||
return EmojiUtil.stripEmoji(text);
|
||||
}
|
||||
}
|
|
@ -33,6 +33,9 @@ class TextEntryDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_im
|
|||
controller = requireListener()
|
||||
|
||||
hiddenTextEntry = HiddenEditText(requireContext())
|
||||
if (!ImageEditorFragment.CAN_RENDER_EMOJI) {
|
||||
hiddenTextEntry.addTextFilter(RemoveEmojiTextFilter())
|
||||
}
|
||||
(view as ViewGroup).addView(hiddenTextEntry)
|
||||
|
||||
view.setOnClickListener {
|
||||
|
|
40
core-util/src/main/java/org/signal/core/util/FontUtil.kt
Normal file
40
core-util/src/main/java/org/signal/core/util/FontUtil.kt
Normal file
|
@ -0,0 +1,40 @@
|
|||
package org.signal.core.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
object FontUtil {
|
||||
private const val SAMPLE_EMOJI = "\uD83C\uDF0D" // 🌍
|
||||
|
||||
/**
|
||||
* Certain platforms cannot render emoji above a certain font size.
|
||||
*
|
||||
* This will attempt to render an emoji at the specified font size and tell you if it's possible.
|
||||
* It does this by rendering an emoji into a 1x1 bitmap and seeing if the resulting pixel is non-transparent.
|
||||
*
|
||||
* https://stackoverflow.com/a/50988748
|
||||
*/
|
||||
@JvmStatic
|
||||
fun canRenderEmojiAtFontSize(size: Float): Boolean {
|
||||
val bitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint()
|
||||
|
||||
paint.textSize = size
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
|
||||
val ascent: Float = abs(paint.ascent())
|
||||
val descent: Float = abs(paint.descent())
|
||||
val halfHeight = (ascent + descent) / 2.0f
|
||||
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
||||
canvas.drawText(SAMPLE_EMOJI, 0.5f, 0.5f + halfHeight - descent, paint)
|
||||
|
||||
return bitmap.getPixel(0, 0) != 0
|
||||
}
|
||||
}
|
|
@ -3,7 +3,9 @@ package org.signal.core.util;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ListUtil {
|
||||
private ListUtil() {}
|
||||
|
@ -18,4 +20,15 @@ public final class ListUtil {
|
|||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> List<T> concat(Collection<T>... items) {
|
||||
final List<T> concat = new ArrayList<>(Stream.of(items).map(Collection::size).reduce(0, Integer::sum));
|
||||
|
||||
for (Collection<T> list : items) {
|
||||
concat.addAll(list);
|
||||
}
|
||||
|
||||
return concat;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ dependencies {
|
|||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.material.material
|
||||
implementation project(':core-util')
|
||||
implementation project(':image-editor')
|
||||
|
||||
implementation libs.glide.glide
|
||||
|
|
|
@ -70,6 +70,8 @@ public final class MainActivity extends AppCompatActivity {
|
|||
|
||||
imageEditorView = findViewById(R.id.image_editor);
|
||||
|
||||
imageEditorView.setTypefaceProvider(typefaceProvider);
|
||||
|
||||
imageEditorView.setUndoRedoStackListener((undoAvailable, redoAvailable) -> {
|
||||
Log.d("ALAN", String.format("Undo/Redo available: %s, %s", undoAvailable ? "Y" : "N", redoAvailable ? "Y" : "N"));
|
||||
if (menu == null) return;
|
||||
|
|
|
@ -17,6 +17,10 @@ import androidx.annotation.Nullable;
|
|||
import org.signal.imageeditor.core.model.EditorElement;
|
||||
import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Invisible {@link android.widget.EditText} that is used during in-image text editing.
|
||||
*/
|
||||
|
@ -37,6 +41,8 @@ public final class HiddenEditText extends androidx.appcompat.widget.AppCompatEdi
|
|||
@Nullable
|
||||
private OnEditOrSelectionChange onEditOrSelectionChange;
|
||||
|
||||
private List<TextFilter> textFilters = new LinkedList<>();
|
||||
|
||||
public HiddenEditText(Context context) {
|
||||
super(context);
|
||||
setAlpha(0);
|
||||
|
@ -54,7 +60,11 @@ public final class HiddenEditText extends androidx.appcompat.widget.AppCompatEdi
|
|||
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
|
||||
super.onTextChanged(text, start, lengthBefore, lengthAfter);
|
||||
if (currentTextEntity != null) {
|
||||
currentTextEntity.setText(text.toString());
|
||||
String filtered = text.toString();
|
||||
for (TextFilter filter : textFilters) {
|
||||
filtered = filter.filter(filtered);
|
||||
}
|
||||
currentTextEntity.setText(filtered);
|
||||
postEditOrSelectionChange();
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +89,18 @@ public final class HiddenEditText extends androidx.appcompat.widget.AppCompatEdi
|
|||
}
|
||||
}
|
||||
|
||||
public void addTextFilter(@NonNull TextFilter filter) {
|
||||
textFilters.add(filter);
|
||||
}
|
||||
|
||||
public void addTextFilters(@NonNull Collection<TextFilter> filters) {
|
||||
textFilters.addAll(filters);
|
||||
}
|
||||
|
||||
public void removeTextFilter(@NonNull TextFilter filter) {
|
||||
textFilters.remove(filter);
|
||||
}
|
||||
|
||||
private void endEdit() {
|
||||
if (onEndEdit != null) {
|
||||
onEndEdit.run();
|
||||
|
@ -173,4 +195,11 @@ public final class HiddenEditText extends androidx.appcompat.widget.AppCompatEdi
|
|||
public interface OnEditOrSelectionChange {
|
||||
void onChange(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer);
|
||||
}
|
||||
|
||||
public interface TextFilter {
|
||||
/**
|
||||
* Given an input string, return a filtered version.
|
||||
*/
|
||||
String filter(@NonNull String text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ import org.signal.imageeditor.core.renderers.BezierDrawingRenderer;
|
|||
import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
|
||||
import org.signal.imageeditor.core.renderers.TrashRenderer;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ImageEditorView
|
||||
* <p>
|
||||
|
@ -71,6 +74,8 @@ public final class ImageEditorView extends FrameLayout {
|
|||
@Nullable
|
||||
private DragListener dragListener;
|
||||
|
||||
private final List<HiddenEditText.TextFilter> textFilters = new LinkedList<>();
|
||||
|
||||
private final Matrix viewMatrix = new Matrix();
|
||||
private final RectF viewPort = Bounds.newFullBounds();
|
||||
private final RectF visibleViewPort = Bounds.newFullBounds();
|
||||
|
@ -116,6 +121,8 @@ public final class ImageEditorView extends FrameLayout {
|
|||
editText.clearFocus();
|
||||
editText.setOnEndEdit(this::doneTextEditing);
|
||||
editText.setOnEditOrSelectionChange(this::zoomToFitText);
|
||||
editText.addTextFilters(textFilters);
|
||||
|
||||
return editText;
|
||||
}
|
||||
|
||||
|
@ -150,6 +157,16 @@ public final class ImageEditorView extends FrameLayout {
|
|||
this.typefaceProvider = typefaceProvider;
|
||||
}
|
||||
|
||||
public void addTextInputFilter(@NonNull HiddenEditText.TextFilter inputFilter) {
|
||||
textFilters.add(inputFilter);
|
||||
editText = createAHiddenTextEntryField();
|
||||
}
|
||||
|
||||
public void removeTextInputFilter(@NonNull HiddenEditText.TextFilter inputFilter) {
|
||||
textFilters.remove(inputFilter);
|
||||
editText = createAHiddenTextEntryField();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (rendererContext == null || rendererContext.canvas != canvas || rendererContext.typefaceProvider != typefaceProvider) {
|
||||
|
|
Loading…
Add table
Reference in a new issue