From 2bdd42ee549ac6afe154411567bd0c8f17655447 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 6 Jan 2025 12:56:52 +0100 Subject: [PATCH] Experiment: image editor --- ATTRIBUTION.md | 1 + app/build.gradle | 4 + app/src/main/assets/ATTRIBUTION.md | 1 + .../eu/faircode/email/FragmentCompose.java | 172 ++++++++++++++++++ app/src/main/res/drawable/twotone_flip_24.xml | 10 + .../res/drawable/twotone_rotate_right_24.xml | 11 ++ .../twotone_rotate_right_white_18.png | Bin 0 -> 230 bytes .../twotone_rotate_right_white_20.png | Bin 0 -> 250 bytes .../twotone_rotate_right_white_24.png | Bin 0 -> 269 bytes .../twotone_rotate_right_white_36.png | Bin 0 -> 412 bytes .../twotone_rotate_right_white_48.png | Bin 0 -> 509 bytes app/src/main/res/layout/dialog_edit_image.xml | 77 ++++++++ 12 files changed, 276 insertions(+) create mode 100644 app/src/main/res/drawable/twotone_flip_24.xml create mode 100644 app/src/main/res/drawable/twotone_rotate_right_24.xml create mode 100644 app/src/main/res/drawable/twotone_rotate_right_white_18.png create mode 100644 app/src/main/res/drawable/twotone_rotate_right_white_20.png create mode 100644 app/src/main/res/drawable/twotone_rotate_right_white_24.png create mode 100644 app/src/main/res/drawable/twotone_rotate_right_white_36.png create mode 100644 app/src/main/res/drawable/twotone_rotate_right_white_48.png create mode 100644 app/src/main/res/layout/dialog_edit_image.xml diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index a21b07872f..c41e0bf35d 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -58,3 +58,4 @@ FairEmail uses parts or all of: * [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt). * [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt). * [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE). +* [Android Image Cropper](https://github.com/CanHub/Android-Image-Cropper). Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc. [Apache License 2.0](https://github.com/CanHub/Android-Image-Cropper/blob/main/LICENSE.txt). diff --git a/app/build.gradle b/app/build.gradle index f489c7a31a..961e3f22c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -617,6 +617,7 @@ dependencies { def tinylog_version = "2.7.0" def zxing_version = "3.5.3" def evalex_version = "3.3.0" + def image_cropper_version = "4.6.0" // https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version" @@ -900,4 +901,7 @@ dependencies { // https://github.com/ezylang/EvalEx implementation "com.ezylang:EvalEx:$evalex_version" + + // https://github.com/CanHub/Android-Image-Cropper + implementation "com.vanniktech:android-image-cropper:$image_cropper_version" } diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index a21b07872f..c41e0bf35d 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -58,3 +58,4 @@ FairEmail uses parts or all of: * [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt). * [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt). * [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE). +* [Android Image Cropper](https://github.com/CanHub/Android-Image-Cropper). Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc. [Apache License 2.0](https://github.com/CanHub/Android-Image-Cropper/blob/main/LICENSE.txt). diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 2e047d633a..489ebeac89 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -26,6 +26,7 @@ import static android.view.inputmethod.EditorInfo.IME_FLAG_NO_FULLSCREEN; import android.Manifest; import android.app.Activity; +import android.app.Dialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; @@ -79,6 +80,7 @@ import android.text.style.RelativeSizeSpan; import android.text.style.URLSpan; import android.util.Pair; import android.util.TypedValue; +import android.view.GestureDetector; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -135,6 +137,7 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.canhub.cropper.CropImageView; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.LabelVisibilityMode; import com.google.android.material.snackbar.Snackbar; @@ -819,6 +822,175 @@ public class FragmentCompose extends FragmentBase { } }); + if (BuildConfig.DEBUG) + etBody.setOnTouchListener(new View.OnTouchListener() { + private final GestureDetector gestureDetector = new GestureDetector(getContext(), + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull MotionEvent event) { + try { + Editable buffer = etBody.getText(); + if (buffer == null) + return false; + + int off = Helper.getOffset(etBody, buffer, event); + ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class); + if (image == null || image.length == 0) + return false; + + String source = image[0].getSource(); + if (source == null || !source.startsWith("cid:")) + return false; + + long id = Long.parseLong(source.substring(source.lastIndexOf('.') + 1)); + + int start = buffer.getSpanStart(image[0]); + int end = buffer.getSpanEnd(image[0]); + + Bundle args = new Bundle(); + args.putLong("id", id); + args.putString("source", source); + args.putInt("start", start); + args.putInt("end", end); + args.putInt("zoom", zoom); + args.putLong("working", working); + + new SimpleTask() { + @Override + protected EntityAttachment onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + return db.attachment().getAttachment(id); + } + + @Override + protected void onExecuted(Bundle args, EntityAttachment attachment) { + if (attachment == null) + return; + + Context context = etBody.getContext(); + + View dview = LayoutInflater.from(context).inflate(R.layout.dialog_edit_image, null); + ImageButton ibRotate = dview.findViewById(R.id.ibRotate); + ImageButton ibFlip = dview.findViewById(R.id.ibFlip); + ImageButton ibCancel = dview.findViewById(R.id.ibCancel); + ImageButton ibSave = dview.findViewById(R.id.ibSave); + CropImageView civ = dview.findViewById(R.id.civ); + + Dialog dialog = new AlertDialog.Builder(context) + .setView(dview) + .create(); + + ibRotate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + civ.rotateImage(90); + } + }); + + ibFlip.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + civ.flipImageHorizontally(); + } + }); + + ibCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + + ibSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + + new SimpleTask() { + @Override + protected Drawable onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + String source = args.getString("source"); + int zoom = args.getInt("zoom"); + long working = args.getLong("working"); + + DB db = DB.getInstance(context); + EntityAttachment attachment = db.attachment().getAttachment(id); + if (attachment == null) + return null; + + Bitmap bm = civ.getCroppedImage(); + if (bm == null) + return null; + + File file = attachment.getFile(context); + + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + bm.compress(Bitmap.CompressFormat.PNG, 90, os); + } + + db.attachment().setName(id, attachment.name, "image/png", file.length()); + + return ImageHelper.decodeImage(context, working, source, true, zoom, 1.0f, etBody); + } + + @Override + protected void onExecuted(Bundle args, Drawable d) { + if (d == null) + return; + + String source = args.getString("source"); + int start = args.getInt("start"); + int end = args.getInt("end"); + + Editable buffer = etBody.getText(); + if (buffer == null) + return; + + ImageSpan[] image = buffer.getSpans(start, end, ImageSpan.class); + if (image == null || image.length == 0) + return; + + int flags = buffer.getSpanFlags(image[0]); + buffer.removeSpan(image[0]); + buffer.setSpan(new ImageSpan(d, source), start, end, flags); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragment(), ex); + } + }.execute(FragmentCompose.this, args, "save:image"); + } + }); + + civ.setImageUriAsync(attachment.getUri(context)); + + dialog.show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(FragmentCompose.this, args, "edit:image"); + + return true; + } catch (Throwable ex) { + Log.e(ex); + } + return false; + } + }); + + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + if (compose_color != Color.TRANSPARENT) tvSignature.setTextColor(compose_color); tvSignature.setTypeface(StyleHelper.getTypeface(compose_font, getContext())); diff --git a/app/src/main/res/drawable/twotone_flip_24.xml b/app/src/main/res/drawable/twotone_flip_24.xml new file mode 100644 index 0000000000..f2584f0aeb --- /dev/null +++ b/app/src/main/res/drawable/twotone_flip_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/twotone_rotate_right_24.xml b/app/src/main/res/drawable/twotone_rotate_right_24.xml new file mode 100644 index 0000000000..b8188c069d --- /dev/null +++ b/app/src/main/res/drawable/twotone_rotate_right_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_18.png b/app/src/main/res/drawable/twotone_rotate_right_white_18.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8da3054c6ffd336fc8cd4d8267ef0cfd5a4bb2 GIT binary patch literal 230 zcmVYH zKm-3d7zl&^^ZzRlREe?O|C&;O@k zI6>{d6&Z@))**>uSO<4FTm*~5;Xwf9YXNaMS`Yw*q@aPR0fc9OtO#7uk_(I$%l{Vt g_x(@9Fok{q00xtSHy1>@T>t<807*qoM6N<$f*_@6`Tzg` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_20.png b/app/src/main/res/drawable/twotone_rotate_right_white_20.png new file mode 100644 index 0000000000000000000000000000000000000000..00153d990e1ce9aa8ec5a7c3d9f60e8a34e1bd74 GIT binary patch literal 250 zcmVYH zzySZ58HfY_XaDCTPT`gR#|SF@ulB#>|NH;{@hbh#|9{bc1lWLE*?FM!&i?`bZvyFO z|21$aTmqDC{m%$?`0F=G$zcQ4y>HnYqDyRx8faZBX*>G?C zcLs{)p(>mQ6cWb_`u__5g~(KhWHpi~7ORmwgCvUS8RXzW5(~u$9&mtPfCe}#5V`^b zDU47QY6IbaprupsB?@(5qLBVC^*;v|=z{MPm6-|Wmnvi6sBD5oG zxC#_D!|GIPpv-AZ;m<%}Zc?I(WDO)cX%e!hQDmlI_cSC}4KT!6{y)JQtWc9)|93zU zV*{tIQd~)64^Ydm{|o-RfcbX+X98nhhp5B{PaZ%2mlKt|!K{=2+y1LC3_}0_fbq-t TQ7r&e00000NkvXXu0mjf=F)j1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_36.png b/app/src/main/res/drawable/twotone_rotate_right_white_36.png new file mode 100644 index 0000000000000000000000000000000000000000..87d77c6d5e287e4070595ec198cde21695c66bdc GIT binary patch literal 412 zcmV;N0b~A&P)KC2U7Y;BnXLxkit6YB&9?og(1bF22qo!S1{KRh>~1@ScqLDO>z~{Mi9iW zCXHEGe>UT?vpc)vutOk_Q_ei^1Mhpz`A7V%^0+EdBB@HG5|xBXRH>*Rl>r-eoYRNO zpf9eqS6P)gm-6b$Xc^72MtbnOcRn>W7pBT+Ubz8BUSuBo%(E71s7Uk375M34rfa7r zTd10I!@WhH!Qq`I6`h!9Vdgck)k`y`4xVS`=-87C^%B&3JiHI!TjpEMcn5a?HoJ@V z!D3-UR>59(;1_sWC>oJSyzdVD1`qmQ(8@%WU9Gam{@B_6T&}SFIRlwPO&1Pigst`! z$zk3Uu1JKj_NC3G77CX(!fYBFX^dq4&3Jpy7>S6mk_ooqhzO(B{XoM~nR$AhUa*b6U1QrVylJJup1tl70*D*FG|KTx+bc(|>c3juxr0000 zO-lk%6ozj(l@Ts-mr+Jg|0RixR?#YgHvNGJp&$wbDb!U3K`p|f`UxQvwTk{gn>3;i zY9SH2ds=iI{J2+VobDp>>~!AqKEs`J&pAT;t6eVDCkqA; zgh}v@D!e1GSf+?RydfCkmd0^PXY2N3JXBacktSMWpTXzl?Vl^m7wE33A~^J7Z*grK zofA})>3;R$;3J0R9I#;^=4 znUfg>{LKLqQk + + + + + + + + + + + + \ No newline at end of file