diff --git a/README.md b/README.md index 3121df8..25c6a9e 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ You can receive the new processed image path and it's edit status like this- super.onActivityResult(requestCode, resultCode, data); if (requestCode == PHOTO_EDITOR_REQUEST_CODE) { // same code you used while starting - String newFilePath = data.getStringExtra(EditImageActivity.OUTPUT_PATH); + String newFilePath = data.getStringExtra(ImageEditorIntentBuilder.OUTPUT_PATH); boolean isImageEdit = data.getBooleanExtra(EditImageActivity.IMAGE_IS_EDIT, false); } } diff --git a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/EditImageActivity.java b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/EditImageActivity.java index aec2afa..ce48c15 100644 --- a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/EditImageActivity.java +++ b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/EditImageActivity.java @@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; @@ -79,6 +80,7 @@ public class EditImageActivity extends BaseActivity implements OnLoadingDialogLi Manifest.permission.WRITE_EXTERNAL_STORAGE }; + public Uri sourceUri; public String sourceFilePath; public String outputFilePath; public String editorTitle; @@ -116,7 +118,10 @@ public class EditImageActivity extends BaseActivity implements OnLoadingDialogLi private CompositeDisposable compositeDisposable = new CompositeDisposable(); public static void start(Activity activity, Intent intent, int requestCode) { - if (TextUtils.isEmpty(intent.getStringExtra(ImageEditorIntentBuilder.SOURCE_PATH))) { + String sourcePath = intent.getStringExtra(ImageEditorIntentBuilder.SOURCE_PATH); + String sourceUriStr = intent.getStringExtra(ImageEditorIntentBuilder.SOURCE_URI); + + if (TextUtils.isEmpty(sourcePath) && TextUtils.isEmpty(sourceUriStr)) { Toast.makeText(activity, R.string.iamutkarshtiwari_github_io_ananas_not_selected, Toast.LENGTH_SHORT).show(); return; } @@ -151,6 +156,11 @@ private void getData() { isPortraitForced = getIntent().getBooleanExtra(ImageEditorIntentBuilder.FORCE_PORTRAIT, false); isSupportActionBarEnabled = getIntent().getBooleanExtra(ImageEditorIntentBuilder.SUPPORT_ACTION_BAR_VISIBILITY, false); + String sourceUriStr = getIntent().getStringExtra(ImageEditorIntentBuilder.SOURCE_URI); + if (!TextUtils.isEmpty(sourceUriStr)) { + sourceUri = Uri.parse(sourceUriStr); + } + sourceFilePath = getIntent().getStringExtra(ImageEditorIntentBuilder.SOURCE_PATH); outputFilePath = getIntent().getStringExtra(ImageEditorIntentBuilder.OUTPUT_PATH); editorTitle = getIntent().getStringExtra(ImageEditorIntentBuilder.EDITOR_TITLE); @@ -228,7 +238,11 @@ private void initView() { ActivityCompat.requestPermissions(this, requiredPermissions, PERMISSIONS_REQUEST_CODE); } - loadImageFromFile(sourceFilePath); + if (!TextUtils.isEmpty(sourceFilePath)) { + loadImageFromFile(sourceFilePath); + } else { + loadImageFromUri(sourceUri); + } } private void setOnMainBitmapChangeListener(OnMainBitmapChangeListener listener) { @@ -243,6 +257,7 @@ public void onRequestPermissionsResult(int requestCode, // If request is cancelled, the result arrays are empty. if (!(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + setResult(RESULT_CANCELED); finish(); } break; @@ -303,11 +318,17 @@ public void onBackPressed() { break; default: if (canAutoExit()) { - onSaveTaskDone(); + setResult(RESULT_CANCELED); + finish(); } else { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); alertDialogBuilder.setMessage(R.string.iamutkarshtiwari_github_io_ananas_exit_without_save) - .setCancelable(false).setPositiveButton(R.string.iamutkarshtiwari_github_io_ananas_confirm, (dialog, id) -> finish()).setNegativeButton(R.string.iamutkarshtiwari_github_io_ananas_cancel, (dialog, id) -> dialog.cancel()); + .setCancelable(false) + .setPositiveButton(R.string.iamutkarshtiwari_github_io_ananas_confirm, (dialog, id) -> { + setResult(RESULT_CANCELED); + finish(); + }) + .setNegativeButton(R.string.iamutkarshtiwari_github_io_ananas_cancel, (dialog, id) -> dialog.cancel()); AlertDialog alertDialog = alertDialogBuilder.create(); alertDialog.show(); @@ -337,6 +358,11 @@ public void changeMainBitmap(Bitmap newBit, boolean needPushUndoStack) { protected void onSaveTaskDone() { Intent returnIntent = new Intent(); + + if (sourceUri != null) { + returnIntent.putExtra(ImageEditorIntentBuilder.SOURCE_URI, sourceUri.toString()); + } + returnIntent.putExtra(ImageEditorIntentBuilder.SOURCE_PATH, sourceFilePath); returnIntent.putExtra(ImageEditorIntentBuilder.OUTPUT_PATH, outputFilePath); returnIntent.putExtra(IS_IMAGE_EDITED, numberOfOperations > 0); @@ -377,6 +403,19 @@ private Single saveImage(Bitmap finalBitmap) { }); } + private void loadImageFromUri(Uri uri) { + compositeDisposable.clear(); + + Disposable loadImageDisposable = loadImage(uri) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe(subscriber -> loadingDialog.show()) + .doFinally(() -> loadingDialog.dismiss()) + .subscribe(processedBitmap -> changeMainBitmap(processedBitmap, false), e -> showToast(R.string.iamutkarshtiwari_github_io_ananas_load_error)); + + compositeDisposable.add(loadImageDisposable); + } + private void loadImageFromFile(String filePath) { compositeDisposable.clear(); @@ -395,6 +434,11 @@ private Single loadImage(String filePath) { imageHeight)); } + private Single loadImage(Uri uri) { + return Single.fromCallable(() -> BitmapUtils.decodeSampledBitmap(this, uri, + imageWidth, imageHeight)); + } + private void showToast(@StringRes int resId) { Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); } diff --git a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/ImageEditorIntentBuilder.kt b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/ImageEditorIntentBuilder.kt index 87c9e28..1a1d477 100644 --- a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/ImageEditorIntentBuilder.kt +++ b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/ImageEditorIntentBuilder.kt @@ -2,6 +2,7 @@ package iamutkarshtiwari.github.io.ananas.editimage import android.content.Context import android.content.Intent +import android.net.Uri class ImageEditorIntentBuilder @JvmOverloads constructor(private val context: Context, private val sourcePath: String?, @@ -11,6 +12,17 @@ class ImageEditorIntentBuilder @JvmOverloads constructor(private val context: Co EditImageActivity::class.java ) ) { + private var sourceUri: Uri? = null + + @JvmOverloads constructor(context: Context, + sourceUri: Uri, + outputPath: String?, + intent: Intent = Intent( + context, + EditImageActivity::class.java + )) : this(context, null, outputPath, intent) { + this.sourceUri = sourceUri + } fun withAddText(): ImageEditorIntentBuilder { intent.putExtra(ADD_TEXT_FEATURE, true) @@ -62,8 +74,16 @@ class ImageEditorIntentBuilder @JvmOverloads constructor(private val context: Co return this } + fun withSourceUri(sourceUri: Uri): ImageEditorIntentBuilder { + this.sourceUri = sourceUri + intent.putExtra(SOURCE_URI, sourceUri.toString()) + intent.removeExtra(SOURCE_PATH) + return this + } + fun withSourcePath(sourcePath: String): ImageEditorIntentBuilder { intent.putExtra(SOURCE_PATH, sourcePath) + intent.removeExtra(SOURCE_URI) return this } @@ -85,10 +105,14 @@ class ImageEditorIntentBuilder @JvmOverloads constructor(private val context: Co @Throws(Exception::class) fun build(): Intent { - if (sourcePath.isNullOrBlank()) { - throw Exception("Output image path required. Use withOutputPath(path) to provide the output image path.") - } else { + if (sourcePath.isNullOrBlank() && sourceUri == null) { + throw Exception("Source image required. Use withSourcePath(path) or withSourceUri(uri) to provide the source.") + } else if (!sourcePath.isNullOrBlank() && sourceUri != null) { + throw Exception("Multiple source images specified. Use either withSourcePath(path) or withSourceUri(uri) to provide the source.") + } else if (!sourcePath.isNullOrBlank()) { intent.putExtra(SOURCE_PATH, sourcePath) + } else { + intent.putExtra(SOURCE_URI, sourceUri.toString()) } if (outputPath.isNullOrBlank()) { @@ -111,6 +135,7 @@ class ImageEditorIntentBuilder @JvmOverloads constructor(private val context: Co const val BEAUTY_FEATURE = "beauty_feature" const val STICKER_FEATURE = "sticker_feature" + const val SOURCE_URI = "source_uri" const val SOURCE_PATH = "source_path" const val OUTPUT_PATH = "output_path" const val FORCE_PORTRAIT = "force_portrait" diff --git a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/utils/BitmapUtils.java b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/utils/BitmapUtils.java index 94d80db..b21fb84 100644 --- a/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/utils/BitmapUtils.java +++ b/ananas/src/main/java/iamutkarshtiwari/github/io/ananas/editimage/utils/BitmapUtils.java @@ -17,6 +17,8 @@ package iamutkarshtiwari.github.io.ananas.editimage.utils; import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; @@ -24,8 +26,10 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.media.ExifInterface; +import android.net.Uri; import android.os.Environment; import android.util.Log; import android.view.Display; @@ -33,21 +37,32 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + public class BitmapUtils { /** * Used to tag logs */ @SuppressWarnings("unused") private static final String TAG = "BitmapUtils"; + private static final Rect EMPTY_RECT = new Rect(); public static final long MAX_SZIE = 1024 * 512;// 500KB + /** Used to know the max texture size allowed to be rendered */ + private static int mMaxTextureSize; + public static int getOrientation(final String imagePath) { int rotate = 0; try { @@ -350,7 +365,6 @@ public static Bitmap getSampledBitmap(String filePath, int reqWidth, int reqHeig return BitmapFactory.decodeFile(filePath, options); } - public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; @@ -373,6 +387,170 @@ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWi return inSampleSize; } + public static Bitmap decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) { + + try { + ContentResolver resolver = context.getContentResolver(); + + // First decode with inJustDecodeBounds=true to check dimensions + BitmapFactory.Options options = decodeImageForOption(resolver, uri); + + if(options.outWidth == -1 && options.outHeight == -1) + throw new RuntimeException("File is not a picture"); + + // Calculate inSampleSize + options.inSampleSize = + Math.max( + calculateInSampleSizeByReqestedSize( + options.outWidth, options.outHeight, reqWidth, reqHeight), + calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight)); + + // Decode bitmap with inSampleSize set + Bitmap bitmap = decodeImage(resolver, uri, options); + + return bitmap; + + } catch (Exception e) { + throw new RuntimeException( + "Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e); + } + } + + /** Decode image from uri using "inJustDecodeBounds" to get the image dimensions. */ + private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri) + throws FileNotFoundException { + InputStream stream = null; + try { + stream = resolver.openInputStream(uri); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(stream, EMPTY_RECT, options); + options.inJustDecodeBounds = false; + return options; + } finally { + closeSafe(stream); + } + } + + /** + * Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise + * the inSampleSize until success. + */ + private static Bitmap decodeImage( + ContentResolver resolver, Uri uri, BitmapFactory.Options options) + throws FileNotFoundException { + do { + InputStream stream = null; + try { + stream = resolver.openInputStream(uri); + return BitmapFactory.decodeStream(stream, EMPTY_RECT, options); + } catch (OutOfMemoryError e) { + options.inSampleSize *= 2; + } finally { + closeSafe(stream); + } + } while (options.inSampleSize <= 512); + throw new RuntimeException("Failed to decode image: " + uri); + } + + /** + * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width + * larger than the requested height and width. + */ + private static int calculateInSampleSizeByReqestedSize( + int width, int height, int reqWidth, int reqHeight) { + int inSampleSize = 1; + if (height > reqHeight || width > reqWidth) { + while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + /** + * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width + * smaller than max texture size allowed for the device. + */ + private static int calculateInSampleSizeByMaxTextureSize(int width, int height) { + int inSampleSize = 1; + if (mMaxTextureSize == 0) { + mMaxTextureSize = getMaxTextureSize(); + } + if (mMaxTextureSize > 0) { + while ((height / inSampleSize) > mMaxTextureSize + || (width / inSampleSize) > mMaxTextureSize) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + /** + * Get the max size of bitmap allowed to be rendered on the device.
+ * http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit. + */ + private static int getMaxTextureSize() { + // Safe minimum default size + final int IMAGE_MAX_BITMAP_DIMENSION = 2048; + + try { + // Get EGL Display + EGL10 egl = (EGL10) EGLContext.getEGL(); + EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + // Initialise + int[] version = new int[2]; + egl.eglInitialize(display, version); + + // Query total number of configurations + int[] totalConfigurations = new int[1]; + egl.eglGetConfigs(display, null, 0, totalConfigurations); + + // Query actual list configurations + EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]]; + egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations); + + int[] textureSize = new int[1]; + int maximumTextureSize = 0; + + // Iterate through all the configurations to located the maximum texture size + for (int i = 0; i < totalConfigurations[0]; i++) { + // Only need to check for width since opengl textures are always squared + egl.eglGetConfigAttrib( + display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize); + + // Keep track of the maximum texture size + if (maximumTextureSize < textureSize[0]) { + maximumTextureSize = textureSize[0]; + } + } + + // Release + egl.eglTerminate(display); + + // Return largest texture size found, or default + return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION); + } catch (Exception e) { + return IMAGE_MAX_BITMAP_DIMENSION; + } + } + + /** + * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log + * exception thrown. + * + * @param closeable the closable object to close + */ + private static void closeSafe(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + public static boolean saveBitmap(Bitmap bm, String filePath) { File f = new File(filePath); if (f.exists()) {