diff --git a/build.gradle b/build.gradle index c983c8e80..40426bd16 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { } repositories { + mavenLocal() mavenCentral() maven { url 'https://maven.google.com/' @@ -15,7 +16,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.3.0' } } @@ -24,10 +25,10 @@ def isReleaseBuild() { } allprojects { - version = VERSION_NAME group = GROUP repositories { + mavenLocal() mavenCentral() maven { url 'https://maven.google.com/' @@ -35,7 +36,3 @@ allprojects { } } } - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/gradle.properties b/gradle.properties index 6a4787037..880efa2d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,6 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=2.2.9-native -VERSION_CODE=28 GROUP=com.yalantis POM_DESCRIPTION=Android Library for cropping images @@ -31,4 +29,8 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 POM_LICENCE_DIST=repo POM_DEVELOPER_ID=yalantis POM_DEVELOPER_NAME=Yalantis -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true + +# TODO remove after migration to kotlin +android.nonFinalResIds=false +android.nonTransitiveRClass=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 90c4baa40..757fe7347 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/mavenpush.gradle b/mavenpush.gradle index 4b99779e9..5f12f541d 100644 --- a/mavenpush.gradle +++ b/mavenpush.gradle @@ -20,23 +20,6 @@ def getRepositoryPassword() { return hasProperty('nexusPassword') ? nexusPassword : "" } - -tasks.register('androidJavadocs', Javadoc) { - source = android.sourceSets.main.java.sourceFiles -} - -tasks.register('androidJavadocsJar', Jar) { - classifier = 'javadoc' - //basename = artifact_id - from androidJavadocs.destinationDir -} - -tasks.register('androidSourcesJar', Jar) { - classifier = 'sources' - //basename = artifact_id - from android.sourceSets.main.java.sourceFiles -} - publishing { repositories { maven { @@ -47,40 +30,56 @@ publishing { } } } + repositories { + mavenLocal() + } publications { - maven(MavenPublication) { + nativeRelease(MavenPublication) { afterEvaluate { project -> - from components.release - artifact androidSourcesJar - artifact androidJavadocsJar - version = project.version + def baseName = project.android.defaultConfig.versionName + def suffix = project.android.productFlavors.getByName("native").versionNameSuffix + version = baseName + (suffix ? suffix : "") + from components.nativeRelease } + applyDefaultPomInfo(nativeRelease) + } + nonNativeRelease(MavenPublication) { + alias = true + afterEvaluate { project -> + def baseName = project.android.defaultConfig.versionName + def suffix = project.android.productFlavors.getByName("nonNative").versionNameSuffix + version = baseName + (suffix ? suffix : "") + from components.nonNativeRelease + } + applyDefaultPomInfo(nonNativeRelease) + } + } +} - pom { - name = POM_NAME - packaging = POM_PACKAGING - description = POM_DESCRIPTION - url = POM_URL - scm { - url = POM_SCM_URL - connection = POM_SCM_CONNECTION - developerConnection = POM_SCM_DEV_CONNECTION - } +def applyDefaultPomInfo(MavenPublication publication) { + publication.pom { + name = POM_NAME + packaging = POM_PACKAGING + description = POM_DESCRIPTION + url = POM_URL + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } - licenses { - license { - name = POM_LICENCE_NAME - url = POM_LICENCE_URL - distribution = POM_LICENCE_DIST - } - } + licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST + } + } - developers { - developer { - id = POM_DEVELOPER_ID - name = POM_DEVELOPER_NAME - } - } + developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME } } } @@ -89,7 +88,8 @@ publishing { signing { required { isReleaseBuild() && gradle.taskGraph.hasTask("publishing") } - sign publishing.publications.maven + sign publishing.publications.nativeRelease + sign publishing.publications.nonNativeRelease sign configurations.archives } diff --git a/sample/build.gradle b/sample/build.gradle index c3bdfe840..32a01e0b3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -4,12 +4,15 @@ android { compileSdk 33 defaultConfig { applicationId "com.yalantis.ucrop.sample" - minSdkVersion 14 + namespace "com.yalantis.ucrop.sample" + minSdkVersion 21 targetSdkVersion 33 versionCode 13 versionName "1.2.4" } - flavorDimensions "default" + buildFeatures { + buildConfig = true + } buildTypes { release { minifyEnabled false @@ -23,12 +26,22 @@ android { lintOptions { abortOnError false } + flavorDimensions += "processing_type" + flavorDimensions += "app_type" productFlavors { activity { - buildConfigField("int","RequestMode", "1") + dimension = "app_type" + buildConfigField("int","REQUEST_MODE", "1") } fragment { - buildConfigField("int","RequestMode", "2") + dimension = "app_type" + buildConfigField("int","REQUEST_MODE", "2") + } + create("native") { + dimension = "processing_type" + } + create("nonNative") { + dimension = "processing_type" } } } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index a9d0c2e53..846289d25 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java index a57566a51..43794dc5e 100644 --- a/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java +++ b/sample/src/main/java/com/yalantis/ucrop/sample/SampleActivity.java @@ -25,6 +25,13 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; + import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCropActivity; import com.yalantis.ucrop.UCropFragment; @@ -34,13 +41,6 @@ import java.util.Locale; import java.util.Random; -import androidx.annotation.ColorInt; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; - /** * Created by Oleksii Shliama (https://github.com/shliama). */ @@ -62,7 +62,7 @@ public class SampleActivity extends BaseActivity implements UCropFragmentCallbac private CheckBox mCheckBoxFreeStyleCrop; private Toolbar toolbar; private ScrollView settingsView; - private int requestMode = BuildConfig.RequestMode; + private int requestMode = BuildConfig.REQUEST_MODE; private UCropFragment fragment; private boolean mShowLoader; diff --git a/ucrop/build.gradle b/ucrop/build.gradle index c3c05fc7a..08f4b9332 100644 --- a/ucrop/build.gradle +++ b/ucrop/build.gradle @@ -1,15 +1,34 @@ apply plugin: 'com.android.library' +apply plugin: 'maven-publish' apply from: '../mavenpush.gradle' android { compileSdk 33 defaultConfig { - minSdkVersion 14 + minSdkVersion 21 targetSdkVersion 33 versionCode 27 - versionName "2.2.9-native" + versionName "2.3.0" + namespace "com.yalantis.ucrop" vectorDrawables.useSupportLibrary = true + + aarMetadata { + minCompileSdk = 21 + } + } + publishing { + singleVariant('nativeRelease') { + withSourcesJar() + withJavadocJar() + } + singleVariant('nonNativeRelease') { + withSourcesJar() + withJavadocJar() + } + } + buildFeatures { + buildConfig = true } buildTypes { release { @@ -17,6 +36,25 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + flavorDimensions += "processing_type" + productFlavors { + create("native") { + dimension = "processing_type" + versionNameSuffix = "-native" + buildConfigField("String", "TYPE", '"NATIVE"') + } + create("nonNative") { + dimension = "processing_type" + buildConfigField("String", "TYPE", '"NON_NATIVE"') + } + } + + androidComponents { + onVariants(selector().withFlavor('processing_type', 'nonNative')) { + packaging.jniLibs.excludes.add('**/**.so') + } + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -26,11 +64,6 @@ android { } resourcePrefix 'ucrop_' - - sourceSets.main { - jni.srcDirs = [] - } - } dependencies { diff --git a/ucrop/gradle.properties b/ucrop/gradle.properties index 839db2f0e..6bf94fe8e 100644 --- a/ucrop/gradle.properties +++ b/ucrop/gradle.properties @@ -1,3 +1,7 @@ POM_NAME=uCrop POM_ARTIFACT_ID=ucrop -POM_PACKAGING=aar \ No newline at end of file +POM_PACKAGING=aar + +# TODO remove after migration to kotlin +android.nonFinalResIds=false +android.nonTransitiveRClass=false \ No newline at end of file diff --git a/ucrop/src/main/java/com/yalantis/ucrop/backend/UCropBackendType.java b/ucrop/src/main/java/com/yalantis/ucrop/backend/UCropBackendType.java new file mode 100644 index 000000000..8dec5817d --- /dev/null +++ b/ucrop/src/main/java/com/yalantis/ucrop/backend/UCropBackendType.java @@ -0,0 +1,12 @@ +package com.yalantis.ucrop.backend; + +public enum UCropBackendType { + NATIVE("NATIVE"), + DEFAULT("NON_NATIVE"); + + public final String label; + + private UCropBackendType(String label) { + this.label = label; + } +} diff --git a/ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java b/ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java index 91815bd0f..8cfab0952 100755 --- a/ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/callback/BitmapLoadCallback.java @@ -1,15 +1,16 @@ package com.yalantis.ucrop.callback; import android.graphics.Bitmap; - -import com.yalantis.ucrop.model.ExifInfo; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.yalantis.ucrop.model.ExifInfo; + public interface BitmapLoadCallback { - void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull String imageInputPath, @Nullable String imageOutputPath); + void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull Uri imageInputUri, @Nullable Uri imageOutputUri); void onFailure(@NonNull Exception bitmapWorkerException); diff --git a/ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java b/ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java index 17d8a7e72..543417e9a 100644 --- a/ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/model/CropParameters.java @@ -1,6 +1,7 @@ package com.yalantis.ucrop.model; import android.graphics.Bitmap; +import android.net.Uri; /** * Created by Oleksii Shliama [https://github.com/shliama] on 6/21/16. @@ -14,6 +15,8 @@ public class CropParameters { private String mImageInputPath, mImageOutputPath; private ExifInfo mExifInfo; + private Uri mContentImageInputUri, mContentImageOutputUri; + public CropParameters(int maxResultImageSizeX, int maxResultImageSizeY, Bitmap.CompressFormat compressFormat, int compressQuality, @@ -55,4 +58,19 @@ public ExifInfo getExifInfo() { return mExifInfo; } + public Uri getContentImageInputUri() { + return mContentImageInputUri; + } + + public void setContentImageInputUri(Uri mContentImageInputUri) { + this.mContentImageInputUri = mContentImageInputUri; + } + + public Uri getContentImageOutputUri() { + return mContentImageOutputUri; + } + + public void setContentImageOutputUri(Uri mContentImageOutputUri) { + this.mContentImageOutputUri = mContentImageOutputUri; + } } diff --git a/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java index e779c28d3..c42c4b6de 100644 --- a/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapCropTask.java @@ -7,6 +7,10 @@ import android.os.AsyncTask; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.exifinterface.media.ExifInterface; + import com.yalantis.ucrop.callback.BitmapCropCallback; import com.yalantis.ucrop.model.CropParameters; import com.yalantis.ucrop.model.ExifInfo; @@ -17,10 +21,6 @@ import java.io.File; import java.io.IOException; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.exifinterface.media.ExifInterface; - /** * Crops part of image that fills the crop bounds. *

diff --git a/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java index 6222e92ba..04dc5b865 100755 --- a/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapLoadTask.java @@ -21,6 +21,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.util.Objects; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -40,7 +42,7 @@ public class BitmapLoadTask extends AsyncTask mContext; private Uri mInputUri; private Uri mOutputUri; private final int mRequiredWidth; @@ -69,7 +71,7 @@ public BitmapLoadTask(@NonNull Context context, @NonNull Uri inputUri, @Nullable Uri outputUri, int requiredWidth, int requiredHeight, BitmapLoadCallback loadCallback) { - mContext = context; + mContext = new WeakReference<>(context); mInputUri = inputUri; mOutputUri = outputUri; mRequiredWidth = requiredWidth; @@ -80,6 +82,11 @@ public BitmapLoadTask(@NonNull Context context, @Override @NonNull protected BitmapWorkerResult doInBackground(Void... params) { + Context context = mContext.get(); + if (context == null) { + return new BitmapWorkerResult(new NullPointerException("context is null")); + } + if (mInputUri == null) { return new BitmapWorkerResult(new NullPointerException("Input Uri cannot be null")); } @@ -100,7 +107,7 @@ protected BitmapWorkerResult doInBackground(Void... params) { boolean decodeAttemptSuccess = false; while (!decodeAttemptSuccess) { try { - InputStream stream = mContext.getContentResolver().openInputStream(mInputUri); + InputStream stream = context.getContentResolver().openInputStream(mInputUri); try { decodeSampledBitmap = BitmapFactory.decodeStream(stream, null, options); if (options.outWidth == -1 || options.outHeight == -1) { @@ -124,7 +131,7 @@ protected BitmapWorkerResult doInBackground(Void... params) { return new BitmapWorkerResult(new IllegalArgumentException("Bitmap could not be decoded from the Uri: [" + mInputUri + "]")); } - int exifOrientation = BitmapLoadUtils.getExifOrientation(mContext, mInputUri); + int exifOrientation = BitmapLoadUtils.getExifOrientation(context, mInputUri); int exifDegrees = BitmapLoadUtils.exifToDegrees(exifOrientation); int exifTranslation = BitmapLoadUtils.exifToTranslation(exifOrientation); @@ -174,16 +181,17 @@ private void copyFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws Nul throw new NullPointerException("Output Uri is null - cannot copy image"); } + Context context = Objects.requireNonNull(mContext.get(), "Context is null"); InputStream inputStream = null; OutputStream outputStream = null; try { - inputStream = mContext.getContentResolver().openInputStream(inputUri); + inputStream = context.getContentResolver().openInputStream(inputUri); if (inputStream == null) { throw new NullPointerException("InputStream for given input Uri is null"); } if (isContentUri(outputUri)) { - outputStream = mContext.getContentResolver().openOutputStream(outputUri); + outputStream = context.getContentResolver().openOutputStream(outputUri); } else { outputStream = new FileOutputStream(new File(outputUri.getPath())); } @@ -210,6 +218,11 @@ private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws throw new NullPointerException("Output Uri is null - cannot download image"); } + Context context = mContext.get(); + if (context == null) { + throw new NullPointerException("Context is null"); + } + OkHttpClient client = UCropHttpClientStore.INSTANCE.getClient(); BufferedSource source = null; @@ -225,7 +238,7 @@ private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws OutputStream outputStream; if (isContentUri(mOutputUri)) { - outputStream = mContext.getContentResolver().openOutputStream(outputUri); + outputStream = context.getContentResolver().openOutputStream(outputUri); } else { outputStream = new FileOutputStream(new File(outputUri.getPath())); } @@ -253,7 +266,7 @@ private void downloadFile(@NonNull Uri inputUri, @Nullable Uri outputUri) throws @Override protected void onPostExecute(@NonNull BitmapWorkerResult result) { if (result.mBitmapWorkerException == null) { - mBitmapLoadCallback.onBitmapLoaded(result.mBitmapResult, result.mExifInfo, mInputUri.getPath(), (mOutputUri == null) ? null : mOutputUri.getPath()); + mBitmapLoadCallback.onBitmapLoaded(result.mBitmapResult, result.mExifInfo, mInputUri, (mOutputUri == null) ? null : mOutputUri); } else { mBitmapLoadCallback.onFailure(result.mBitmapWorkerException); } @@ -282,4 +295,4 @@ private boolean isFileUri(Uri uri) { final String schema = uri.getScheme(); return schema.equals("file"); } -} \ No newline at end of file +} diff --git a/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapNonNativeCropTask.java b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapNonNativeCropTask.java new file mode 100644 index 000000000..f554e9383 --- /dev/null +++ b/ucrop/src/main/java/com/yalantis/ucrop/task/BitmapNonNativeCropTask.java @@ -0,0 +1,257 @@ +package com.yalantis.ucrop.task; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.exifinterface.media.ExifInterface; + +import com.yalantis.ucrop.callback.BitmapCropCallback; +import com.yalantis.ucrop.model.CropParameters; +import com.yalantis.ucrop.model.ExifInfo; +import com.yalantis.ucrop.model.ImageState; +import com.yalantis.ucrop.util.BitmapLoadUtils; +import com.yalantis.ucrop.util.FileUtils; +import com.yalantis.ucrop.util.ImageHeaderParser; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.ref.WeakReference; + +/** + * Crops part of image that fills the crop bounds. + *

+ * First image is downscaled if max size was set and if resulting image is larger that max size. + * Then image is rotated accordingly. + * Finally new Bitmap object is created and saved to file. + */ +public class BitmapNonNativeCropTask extends AsyncTask { + + private static final String TAG = "BitmapCropTask"; + + private static final String CONTENT_SCHEME = "content"; + + private final WeakReference mContext; + + private Bitmap mViewBitmap; + + private final RectF mCropRect; + private final RectF mCurrentImageRect; + + private float mCurrentScale, mCurrentAngle; + private final int mMaxResultImageSizeX, mMaxResultImageSizeY; + + private final Bitmap.CompressFormat mCompressFormat; + private final int mCompressQuality; + private final String mImageInputPath, mImageOutputPath; + private final Uri mImageInputUri, mImageOutputUri; + private final ExifInfo mExifInfo; + private final BitmapCropCallback mCropCallback; + + private int mCroppedImageWidth, mCroppedImageHeight; + private int cropOffsetX, cropOffsetY; + + public BitmapNonNativeCropTask(@NonNull Context context, @Nullable Bitmap viewBitmap, @NonNull ImageState imageState, @NonNull CropParameters cropParameters, + @Nullable BitmapCropCallback cropCallback) { + + mContext = new WeakReference<>(context); + + mViewBitmap = viewBitmap; + mCropRect = imageState.getCropRect(); + mCurrentImageRect = imageState.getCurrentImageRect(); + + mCurrentScale = imageState.getCurrentScale(); + mCurrentAngle = imageState.getCurrentAngle(); + mMaxResultImageSizeX = cropParameters.getMaxResultImageSizeX(); + mMaxResultImageSizeY = cropParameters.getMaxResultImageSizeY(); + + mCompressFormat = cropParameters.getCompressFormat(); + mCompressQuality = cropParameters.getCompressQuality(); + + mImageInputPath = cropParameters.getImageInputPath(); + mImageOutputPath = cropParameters.getImageOutputPath(); + mImageInputUri = cropParameters.getContentImageInputUri(); + mImageOutputUri = cropParameters.getContentImageOutputUri(); + mExifInfo = cropParameters.getExifInfo(); + + mCropCallback = cropCallback; + } + + @Override + @Nullable + protected Throwable doInBackground(Void... params) { + if (mViewBitmap == null) { + return new NullPointerException("ViewBitmap is null"); + } else if (mViewBitmap.isRecycled()) { + return new NullPointerException("ViewBitmap is recycled"); + } else if (mCurrentImageRect.isEmpty()) { + return new NullPointerException("CurrentImageRect is empty"); + } + + if (mImageOutputUri == null) { + return new NullPointerException("ImageOutputUri is null"); + } + + try { + crop(); + mViewBitmap = null; + } catch (Throwable throwable) { + return throwable; + } + + return null; + } + + private boolean crop() throws IOException { + Context context = mContext.get(); + if (context == null) { + return false; + } + + // Downsize if needed + if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) { + float cropWidth = mCropRect.width() / mCurrentScale; + float cropHeight = mCropRect.height() / mCurrentScale; + + if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) { + + float scaleX = mMaxResultImageSizeX / cropWidth; + float scaleY = mMaxResultImageSizeY / cropHeight; + float resizeScale = Math.min(scaleX, scaleY); + + Bitmap resizedBitmap = Bitmap.createScaledBitmap(mViewBitmap, + Math.round(mViewBitmap.getWidth() * resizeScale), + Math.round(mViewBitmap.getHeight() * resizeScale), false); + if (mViewBitmap != resizedBitmap) { + mViewBitmap.recycle(); + } + mViewBitmap = resizedBitmap; + + mCurrentScale /= resizeScale; + } + } + + // Rotate if needed + if (mCurrentAngle != 0) { + Matrix tempMatrix = new Matrix(); + tempMatrix.setRotate(mCurrentAngle, mViewBitmap.getWidth() / 2, mViewBitmap.getHeight() / 2); + + Bitmap rotatedBitmap = Bitmap.createBitmap(mViewBitmap, 0, 0, mViewBitmap.getWidth(), mViewBitmap.getHeight(), + tempMatrix, true); + if (mViewBitmap != rotatedBitmap) { + mViewBitmap.recycle(); + } + mViewBitmap = rotatedBitmap; + } + + cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale); + cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale); + mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale); + mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale); + + boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight); + Log.i(TAG, "Should crop: " + shouldCrop); + + if (shouldCrop) { + saveImage(Bitmap.createBitmap(mViewBitmap, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight)); + if (mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) { + copyExifForOutputFile(context); + } + return true; + } else { + FileUtils.copyFile(context ,mImageInputUri, mImageOutputUri); + return false; + } + } + + private void copyExifForOutputFile(Context context) throws IOException { + boolean hasImageInputUriContentSchema = BitmapLoadUtils.hasContentScheme(mImageInputUri); + boolean hasImageOutputUriContentSchema = BitmapLoadUtils.hasContentScheme(mImageOutputUri); + /* + * ImageHeaderParser.copyExif with output uri as a parameter + * uses ExifInterface constructor with FileDescriptor param for overriding output file exif info, + * which doesn't support ExitInterface.saveAttributes call for SDK lower than 21. + * + * See documentation for ImageHeaderParser.copyExif and ExifInterface.saveAttributes implementation. + */ + if (hasImageInputUriContentSchema && hasImageOutputUriContentSchema) { + ImageHeaderParser.copyExif(context, mCroppedImageWidth, mCroppedImageHeight, mImageInputUri, mImageOutputUri); + } else if (hasImageInputUriContentSchema) { + ImageHeaderParser.copyExif(context, mCroppedImageWidth, mCroppedImageHeight, mImageInputUri, mImageOutputPath); + } else if (hasImageOutputUriContentSchema) { + ExifInterface originalExif = new ExifInterface(mImageInputPath); + ImageHeaderParser.copyExif(context, originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputUri); + } else { + ExifInterface originalExif = new ExifInterface(mImageInputPath); + ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, mImageOutputPath); + } + } + + private void saveImage(@NonNull Bitmap croppedBitmap) { + Context context = mContext.get(); + if (context == null) { + return; + } + + OutputStream outputStream = null; + ByteArrayOutputStream outStream = null; + try { + outputStream = context.getContentResolver().openOutputStream(mImageOutputUri); + outStream = new ByteArrayOutputStream(); + croppedBitmap.compress(mCompressFormat, mCompressQuality, outStream); + outputStream.write(outStream.toByteArray()); + croppedBitmap.recycle(); + } catch (IOException exc) { + Log.e(TAG, exc.getLocalizedMessage()); + } finally { + BitmapLoadUtils.close(outputStream); + BitmapLoadUtils.close(outStream); + } + } + + /** + * Check whether an image should be cropped at all or just file can be copied to the destination path. + * For each 1000 pixels there is one pixel of error due to matrix calculations etc. + * + * @param width - crop area width + * @param height - crop area height + * @return - true if image must be cropped, false - if original image fits requirements + */ + private boolean shouldCrop(int width, int height) { + int pixelError = 1; + pixelError += Math.round(Math.max(width, height) / 1000f); + return (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) + || Math.abs(mCropRect.left - mCurrentImageRect.left) > pixelError + || Math.abs(mCropRect.top - mCurrentImageRect.top) > pixelError + || Math.abs(mCropRect.bottom - mCurrentImageRect.bottom) > pixelError + || Math.abs(mCropRect.right - mCurrentImageRect.right) > pixelError + || mCurrentAngle != 0; + } + + @Override + protected void onPostExecute(@Nullable Throwable t) { + if (mCropCallback != null) { + if (t == null) { + Uri uri; + + if (BitmapLoadUtils.hasContentScheme(mImageOutputUri)) { + uri = mImageOutputUri; + } else { + uri = Uri.fromFile(new File(mImageOutputPath)); + } + mCropCallback.onBitmapCropped(uri, cropOffsetX, cropOffsetY, mCroppedImageWidth, mCroppedImageHeight); + } else { + mCropCallback.onCropFailure(t); + } + } + } + +} diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java b/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java index 5e482f689..136bc145a 100755 --- a/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/util/BitmapLoadUtils.java @@ -12,24 +12,24 @@ import android.view.Display; import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.exifinterface.media.ExifInterface; + import com.yalantis.ucrop.callback.BitmapLoadCallback; import com.yalantis.ucrop.task.BitmapLoadTask; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.exifinterface.media.ExifInterface; /** * Created by Oleksii Shliama (https://github.com/shliama). */ public class BitmapLoadUtils { + private static final String CONTENT_SCHEME = "content"; + private static final String TAG = "BitmapLoadUtils"; public static void decodeBitmapInBackground(@NonNull Context context, @@ -173,4 +173,8 @@ public static void close(@Nullable Closeable c) { } } + public static boolean hasContentScheme(Uri uri) { + return uri != null && CONTENT_SCHEME.equals(uri.getScheme()); + } + } \ No newline at end of file diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java b/ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java index 913bf8c4e..72f4401c4 100644 --- a/ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/util/FileUtils.java @@ -28,15 +28,17 @@ import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.channels.FileChannel; import java.util.Locale; -import androidx.annotation.NonNull; - /** * @author Peli * @author paulburke (ipaulpro) @@ -215,6 +217,9 @@ else if ("file".equalsIgnoreCase(uri.getScheme())) { * In the event that the paths are the same, trying to copy one file to the other * will cause both files to become null. * Simply skipping this step if the paths are identical. + * + * @param pathFrom Represents the source file + * @param pathTo Represents the destination file */ public static void copyFile(@NonNull String pathFrom, @NonNull String pathTo) throws IOException { if (pathFrom.equalsIgnoreCase(pathTo)) { @@ -227,11 +232,45 @@ public static void copyFile(@NonNull String pathFrom, @NonNull String pathTo) th inputChannel = new FileInputStream(new File(pathFrom)).getChannel(); outputChannel = new FileOutputStream(new File(pathTo)).getChannel(); inputChannel.transferTo(0, inputChannel.size(), outputChannel); - inputChannel.close(); } finally { if (inputChannel != null) inputChannel.close(); if (outputChannel != null) outputChannel.close(); } } + /** + * Copies one file into the other with the given Uris. + * In the event that the Uris are the same, trying to copy one file to the other + * will cause both files to become null. + * Simply skipping this step if the paths are identical. + * + * @param context The context from which to require the {@link android.content.ContentResolver} + * @param uriFrom Represents the source file + * @param uriTo Represents the destination file + */ + public static void copyFile(@NonNull Context context, @NonNull Uri uriFrom, @NonNull Uri uriTo) throws IOException { + if (uriFrom.equals(uriTo)) { + return; + } + + InputStream isFrom = null; + OutputStream osTo = null; + try { + isFrom = context.getContentResolver().openInputStream(uriFrom); + osTo = context.getContentResolver().openOutputStream(uriTo); + + if (isFrom instanceof FileInputStream && osTo instanceof FileOutputStream) { + FileChannel inputChannel = ((FileInputStream)isFrom).getChannel(); + FileChannel outputChannel = ((FileOutputStream)osTo).getChannel(); + inputChannel.transferTo(0, inputChannel.size(), outputChannel); + } else { + throw new IllegalArgumentException("The input or output URI don't represent a file. " + + "uCrop requires then to represent files in order to work properly."); + } + } finally { + if (isFrom != null) isFrom.close(); + if (osTo != null) osTo.close(); + } + } + } diff --git a/ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java b/ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java index ebde7c5eb..965e422b4 100644 --- a/ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/util/ImageHeaderParser.java @@ -30,17 +30,23 @@ package com.yalantis.ucrop.util; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.RequiresApi; +import androidx.exifinterface.media.ExifInterface; + +import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; -import androidx.exifinterface.media.ExifInterface; - /** * A class for parsing the exif orientation from an image header. */ @@ -376,7 +382,169 @@ public int read(byte[] buffer, int byteCount) throws IOException { } } + /** + * Copy exif information represented by originalExif into the file represented by imageOutputPath. + * + * @param originalExif The exif info from the original input file + * @param width output image new width + * @param height output image new height + * @param imageOutputPath The path to the output file + */ public static void copyExif(ExifInterface originalExif, int width, int height, String imageOutputPath) { + + try { + ExifInterface newExif = new ExifInterface(imageOutputPath); + + copyExifAttributes(originalExif, newExif, width, height); + + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + } + } + + /** + * Copy exif information from the file represented by imageInputUri into the file represented by imageOutputPath and + * overwrites it's width and height with the given ones. + * + * @param context The context from which to obtain a content resolver + * @param width output image new width + * @param height output image new height + * @param imageInputUri The {@link Uri} that represents the input file + * @param imageOutputPath The path to the output file + */ + public static void copyExif(Context context, int width, int height, Uri imageInputUri, String imageOutputPath) { + if (context == null) { + Log.d(TAG, "context is null"); + return; + } + + InputStream ins = null; + try { + ins = context.getContentResolver().openInputStream(imageInputUri); + ExifInterface originalExif = new ExifInterface(ins); + + ExifInterface newExif = new ExifInterface(imageOutputPath); + + copyExifAttributes(originalExif, newExif, width, height); + + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } finally { + if (ins != null) { + try { + ins.close(); + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } + } + } + + } + + /** + * Copy exif information from the file represented by imageInputUri into the file represented by imageOutputUri and + * overwrites it's width and height with the given ones. + * This is done by {@link ExifInterface} through a seekable {@link FileDescriptor} and this is only possible + * starting on Lollipop version of Android. + * + * @param context The context from which to obtain a content resolver + * @param width output image new width + * @param height output image new height + * @param imageInputUri The {@link Uri} that represents the input file + * @param imageOutputUri The {@link Uri} that represents the output file + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void copyExif(Context context, int width, int height, Uri imageInputUri, Uri imageOutputUri) { + if (context == null) { + Log.d(TAG, "context is null"); + return; + } + + InputStream ins = null; + ParcelFileDescriptor outFd = null; + try { + ins = context.getContentResolver().openInputStream(imageInputUri); + ExifInterface originalExif = new ExifInterface(ins); + + outFd = context.getContentResolver().openFileDescriptor(imageOutputUri, "rw"); + ExifInterface newExif = new ExifInterface(outFd.getFileDescriptor()); + copyExifAttributes(originalExif, newExif, width, height); + + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } finally { + if (ins != null) { + try { + ins.close(); + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } + } + if (outFd != null) { + try { + outFd.close(); + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } + } + } + + } + + /** + * Copy exif information represented by originalExif into the file represented by imageOutputUri and overwrites it's + * width and height with the given ones. + * This is done by {@link ExifInterface} through a seekable {@link FileDescriptor} and this is only possible + * starting on Lollipop version of Android. + * + * @param context The context from which to obtain a content resolver + * @param originalExif The exif info from the original input file + * @param width output image new width + * @param height output image new height + * @param imageOutputUri The {@link Uri} that represents the output file + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static void copyExif(Context context, ExifInterface originalExif, int width, int height, Uri imageOutputUri) { + + if (context == null) { + Log.d(TAG, "context is null"); + return; + } + + ParcelFileDescriptor outFd = null; + try { + + // In order to the ExifInterface be able to validate JPEG info from the file, the FileDescriptor must to be + // opened en "rw" (read and write) mode + outFd = context.getContentResolver().openFileDescriptor(imageOutputUri, "rw"); + ExifInterface newExif = new ExifInterface(outFd.getFileDescriptor()); + + copyExifAttributes(originalExif, newExif, width, height); + + } catch (IOException e) { + Log.d(TAG, e.getMessage()); + } finally { + if (outFd != null) { + try { + outFd.close(); + } catch (IOException e) { + Log.d(TAG, e.getMessage(), e); + } + } + } + } + + /** + * Copy Exif attributes from the originalExif to the newExif and overwrites it's width and height with the given ones. + * + * @param originalExif Original exif information + * @param newExif New exif information + * @param width Width for overwriting into the newExif + * @param height Height for overwriting into the newExif + * @throws IOException If it occurs some IO error while trying to save the new exif info. + */ + private static void copyExifAttributes(ExifInterface originalExif, ExifInterface newExif, int width, int height) throws IOException { + String[] attributes = new String[]{ ExifInterface.TAG_F_NUMBER, ExifInterface.TAG_DATETIME, @@ -402,25 +570,18 @@ public static void copyExif(ExifInterface originalExif, int width, int height, S ExifInterface.TAG_WHITE_BALANCE }; - try { - ExifInterface newExif = new ExifInterface(imageOutputPath); - String value; - for (String attribute : attributes) { - value = originalExif.getAttribute(attribute); - if (!TextUtils.isEmpty(value)) { - newExif.setAttribute(attribute, value); - } + String value; + for (String attribute : attributes) { + value = originalExif.getAttribute(attribute); + if (!TextUtils.isEmpty(value)) { + newExif.setAttribute(attribute, value); } - newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width)); - newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height)); - newExif.setAttribute(ExifInterface.TAG_ORIENTATION, "0"); - - newExif.saveAttributes(); - - } catch (IOException e) { - Log.d(TAG, e.getMessage()); } - } + newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width)); + newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height)); + newExif.setAttribute(ExifInterface.TAG_ORIENTATION, "0"); + newExif.saveAttributes(); + } } diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java index 65d892d27..2539c1c9a 100644 --- a/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/view/CropImageView.java @@ -9,22 +9,25 @@ import android.os.AsyncTask; import android.util.AttributeSet; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yalantis.ucrop.BuildConfig; import com.yalantis.ucrop.R; +import com.yalantis.ucrop.backend.UCropBackendType; import com.yalantis.ucrop.callback.BitmapCropCallback; import com.yalantis.ucrop.callback.CropBoundsChangeListener; import com.yalantis.ucrop.model.CropParameters; import com.yalantis.ucrop.model.ImageState; import com.yalantis.ucrop.task.BitmapCropTask; +import com.yalantis.ucrop.task.BitmapNonNativeCropTask; import com.yalantis.ucrop.util.CubicEasing; import com.yalantis.ucrop.util.RectUtils; import java.lang.ref.WeakReference; import java.util.Arrays; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - /** * Created by Oleksii Shliama (https://github.com/shliama). *

@@ -84,8 +87,16 @@ public void cropAndSaveImage(@NonNull Bitmap.CompressFormat compressFormat, int compressFormat, compressQuality, getImageInputPath(), getImageOutputPath(), getExifInfo()); - new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + if (BuildConfig.TYPE.equals(UCropBackendType.NATIVE.label)) { + new BitmapCropTask(getViewBitmap(), imageState, cropParameters, cropCallback) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + cropParameters.setContentImageInputUri(getImageInputUri()); + cropParameters.setContentImageOutputUri(getImageOutputUri()); + + new BitmapNonNativeCropTask(getContext(), getViewBitmap(), imageState, cropParameters, cropCallback) + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } } /** diff --git a/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java b/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java index 91d348b2f..6d1dcc60b 100755 --- a/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java +++ b/ucrop/src/main/java/com/yalantis/ucrop/view/TransformImageView.java @@ -9,17 +9,17 @@ import android.util.AttributeSet; import android.util.Log; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; + import com.yalantis.ucrop.callback.BitmapLoadCallback; import com.yalantis.ucrop.model.ExifInfo; import com.yalantis.ucrop.util.BitmapLoadUtils; import com.yalantis.ucrop.util.FastBitmapDrawable; import com.yalantis.ucrop.util.RectUtils; -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; - /** * Created by Oleksii Shliama (https://github.com/shliama). *

@@ -53,6 +53,7 @@ public class TransformImageView extends AppCompatImageView { private int mMaxBitmapSize = 0; private String mImageInputPath, mImageOutputPath; + private Uri mImageInputUri, mImageOutputUri; private ExifInfo mExifInfo; /** @@ -126,6 +127,14 @@ public String getImageOutputPath() { return mImageOutputPath; } + public Uri getImageInputUri() { + return mImageInputUri; + } + + public Uri getImageOutputUri() { + return mImageOutputUri; + } + public ExifInfo getExifInfo() { return mExifInfo; } @@ -143,9 +152,11 @@ public void setImageUri(@NonNull Uri imageUri, @Nullable Uri outputUri) throws E new BitmapLoadCallback() { @Override - public void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull String imageInputPath, @Nullable String imageOutputPath) { - mImageInputPath = imageInputPath; - mImageOutputPath = imageOutputPath; + public void onBitmapLoaded(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo, @NonNull Uri imageInputUri, @Nullable Uri imageOutputUri) { + mImageInputUri = imageInputUri; + mImageOutputUri = imageOutputUri; + mImageInputPath = imageInputUri.getPath(); + mImageOutputPath = imageOutputUri != null ? imageOutputUri.getPath() : null; mExifInfo = exifInfo; mBitmapDecoded = true; diff --git a/ucrop/src/main/jni/Application.mk b/ucrop/src/main/jni/Application.mk index 198bfde4d..3554be09b 100644 --- a/ucrop/src/main/jni/Application.mk +++ b/ucrop/src/main/jni/Application.mk @@ -1,6 +1,6 @@ -APP_STL := gnustl_static +APP_STL := c++_static APP_ABI := armeabi armeabi-v7a x86 x86_64 arm64-v8a APP_CPPFLAGS += -frtti APP_CPPFLAGS += -fexceptions APP_CPPFLAGS += -DANDROID -APP_PLATFORM := android-14 \ No newline at end of file +APP_PLATFORM := android-21 \ No newline at end of file