diff --git a/image-editor/app/.gitignore b/image-editor/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/image-editor/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/image-editor/app/build.gradle b/image-editor/app/build.gradle new file mode 100644 index 000000000..c54e9cd8b --- /dev/null +++ b/image-editor/app/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'witness' +} + +apply from: 'witness-verifications.gradle' + +android { + compileSdk COMPILE_SDK + + defaultConfig { + applicationId "org.signal.imageeditor.app" + versionCode 1 + versionName "1.0" + + minSdk MINIMUM_SDK + targetSdk TARGET_SDK + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JAVA_VERSION + targetCompatibility JAVA_VERSION + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencyVerification { + configuration = '(debug|release)RuntimeClasspath' +} + +dependencies { + implementation libs.androidx.core.ktx + implementation libs.androidx.appcompat + implementation libs.material.material + implementation project(':image-editor') + + implementation libs.glide.glide + kapt libs.glide.compiler +} \ No newline at end of file diff --git a/image-editor/app/proguard-rules.pro b/image-editor/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/image-editor/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt b/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..f3e60adf1 --- /dev/null +++ b/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.imageeditor.app + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.imageeditor.app", appContext.packageName) + } +} \ No newline at end of file diff --git a/image-editor/app/src/main/AndroidManifest.xml b/image-editor/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2102741cd --- /dev/null +++ b/image-editor/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000..cb5a2d5e7 --- /dev/null +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java @@ -0,0 +1,275 @@ +package org.signal.imageeditor.app; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +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.UndoRedoStackListener; +import org.signal.imageeditor.core.model.EditorElement; +import org.signal.imageeditor.core.model.EditorModel; +import org.signal.imageeditor.core.renderers.MultiLineTextRenderer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; + +public final class MainActivity extends AppCompatActivity { + + private static final String TAG = "MainActivity"; + + private ImageEditorView imageEditorView; + private Menu menu; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(R.string.app_name_short); + setSupportActionBar(toolbar); + + imageEditorView = findViewById(R.id.image_editor); + + imageEditorView.setUndoRedoStackListener((undoAvailable, redoAvailable) -> { + Log.d("ALAN", String.format("Undo/Redo available: %s, %s", undoAvailable ? "Y" : "N", redoAvailable ? "Y" : "N")); + if (menu == null) return; + MenuItem undo = menu.findItem(R.id.action_undo); + MenuItem redo = menu.findItem(R.id.action_redo); + if (undo != null) undo.setVisible(undoAvailable); + if (redo != null) redo.setVisible(redoAvailable); + }); + + EditorModel model = null; + if (savedInstanceState != null) { + model = savedInstanceState.getParcelable("MODEL"); + Log.d("ALAN", "Restoring instance " + (model != null ? model.hashCode() : 0)); + } + + if (model == null) { + model = initialModel(); + Log.d("ALAN", "New instance created " + model.hashCode()); + } + + imageEditorView.setModel(model); + + imageEditorView.setTapListener(new ImageEditorView.TapListener() { + @Override + public void onEntityDown(@Nullable EditorElement editorElement) { + Log.d("ALAN", "Entity down " + editorElement); + } + + @Override + public void onEntitySingleTap(@Nullable EditorElement editorElement) { + Log.d("ALAN", "Entity single tapped " + editorElement); + } + + @Override + public void onEntityDoubleTap(@NonNull EditorElement editorElement) { + Log.d("ALAN", "Entity double tapped " + editorElement); + if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { + imageEditorView.startTextEditing(editorElement); + } else { + imageEditorView.deleteElement(editorElement); + } + } + }); + } + + private static EditorModel initialModel() { + + EditorModel model = EditorModel.create(); + + EditorElement image = new EditorElement(new UrlRenderer("https://cdn.aarp.net/content/dam/aarp/home-and-family/your-home/2018/06/1140-house-inheriting.imgcache.rev68c065601779c5d76b913cf9ec3a977e.jpg")); + image.getFlags().setSelectable(false).persist(); + model.addElement(image); + + EditorElement elementC = new EditorElement(new UrlRenderer("https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/SNice.svg/220px-SNice.svg.png")); + elementC.getLocalMatrix().postScale(0.2f, 0.2f); + //elementC.getLocalMatrix().postRotate(30); + model.addElement(elementC); + + EditorElement elementE = new EditorElement(new UrlRenderer("https://www.vitalessentialsraw.com/assets/images/background-images/laying-grey-cat.png")); + elementE.getLocalMatrix().postScale(0.2f, 0.2f); + //elementE.getLocalMatrix().postRotate(60); + model.addElement(elementE); + + EditorElement elementD = new EditorElement(new UrlRenderer("https://petspluslubbocktx.com/files/2016/11/DC-Cat-Weight-Management.png")); + elementD.getLocalMatrix().postScale(0.2f, 0.2f); + //elementD.getLocalMatrix().postRotate(60); + model.addElement(elementD); + + EditorElement elementF = new EditorElement(new UrlRenderer("https://purepng.com/public/uploads/large/purepng.com-black-top-hathatsstandard-sizeblacktop-14215263591972x0zh.png")); + elementF.getLocalMatrix().postScale(0.2f, 0.2f); + //elementF.getLocalMatrix().postRotatF(60); + model.addElement(elementF); + + EditorElement elementG = new EditorElement(new UriRenderer(Uri.parse("file:///android_asset/food/apple.png"))); + elementG.getLocalMatrix().postScale(0.2f, 0.2f); + //elementG.getLocalMatrix().postRotatG(60); + model.addElement(elementG); + + EditorElement elementH = new EditorElement(new MultiLineTextRenderer("Hello, World!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR)); + //elementH.getLocalMatrix().postScale(0.2f, 0.2f); + model.addElement(elementH); + + EditorElement elementH2 = new EditorElement(new MultiLineTextRenderer("Hello, World 2!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR)); + //elementH.getLocalMatrix().postScale(0.2f, 0.2f); + model.addElement(elementH2); + + return model; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable("MODEL", imageEditorView.getModel()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.action_menu, menu); + this.menu = menu; + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_undo: + imageEditorView.getModel().undo(); + Log.d(TAG, String.format("Model is %s", imageEditorView.getModel().isChanged() ? "changed" : "unchanged")); + return true; + + case R.id.action_redo: + imageEditorView.getModel().redo(); + return true; + + case R.id.action_crop: + imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize); + imageEditorView.getModel().startCrop(); + return true; + + case R.id.action_done: + imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize); + imageEditorView.getModel().doneCrop(); + return true; + + case R.id.action_draw: + imageEditorView.setDrawingBrushColor(0xffffff00); + imageEditorView.startDrawing(0.02f, Paint.Cap.ROUND, false); + return true; + + case R.id.action_rotate_right_90: + imageEditorView.getModel().rotate90clockwise(); + return true; + + case R.id.action_rotate_left_90: + imageEditorView.getModel().rotate90anticlockwise(); + return true; + + case R.id.action_flip_horizontal: + imageEditorView.getModel().flipHorizontal(); + return true; + + case R.id.action_flip_vertical: + imageEditorView.getModel().flipVertical(); + return true; + + case R.id.action_edit_text: + editText(); + return true; + + case R.id.action_lock_crop_aspect: + imageEditorView.getModel().setCropAspectLock(!imageEditorView.getModel().isCropAspectLocked()); + return true; + + case R.id.action_save: + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, + 0); + } else { + Bitmap bitmap = imageEditorView.getModel().render(this); + try { + Uri uri = saveBmp(bitmap); + + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(uri, "image/*"); + startActivity(intent); + + } finally { + bitmap.recycle(); + } + } + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void editText() { + imageEditorView.getModel().getRoot().findElement(new Matrix(), new Matrix(), (element, inverseMatrix) -> { + if (element.getRenderer() instanceof MultiLineTextRenderer) { + imageEditorView.startTextEditing(element); + return true; + } + return false; + } + ); + } + + private Uri saveBmp(Bitmap bitmap) { + String path = Environment.getExternalStorageDirectory().toString(); + + File filePath = new File(path); + File imageEditor = new File(filePath, "ImageEditor"); + if (!imageEditor.exists()) { + imageEditor.mkdir(); + } + + int counter = 0; + File file; + do { + counter++; + file = new File(imageEditor, String.format(Locale.US, "ImageEditor_%03d.jpg", counter)); + } while (file.exists()); + + try { + try (OutputStream stream = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); + } + return Uri.parse(MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName())); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java new file mode 100644 index 000000000..934542c77 --- /dev/null +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java @@ -0,0 +1,10 @@ +package org.signal.imageeditor.app; + +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public class TheAppGlideModule extends AppGlideModule { + + +} diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java new file mode 100644 index 000000000..008f9db21 --- /dev/null +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java @@ -0,0 +1,12 @@ +package org.signal.imageeditor.app.renderers; + +import org.signal.imageeditor.core.Bounds; +import org.signal.imageeditor.core.Renderer; + +public abstract class StandardHitTestRenderer implements Renderer { + + @Override + public boolean hitTest(float x, float y) { + return Bounds.contains(x, y); + } +} diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java new file mode 100644 index 000000000..3f5fa6f1f --- /dev/null +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java @@ -0,0 +1,138 @@ +package org.signal.imageeditor.app.renderers; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +import org.signal.imageeditor.app.GlideApp; +import org.signal.imageeditor.core.Bounds; +import org.signal.imageeditor.core.Renderer; +import org.signal.imageeditor.core.RendererContext; + +public final class UriRenderer implements Renderer, Parcelable { + + private final Uri imageUri; + + private final Paint paint = new Paint(); + + private final Matrix temp1 = new Matrix(); + private final Matrix temp2 = new Matrix(); + + @Nullable + private Bitmap bitmap; + + public UriRenderer(Uri imageUri) { + this.imageUri = imageUri; + paint.setAntiAlias(true); + } + + private UriRenderer(Parcel in) { + this(Uri.parse(in.readString())); + } + + @Override + public void render(@NonNull RendererContext rendererContext) { + if (bitmap != null && bitmap.isRecycled()) bitmap = null; + + if (bitmap == null) { + GlideApp.with(rendererContext.context) + .asBitmap() + .load(imageUri) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { + setBitmap(resource); + rendererContext.rendererReady.onReady(UriRenderer.this, cropMatrix(resource), new Point(resource.getWidth(), resource.getHeight())); + } + }); + } + + if (bitmap != null) { + rendererContext.save(); + rendererContext.canvasMatrix.concat(temp1); + + // FYI units are pixels at this point. + paint.setAlpha(rendererContext.getAlpha(255)); + rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint); + rendererContext.restore(); + } else { + rendererContext.canvas.drawRect(-0.5f, -0.5f, 0.5f, 0.5f, paint); + } + } + + @Override + public boolean hitTest(float x, float y) { + return pixelNotAlpha(x, y); + } + + private boolean pixelNotAlpha(float x, float y) { + if (bitmap == null) return false; + + temp1.invert(temp2); + + float[] onBmp = new float[2]; + temp2.mapPoints(onBmp, new float[]{ x, y }); + + int xInt = (int) onBmp[0]; + int yInt = (int) onBmp[1]; + + if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) { + return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0; + } else { + return false; + } + } + + private void setBitmap(Bitmap bitmap) { + if (bitmap != null) { + this.bitmap = bitmap; + RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + } + } + + private static Matrix cropMatrix(Bitmap bitmap) { + Matrix matrix = new Matrix(); + if (bitmap.getWidth() > bitmap.getHeight()) { + matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth()); + } else { + matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1); + } + return matrix; + } + + public static final Creator CREATOR = new Creator() { + @Override + public UriRenderer createFromParcel(Parcel in) { + return new UriRenderer(in); + } + + @Override + public UriRenderer[] newArray(int size) { + return new UriRenderer[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(imageUri.toString()); + } +} diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java new file mode 100644 index 000000000..01d5f9ce5 --- /dev/null +++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java @@ -0,0 +1,263 @@ +package org.signal.imageeditor.app.renderers; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.os.Parcel; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +import org.signal.imageeditor.app.GlideApp; +import org.signal.imageeditor.core.Bounds; +import org.signal.imageeditor.core.RendererContext; + +import java.util.concurrent.ExecutionException; + +public final class UrlRenderer extends StandardHitTestRenderer { + + private static final String TAG = "UrlRenderer"; + private final String url; + private final Paint paint = new Paint(); + + private final Matrix temp1 = new Matrix(); + private final Matrix temp2 = new Matrix(); + + private Bitmap bitmap; + + public UrlRenderer(@Nullable String url) { + this.url = url; + paint.setAntiAlias(true); + } + + private UrlRenderer(Parcel in) { + this(in.readString()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public UrlRenderer createFromParcel(Parcel in) { + return new UrlRenderer(in); + } + + @Override + public UrlRenderer[] newArray(int size) { + return new UrlRenderer[size]; + } + }; + + @Override + public void render(@NonNull RendererContext rendererContext) { + if (bitmap != null && bitmap.isRecycled()) bitmap = null; + + if (bitmap == null) { + if (rendererContext.isBlockingLoad()) { + try { + setBitmap(rendererContext, GlideApp.with(rendererContext.context) + .asBitmap() + .load(url) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .submit() + .get()); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } else { + GlideApp.with(rendererContext.context) + .asBitmap() + .load(url) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(new SimpleTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { + setBitmap(rendererContext, resource); + } + }); + } + } + + if (bitmap != null) { + rendererContext.save(); + rendererContext.getCurrent(temp2); + temp2.preConcat(temp1); + rendererContext.canvas.concat(temp1); + + // FYI units are pixels at this point. + paint.setAlpha(rendererContext.getAlpha(255)); + rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint); + rendererContext.restore(); + } else { + if (rendererContext.isBlockingLoad()) { + Log.e(TAG, "blocking but drawing null :("); + } + rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint); + } + + drawDebugInfo(rendererContext); + } + + private void drawDebugInfo(RendererContext rendererContext) { +// float width = bitmap.getWidth(); +// float height = bitmap.getWidth(); + + //RectF bounds = new RectF(Bounds.LEFT, Bounds.TOP/2f, Bounds.RIGHT,Bounds.BOTTOM/2f );//Bounds.FULL_BOUNDS; + RectF bounds = Bounds.FULL_BOUNDS; + + Paint paint = new Paint(); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(0xffffff00); + rendererContext.canvas.drawRect(bounds, paint); + + RectF fullBounds = new RectF(); + rendererContext.mapRect(fullBounds, bounds); + + rendererContext.save(); + + RectF dst = new RectF(); + rendererContext.mapRect(dst, bounds); + paint.setColor(0xffff00ff); + rendererContext.canvasMatrix.setToIdentity(); + rendererContext.canvas.drawRect(dst, paint); + + rendererContext.restore(); + + rendererContext.save(); + + Matrix unrotated = new Matrix(); + rendererContext.getCurrent(unrotated); + findUnrotateMatrix(unrotated); + + Matrix rotated = new Matrix(); + rendererContext.getCurrent(rotated); + findRotateMatrix(rotated); + + RectF dst2 = new RectF(); + unrotated.mapRect(dst2, Bounds.FULL_BOUNDS); // works because square, do we need rotated here? + + float scaleX = Bounds.FULL_BOUNDS.width() / dst2.width(); + float scaleY = Bounds.FULL_BOUNDS.height() / dst2.height(); + + rendererContext.canvasMatrix.concat(unrotated); + Matrix matrix = new Matrix(); + matrix.setScale(scaleX, scaleY); + rendererContext.canvasMatrix.concat(matrix); + + paint.setColor(0xff0000ff); + rendererContext.canvas.drawRect(bounds, paint); + + rendererContext.restore(); + } + +/** + * Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it. + */ + private void findUnrotateMatrix(@NonNull Matrix matrix) { + float[] values = new float[9]; + + matrix.getValues(values); + + float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]); + float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]); + + values[0] /= xScale; + values[1] /= -yScale; + values[2] = 0; + + values[3] /= -xScale; + values[4] /= yScale; + values[5] = 0; + + matrix.setValues(values); + } + + /** + * Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it. + */ + private void findRotateMatrix(@NonNull Matrix matrix) { + float[] values = new float[9]; + + matrix.getValues(values); + + float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]); + float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]); + + values[0] /= xScale; + values[1] /= yScale; + values[2] = 0; + + values[3] /= xScale; + values[4] /= yScale; + values[5] = 0; + + matrix.setValues(values); + } + + @Override + public boolean hitTest(float x, float y) { + return super.hitTest(x, y) && pixelNotAlpha(x, y); + } + + private boolean pixelNotAlpha(float x, float y) { + if (bitmap == null) return false; + + temp1.invert(temp2); + + float[] onBmp = new float[2]; + temp2.mapPoints(onBmp, new float[]{ x, y }); + + int xInt = (int) onBmp[0]; + int yInt = (int) onBmp[1]; + + if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) { + return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0; + } else { + return xInt >= 0 && xInt <= bitmap.getWidth() && yInt >= 0 && yInt <= bitmap.getHeight(); + } + } + + private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) { + this.bitmap = bitmap; + if (bitmap != null) { + RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + rendererContext.rendererReady.onReady(this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight())); + } + } + + private void setBitmap(Bitmap bitmap) { + if (bitmap != null) { + this.bitmap = bitmap; + RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + } + } + + private static Matrix cropMatrix(Bitmap bitmap) { + Matrix matrix = new Matrix(); + if (bitmap.getWidth() > bitmap.getHeight()) { + matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth()); + } else { + matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1); + } + return matrix; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(url); + } +} diff --git a/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 000000000..3c728c59f --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml new file mode 100644 index 000000000..5a4749cdb --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml new file mode 100644 index 000000000..2bc2762d4 --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_launcher_background.xml b/image-editor/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml new file mode 100644 index 000000000..424e788f5 --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml new file mode 100644 index 000000000..2fd476dcd --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml new file mode 100644 index 000000000..a98657481 --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml new file mode 100644 index 000000000..a561d632a --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml new file mode 100644 index 000000000..5558e37d7 --- /dev/null +++ b/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/image-editor/app/src/main/res/layout/main_activity.xml b/image-editor/app/src/main/res/layout/main_activity.xml new file mode 100644 index 000000000..d2d1baf86 --- /dev/null +++ b/image-editor/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/image-editor/app/src/main/res/menu/action_menu.xml b/image-editor/app/src/main/res/menu/action_menu.xml new file mode 100644 index 000000000..ac001f4d0 --- /dev/null +++ b/image-editor/app/src/main/res/menu/action_menu.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/image-editor/app/src/main/res/values/colors.xml b/image-editor/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..d2cd14a9f --- /dev/null +++ b/image-editor/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + \ No newline at end of file diff --git a/image-editor/app/src/main/res/values/strings.xml b/image-editor/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..e4504e48b --- /dev/null +++ b/image-editor/app/src/main/res/values/strings.xml @@ -0,0 +1,17 @@ + + Image Editor Sample App + Image Editor + + Undo + Redo + Crop + Done + Save + Draw + Rotate 90 right + Rotate 90 left + Flip horizontal + Flip vertical + Edit Text + Lock crop aspect + \ No newline at end of file diff --git a/image-editor/app/src/main/res/values/themes.xml b/image-editor/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..ab7e2b3f8 --- /dev/null +++ b/image-editor/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt b/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt new file mode 100644 index 000000000..03c24560b --- /dev/null +++ b/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.imageeditor.app + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/image-editor/app/witness-verifications.gradle b/image-editor/app/witness-verifications.gradle new file mode 100644 index 000000000..77afd866c --- /dev/null +++ b/image-editor/app/witness-verifications.gradle @@ -0,0 +1,150 @@ +// Auto-generated, use ./gradlew calculateChecksums to regenerate + +dependencyVerification { + verify = [ + + ['androidx.activity:activity:1.0.0', + 'd1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d'], + + ['androidx.annotation:annotation-experimental:1.0.0', + 'b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11'], + + ['androidx.annotation:annotation:1.2.0', + '9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36'], + + ['androidx.appcompat:appcompat-resources:1.2.0', + 'c470297c03ff3de1c3d15dacf0be0cae63abc10b52f021dd07ae28daa3100fe5'], + + ['androidx.appcompat:appcompat:1.2.0', + '3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70'], + + ['androidx.arch.core:core-common:2.1.0', + 'fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889'], + + ['androidx.arch.core:core-runtime:2.0.0', + '87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e'], + + ['androidx.cardview:cardview:1.0.0', + '1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'], + + ['androidx.collection:collection:1.1.0', + '632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'], + + ['androidx.constraintlayout:constraintlayout-solver:2.0.1', + 'b23732edbb3511d937fea1ffef047b0e6c001b50c1921f0d959fc384d706ec6a'], + + ['androidx.constraintlayout:constraintlayout:2.0.1', + 'ec15b5d4a2eff07888bc1499ce2e2c6efe24c0ed60cc57b08c9dc4b6fd3c2189'], + + ['androidx.coordinatorlayout:coordinatorlayout:1.1.0', + '44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb'], + + ['androidx.core:core-ktx:1.5.0', + '5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42'], + + ['androidx.core:core:1.5.0', + '2b279712795689069cfb63e48b3ab63c32a5649bdda44c482eb8f81ca1a72161'], + + ['androidx.cursoradapter:cursoradapter:1.0.0', + 'a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564'], + + ['androidx.customview:customview:1.0.0', + '20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2'], + + ['androidx.documentfile:documentfile:1.0.0', + '865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487'], + + ['androidx.drawerlayout:drawerlayout:1.0.0', + '9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'], + + ['androidx.dynamicanimation:dynamicanimation:1.0.0', + 'ce005162c229bf308d2d5b12fb6cad0874069cbbeaccee63a8193bd08d40de04'], + + ['androidx.exifinterface:exifinterface:1.0.0', + 'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'], + + ['androidx.fragment:fragment:1.1.0', + 'a14c8b8f2153f128e800fbd266a6beab1c283982a29ec570d2cc05d307d81496'], + + ['androidx.interpolator:interpolator:1.0.0', + '33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a'], + + ['androidx.legacy:legacy-support-core-utils:1.0.0', + 'a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7'], + + ['androidx.lifecycle:lifecycle-common:2.1.0', + '76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643'], + + ['androidx.lifecycle:lifecycle-livedata-core:2.0.0', + 'fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc'], + + ['androidx.lifecycle:lifecycle-livedata:2.0.0', + 'c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39'], + + ['androidx.lifecycle:lifecycle-runtime:2.1.0', + 'e5173897b965e870651e83d9d5af1742d3f532d58863223a390ce3a194c8312b'], + + ['androidx.lifecycle:lifecycle-viewmodel:2.1.0', + 'ba55fb7ac1b2828d5327cda8acf7085d990b2b4c43ef336caa67686249b8523d'], + + ['androidx.loader:loader:1.0.0', + '11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025'], + + ['androidx.localbroadcastmanager:localbroadcastmanager:1.0.0', + 'e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8'], + + ['androidx.print:print:1.0.0', + '1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd'], + + ['androidx.recyclerview:recyclerview:1.1.0', + 'f0d2b5a67d0a91ee1b1c73ef2b636a81f3563925ddd15a1d4e1c41ec28de7a4f'], + + ['androidx.savedstate:savedstate:1.0.0', + '2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83'], + + ['androidx.transition:transition:1.2.0', + 'a1e059b3bc0b43a58dec0efecdcaa89c82d2bca552ea5bacf6656c46e853157e'], + + ['androidx.vectordrawable:vectordrawable-animated:1.1.0', + '76da2c502371d9c38054df5e2b248d00da87809ed058f3363eae87ce5e2403f8'], + + ['androidx.vectordrawable:vectordrawable:1.1.0', + '46fd633ac01b49b7fcabc263bf098c5a8b9e9a69774d234edcca04fb02df8e26'], + + ['androidx.versionedparcelable:versionedparcelable:1.1.1', + '57e8d93260d18d5b9007c9eed3c64ad159de90c8609ebfc74a347cbd514535a4'], + + ['androidx.viewpager2:viewpager2:1.0.0', + 'e95c0031d4cc247cd48196c6287e58d2cee54d9c79b85afea7c90920330275af'], + + ['androidx.viewpager:viewpager:1.0.0', + '147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682'], + + ['com.github.bumptech.glide:annotations:4.11.0', + 'd219d238006d824962176229d4708abcdddcfe342c6a18a5d0fa48d6f0479b3e'], + + ['com.github.bumptech.glide:disklrucache:4.11.0', + 'd06775a5171b777aa3db031eb0dd4a1dbe3f00dda35b5574dfd953f6b0d5ef18'], + + ['com.github.bumptech.glide:gifdecoder:4.11.0', + '197a1cd5b76855aa02b230c13974e293229b901dc2b96fab4315201e78baa804'], + + ['com.github.bumptech.glide:glide:4.11.0', + '5c294e6a5f0f812cef876b8412954c1822da184af38e082a5b766e3c4f4fcd95'], + + ['com.google.android.material:material:1.3.0', + 'cbf1e7d69fc236cdadcbd1ec5f6c0a1a41aca6ad1ef7f8481058956270ab1f0a'], + + ['com.google.protobuf:protobuf-javalite:3.10.0', + '215a94dbe100130295906b531bb72a26965c7ac8fcd9a75bf8054a8ac2abf4b4'], + + ['org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32', + 'e1ff6f55ee9e7591dcc633f7757bac25a7edb1cc7f738b37ec652f10f66a4145'], + + ['org.jetbrains.kotlin:kotlin-stdlib:1.4.32', + '13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba'], + + ['org.jetbrains:annotations:13.0', + 'ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478'], + ] +} diff --git a/settings.gradle b/settings.gradle index a86798ccb..a707d9349 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ include ':video' include ':device-transfer' include ':device-transfer-app' include ':image-editor' +include ':image-editor-app' project(':app').name = 'Signal-Android' project(':paging').projectDir = file('paging/lib') @@ -21,7 +22,8 @@ project(':device-transfer-app').projectDir = file('device-transfer/app') project(':libsignal-service').projectDir = file('libsignal/service') project(':image-editor').projectDir = file('image-editor/lib') +project(':image-editor-app').projectDir = file('image-editor/app') rootProject.name='Signal' -apply from: 'dependencies.gradle' \ No newline at end of file +apply from: 'dependencies.gradle'