Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

1
image-editor/app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,20 @@
plugins {
id("signal-sample-app")
id("com.google.devtools.ksp")
alias(libs.plugins.compose.compiler)
}
android {
namespace = "org.signal.imageeditor.app"
defaultConfig {
applicationId = "org.signal.imageeditor.app"
}
}
dependencies {
implementation(project(":image-editor"))
implementation(libs.glide.glide)
ksp(libs.glide.ksp)
}

21
image-editor/app/proguard-rules.pro vendored Normal file
View file

@ -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

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Signal">
<activity
android:name="org.signal.imageeditor.app.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,272 @@
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.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
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.RendererContext;
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;
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);
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.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;
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(0xFF000000);
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) {
int itemId = item.getItemId();
if (itemId == R.id.action_undo) {
imageEditorView.getModel().undo();
Log.d(TAG, String.format("Model is %s", imageEditorView.getModel().isChanged() ? "changed" : "unchanged"));
return true;
} else if (itemId == R.id.action_redo) {
imageEditorView.getModel().redo();
return true;
} else if (itemId == R.id.action_crop) {
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
imageEditorView.getModel().startCrop();
return true;
} else if (itemId == R.id.action_done) {
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
imageEditorView.getModel().doneCrop();
return true;
} else if (itemId == R.id.action_draw) {
imageEditorView.setDrawingBrushColor(0xffffff00);
imageEditorView.startDrawing(0.02f, Paint.Cap.ROUND, false);
return true;
} else if (itemId == R.id.action_rotate_left_90) {
imageEditorView.getModel().rotate90anticlockwise();
return true;
} else if (itemId == R.id.action_flip_horizontal) {
imageEditorView.getModel().flipHorizontal();
return true;
} else if (itemId == R.id.action_edit_text) {
editText();
return true;
} else if (itemId == R.id.action_lock_crop_aspect) {
imageEditorView.getModel().setCropAspectLock(!imageEditorView.getModel().isCropAspectLocked());
return true;
} else if (itemId == 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, typefaceProvider);
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;
}
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);
}
}
}

View file

@ -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 {
}

View file

@ -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);
}
}

View file

@ -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.Glide;
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.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) {
Glide.with(rendererContext.context)
.asBitmap()
.load(imageUri)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> 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<UriRenderer> CREATOR = new Creator<UriRenderer>() {
@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());
}
}

View file

@ -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.Glide;
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.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<UrlRenderer> CREATOR = new Creator<UrlRenderer>() {
@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, Glide.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 {
Glide.with(rendererContext.context)
.asBitmap()
.load(url)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> 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);
}
}

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15,21h2v-2h-2v2zM19,9h2L21,7h-2v2zM3,5v14c0,1.1 0.9,2 2,2h4v-2L5,19L5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM11,23h2L13,1h-2v22zM19,17h2v-2h-2v2zM15,5h2L17,3h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2z"/>
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11h2.02c0.14,-0.87 0.49,-1.72 1.02,-2.47zM6.09,13L4.07,13c0.17,1.39 0.72,2.73 1.62,3.89l1.41,-1.42c-0.52,-0.75 -0.87,-1.59 -1.01,-2.47zM7.1,18.32c1.16,0.9 2.51,1.44 3.9,1.61L11,17.9c-0.87,-0.15 -1.71,-0.49 -2.46,-1.03L7.1,18.32zM13,4.07L13,1L8.45,5.55 13,10L13,6.09c2.84,0.48 5,2.94 5,5.91s-2.16,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93s-3.05,-7.44 -7,-7.93z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
</vector>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<org.signal.imageeditor.core.ImageEditorView
android:id="@+id/image_editor"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_crop"
android:icon="@drawable/ic_crop_black_24dp"
android:title="@string/crop"
app:showAsAction="always" />
<item
android:id="@+id/action_done"
android:icon="@drawable/ic_check_black_24dp"
android:title="@string/done"
app:showAsAction="always" />
<item
android:id="@+id/action_undo"
android:icon="@drawable/ic_undo_black_24dp"
android:title="@string/undo"
app:showAsAction="always" />
<item
android:id="@+id/action_redo"
android:icon="@drawable/ic_redo_black_24dp"
android:title="@string/redo"
app:showAsAction="always" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save_black_24dp"
android:title="@string/save"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_draw"
android:title="@string/draw"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_rotate_left_90"
android:icon="@drawable/ic_rotate_left_black_24dp"
android:title="@string/rotate_90_left"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_flip_horizontal"
android:icon="@drawable/ic_flip_black_24dp"
android:title="@string/flip_horizontal"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_edit_text"
android:title="@string/edit_text"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_lock_crop_aspect"
android:title="@string/lock_crop_aspect"
app:showAsAction="ifRoom" />
</menu>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>

View file

@ -0,0 +1,17 @@
<resources>
<string name="app_name">Image Editor Sample App</string>
<string name="app_name_short">Image Editor</string>
<string name="undo">Undo</string>
<string name="redo">Redo</string>
<string name="crop">Crop</string>
<string name="done">Done</string>
<string name="save">Save</string>
<string name="draw">Draw</string>
<string name="rotate_90_right">Rotate 90 right</string>
<string name="rotate_90_left">Rotate 90 left</string>
<string name="flip_horizontal">Flip horizontal</string>
<string name="flip_vertical">Flip vertical</string>
<string name="edit_text">Edit Text</string>
<string name="lock_crop_aspect">Lock crop aspect</string>
</resources>

View file

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Signal" parent="Theme.MaterialComponents.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View file

@ -0,0 +1,16 @@
package com.example.imageeditor.app
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* 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)
}
}