From 8c20d9971713e0a02fedf1eaad7835346e7986f0 Mon Sep 17 00:00:00 2001 From: k3b <1374583+k3b@users.noreply.github.com> Date: Mon, 15 Apr 2019 16:58:36 +0200 Subject: [PATCH] Implemented save as cropped image --- README.md | 11 ++ app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 2 +- ...vity.java => CropAreasChooseActivity.java} | 106 ++++++++++++++++-- .../lossless_jpg_crop/ImageProcessor.java | 9 +- app/src/main/res/layout/activity_main.xml | 2 +- 6 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 README.md rename app/src/main/java/de/k3b/android/lossless_jpg_crop/{MainActivity.java => CropAreasChooseActivity.java} (63%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a08150 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +llCrop allows you to either [remove unwanted outer areas from a jpg-photo](https://en.wikipedia.org/wiki/Cropping_(image)) +or to create zoom-ins. + +Just load a jpeg photo, select a rectangle and save the rectangle as a new photo-file. + +While there are many apps that can crop images (and may have many more features) these apps cause quality-losses caused by +jpg-re-encoding. + +llCrop ("ll" stands for loss-less) can do cropping without quality-losses because it crops in the raw jpg-photo-data without +the need for jpg-image-re-encoding. + diff --git a/app/build.gradle b/app/build.gradle index c08775f..e65bf5f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,9 @@ android { compileSdkVersion 28 defaultConfig { applicationId "de.k3b.android.lossless_jpg_crop" - minSdkVersion 15 + + // SAF ACTION_CREATE_DOCUMENT requires api-19 and later + minSdkVersion 19 targetSdkVersion 28 versionCode 1 versionName "1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a5ac7ef..6dffe4f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> - + diff --git a/app/src/main/java/de/k3b/android/lossless_jpg_crop/MainActivity.java b/app/src/main/java/de/k3b/android/lossless_jpg_crop/CropAreasChooseActivity.java similarity index 63% rename from app/src/main/java/de/k3b/android/lossless_jpg_crop/MainActivity.java rename to app/src/main/java/de/k3b/android/lossless_jpg_crop/CropAreasChooseActivity.java index b4b2a89..114505f 100644 --- a/app/src/main/java/de/k3b/android/lossless_jpg_crop/MainActivity.java +++ b/app/src/main/java/de/k3b/android/lossless_jpg_crop/CropAreasChooseActivity.java @@ -3,14 +3,12 @@ import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Rect; -import android.graphics.RectF; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; +import android.support.v4.provider.DocumentFile; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -18,17 +16,22 @@ import net.realify.lib.androidimagecropper.CropImageView; +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -public class MainActivity extends BaseActivity { +public class CropAreasChooseActivity extends BaseActivity { private static final String TAG = "ResultActivity"; private static final int REQUEST_GET_PICTURE = 1; protected static final int REQUEST_GET_PICTURE_PERMISSION = 101; private static final int REQUEST_SAVE_PICTURE = 2; private static final int REQUEST_SAVE_PICTURE_PERMISSION = 102; + private static final String CURRENT_CROP_AREA = "CURRENT_CROP_AREA"; + private CropImageView uCropView = null; private ImageProcessor mSpectrum; @Override @@ -45,7 +48,7 @@ protected void onCreate(Bundle savedInstanceState) { pickFromGallery(); } else { try { - CropImageView uCropView = findViewById(R.id.ucrop); + uCropView = findViewById(R.id.ucrop); /* InputStream stream = getContentResolver().openInputStream(uri); @@ -54,6 +57,12 @@ protected void onCreate(Bundle savedInstanceState) { uCropView.setImageBitmap(bitmap); */ uCropView.setImageUriAsync(uri); + + Rect crop = (Rect) ((savedInstanceState == null) + ? null + : savedInstanceState.getParcelable(CURRENT_CROP_AREA)); + + uCropView.setCropRect(crop); } catch (Exception e) { Log.e(TAG, "setImageUri", e); Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); @@ -62,6 +71,14 @@ protected void onCreate(Bundle savedInstanceState) { } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + Rect crop = getCropRect(); + outState.putParcelable(CURRENT_CROP_AREA, crop); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); @@ -99,7 +116,7 @@ private void onGetPictureResult(int resultCode, Intent data) { if (resultCode == RESULT_OK) { final Uri selectedUri = data.getData(); if (selectedUri != null) { - Intent intent = new Intent(Intent.ACTION_VIEW, selectedUri, this, MainActivity.class); + Intent intent = new Intent(Intent.ACTION_VIEW, selectedUri, this, CropAreasChooseActivity.class); this.startActivity(intent); finish(); return; @@ -119,11 +136,10 @@ private void saveCroppedImage() { } else { Uri imageUri = getIntent().getData(); - CropImageView uCropView = findViewById(R.id.ucrop); - - Rect crop = uCropView.getCropRect(); + Rect crop = getCropRect(); - debug(imageUri, crop); + openOutputUriPicker(REQUEST_SAVE_PICTURE); + // debug(imageUri, crop); /** !!!! mImageView.getCurrentCropImageState() scale must be fixed according to image with/hight/orientation @@ -146,6 +162,71 @@ private void saveCroppedImage() { } } + private boolean openOutputUriPicker(int folderpickerCode) { + Uri inUri = getIntent().getData(); + String originalFileName = (inUri == null) ? "" : inUri.getLastPathSegment(); + String proposedFileName = replaceExtension(originalFileName, "_llcrop.jpg"); + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .setType("image/jpeg") + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra(Intent.EXTRA_TITLE, proposedFileName) + .setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + ; + startActivityForResult(intent, folderpickerCode); + return true; + } + + private void onOpenOutputUriPickerResult(int resultCode, Uri outUri) { + + if (resultCode == RESULT_OK) { + final Uri inUri = getIntent().getData(); + Rect rect = getCropRect(); + InputStream inStream = null; + OutputStream outStream = null; + + final String context_message = "Cropping '" + inUri + "'(" + rect + ") => '" + outUri + "'"; + Log.d(TAG, context_message); + + try { + inStream = getContentResolver().openInputStream(inUri); + outStream = getContentResolver().openOutputStream(outUri, "w"); + this.mSpectrum.crop(inStream, outStream, rect, 0); + finish(); + return; + } catch (Exception e) { + Log.e(TAG, "Error " + context_message + e.getMessage(), e); + } finally { + close(outStream, outStream); + close(inStream, inStream); + } + } + } + + private static void close(Closeable stream, Object source) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing " + source, e); + } + } + } + + /** replaceExtension("/path/to/image.jpg", ".xmp") becomes "/path/to/image.xmp" */ + private static String replaceExtension(String path, String extension) { + if (path == null) return null; + int ext = path.lastIndexOf("."); + return ((ext >= 0) ? path.substring(0, ext) : path) + extension; + } + + private Rect getCropRect() { + if (uCropView == null) return null; + return uCropView.getCropRect(); + } + private void debug(Uri imageUri, Object currentCropImageState) { Log.d(TAG, imageUri.getLastPathSegment() + "(" + currentCropImageState + ")"); } @@ -179,6 +260,11 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { onGetPictureResult(resultCode, data); return; } + if (requestCode == REQUEST_SAVE_PICTURE) { + onOpenOutputUriPickerResult(resultCode, data.getData()); + return; + } + super.onActivityResult(requestCode, resultCode, data); } diff --git a/app/src/main/java/de/k3b/android/lossless_jpg_crop/ImageProcessor.java b/app/src/main/java/de/k3b/android/lossless_jpg_crop/ImageProcessor.java index c85c97d..816f91b 100644 --- a/app/src/main/java/de/k3b/android/lossless_jpg_crop/ImageProcessor.java +++ b/app/src/main/java/de/k3b/android/lossless_jpg_crop/ImageProcessor.java @@ -1,6 +1,7 @@ package de.k3b.android.lossless_jpg_crop; import android.content.Context; +import android.graphics.Rect; import android.util.Log; import com.facebook.spectrum.Configuration; @@ -25,7 +26,7 @@ public static void init(Context context) { SpectrumSoLoader.init(context); } - public void ImageProcessor() { + public ImageProcessor() { mSpectrum = Spectrum.make( new SpectrumLogcatLogger(Log.INFO), Configuration.makeEmpty(), @@ -33,6 +34,10 @@ public void ImageProcessor() { // DefaultPlugins.get()); // JPEG, PNG and WebP plugins } + public void crop(InputStream inputStream, OutputStream outputStream, Rect rect, int degrees) { + crop(inputStream, outputStream, rect.left, rect.top, rect.right, rect.bottom, degrees); + } + public void crop(InputStream inputStream, OutputStream outputStream, int left, int top, int right, int bottom, int degrees) { final EncodeRequirement encoding = new EncodeRequirement(EncodedImageFormat.JPEG, 80, EncodeRequirement.Mode.LOSSLESS); @@ -45,7 +50,7 @@ public void crop(InputStream inputStream, OutputStream outputStream, int left, i .cropAbsoluteToOrigin(left, top, right, bottom, false) // forceUpOrientation=true - .rotate(degrees, false, false, true) + // .rotate(degrees, false, false, true) .build(), "my_callsite_identifier"); } catch (Exception ex) { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d2b6f21..cb61b3f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context=".MainActivity" + tools:context=".CropAreasChooseActivity" >