├── .documentation └── art │ ├── cover.png │ ├── showcase-1.gif │ └── showcase-2.gif ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── publish-release.yml │ └── publish-snapshot.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── cropper ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── canhub │ │ │ └── cropper │ │ │ ├── BitmapCroppingWorkerJob.kt │ │ │ ├── BitmapLoadingWorkerJob.kt │ │ │ ├── BitmapUtils.kt │ │ │ ├── CropException.kt │ │ │ ├── CropFileProvider.kt │ │ │ ├── CropImage.kt │ │ │ ├── CropImageActivity.kt │ │ │ ├── CropImageAnimation.kt │ │ │ ├── CropImageContract.kt │ │ │ ├── CropImageContractOptions.kt │ │ │ ├── CropImageIntentChooser.kt │ │ │ ├── CropImageOptions.kt │ │ │ ├── CropImageView.kt │ │ │ ├── CropOverlayView.kt │ │ │ ├── CropWindowHandler.kt │ │ │ ├── CropWindowMoveHandler.kt │ │ │ ├── ParcelableUtils.kt │ │ │ └── utils │ │ │ ├── GetFilePathFromUri.kt │ │ │ └── GetUriForFile.kt │ └── res │ │ ├── drawable │ │ ├── ic_arrow_back_24.xml │ │ ├── ic_flip_24.xml │ │ ├── ic_rotate_left_24.xml │ │ └── ic_rotate_right_24.xml │ │ ├── layout │ │ ├── crop_image_activity.xml │ │ └── crop_image_view.xml │ │ ├── menu │ │ └── crop_image_menu.xml │ │ ├── values-ar │ │ └── strings.xml │ │ ├── values-cs │ │ └── strings.xml │ │ ├── values-da │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-es-rGT │ │ └── strings.xml │ │ ├── values-es │ │ └── strings.xml │ │ ├── values-et │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-gu │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-is │ │ └── strings.xml │ │ ├── values-it │ │ └── strings.xml │ │ ├── values-iw │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-kn │ │ └── strings.xml │ │ ├── values-ko │ │ └── strings.xml │ │ ├── values-ml │ │ └── strings.xml │ │ ├── values-ms │ │ └── strings.xml │ │ ├── values-nb │ │ └── strings.xml │ │ ├── values-nl │ │ └── strings.xml │ │ ├── values-pl │ │ └── strings.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values-ru-rRU │ │ └── strings.xml │ │ ├── values-sv │ │ └── strings.xml │ │ ├── values-ta │ │ └── strings.xml │ │ ├── values-te │ │ └── strings.xml │ │ ├── values-tr │ │ └── strings.xml │ │ ├── values-ur │ │ └── strings.xml │ │ ├── values-vi │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── attrs.xml │ │ └── strings.xml │ │ └── xml │ │ └── cropper_library_file_paths.xml │ └── test │ ├── kotlin │ └── com │ │ └── canhub │ │ └── cropper │ │ ├── BitmapUtilsTest.kt │ │ ├── ContractTestFragment.kt │ │ ├── CropImageContractTest.kt │ │ └── CropImageViewTest.kt │ ├── resources │ └── small-tree.jpg │ └── snapshots │ └── images │ └── com.canhub.cropper_CropImageViewTest_ovalBitmap.png ├── gradle.properties ├── gradle ├── gradlew ├── gradlew.bat ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint.xml ├── renovate.json ├── sample ├── build.gradle.kts ├── project.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── canhub │ │ └── cropper │ │ └── sample │ │ ├── MainActivity.kt │ │ ├── SampleApplication.kt │ │ ├── SampleCropFragment.kt │ │ ├── SampleCustomActivity.kt │ │ ├── SampleResultScreen.kt │ │ ├── SampleUsingImageViewFragment.kt │ │ └── optionsdialog │ │ └── SampleOptionsBottomSheet.kt │ └── res │ ├── color │ ├── chip_bg_states.xml │ ├── chip_text_states.xml │ ├── switch_thumb_selector.xml │ └── switch_track_selector.xml │ ├── drawable-nodpi │ ├── canhub_logo_purple.png │ ├── canhub_logo_white.png │ ├── cat.jpg │ └── checktile.png │ ├── drawable │ ├── backdrop.xml │ ├── bg_bottom_sheet_rounded.xml │ ├── bg_draggable_view.xml │ ├── bg_purple_gradient.xml │ ├── checkerboard.xml │ ├── ic_arrow_back_24.xml │ ├── ic_gear_24.xml │ ├── ic_launcher.xml │ ├── ic_mage_search_24.xml │ └── muted.xml │ ├── layout │ ├── activity_crop_result.xml │ ├── activity_main.xml │ ├── chip_corner_shape.xml │ ├── chip_crop_shape.xml │ ├── chip_guidelines.xml │ ├── chip_max_zoom.xml │ ├── chip_ratio.xml │ ├── chip_scale_type.xml │ ├── extended_activity.xml │ ├── fragment_camera.xml │ ├── fragment_crop_image_view.xml │ ├── fragment_options.xml │ ├── switch_auto_zoom.xml │ ├── switch_center_move.xml │ ├── switch_crop_overlay.xml │ ├── switch_crop_text_label.xml │ ├── switch_flip_horizontal.xml │ ├── switch_flip_vertical.xml │ ├── switch_multi_touch.xml │ └── switch_progress_bar.xml │ ├── menu │ └── main.xml │ ├── values-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── styles.xml │ └── themes.xml └── settings.gradle.kts /.documentation/art/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/.documentation/art/cover.png -------------------------------------------------------------------------------- /.documentation/art/showcase-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/.documentation/art/showcase-1.gif -------------------------------------------------------------------------------- /.documentation/art/showcase-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/.documentation/art/showcase-2.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_code_style=intellij_idea 3 | indent_size=2 4 | continuation_indent_size=2 5 | ij_kotlin_allow_trailing_comma=true 6 | ij_kotlin_allow_trailing_comma_on_call_site=true 7 | insert_final_newline=true 8 | ktlint_standard_annotation=disabled 9 | ktlint_standard_max-line-length=disabled 10 | ktlint_standard_filename=disabled 11 | ktlint_standard_spacing-between-declarations-with-annotations=disabled 12 | ktlint_standard_blank-line-between-when-conditions=disabled 13 | ktlint_standard_backing-property-naming=disabled 14 | ktlint_standard_kdoc=disabled 15 | ktlint_standard_condition-wrapping=disabled 16 | ktlint_experimental=enabled -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [canato, vanniktech] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request, merge_group] 4 | 5 | jobs: 6 | build: 7 | name: JDK ${{ matrix.java_version }} 8 | runs-on: macOS-latest 9 | 10 | strategy: 11 | matrix: 12 | java_version: [17] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Gradle Wrapper Validation 19 | uses: gradle/actions/wrapper-validation@v4 20 | 21 | - name: Setup gradle 22 | uses: gradle/gradle-build-action@v3 23 | 24 | - name: Install JDK ${{ matrix.java_version }} 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: 'zulu' 28 | java-version: ${{ matrix.java_version }} 29 | 30 | - name: Build with Gradle 31 | run: ./gradlew licensee ktlint testDebug build --stacktrace 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: macOS-latest 11 | if: github.repository == 'CanHub/Android-Image-Cropper' 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version: 17 22 | 23 | - name: Setup gradle 24 | uses: gradle/gradle-build-action@v3 25 | 26 | - name: Publish release 27 | run: ./gradlew publishAllPublicationsToMavenCentralRepository 28 | env: 29 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 30 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 31 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 32 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: macOS-latest 12 | if: github.repository == 'CanHub/Android-Image-Cropper' 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Install JDK 17 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: 17 23 | 24 | - name: Setup gradle 25 | uses: gradle/gradle-build-action@v3 26 | 27 | - name: Retrieve version 28 | run: | 29 | echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV 30 | 31 | - name: Publish snapshot 32 | run: ./gradlew publishAllPublicationsToMavenCentralRepository 33 | if: endsWith(env.VERSION_NAME, '-SNAPSHOT') 34 | env: 35 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 36 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | github.properties 2 | 3 | # built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # files for the dex VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Local configuration file (sdk path, etc) 19 | local.properties 20 | 21 | # Mac OS X internal files 22 | .DS_Store 23 | 24 | # Eclipse generated files/folders 25 | .metadata/ 26 | .settings/ 27 | 28 | #IntelliJ IDEA 29 | .idea/caches 30 | .idea/dictionaries 31 | .idea/libraries 32 | .idea/modules 33 | .idea/compiler.xml 34 | .idea/gradle.xml 35 | .idea/jarRepositories.xml 36 | .idea/misc.xml 37 | .idea/vcs.xml 38 | .idea/workspace.xml 39 | .idea/modules.xml 40 | /.idea/assetWizardSettings.xml 41 | *.iml 42 | *.ipr 43 | *.iws 44 | out 45 | 46 | # Gradle folder 47 | .gradle/ 48 | build/ 49 | 50 | 51 | .idea 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CanHub](.documentation/art/cover.png?raw=true)](https://github.com/canhub) 2 | 3 | Android Image Cropper 4 | ===================== 5 | 6 | - **Powerful** (Zoom, Rotation, Multi-Source) 7 | - **Customizable** (Shape, Limits, Style) 8 | - **Optimized** (Async, Sampling, Matrix) 9 | - **Simple** image cropping library for Android 10 | 11 | ![Crop demo](.documentation/art/showcase-1.gif?raw=true) 12 | 13 | ## Add to your project 14 | 15 | ```groovy 16 | dependencies { 17 | implementation("com.vanniktech:android-image-cropper:4.6.0") 18 | } 19 | ``` 20 | 21 | ## Using the Library 22 | 23 | There are 3 ways of using the library. Check out the sample app for all details. 24 | 25 | ### [1. Calling crop directly](./sample/src/main/kotlin/com/canhub/cropper/sample/SampleCropFragment.kt) 26 | 27 | **Note:** This way is deprecated and will be removed in future versions. The path forward is to write your own Activity, handle all the `Uri` stuff yourself and use `CropImageView`. 28 | 29 | ```kotlin 30 | class MainActivity : AppCompatActivity() { 31 | private val cropImage = registerForActivityResult(CropImageContract()) { result -> 32 | if (result.isSuccessful) { 33 | // Use the cropped image URI. 34 | val croppedImageUri = result.uriContent 35 | val croppedImageFilePath = result.getUriFilePath(this) // optional usage 36 | // Process the cropped image URI as needed. 37 | } else { 38 | // An error occurred. 39 | val exception = result.error 40 | // Handle the error. 41 | } 42 | } 43 | 44 | private fun startCrop() { 45 | // Start cropping activity with guidelines. 46 | cropImage.launch( 47 | CropImageContractOptions( 48 | cropImageOptions = CropImageOptions( 49 | guidelines = Guidelines.ON 50 | ) 51 | ) 52 | ) 53 | 54 | // Start cropping activity with gallery picker only. 55 | cropImage.launch( 56 | CropImageContractOptions( 57 | pickImageContractOptions = PickImageContractOptions( 58 | includeGallery = true, 59 | includeCamera = false 60 | ) 61 | ) 62 | ) 63 | 64 | // Start cropping activity for a pre-acquired image with custom settings. 65 | cropImage.launch( 66 | CropImageContractOptions( 67 | uri = imageUri, 68 | cropImageOptions = CropImageOptions( 69 | guidelines = Guidelines.ON, 70 | outputCompressFormat = Bitmap.CompressFormat.PNG 71 | ) 72 | ) 73 | ) 74 | } 75 | 76 | // Call the startCrop function when needed. 77 | } 78 | ``` 79 | 80 | ### [2. Using CropView](./sample/src/main/kotlin/com/canhub/cropper/sample/SampleUsingImageViewFragment.kt) 81 | 82 | **Note:** This is the only way forward, add `CropImageView` into your own activity and do whatever you wish. Checkout the sample for more details. 83 | 84 | ```xml 85 | 86 | 92 | ``` 93 | 94 | - Set image to crop 95 | 96 | ```kotlin 97 | cropImageView.setImageUriAsync(uri) 98 | // Or prefer using uri for performance and better user experience. 99 | cropImageView.setImageBitmap(bitmap) 100 | ``` 101 | 102 | - Get cropped image 103 | 104 | ```kotlin 105 | // Subscribe to async event using cropImageView.setOnCropImageCompleteListener(listener) 106 | cropImageView.getCroppedImageAsync() 107 | // Or. 108 | val cropped: Bitmap = cropImageView.getCroppedImage() 109 | ``` 110 | 111 | ### [3. Extend to make a custom activity](./sample/src/main/kotlin/com/canhub/cropper/sample/SampleCustomActivity.kt) 112 | 113 | **Note:** This way is also deprecated and will be removed in future versions. The path forward is to write your own Activity, handle all the `Uri` stuff yourself and use `CropImageView`. 114 | 115 | If you want to extend the `CropImageActivity` please be aware you will need to set up your `CropImageView` 116 | 117 | - Add `CropImageActivity` into your AndroidManifest.xml 118 | ```xml 119 | 120 | 124 | ``` 125 | 126 | - Set up your `CropImageView` after call `super.onCreate(savedInstanceState)` 127 | 128 | ```kotlin 129 | override fun onCreate(savedInstanceState: Bundle?) { 130 | super.onCreate(savedInstanceState) 131 | setCropImageView(binding.cropImageView) 132 | } 133 | ``` 134 | 135 | #### Custom dialog for image source pick 136 | 137 | When calling crop directly the library will prompt a dialog for the user choose between gallery or camera (If you keep both enable). 138 | We use the Android default AlertDialog for this. If you wanna customised it with your app theme you need to override the method `showImageSourceDialog(..)` when extending the activity _(above)_ 139 | 140 | ```kotlin 141 | override fun showImageSourceDialog(openSource: (Source) -> Unit) { 142 | super.showImageSourceDialog(openCamera) 143 | } 144 | ``` 145 | 146 | ## Posts 147 | 148 | - [Android cropping image from camera or gallery](https://canato.medium.com/android-cropping-image-from-camera-or-gallery-fbe732800b08) 149 | 150 | ## Migrating from Android Image Cropper 151 | 152 | Start by using [Version 4.3.3](https://github.com/CanHub/Android-Image-Cropper/releases/tag/4.3.3): 153 | 154 | ```groovy 155 | dependencies { 156 | implementation("com.vanniktech:android-image-cropper:4.3.3") 157 | } 158 | ``` 159 | 160 | ### Update all imports 161 | 162 | ```diff 163 | -import com.theartofdev.edmodo.cropper.CropImage 164 | -import com.theartofdev.edmodo.cropper.CropImageActivity 165 | +import com.canhub.cropper.CropImage 166 | +import com.canhub.cropper.CropImageActivity 167 | ``` 168 | 169 | ### Update all XML references 170 | 171 | ```diff 172 | - { 52 | apply(plugin = "org.gradle.android.cache-fix") 53 | } 54 | 55 | tasks.withType(Test::class.java).all { 56 | testLogging.exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cropper/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.dokka") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.android.library") 5 | id("org.jetbrains.kotlin.plugin.parcelize") 6 | id("com.vanniktech.maven.publish") 7 | id("app.cash.licensee") 8 | id("app.cash.paparazzi") 9 | } 10 | 11 | licensee { 12 | allow("Apache-2.0") 13 | } 14 | 15 | kotlin { 16 | jvmToolchain { 17 | languageVersion.set(JavaLanguageVersion.of(11)) 18 | } 19 | } 20 | 21 | android { 22 | namespace = "com.canhub.cropper" 23 | 24 | compileSdk = libs.versions.compileSdk.get().toInt() 25 | 26 | defaultConfig { 27 | minSdk = libs.versions.minSdk.get().toInt() 28 | } 29 | 30 | buildFeatures { 31 | viewBinding = true 32 | } 33 | 34 | compileOptions { 35 | sourceCompatibility = JavaVersion.VERSION_11 36 | targetCompatibility = JavaVersion.VERSION_11 37 | } 38 | 39 | testOptions { 40 | unitTests.isIncludeAndroidResources = true 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation(libs.androidx.activity.ktx) 46 | implementation(libs.androidx.appcompat) 47 | implementation(libs.androidx.core.ktx) 48 | implementation(libs.androidx.exifinterface) 49 | implementation(libs.kotlinx.coroutines.android) 50 | implementation(libs.kotlinx.coroutines.core) 51 | } 52 | 53 | dependencies { 54 | testImplementation(libs.androidx.fragment.testing) 55 | testImplementation(libs.androidx.test.junit) 56 | testImplementation(libs.junit) 57 | testImplementation(libs.mock) 58 | testImplementation(libs.robolectric) 59 | } 60 | 61 | // Workaround https://github.com/cashapp/paparazzi/issues/1231 62 | plugins.withId("app.cash.paparazzi") { 63 | // Defer until afterEvaluate so that testImplementation is created by Android plugin. 64 | afterEvaluate { 65 | dependencies.constraints { 66 | add("testImplementation", "com.google.guava:guava") { 67 | attributes { 68 | attribute( 69 | TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, 70 | objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.STANDARD_JVM), 71 | ) 72 | } 73 | because( 74 | "LayoutLib and sdk-common depend on Guava's -jre published variant." + 75 | "See https://github.com/cashapp/paparazzi/issues/906.", 76 | ) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cropper/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AndroidImageCropper 2 | POM_ARTIFACT_ID=android-image-cropper 3 | -------------------------------------------------------------------------------- /cropper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/BitmapCroppingWorkerJob.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.isActive 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | import java.lang.ref.WeakReference 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | internal class BitmapCroppingWorkerJob( 16 | private val context: Context, 17 | private val cropImageViewReference: WeakReference, 18 | private val uri: Uri?, 19 | private val bitmap: Bitmap?, 20 | private val cropPoints: FloatArray, 21 | private val degreesRotated: Int, 22 | private val orgWidth: Int, 23 | private val orgHeight: Int, 24 | private val fixAspectRatio: Boolean, 25 | private val aspectRatioX: Int, 26 | private val aspectRatioY: Int, 27 | private val reqWidth: Int, 28 | private val reqHeight: Int, 29 | private val flipHorizontally: Boolean, 30 | private val flipVertically: Boolean, 31 | private val options: CropImageView.RequestSizeOptions, 32 | private val saveCompressFormat: Bitmap.CompressFormat, 33 | private val saveCompressQuality: Int, 34 | private val customOutputUri: Uri?, 35 | ) : CoroutineScope { 36 | private var job: Job = Job() 37 | 38 | override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job 39 | 40 | fun start() { 41 | job = launch(Dispatchers.Default) { 42 | try { 43 | if (isActive) { 44 | val bitmapSampled: BitmapUtils.BitmapSampled 45 | when { 46 | uri != null -> { 47 | bitmapSampled = BitmapUtils.cropBitmap( 48 | context = context, 49 | loadedImageUri = uri, 50 | cropPoints = cropPoints, 51 | degreesRotated = degreesRotated, 52 | orgWidth = orgWidth, 53 | orgHeight = orgHeight, 54 | fixAspectRatio = fixAspectRatio, 55 | aspectRatioX = aspectRatioX, 56 | aspectRatioY = aspectRatioY, 57 | reqWidth = reqWidth, 58 | reqHeight = reqHeight, 59 | flipHorizontally = flipHorizontally, 60 | flipVertically = flipVertically, 61 | ) 62 | } 63 | bitmap != null -> { 64 | bitmapSampled = BitmapUtils.cropBitmapObjectHandleOOM( 65 | bitmap = bitmap, 66 | cropPoints = cropPoints, 67 | degreesRotated = degreesRotated, 68 | fixAspectRatio = fixAspectRatio, 69 | aspectRatioX = aspectRatioX, 70 | aspectRatioY = aspectRatioY, 71 | flipHorizontally = flipHorizontally, 72 | flipVertically = flipVertically, 73 | ) 74 | } 75 | else -> { 76 | onPostExecute( 77 | Result( 78 | bitmap = null, 79 | uri = null, 80 | error = null, 81 | sampleSize = 1, 82 | ), 83 | ) 84 | return@launch 85 | } 86 | } 87 | 88 | val resizedBitmap = BitmapUtils.resizeBitmap( 89 | bitmap = bitmapSampled.bitmap, 90 | reqWidth = reqWidth, 91 | reqHeight = reqHeight, 92 | options = options, 93 | ) 94 | 95 | launch(Dispatchers.IO) { 96 | val newUri = BitmapUtils.writeBitmapToUri( 97 | context = context, 98 | bitmap = resizedBitmap, 99 | compressFormat = saveCompressFormat, 100 | compressQuality = saveCompressQuality, 101 | customOutputUri = customOutputUri, 102 | ) 103 | 104 | onPostExecute( 105 | Result( 106 | bitmap = resizedBitmap, 107 | uri = newUri, 108 | sampleSize = bitmapSampled.sampleSize, 109 | error = null, 110 | ), 111 | ) 112 | } 113 | } 114 | } catch (throwable: Exception) { 115 | onPostExecute( 116 | Result( 117 | bitmap = null, 118 | uri = null, 119 | error = throwable, 120 | sampleSize = 1, 121 | ), 122 | ) 123 | } 124 | } 125 | } 126 | 127 | private suspend fun onPostExecute(result: Result) { 128 | withContext(Dispatchers.Main) { 129 | var completeCalled = false 130 | if (isActive) { 131 | cropImageViewReference.get()?.let { 132 | completeCalled = true 133 | it.onImageCroppingAsyncComplete(result) 134 | } 135 | } 136 | 137 | if (!completeCalled && result.bitmap != null) { 138 | // Fast release of unused bitmap. 139 | result.bitmap.recycle() 140 | } 141 | } 142 | } 143 | 144 | fun cancel() = job.cancel() 145 | 146 | internal data class Result( 147 | val bitmap: Bitmap?, 148 | val uri: Uri?, 149 | val error: Exception?, 150 | val sampleSize: Int, 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/BitmapLoadingWorkerJob.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.net.Uri 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.isActive 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | import java.lang.ref.WeakReference 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | internal class BitmapLoadingWorkerJob internal constructor( 16 | private val context: Context, 17 | cropImageView: CropImageView, 18 | internal val uri: Uri, 19 | ) : CoroutineScope { 20 | private val width: Int 21 | private val height: Int 22 | private val cropImageViewReference = WeakReference(cropImageView) 23 | private var job: Job = Job() 24 | 25 | override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job 26 | 27 | init { 28 | val metrics = cropImageView.resources.displayMetrics 29 | val densityAdjustment = if (metrics.density > 1) (1.0 / metrics.density) else 1.0 30 | width = (metrics.widthPixels * densityAdjustment).toInt() 31 | height = (metrics.heightPixels * densityAdjustment).toInt() 32 | } 33 | 34 | fun start() { 35 | job = launch(Dispatchers.Default) { 36 | try { 37 | if (isActive) { 38 | val decodeResult = BitmapUtils.decodeSampledBitmap( 39 | context = context, 40 | uri = uri, 41 | reqWidth = width, 42 | reqHeight = height, 43 | ) 44 | 45 | if (isActive) { 46 | val orientateResult = BitmapUtils.orientateBitmapByExif( 47 | bitmap = decodeResult.bitmap, 48 | context = context, 49 | uri = uri, 50 | ) 51 | 52 | onPostExecute( 53 | Result( 54 | uri = uri, 55 | bitmap = orientateResult.bitmap, 56 | loadSampleSize = decodeResult.sampleSize, 57 | degreesRotated = orientateResult.degrees, 58 | flipHorizontally = orientateResult.flipHorizontally, 59 | flipVertically = orientateResult.flipVertically, 60 | error = null, 61 | ), 62 | ) 63 | } 64 | } 65 | } catch (e: Exception) { 66 | onPostExecute( 67 | Result( 68 | uri = uri, 69 | bitmap = null, 70 | loadSampleSize = 0, 71 | degreesRotated = 0, 72 | flipHorizontally = false, 73 | flipVertically = false, 74 | error = e, 75 | ), 76 | ) 77 | } 78 | } 79 | } 80 | 81 | private suspend fun onPostExecute(result: Result) { 82 | withContext(Dispatchers.Main) { 83 | var completeCalled = false 84 | if (isActive) { 85 | cropImageViewReference.get()?.let { 86 | completeCalled = true 87 | it.onSetImageUriAsyncComplete(result) 88 | } 89 | } 90 | 91 | if (!completeCalled && result.bitmap != null) { 92 | // Fast release of unused bitmap. 93 | result.bitmap.recycle() 94 | } 95 | } 96 | } 97 | 98 | fun cancel() = job.cancel() 99 | 100 | internal data class Result( 101 | val uri: Uri, 102 | val bitmap: Bitmap?, 103 | val loadSampleSize: Int, 104 | val degreesRotated: Int, 105 | val flipHorizontally: Boolean, 106 | val flipVertically: Boolean, 107 | val error: Exception?, 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropException.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.net.Uri 4 | 5 | sealed class CropException(message: String) : Exception(message) { 6 | class Cancellation : CropException("$EXCEPTION_PREFIX cropping has been cancelled by the user") { 7 | internal companion object { 8 | private const val serialVersionUID: Long = -6896269134508601990L 9 | } 10 | } 11 | 12 | class FailedToLoadBitmap(uri: Uri, message: String?) : CropException("$EXCEPTION_PREFIX Failed to load sampled bitmap: $uri\r\n$message") { 13 | internal companion object { 14 | private const val serialVersionUID: Long = 7791142932960927332L 15 | } 16 | } 17 | 18 | class FailedToDecodeImage(uri: Uri) : CropException("$EXCEPTION_PREFIX Failed to decode image: $uri") { 19 | internal companion object { 20 | private const val serialVersionUID: Long = 3516154387706407275L 21 | } 22 | } 23 | 24 | internal companion object { 25 | private const val serialVersionUID: Long = 4933890872862969613L 26 | const val EXCEPTION_PREFIX = "crop:" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropFileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import androidx.core.content.FileProvider 4 | 5 | /** 6 | * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. 7 | * 8 | * See https://developer.android.com/guide/topics/manifest/provider-element.html for details. 9 | */ 10 | class CropFileProvider : FileProvider(R.xml.cropper_library_file_paths) { 11 | // This class intentionally left blank. 12 | // https://android-review.googlesource.com/c/platform/frameworks/support/+/1978527 13 | } 14 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropImage.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.PorterDuff 7 | import android.graphics.PorterDuffXfermode 8 | import android.graphics.Rect 9 | import android.graphics.RectF 10 | import android.net.Uri 11 | import android.os.Parcel 12 | import android.os.Parcelable 13 | import com.canhub.cropper.CropImageView.CropResult 14 | 15 | /** 16 | * Helper to simplify crop image work like starting pick-image activity and handling camera/gallery 17 | * intents.

18 | * The goal of the helper is to simplify the starting and most-common usage of image cropping and 19 | * not all-purpose all possible scenario one-to-rule-them-all code base. So feel free to use it as 20 | * is and as a wiki to make your own.

21 | * Added value you get out-of-the-box is some edge case handling that you may miss otherwise, like 22 | * the stupid-ass Android camera result URI that may differ from version to version and from device 23 | * to device. 24 | */ 25 | object CropImage { 26 | 27 | /** 28 | * The key used to pass crop image source URI to [CropImageActivity]. 29 | */ 30 | const val CROP_IMAGE_EXTRA_SOURCE = "CROP_IMAGE_EXTRA_SOURCE" 31 | 32 | /** 33 | * The key used to pass crop image options to [CropImageActivity]. 34 | */ 35 | const val CROP_IMAGE_EXTRA_OPTIONS = "CROP_IMAGE_EXTRA_OPTIONS" 36 | 37 | /** 38 | * The key used to pass crop image bundle data to [CropImageActivity]. 39 | */ 40 | const val CROP_IMAGE_EXTRA_BUNDLE = "CROP_IMAGE_EXTRA_BUNDLE" 41 | 42 | /** 43 | * The key used to pass crop image result data back from [CropImageActivity]. 44 | */ 45 | const val CROP_IMAGE_EXTRA_RESULT = "CROP_IMAGE_EXTRA_RESULT" 46 | 47 | /** 48 | * The result code used to return error from [CropImageActivity]. 49 | */ 50 | const val CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE = 204 51 | 52 | /** 53 | * Create a new bitmap that has all pixels beyond the oval shape transparent. Old bitmap is 54 | * recycled. 55 | */ 56 | fun toOvalBitmap(bitmap: Bitmap): Bitmap { 57 | val width = bitmap.width 58 | val height = bitmap.height 59 | val output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 60 | val canvas = Canvas(output) 61 | val color = -0xbdbdbe 62 | val paint = Paint() 63 | paint.isAntiAlias = true 64 | canvas.drawARGB(0, 0, 0, 0) 65 | paint.color = color 66 | val rect = RectF(0f, 0f, width.toFloat(), height.toFloat()) 67 | canvas.drawOval(rect, paint) 68 | paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) 69 | canvas.drawBitmap(bitmap, 0f, 0f, paint) 70 | bitmap.recycle() 71 | return output 72 | } 73 | 74 | /** 75 | * Result data of Crop Image Activity. 76 | */ 77 | open class ActivityResult : 78 | CropResult, 79 | Parcelable { 80 | 81 | constructor( 82 | originalUri: Uri?, 83 | uriContent: Uri?, 84 | error: Exception?, 85 | cropPoints: FloatArray?, 86 | cropRect: Rect?, 87 | rotation: Int, 88 | wholeImageRect: Rect?, 89 | sampleSize: Int, 90 | ) : super( 91 | originalBitmap = null, 92 | originalUri = originalUri, 93 | bitmap = null, 94 | uriContent = uriContent, 95 | error = error, 96 | cropPoints = cropPoints!!, 97 | cropRect = cropRect, 98 | wholeImageRect = wholeImageRect, 99 | rotation = rotation, 100 | sampleSize = sampleSize, 101 | ) 102 | 103 | @Suppress("DEPRECATION") 104 | protected constructor(`in`: Parcel) : super( 105 | originalBitmap = null, 106 | originalUri = `in`.readParcelable(Uri::class.java.classLoader) as Uri?, 107 | bitmap = null, 108 | uriContent = `in`.readParcelable(Uri::class.java.classLoader) as Uri?, 109 | error = `in`.readSerializable() as Exception?, 110 | cropPoints = `in`.createFloatArray()!!, 111 | cropRect = `in`.readParcelable(Rect::class.java.classLoader) as Rect?, 112 | wholeImageRect = `in`.readParcelable(Rect::class.java.classLoader) as Rect?, 113 | rotation = `in`.readInt(), 114 | sampleSize = `in`.readInt(), 115 | ) 116 | 117 | override fun writeToParcel(dest: Parcel, flags: Int) { 118 | dest.writeParcelable(originalUri, flags) 119 | dest.writeParcelable(uriContent, flags) 120 | dest.writeSerializable(error) 121 | dest.writeFloatArray(cropPoints) 122 | dest.writeParcelable(cropRect, flags) 123 | dest.writeParcelable(wholeImageRect, flags) 124 | dest.writeInt(rotation) 125 | dest.writeInt(sampleSize) 126 | } 127 | 128 | override fun describeContents(): Int = 0 129 | 130 | companion object { 131 | 132 | @JvmField 133 | val CREATOR: Parcelable.Creator = 134 | object : Parcelable.Creator { 135 | override fun createFromParcel(`in`: Parcel): ActivityResult = 136 | ActivityResult(`in`) 137 | 138 | override fun newArray(size: Int): Array = arrayOfNulls(size) 139 | } 140 | } 141 | } 142 | 143 | object CancelledResult : CropResult( 144 | originalBitmap = null, 145 | originalUri = null, 146 | bitmap = null, 147 | uriContent = null, 148 | error = CropException.Cancellation(), 149 | cropPoints = floatArrayOf(), 150 | cropRect = null, 151 | wholeImageRect = null, 152 | rotation = 0, 153 | sampleSize = 0, 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropImageAnimation.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.graphics.Matrix 4 | import android.graphics.RectF 5 | import android.view.animation.AccelerateDecelerateInterpolator 6 | import android.view.animation.Animation 7 | import android.view.animation.Animation.AnimationListener 8 | import android.view.animation.Transformation 9 | import android.widget.ImageView 10 | 11 | /** 12 | * Animation to handle smooth cropping image matrix transformation change, specifically for 13 | * zoom-in/out. 14 | */ 15 | internal class CropImageAnimation( 16 | private val imageView: ImageView, 17 | private val cropOverlayView: CropOverlayView, 18 | ) : Animation(), 19 | AnimationListener { 20 | 21 | private val startBoundPoints = FloatArray(8) 22 | private val endBoundPoints = FloatArray(8) 23 | private val startCropWindowRect = RectF() 24 | private val endCropWindowRect = RectF() 25 | private val startImageMatrix = FloatArray(9) 26 | private val endImageMatrix = FloatArray(9) 27 | 28 | init { 29 | duration = 300 30 | fillAfter = true 31 | interpolator = AccelerateDecelerateInterpolator() 32 | setAnimationListener(this) 33 | } 34 | 35 | fun setStartState(boundPoints: FloatArray, imageMatrix: Matrix) { 36 | reset() 37 | System.arraycopy(boundPoints, 0, startBoundPoints, 0, 8) 38 | startCropWindowRect.set(cropOverlayView.cropWindowRect) 39 | imageMatrix.getValues(startImageMatrix) 40 | } 41 | 42 | fun setEndState(boundPoints: FloatArray, imageMatrix: Matrix) { 43 | System.arraycopy(boundPoints, 0, endBoundPoints, 0, 8) 44 | endCropWindowRect.set(cropOverlayView.cropWindowRect) 45 | imageMatrix.getValues(endImageMatrix) 46 | } 47 | 48 | override fun applyTransformation(interpolatedTime: Float, t: Transformation) { 49 | val animRect = RectF().apply { 50 | left = (startCropWindowRect.left + (endCropWindowRect.left - startCropWindowRect.left) * interpolatedTime) 51 | top = (startCropWindowRect.top + (endCropWindowRect.top - startCropWindowRect.top) * interpolatedTime) 52 | right = (startCropWindowRect.right + (endCropWindowRect.right - startCropWindowRect.right) * interpolatedTime) 53 | bottom = (startCropWindowRect.bottom + (endCropWindowRect.bottom - startCropWindowRect.bottom) * interpolatedTime) 54 | } 55 | 56 | val animPoints = FloatArray(8) 57 | for (i in animPoints.indices) { 58 | animPoints[i] = (startBoundPoints[i] + (endBoundPoints[i] - startBoundPoints[i]) * interpolatedTime) 59 | } 60 | 61 | cropOverlayView.apply { 62 | cropWindowRect = animRect 63 | setBounds(animPoints, imageView.width, imageView.height) 64 | invalidate() 65 | } 66 | 67 | val animMatrix = FloatArray(9) 68 | for (i in animMatrix.indices) { 69 | animMatrix[i] = (startImageMatrix[i] + (endImageMatrix[i] - startImageMatrix[i]) * interpolatedTime) 70 | } 71 | 72 | imageView.apply { 73 | imageMatrix.setValues(animMatrix) 74 | invalidate() 75 | } 76 | } 77 | 78 | override fun onAnimationStart(animation: Animation) = Unit 79 | override fun onAnimationEnd(animation: Animation) { 80 | imageView.clearAnimation() 81 | } 82 | 83 | override fun onAnimationRepeat(animation: Animation) = Unit 84 | } 85 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropImageContract.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.canhub.cropper 4 | 5 | import android.app.Activity 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Bundle 9 | import androidx.activity.result.contract.ActivityResultContract 10 | 11 | /** 12 | * An [ActivityResultContract] to start an activity that allows the user to crop an image. 13 | * The UI can be customized using [CropImageOptions]. 14 | * If you do not provide an [CropImageContractOptions.uri] in the input the user will be asked to pick an image before cropping. 15 | */ 16 | @Deprecated( 17 | """ 18 | This ActivityResultContract is deprecated. 19 | Please either roll your own ActivityResultContract with the desired behavior or copy paste this. 20 | """, 21 | ) 22 | class CropImageContract : ActivityResultContract() { 23 | override fun createIntent(context: Context, input: CropImageContractOptions) = Intent(context, CropImageActivity::class.java).apply { 24 | putExtra( 25 | CropImage.CROP_IMAGE_EXTRA_BUNDLE, 26 | Bundle(2).apply { 27 | putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri) 28 | putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.cropImageOptions) 29 | }, 30 | ) 31 | } 32 | 33 | override fun parseResult( 34 | resultCode: Int, 35 | intent: Intent?, 36 | ): CropImageView.CropResult { 37 | val result = intent?.parcelable(CropImage.CROP_IMAGE_EXTRA_RESULT) 38 | 39 | return if (result == null || resultCode == Activity.RESULT_CANCELED) { 40 | CropImage.CancelledResult 41 | } else { 42 | result 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropImageContractOptions.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.net.Uri 4 | 5 | @Deprecated( 6 | """ 7 | This ActivityResultContract is deprecated. 8 | Please either roll your own ActivityResultContract with the desired behavior or copy paste this. 9 | """, 10 | ) 11 | data class CropImageContractOptions( 12 | val uri: Uri?, 13 | val cropImageOptions: CropImageOptions, 14 | ) 15 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/CropImageOptions.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Bitmap.CompressFormat 5 | import android.graphics.Color 6 | import android.graphics.Rect 7 | import android.net.Uri 8 | import android.os.Parcelable 9 | import android.util.TypedValue 10 | import androidx.annotation.ColorInt 11 | import androidx.annotation.DrawableRes 12 | import androidx.annotation.Px 13 | import com.canhub.cropper.CropImageView.CropShape 14 | import com.canhub.cropper.CropImageView.Guidelines 15 | import com.canhub.cropper.CropImageView.RequestSizeOptions 16 | import kotlinx.parcelize.Parcelize 17 | 18 | @Parcelize data class CropImageOptions @JvmOverloads constructor( 19 | @JvmField var imageSourceIncludeGallery: Boolean = true, 20 | @JvmField var imageSourceIncludeCamera: Boolean = true, 21 | @JvmField var cropShape: CropShape = CropShape.RECTANGLE, 22 | @JvmField var cornerShape: CropImageView.CropCornerShape = CropImageView.CropCornerShape.RECTANGLE, 23 | @JvmField @Px var cropCornerRadius: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, Resources.getSystem().displayMetrics), 24 | /** 25 | * An edge of the crop window will snap to the corresponding edge of a specified bounding box when 26 | * the crop window edge is less than or equal to this distance away from the bounding 27 | * box edge. 28 | */ 29 | @JvmField @Px var snapRadius: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, Resources.getSystem().displayMetrics), 30 | /** The radius of the touchable area around the handle. */ 31 | @JvmField @Px var touchRadius: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f, Resources.getSystem().displayMetrics), 32 | @JvmField var guidelines: Guidelines = Guidelines.ON, 33 | @JvmField var scaleType: CropImageView.ScaleType = CropImageView.ScaleType.FIT_CENTER, 34 | @JvmField var showCropOverlay: Boolean = true, 35 | @JvmField var showCropLabel: Boolean = false, 36 | @JvmField var showProgressBar: Boolean = true, 37 | @JvmField @ColorInt var progressBarColor: Int = Color.rgb(153, 51, 153), 38 | @JvmField var autoZoomEnabled: Boolean = true, 39 | /** Multitouch allows to resize and drag the cropping window at the same time. */ 40 | @JvmField var multiTouchEnabled: Boolean = false, 41 | /** If the crop window can be moved by dragging the crop window in the center. */ 42 | @JvmField var centerMoveEnabled: Boolean = true, 43 | /** If you are allowed to change the crop window by resizing it. */ 44 | @JvmField var canChangeCropWindow: Boolean = true, 45 | @JvmField var maxZoom: Int = 4, 46 | /** In percentage. 0.1 means 10% on both sides. */ 47 | @JvmField var initialCropWindowPaddingRatio: Float = 0.0f, 48 | @JvmField var fixAspectRatio: Boolean = false, 49 | @JvmField var aspectRatioX: Int = 1, 50 | @JvmField var aspectRatioY: Int = 1, 51 | @JvmField @Px var borderLineThickness: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, Resources.getSystem().displayMetrics), 52 | @JvmField @ColorInt var borderLineColor: Int = Color.argb(170, 255, 255, 255), 53 | @JvmField @Px var borderCornerThickness: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, Resources.getSystem().displayMetrics), 54 | @JvmField @Px var borderCornerOffset: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, Resources.getSystem().displayMetrics), 55 | @JvmField @Px var borderCornerLength: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14f, Resources.getSystem().displayMetrics), 56 | @JvmField @ColorInt var borderCornerColor: Int = Color.WHITE, 57 | @JvmField @ColorInt var circleCornerFillColorHexValue: Int = Color.WHITE, 58 | @JvmField @Px var guidelinesThickness: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1f, Resources.getSystem().displayMetrics), 59 | @JvmField @ColorInt var guidelinesColor: Int = Color.argb(170, 255, 255, 255), 60 | @JvmField @ColorInt var backgroundColor: Int = Color.argb(119, 0, 0, 0), 61 | @JvmField @Px var minCropWindowWidth: Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, Resources.getSystem().displayMetrics).toInt(), 62 | @JvmField @Px var minCropWindowHeight: Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42f, Resources.getSystem().displayMetrics).toInt(), 63 | @JvmField @Px var minCropResultWidth: Int = 40, 64 | @JvmField @Px var minCropResultHeight: Int = 40, 65 | @JvmField @Px var maxCropResultWidth: Int = 99999, 66 | @JvmField @Px var maxCropResultHeight: Int = 99999, 67 | @JvmField var activityTitle: CharSequence = "", 68 | @JvmField @ColorInt var activityMenuIconColor: Int = 0, 69 | @JvmField @ColorInt var activityMenuTextColor: Int? = null, 70 | /** The Android Uri to save the cropped image to. */ 71 | @JvmField var customOutputUri: Uri? = null, 72 | @JvmField var outputCompressFormat: CompressFormat = CompressFormat.JPEG, 73 | @JvmField var outputCompressQuality: Int = 90, 74 | /** The width to resize the cropped image to. */ 75 | @JvmField @Px var outputRequestWidth: Int = 0, 76 | /** The height to resize the cropped image to. */ 77 | @JvmField @Px var outputRequestHeight: Int = 0, 78 | @JvmField var outputRequestSizeOptions: RequestSizeOptions = RequestSizeOptions.NONE, 79 | /** If the result of crop image activity should not save the cropped image bitmap. */ 80 | @JvmField var noOutputImage: Boolean = false, 81 | /** Will be set after the image has loaded. */ 82 | @JvmField var initialCropWindowRectangle: Rect? = null, 83 | /** Will be set after the image has loaded. */ 84 | @JvmField var initialRotation: Int = -1, 85 | @JvmField var allowRotation: Boolean = true, 86 | @JvmField var allowFlipping: Boolean = true, 87 | @JvmField var allowCounterRotation: Boolean = false, 88 | /** The amount of degrees to rotate clockwise or counter-clockwise. */ 89 | @JvmField var rotationDegrees: Int = 90, 90 | @JvmField var flipHorizontally: Boolean = false, 91 | @JvmField var flipVertically: Boolean = false, 92 | @JvmField var cropMenuCropButtonTitle: CharSequence? = null, 93 | @JvmField @DrawableRes var cropMenuCropButtonIcon: Int = 0, 94 | @JvmField var skipEditing: Boolean = false, 95 | /** Enabling this option replaces the current AlertDialog to choose the image source with an Intent chooser. */ 96 | @JvmField var showIntentChooser: Boolean = false, 97 | @JvmField var intentChooserTitle: String? = null, 98 | /** Reorders intent list displayed with the app package names passed here. */ 99 | @JvmField var intentChooserPriorityList: List? = emptyList(), 100 | @JvmField @Px var cropperLabelTextSize: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20f, Resources.getSystem().displayMetrics), 101 | @JvmField @ColorInt var cropperLabelTextColor: Int = Color.WHITE, 102 | @JvmField var cropperLabelText: String? = "", 103 | @JvmField @ColorInt var activityBackgroundColor: Int = Color.WHITE, 104 | @JvmField @ColorInt var toolbarColor: Int? = null, 105 | @JvmField @ColorInt var toolbarTitleColor: Int? = null, 106 | @JvmField @ColorInt var toolbarBackButtonColor: Int? = null, 107 | @JvmField @ColorInt var toolbarTintColor: Int? = null, 108 | ) : Parcelable { 109 | init { 110 | require(maxZoom >= 0) { "Cannot set max zoom to a number < 1" } 111 | require(touchRadius >= 0) { "Cannot set touch radius value to a number <= 0 " } 112 | require(!(initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5)) { "Cannot set initial crop window padding value to a number < 0 or >= 0.5" } 113 | require(aspectRatioX > 0) { "Cannot set aspect ratio value to a number less than or equal to 0." } 114 | require(aspectRatioY > 0) { "Cannot set aspect ratio value to a number less than or equal to 0." } 115 | require(borderLineThickness >= 0) { "Cannot set line thickness value to a number less than 0." } 116 | require(borderCornerThickness >= 0) { "Cannot set corner thickness value to a number less than 0." } 117 | require(guidelinesThickness >= 0) { "Cannot set guidelines thickness value to a number less than 0." } 118 | require(minCropWindowHeight >= 0) { "Cannot set min crop window height value to a number < 0 " } 119 | require(minCropResultWidth >= 0) { "Cannot set min crop result width value to a number < 0 " } 120 | require(minCropResultHeight >= 0) { "Cannot set min crop result height value to a number < 0 " } 121 | require(maxCropResultWidth >= minCropResultWidth) { "Cannot set max crop result width to smaller value than min crop result width" } 122 | require(maxCropResultHeight >= minCropResultHeight) { "Cannot set max crop result height to smaller value than min crop result height" } 123 | require(outputRequestWidth >= 0) { "Cannot set request width value to a number < 0 " } 124 | require(outputRequestHeight >= 0) { "Cannot set request height value to a number < 0 " } 125 | require(!(rotationDegrees < 0 || rotationDegrees > DEGREES_360)) { "Cannot set rotation degrees value to a number < 0 or > 360" } 126 | } 127 | } 128 | 129 | internal const val DEGREES_360 = 360 130 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/ParcelableUtils.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | 7 | inline fun Bundle.parcelable(key: String): T? = when { 8 | // Does not work yet, https://issuetracker.google.com/issues/240585930 9 | // SDK_INT >= 33 -> getParcelable(key, T::class.java) 10 | else -> @Suppress("DEPRECATION") getParcelable(key) as? T 11 | } 12 | 13 | inline fun Intent.parcelable(key: String): T? = when { 14 | // Does not work yet, https://issuetracker.google.com/issues/240585930 15 | // SDK_INT >= 33 -> getParcelable(key, T::class.java) 16 | else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T 17 | } 18 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/utils/GetFilePathFromUri.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.utils 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.webkit.MimeTypeMap 7 | import java.io.File 8 | import java.io.FileOutputStream 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.io.OutputStream 12 | import java.text.SimpleDateFormat 13 | import java.util.Date 14 | import java.util.Locale.getDefault 15 | 16 | internal fun getFilePathFromUri(context: Context, uri: Uri, uniqueName: Boolean): String = 17 | if (uri.path?.contains("file://") == true) { 18 | uri.path!! 19 | } else { 20 | getFileFromContentUri(context, uri, uniqueName).path 21 | } 22 | 23 | private fun getFileFromContentUri(context: Context, contentUri: Uri, uniqueName: Boolean): File { 24 | // Preparing Temp file name 25 | val fileExtension = getFileExtension(context, contentUri) ?: "" 26 | val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", getDefault()).format(Date()) 27 | val fileName = ("temp_file_" + if (uniqueName) timeStamp else "") + ".$fileExtension" 28 | // Creating Temp file 29 | val tempFile = File(context.cacheDir, fileName) 30 | tempFile.createNewFile() 31 | // Initialize streams 32 | var oStream: FileOutputStream? = null 33 | var inputStream: InputStream? = null 34 | 35 | try { 36 | oStream = FileOutputStream(tempFile) 37 | inputStream = context.contentResolver.openInputStream(contentUri) 38 | 39 | inputStream?.let { copy(inputStream, oStream) } 40 | oStream.flush() 41 | } catch (e: Exception) { 42 | e.printStackTrace() 43 | } finally { 44 | // Close streams 45 | inputStream?.close() 46 | oStream?.close() 47 | } 48 | 49 | return tempFile 50 | } 51 | 52 | private fun getFileExtension(context: Context, uri: Uri): String? = 53 | if (uri.scheme == ContentResolver.SCHEME_CONTENT) { 54 | MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri)) 55 | } else { 56 | uri.path?.let { MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(it)).toString()) } 57 | } 58 | 59 | @Throws(IOException::class) 60 | private fun copy(source: InputStream, target: OutputStream) { 61 | val buf = ByteArray(8192) 62 | var length: Int 63 | while (source.read(buf).also { length = it } > 0) { 64 | target.write(buf, 0, length) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cropper/src/main/kotlin/com/canhub/cropper/utils/GetUriForFile.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.utils 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Build.VERSION.SDK_INT 6 | import android.util.Log 7 | import androidx.core.content.FileProvider 8 | import java.io.File 9 | import java.io.FileInputStream 10 | import java.io.FileOutputStream 11 | import java.io.InputStream 12 | import java.io.OutputStream 13 | import java.nio.file.Files 14 | import java.nio.file.Paths 15 | 16 | internal fun Context.authority() = "$packageName.cropper.fileprovider" 17 | 18 | /** 19 | * This class exist because of two issues. One is related to the new Scope Storage for OS 10+ 20 | * Where we should not access external storage anymore. Because of this we cannot get an external uri 21 | * 22 | * Using FileProvider to retrieve the path can return a value that is not the real one for some devices 23 | * This happens in specific devices and OSs. Because of this is needed to do a lot of if/else and 24 | * try/catch to just use the latest cases when needed. 25 | * 26 | * This code is not good, but work. I don't suggest anyone to reproduce it. 27 | * 28 | * Most of the devices will work fine, but if you worry about memory usage, please remember to clean 29 | * the cache from time to time, 30 | */ 31 | internal fun getUriForFile(context: Context, file: File): Uri { 32 | val authority = context.authority() 33 | try { 34 | Log.i("AIC", "Try get URI for scope storage - content://") 35 | return FileProvider.getUriForFile(context, authority, file) 36 | } catch (e: Exception) { 37 | try { 38 | Log.e("AIC", "${e.message}") 39 | Log.w( 40 | "AIC", 41 | "ANR Risk -- Copying the file the location cache to avoid 'external-files-path' bug for N+ devices", 42 | ) 43 | // Note: Periodically clear this cache 44 | val cacheFolder = File(context.cacheDir, "CROP_LIB_CACHE") 45 | val cacheLocation = File(cacheFolder, file.name) 46 | var input: InputStream? = null 47 | var output: OutputStream? = null 48 | try { 49 | input = FileInputStream(file) 50 | output = FileOutputStream(cacheLocation) // appending output stream 51 | input.copyTo(output) 52 | Log.i( 53 | "AIC", 54 | "Completed Android N+ file copy. Attempting to return the cached file", 55 | ) 56 | return FileProvider.getUriForFile(context, authority, cacheLocation) 57 | } catch (e: Exception) { 58 | Log.e("AIC", "${e.message}") 59 | Log.i("AIC", "Trying to provide URI manually") 60 | val path = "content://$authority/files/my_images/" 61 | 62 | if (SDK_INT >= 26) { 63 | Files.createDirectories(Paths.get(path)) 64 | } else { 65 | val directory = File(path) 66 | if (!directory.exists()) directory.mkdirs() 67 | } 68 | 69 | return Uri.parse("$path${file.name}") 70 | } finally { 71 | input?.close() 72 | output?.close() 73 | } 74 | } catch (e: Exception) { 75 | Log.e("AIC", "${e.message}") 76 | 77 | if (SDK_INT < 29) { 78 | val cacheDir = context.externalCacheDir 79 | cacheDir?.let { 80 | try { 81 | Log.i( 82 | "AIC", 83 | "Use External storage, do not work for OS 29 and above", 84 | ) 85 | return Uri.fromFile(File(cacheDir.path, file.absolutePath)) 86 | } catch (e: Exception) { 87 | Log.e("AIC", "${e.message}") 88 | } 89 | } 90 | } 91 | // If nothing else work we try 92 | Log.i("AIC", "Try get URI using file://") 93 | return Uri.fromFile(file) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cropper/src/main/res/drawable/ic_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /cropper/src/main/res/drawable/ic_flip_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /cropper/src/main/res/drawable/ic_rotate_left_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /cropper/src/main/res/drawable/ic_rotate_right_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /cropper/src/main/res/layout/crop_image_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /cropper/src/main/res/layout/crop_image_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 13 | 19 | 25 | 26 | -------------------------------------------------------------------------------- /cropper/src/main/res/menu/crop_image_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 15 | 20 | 21 | 24 | 27 | 28 | 29 | 33 | 34 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | أدر عكس اتجاه عقارب الساعة 4 | أدر 5 | قُصّ 6 | اقلب 7 | اقلب أفقيًا 8 | اقلب رأسيًا 9 | اختر مصدرًا 10 | الة تصوير 11 | صالة عرض 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-cs/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Otočit proti směru hodinových ručiček 4 | Otočit 5 | Oříznout 6 | Překlopit 7 | Překlopit vodorovně 8 | Překlopit svisle 9 | Vybrat zdroj 10 | Fotoaparát 11 | Galerie 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-da/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rotér mod uret 4 | Roter 5 | Brug billede 6 | Flip 7 | Flip horisontalt 8 | Flip vertikalt 9 | Vælg billede 10 | Kamera 11 | Galleri 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | gegen den Uhrzeigersinn drehen 4 | drehen 5 | zuschneiden 6 | spiegeln 7 | horizontal spiegeln 8 | vertikal spiegeln 9 | Quelle wählen 10 | Kamera 11 | Galerie 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-es-rGT/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Girar a la izquierda 4 | Girar a la derecha 5 | Cortar 6 | Dar la vuelta 7 | Voltear horizontalmente 8 | Voltear verticalmente 9 | Seleccionar fuente 10 | Cámara 11 | Galería 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rotar a la izquierda 4 | Rotar a la derecha 5 | Cortar 6 | Dar la vuelta 7 | Girar horizontalmente 8 | Girar verticalmente 9 | Seleccionar fuente 10 | Cámara 11 | Galería 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-et/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pööra vastupäeva 4 | Pööra 5 | Lõika 6 | Pööra ümber 7 | Peegelda horisontaalselt 8 | Peegelda vertikaalselt 9 | Valige allikas 10 | Kaamera 11 | Galeriis 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-fa/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | چرخش در جهت عقربه های ساعت 4 | چرخش 5 | بریدن (کراپ) 6 | آیینه کردن 7 | آیینه کردن به صورت افقی 8 | آیینه کردن به صورت عمودی 9 | منبع را انتخاب کنید 10 | دوربین 11 | گالری 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pivoter à gauche 4 | Pivoter à droite 5 | Redimensionner 6 | Retourner 7 | Retourner horizontalement 8 | Retourner verticalement 9 | Sélectionner la source 10 | Appareil photo 11 | Galerie 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-gu/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | વિરુદ્ધ ઘૂરો 4 | ઘૂરો 5 | કાપો 6 | ફ્લિપ 7 | જ્યાં સુધીનું ફ્લિપ 8 | લંબાઈમાં ફ્લિપ 9 | સ્રોત પસંદ કરો 10 | કેમેરા 11 | ગેલેરી 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-hi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | घड़ी की सुई के विपरीत दिशा में घुमाइए 4 | घुमाएँ 5 | काटे 6 | फ्लिप 7 | क्षैतिज फ्लिप 8 | लंबवत फ्लिप करें 9 | सोर्स चुनें 10 | कैमरा 11 | गैलरी 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-in/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Putar berlawanan arah jarum jam 4 | Putar 5 | Potong 6 | Balik 7 | Balik secara horizontal 8 | Balik secara vertikal 9 | Pilih sumber 10 | Kamera 11 | Galeri 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-is/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Snúa rangsælis 4 | Snúa 5 | Klippa 6 | Snúa við 7 | Snúa við lárétt 8 | Snúa við lóðrétt 9 | Veldu mynd 10 | Myndavél 11 | Myndasafn 12 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ruota in senso antiorario 4 | Ruota 5 | Ritaglia 6 | Capovolgi 7 | Capovolgi orizzontalmente 8 | Capovolgi verticalmente 9 | Seleziona origine 10 | Fotocamera 11 | Galleria 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-iw/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | סובב נגד כיוון השעון 4 | סובב 5 | חתוך 6 | הפוך 7 | הפוך אופקית 8 | הפוך אנכית 9 | בחר מקור 10 | מצלמה 11 | גלריה 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 左回転 4 | 右回転 5 | 切り取り 6 | 反転 7 | 左右反転 8 | 上下反転 9 | 画像を選択 10 | 撮影 11 | アルバム 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-kn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ಅಪ್ರದಕ್ಷಿಣಾಕಾರವಾಗಿ ತಿರುಗಿಸಿ 4 | ತಿರುಗಿಸಿ 5 | ಚಿತ್ರವನ್ನು ಕ್ರಾಪ್ ಮಾಡಿ 6 | ಫ್ಲಿಪ್ ಮಾಡಿ 7 | ಅಡ್ಡಲಾಗಿ ಫ್ಲಿಪ್ ಮಾಡಿ 8 | ಲಂಬವಾಗಿ ಫ್ಲಿಪ್ ಮಾಡಿ 9 | ಮೂಲವನ್ನು ಆಯ್ಕೆಮಾಡಿ 10 | ಕ್ಯಾಮೆರಾ 11 | ಗ್ಯಾಲರಿ 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 반시계 회전 4 | 회전 5 | 자르기 6 | 반전 7 | 좌우반전 8 | 상하반전 9 | 이미지 선택 10 | 촬영 11 | 앨범 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ml/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | എതിർ ഘടികാരദിശയിൽ തിരിക്കുക 4 | തിരിക്കുക 5 | മുറിക്കുക 6 | മറിക്കുക 7 | തിരശ്ചീനമായി മറിക്കുക 8 | ലംബമായി മറിക്കുക 9 | ഉറവിടം തിരഞ്ഞെടുക്കുക 10 | ക്യാമറ 11 | ഗാലറി 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ms/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Putar arah berlawanan jam 4 | Putar 5 | Potong 6 | Flip 7 | Flip melintang 8 | Flip menegak 9 | Pilih sumber 10 | Kamera 11 | Galeri 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-nb/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Roter teller med urviseren 4 | Roter 5 | Beskjær 6 | Vend 7 | Vend vannrett 8 | Vend loddrett 9 | Velg kilde 10 | Kamera 11 | Galleri 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-nl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tegen de klok in draaien 4 | Draaien 5 | Bijsnijden 6 | Spiegelen 7 | Horizontaal spiegelen 8 | Verticaal spiegelen 9 | Bron selecteren 10 | Camera 11 | Galerij 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Obróć w lewo 4 | Obróć 5 | Przytnij 6 | Odbij 7 | Odbij poziomo 8 | Odbij pionowo 9 | Wybierz źródło 10 | Aparat 11 | Galeria 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Girar para a esquerda 4 | Girar para a direita 5 | Cortar 6 | Espelhar 7 | Espelhar na horizontal 8 | Espelhar na vertifcal 9 | Escolher foto a partir de 10 | Câmera 11 | Galeria 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ru-rRU/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Повернуть налево 4 | Повернуть направо 5 | Обрезать 6 | Отразить 7 | Отразить по горизонтали 8 | Отразить по вертикали 9 | Выбрать источник 10 | Камера 11 | Галерея 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-sv/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rotera vänster 4 | Rotera höger 5 | Beskär 6 | Vänd 7 | Vänd horisontellt 8 | Vänd vertikalt 9 | Välj bild 10 | Kamera 11 | Galleri 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ta/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | எதிர் கடிகார திசையில் சுழற்று 4 | சுழற்று 5 | படத்தை செதுக்கு 6 | புரட்டவும் 7 | கிடைமட்டமாக புரட்டவும் 8 | செங்குத்தாக புரட்டவும் 9 | மூலத்தைத் தேர்ந்தெடுக்கவும் 10 | கேமரா 11 | தொகுப்பு 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-te/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | అపసవ్య దిశలో తిప్పండి 4 | తిప్పండి 5 | చిత్రాన్ని కత్తిరించండి 6 | ఫ్లిప్ 7 | క్షితిజ సమాంతరంగా ఫ్లిప్ 8 | నిలువుగా ఫ్లిప్ 9 | మూలాన్ని ఎంచుకోండి 10 | కెమెరా 11 | గ్యాలరీ 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Saat yönünde döndür 4 | döndürmek 5 | Kırp 6 | çevir 7 | Yatay olarak çevir 8 | Dikey olarak çevir 9 | Kaynağı seçin 10 | Kamera 11 | Galeriden 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-ur/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | گھڑی وار گھڑی گھومیں 4 | گھمائیں 5 | تصویر کو تراشیں 6 | پلٹائیں 7 | افقی پلٹائیں 8 | عمودی طور پر پلٹائیں 9 | ذریعہ منتخب کریں 10 | کیمرہ 11 | گالری 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-vi/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Xoay theo chiều kim đồng hồ 4 | Xoay 5 | Cắt 6 | Lật 7 | Lật theo chiều ngang 8 | Lật theo chiều dọc 9 | Chọn nguồn 10 | Camera 11 | Thư viện 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 逆时针旋转 4 | 旋转 5 | 裁切 6 | 翻转 7 | 水平翻转 8 | 垂直翻转 9 | 选择来源 10 | 相机 11 | 相册 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 逆時針旋轉 4 | 旋轉 5 | 裁切 6 | 翻轉 7 | 水平翻轉 8 | 垂直翻轉 9 | 選擇來源 10 | 相機 11 | 相簿 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 逆时针旋转 4 | 旋转 5 | 裁剪 6 | 翻转 7 | 水平翻转 8 | 垂直翻转 9 | 选择来源 10 | 相机 11 | 图库 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /cropper/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rotate counter clockwise 4 | Rotate 5 | Crop 6 | Flip 7 | Flip horizontally 8 | Flip vertically 9 | Select source 10 | Camera 11 | Gallery 12 | 13 | -------------------------------------------------------------------------------- /cropper/src/main/res/xml/cropper_library_file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /cropper/src/test/kotlin/com/canhub/cropper/BitmapUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import io.mockk.mockkObject 4 | import io.mockk.unmockkObject 5 | import org.junit.After 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Before 8 | import org.junit.Test 9 | 10 | private const val RECT_WIDTH = 10f 11 | private const val RECT_HEIGHT = 13f 12 | private const val RECT_LEFT = 0f 13 | private const val RECT_RIGHT = 10f 14 | private const val RECT_BOTTOM = 15f 15 | private const val RECT_TOP = 2f 16 | private const val RECT_CENTER_X = 5f 17 | private const val RECT_CENTER_Y = 8.5f 18 | private val RECTANGLE_IMAGE_POINTS: FloatArray = floatArrayOf(RECT_LEFT, RECT_TOP, RECT_WIDTH, RECT_TOP, RECT_WIDTH, RECT_BOTTOM, RECT_LEFT, RECT_BOTTOM) 19 | private val LOW_RECT_POINTS: FloatArray = floatArrayOf(RECT_LEFT) 20 | 21 | class BitmapUtilsTest { 22 | 23 | @Before 24 | fun setup() { 25 | mockkObject(BitmapUtils) 26 | } 27 | 28 | @After 29 | fun teardown() { 30 | unmockkObject(BitmapUtils) 31 | } 32 | 33 | @Test 34 | fun `WHEN float array of rectangle points is provided, THEN result should be left value of the bounding rectangle`() { 35 | // WHEN 36 | val rectLeft = BitmapUtils.getRectLeft(RECTANGLE_IMAGE_POINTS) 37 | 38 | // THEN 39 | assertEquals(RECT_LEFT, rectLeft) 40 | } 41 | 42 | @Test 43 | fun `WHEN float array of rectangle points is provided, THEN result should be right value of the bounding rectangle`() { 44 | // WHEN 45 | val rectRight = BitmapUtils.getRectRight(RECTANGLE_IMAGE_POINTS) 46 | 47 | // THEN 48 | assertEquals(RECT_RIGHT, rectRight) 49 | } 50 | 51 | @Test 52 | fun `WHEN float array of rectangle points is provided, THEN result should be bottom value of the bounding rectangle`() { 53 | // WHEN 54 | val rectBottom = BitmapUtils.getRectBottom(RECTANGLE_IMAGE_POINTS) 55 | 56 | // THEN 57 | assertEquals(RECT_BOTTOM, rectBottom) 58 | } 59 | 60 | @Test 61 | fun `WHEN float array of rectangle points is provided, THEN result should be top value of the bounding rectangle`() { 62 | // WHEN 63 | val rectTop = BitmapUtils.getRectTop(RECTANGLE_IMAGE_POINTS) 64 | 65 | // THEN 66 | assertEquals(RECT_TOP, rectTop) 67 | } 68 | 69 | @Test 70 | fun `WHEN float array of rectangle points is provided, THEN result should be rect height`() { 71 | // WHEN 72 | val rectHeight = BitmapUtils.getRectHeight(RECTANGLE_IMAGE_POINTS) 73 | 74 | // THEN 75 | assertEquals(RECT_HEIGHT, rectHeight) 76 | } 77 | 78 | @Test 79 | fun `WHEN float array of rectangle points is provided, THEN result should be rect width`() { 80 | // WHEN 81 | val rectWidth = BitmapUtils.getRectWidth(RECTANGLE_IMAGE_POINTS) 82 | 83 | // THEN 84 | assertEquals(RECT_WIDTH, rectWidth) 85 | } 86 | 87 | @Test 88 | fun `WHEN float array of rectangle points is provided, THEN result should be rect centerX`() { 89 | // WHEN 90 | val rectCenterX = BitmapUtils.getRectCenterX(RECTANGLE_IMAGE_POINTS) 91 | 92 | // THEN 93 | assertEquals(RECT_CENTER_X, rectCenterX) 94 | } 95 | 96 | @Test 97 | fun `WHEN float array of rectangle points is provided, THEN result should be rect centerY`() { 98 | // WHEN 99 | val rectCenterY = BitmapUtils.getRectCenterY(RECTANGLE_IMAGE_POINTS) 100 | 101 | // THEN 102 | assertEquals(RECT_CENTER_Y, rectCenterY) 103 | } 104 | 105 | @Test(expected = ArrayIndexOutOfBoundsException::class) 106 | fun `WHEN low rectangle points is provided to getRectCenterY, THEN resultArrayOutOfIndexException`() { 107 | BitmapUtils.getRectCenterY(LOW_RECT_POINTS) 108 | } 109 | 110 | @Test(expected = ArrayIndexOutOfBoundsException::class) 111 | fun `WHEN low rectangle points is provided to getRectCenterX, THEN resultArrayOutOfIndexException`() { 112 | BitmapUtils.getRectCenterX(LOW_RECT_POINTS) 113 | } 114 | 115 | @Test(expected = ArrayIndexOutOfBoundsException::class) 116 | fun `WHEN low rectangle points is provided to getRectLeft, THEN resultArrayOutOfIndexException`() { 117 | BitmapUtils.getRectLeft(LOW_RECT_POINTS) 118 | } 119 | 120 | @Test(expected = ArrayIndexOutOfBoundsException::class) 121 | fun `WHEN low rectangle points is provided to getRectRight, THEN resultArrayOutOfIndexException`() { 122 | BitmapUtils.getRectRight(LOW_RECT_POINTS) 123 | } 124 | 125 | @Test(expected = ArrayIndexOutOfBoundsException::class) 126 | fun `WHEN low rectangle points is provided getRectTop, THEN resultArrayOutOfIndexException`() { 127 | BitmapUtils.getRectTop(LOW_RECT_POINTS) 128 | } 129 | 130 | @Test(expected = ArrayIndexOutOfBoundsException::class) 131 | fun `WHEN low rectangle points is provided getRectBottom, THEN resultArrayOutOfIndexException`() { 132 | BitmapUtils.getRectBottom(LOW_RECT_POINTS) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cropper/src/test/kotlin/com/canhub/cropper/ContractTestFragment.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.canhub.cropper 4 | 5 | import android.content.Intent 6 | import androidx.activity.result.ActivityResultLauncher 7 | import androidx.activity.result.ActivityResultRegistry 8 | import androidx.fragment.app.Fragment 9 | 10 | class ContractTestFragment( 11 | registry: ActivityResultRegistry, 12 | ) : Fragment() { 13 | 14 | var cropResult: CropImageView.CropResult? = null 15 | 16 | val cropImage: ActivityResultLauncher = registerForActivityResult(CropImageContract(), registry) { result -> 17 | this.cropResult = result 18 | } 19 | 20 | fun cropImage(input: CropImageContractOptions) { 21 | cropImage.launch(input) 22 | } 23 | 24 | fun cropImageIntent(input: CropImageContractOptions): Intent = cropImage.contract.createIntent(requireContext(), input) 25 | } 26 | -------------------------------------------------------------------------------- /cropper/src/test/kotlin/com/canhub/cropper/CropImageContractTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.canhub.cropper 4 | 5 | import android.app.Activity 6 | import android.content.Intent 7 | import android.graphics.Bitmap 8 | import android.graphics.Color 9 | import android.graphics.Rect 10 | import androidx.activity.result.ActivityResultRegistry 11 | import androidx.activity.result.contract.ActivityResultContract 12 | import androidx.core.app.ActivityOptionsCompat 13 | import androidx.core.net.toUri 14 | import androidx.fragment.app.testing.launchFragmentInContainer 15 | import androidx.test.ext.junit.runners.AndroidJUnit4 16 | import org.junit.Assert.assertEquals 17 | import org.junit.Test 18 | import org.junit.runner.RunWith 19 | 20 | @Suppress("DEPRECATION") 21 | @RunWith(AndroidJUnit4::class) 22 | class CropImageContractTest { 23 | 24 | @Test 25 | fun `WHEN providing invalid options THEN cropping should crash`() { 26 | // GIVEN 27 | var result: Exception? = null 28 | val expected: IllegalArgumentException = IllegalArgumentException() 29 | var fragment: ContractTestFragment? = null 30 | val testRegistry = object : ActivityResultRegistry() { 31 | override fun onLaunch( 32 | requestCode: Int, 33 | contract: ActivityResultContract, 34 | input: I, 35 | options: ActivityOptionsCompat?, 36 | ) { 37 | dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) 38 | } 39 | } 40 | 41 | with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { 42 | onFragment { fragment = it } 43 | } 44 | // WHEN 45 | try { 46 | fragment?.cropImageIntent(CropImageContractOptions(null, CropImageOptions().copy(maxZoom = -10))) 47 | } catch (e: Exception) { 48 | result = e 49 | } 50 | // THEN 51 | assertEquals(expected.javaClass, result?.javaClass) 52 | } 53 | 54 | @Test 55 | fun `WHEN cropping is cancelled by user, THEN result should be cancelled`() { 56 | // GIVEN 57 | val expected = CropImage.CancelledResult 58 | var fragment: ContractTestFragment? = null 59 | val testRegistry = object : ActivityResultRegistry() { 60 | override fun onLaunch( 61 | requestCode: Int, 62 | contract: ActivityResultContract, 63 | input: I, 64 | options: ActivityOptionsCompat?, 65 | ) { 66 | dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) 67 | } 68 | } 69 | 70 | with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { 71 | onFragment { fragment = it } 72 | } 73 | // WHEN 74 | fragment?.cropImage(CropImageContractOptions(null, CropImageOptions())) 75 | // THEN 76 | assertEquals(expected, fragment?.cropResult) 77 | } 78 | 79 | @Test 80 | fun `WHEN cropping succeeds, THEN result should be successful`() { 81 | // GIVEN 82 | var fragment: ContractTestFragment? = null 83 | val expected = CropImage.ActivityResult( 84 | originalUri = "content://original".toUri(), 85 | uriContent = "content://content".toUri(), 86 | error = null, 87 | cropPoints = floatArrayOf(), 88 | cropRect = Rect(1, 2, 3, 4), 89 | rotation = 45, 90 | wholeImageRect = Rect(10, 20, 0, 0), 91 | sampleSize = 0, 92 | ) 93 | val testRegistry = object : ActivityResultRegistry() { 94 | override fun onLaunch( 95 | requestCode: Int, 96 | contract: ActivityResultContract, 97 | input: I, 98 | options: ActivityOptionsCompat?, 99 | ) { 100 | val intent = Intent() 101 | intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, expected) 102 | 103 | dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) 104 | } 105 | } 106 | 107 | with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { 108 | onFragment { fragment = it } 109 | } 110 | // WHEN 111 | fragment?.cropImage(CropImageContractOptions(null, CropImageOptions())) 112 | // THEN 113 | assertEquals(expected, fragment?.cropResult) 114 | } 115 | 116 | @Test 117 | fun `WHEN starting crop with all options, THEN intent should contain these options`() { 118 | // GIVEN 119 | var cropImageIntent: Intent? = null 120 | val expectedClassName = CropImageActivity::class.java.name 121 | val expectedSource = "file://testInput".toUri() 122 | val options = CropImageContractOptions( 123 | expectedSource, 124 | CropImageOptions( 125 | cropShape = CropImageView.CropShape.OVAL, 126 | snapRadius = 1f, 127 | touchRadius = 2f, 128 | guidelines = CropImageView.Guidelines.ON_TOUCH, 129 | scaleType = CropImageView.ScaleType.CENTER, 130 | showCropOverlay = true, 131 | autoZoomEnabled = false, 132 | multiTouchEnabled = true, 133 | centerMoveEnabled = false, 134 | maxZoom = 17, 135 | initialCropWindowPaddingRatio = 0.2f, 136 | fixAspectRatio = true, 137 | aspectRatioX = 3, 138 | aspectRatioY = 4, 139 | borderLineThickness = 3f, 140 | borderLineColor = Color.GREEN, 141 | borderCornerThickness = 5f, 142 | borderCornerOffset = 6f, 143 | borderCornerLength = 7f, 144 | borderCornerColor = Color.MAGENTA, 145 | guidelinesThickness = 8f, 146 | guidelinesColor = Color.RED, 147 | backgroundColor = Color.BLUE, 148 | minCropWindowWidth = 5, 149 | minCropWindowHeight = 5, 150 | minCropResultWidth = 10, 151 | minCropResultHeight = 10, 152 | maxCropResultWidth = 5000, 153 | maxCropResultHeight = 5000, 154 | activityTitle = "Test Activity Title", 155 | activityMenuIconColor = Color.BLACK, 156 | customOutputUri = null, 157 | outputCompressFormat = Bitmap.CompressFormat.JPEG, 158 | outputCompressQuality = 85, 159 | outputRequestWidth = 25, 160 | outputRequestHeight = 30, 161 | outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE, 162 | noOutputImage = false, 163 | initialCropWindowRectangle = Rect(4, 5, 6, 7), 164 | initialRotation = 13, 165 | allowRotation = true, 166 | allowFlipping = false, 167 | allowCounterRotation = true, 168 | rotationDegrees = 4, 169 | flipHorizontally = true, 170 | flipVertically = false, 171 | cropMenuCropButtonTitle = "Test Button Title", 172 | cropMenuCropButtonIcon = R.drawable.ic_rotate_left_24, 173 | ), 174 | ) 175 | 176 | val testRegistry = object : ActivityResultRegistry() { 177 | override fun onLaunch( 178 | requestCode: Int, 179 | contract: ActivityResultContract, 180 | input: I, 181 | options: ActivityOptionsCompat?, 182 | ) { 183 | } 184 | } 185 | // WHEN 186 | with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { 187 | onFragment { fragment -> cropImageIntent = fragment.cropImageIntent(options) } 188 | } 189 | 190 | val bundle = cropImageIntent?.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE) 191 | // THEN 192 | assertEquals(expectedClassName, cropImageIntent?.component?.className) 193 | assertEquals(expectedSource, bundle?.parcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE)) 194 | } 195 | 196 | @Test 197 | fun `WHEN cropping fails, THEN result should be unsuccessful`() { 198 | // GIVEN 199 | var fragment: ContractTestFragment? = null 200 | val testRegistry = object : ActivityResultRegistry() { 201 | override fun onLaunch( 202 | requestCode: Int, 203 | contract: ActivityResultContract, 204 | input: I, 205 | options: ActivityOptionsCompat?, 206 | ) { 207 | val result = CropImage.ActivityResult( 208 | originalUri = null, 209 | uriContent = null, 210 | error = Exception("Error!"), 211 | cropPoints = floatArrayOf(), 212 | cropRect = null, 213 | rotation = 0, 214 | wholeImageRect = null, 215 | sampleSize = 0, 216 | ) 217 | val intent = Intent() 218 | intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result) 219 | 220 | dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) 221 | } 222 | } 223 | 224 | with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { 225 | onFragment { fragment = it } 226 | } 227 | // WHEN 228 | fragment?.cropImage(CropImageContractOptions(null, CropImageOptions())) 229 | // THEN 230 | assertEquals(false, fragment?.cropResult?.isSuccessful) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /cropper/src/test/kotlin/com/canhub/cropper/CropImageViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper 2 | 3 | import android.graphics.BitmapFactory 4 | import android.widget.ImageView 5 | import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 6 | import app.cash.paparazzi.Paparazzi 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import java.io.File 10 | 11 | class CropImageViewTest { 12 | @get:Rule 13 | val paparazzi = Paparazzi( 14 | deviceConfig = PIXEL_5, 15 | theme = "Theme.MaterialComponents.DayNight.DarkActionBar", 16 | ) 17 | 18 | @Test fun ovalBitmap() { 19 | val file = fileFromAsset("small-tree.jpg") 20 | val imageView = ImageView(paparazzi.context) 21 | imageView.setImageBitmap(CropImage.toOvalBitmap(BitmapFactory.decodeStream(file.inputStream()))) 22 | paparazzi.snapshot(imageView) 23 | } 24 | 25 | private fun fileFromAsset(name: String) = 26 | File(CropImageViewTest::class.java.classLoader?.getResource(name)?.file!!) 27 | } 28 | -------------------------------------------------------------------------------- /cropper/src/test/resources/small-tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/cropper/src/test/resources/small-tree.jpg -------------------------------------------------------------------------------- /cropper/src/test/snapshots/images/com.canhub.cropper_CropImageViewTest_ovalBitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/cropper/src/test/snapshots/images/com.canhub.cropper_CropImageViewTest_ovalBitmap.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.vanniktech 2 | VERSION_NAME=4.7.0-SNAPSHOT 3 | 4 | POM_DESCRIPTION=Image Cropping Library for Android, optimised for Camera / Gallery. 5 | 6 | POM_URL=https://github.com/CanHub/Android-Image-Cropper 7 | POM_SCM_URL=https://github.com/CanHub/Android-Image-Cropper 8 | POM_SCM_CONNECTION=scm:git:git://github.com/CanHub/Android-Image-Cropper.git 9 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/CanHub/Android-Image-Cropper.git 10 | 11 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 12 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=vanniktech 16 | POM_DEVELOPER_NAME=Niklas Baudy 17 | 18 | android.useAndroidX=true 19 | # We are solely relying on AndroidX. 20 | android.enableJetifier=false 21 | 22 | android.experimental.enableSourceSetPathsMap=true 23 | android.experimental.cacheCompileLibResources=true 24 | android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.experimental.enableSourceSetPathsMap,android.experimental.cacheCompileLibResources 25 | 26 | SONATYPE_HOST=DEFAULT 27 | SONATYPE_AUTOMATIC_RELEASE=true 28 | RELEASE_SIGNING_ENABLED=true 29 | 30 | org.gradle.jvmargs=-Xmx2048m 31 | 32 | android.defaults.buildfeatures.buildconfig=false 33 | android.defaults.buildfeatures.aidl=false 34 | android.defaults.buildfeatures.renderscript=false 35 | android.defaults.buildfeatures.resvalues=false 36 | android.defaults.buildfeatures.shaders=false 37 | -------------------------------------------------------------------------------- /gradle/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradle/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | minSdk = "21" 3 | compileSdk = "34" 4 | targetSdk = "34" 5 | 6 | androidgradleplugin = "8.5.2" 7 | kotlin = "2.0.0" 8 | kotlinxcoroutines = "1.8.1" 9 | ktlint = "1.3.1" 10 | 11 | [libraries] 12 | androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version = "1.9.2" } 13 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } 14 | androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.13.1" } 15 | androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version = "1.3.7" } 16 | androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version = "1.8.4" } 17 | androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } 18 | junit = { module = "junit:junit", version = "4.13.2" } 19 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxcoroutines" } 20 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxcoroutines" } 21 | leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" } 22 | material = { module = "com.google.android.material:material", version = "1.12.0" } 23 | mock = { module = "io.mockk:mockk", version = "1.13.12" } 24 | plugin-android-cache-fix = { module = "org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin", version = "3.0.1" } 25 | plugin-androidgradleplugin = { module = "com.android.tools.build:gradle", version.ref = "androidgradleplugin" } 26 | plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.9.20" } 27 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 28 | plugin-licensee = { module = "app.cash.licensee:licensee-gradle-plugin", version = "1.11.0" } 29 | plugin-paparazzi = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version = "1.3.3" } 30 | plugin-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.29.0" } 31 | robolectric = { module = "org.robolectric:robolectric", version = "4.12.1" } 32 | timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" } 33 | 34 | [plugins] 35 | codequalitytools = { id = "com.vanniktech.code.quality.tools", version = "0.24.0" } 36 | dependencygraphgenerator = { id = "com.vanniktech.dependency.graph.generator", version = "0.8.0" } 37 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableDependencyDashboard" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("org.jetbrains.kotlin.plugin.parcelize") 5 | } 6 | 7 | kotlin { 8 | jvmToolchain { 9 | languageVersion.set(JavaLanguageVersion.of(11)) 10 | } 11 | } 12 | 13 | android { 14 | namespace = "com.example.croppersample" 15 | 16 | compileSdk = libs.versions.compileSdk.get().toInt() 17 | 18 | defaultConfig { 19 | applicationId = "com.example.croppersample" 20 | vectorDrawables.useSupportLibrary = true 21 | minSdk = libs.versions.minSdk.get().toInt() 22 | targetSdk = libs.versions.targetSdk.get().toInt() 23 | versionCode = 1 24 | versionName = project.property("VERSION_NAME").toString() 25 | } 26 | 27 | buildFeatures { 28 | viewBinding = true 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_11 33 | targetCompatibility = JavaVersion.VERSION_11 34 | } 35 | 36 | buildTypes { 37 | release { 38 | isMinifyEnabled = false 39 | isShrinkResources = false 40 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(project(":cropper")) 47 | implementation(libs.androidx.appcompat) 48 | implementation(libs.androidx.core.ktx) 49 | implementation(libs.material) 50 | implementation(libs.timber) 51 | } 52 | 53 | dependencies { 54 | debugImplementation(libs.leakcanary.android) 55 | } 56 | -------------------------------------------------------------------------------- /sample/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | # Project target. 13 | target=android-17 14 | android.library.reference.1=../cropper 15 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.OnBackPressedCallback 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.fragment.app.Fragment 7 | import com.example.croppersample.databinding.ActivityMainBinding 8 | 9 | internal class MainActivity : AppCompatActivity() { 10 | private lateinit var binding: ActivityMainBinding 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | binding = ActivityMainBinding.inflate(layoutInflater) 15 | 16 | setContentView(binding.root) 17 | 18 | binding.sampleCropImageView.setOnClickListener { SampleUsingImageViewFragment().show() } 19 | binding.sampleCustomActivity.setOnClickListener { SampleCustomActivity.start(this) } 20 | binding.sampleCropImage.setOnClickListener { SampleCropFragment().show() } 21 | 22 | onBackPressedDispatcher.addCallback( 23 | this, 24 | object : OnBackPressedCallback(true) { 25 | override fun handleOnBackPressed() { 26 | val fragment = supportFragmentManager.findFragmentById(binding.container.id) 27 | isEnabled = fragment != null 28 | 29 | if (fragment != null) { 30 | supportFragmentManager.beginTransaction().remove(fragment).commit() 31 | } else { 32 | onBackPressedDispatcher.onBackPressed() 33 | } 34 | 35 | isEnabled = true 36 | } 37 | }, 38 | ) 39 | } 40 | 41 | private fun Fragment.show() { 42 | supportFragmentManager 43 | .beginTransaction() 44 | .replace(binding.container.id, this) 45 | .commit() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/SampleApplication.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.sample 2 | 3 | import android.app.Application 4 | import android.os.StrictMode 5 | import timber.log.Timber 6 | 7 | class SampleApplication : Application() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | 11 | Timber.plant(Timber.DebugTree()) 12 | 13 | StrictMode.setThreadPolicy( 14 | StrictMode.ThreadPolicy.Builder().detectAll() 15 | .penaltyLog() 16 | .penaltyFlashScreen() 17 | .build(), 18 | ) 19 | 20 | StrictMode.setVmPolicy( 21 | StrictMode.VmPolicy.Builder() 22 | .detectAll() 23 | .penaltyDeath() 24 | .penaltyLog() 25 | .build(), 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/SampleCropFragment.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.canhub.cropper.sample 4 | 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.os.Environment 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.Toast 12 | import androidx.activity.result.contract.ActivityResultContracts 13 | import androidx.core.content.FileProvider 14 | import androidx.fragment.app.Fragment 15 | import com.canhub.cropper.CropImage 16 | import com.canhub.cropper.CropImageContract 17 | import com.canhub.cropper.CropImageContractOptions 18 | import com.canhub.cropper.CropImageOptions 19 | import com.example.croppersample.databinding.FragmentCameraBinding 20 | import timber.log.Timber 21 | import java.io.File 22 | import java.text.SimpleDateFormat 23 | import java.util.Date 24 | import java.util.Locale 25 | 26 | internal class SampleCropFragment : Fragment() { 27 | private var _binding: FragmentCameraBinding? = null 28 | private val binding get() = _binding!! 29 | 30 | private var outputUri: Uri? = null 31 | private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { 32 | if (it) { 33 | startCameraWithUri() 34 | } else { 35 | showErrorMessage("taking picture failed") 36 | } 37 | } 38 | 39 | private val cropImage = registerForActivityResult(CropImageContract()) { result -> 40 | when { 41 | result.isSuccessful -> { 42 | Timber.tag("AIC-Sample").i("Original bitmap: ${result.originalBitmap}") 43 | Timber.tag("AIC-Sample").i("Original uri: ${result.originalUri}") 44 | Timber.tag("AIC-Sample").i("Output bitmap: ${result.bitmap}") 45 | Timber.tag("AIC-Sample").i("Output uri: ${result.getUriFilePath(requireContext())}") 46 | handleCropImageResult(result.uriContent.toString()) 47 | } 48 | result is CropImage.CancelledResult -> showErrorMessage("cropping image was cancelled by the user") 49 | else -> showErrorMessage("cropping image failed") 50 | } 51 | } 52 | 53 | private val customCropImage = registerForActivityResult(CropImageContract()) { 54 | if (it !is CropImage.CancelledResult) { 55 | handleCropImageResult(it.uriContent.toString()) 56 | } 57 | } 58 | 59 | override fun onCreateView( 60 | inflater: LayoutInflater, 61 | container: ViewGroup?, 62 | savedInstanceState: Bundle?, 63 | ): View { 64 | _binding = FragmentCameraBinding.inflate(layoutInflater, container, false) 65 | return binding.root 66 | } 67 | 68 | override fun onDestroyView() { 69 | super.onDestroyView() 70 | _binding = null 71 | } 72 | 73 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 74 | super.onViewCreated(view, savedInstanceState) 75 | 76 | binding.takePictureBeforeCallLibraryWithUri.setOnClickListener { 77 | setupOutputUri() 78 | takePicture.launch(outputUri) 79 | } 80 | binding.callLibraryWithoutUri.setOnClickListener { 81 | startCameraWithoutUri(includeCamera = true, includeGallery = true) 82 | } 83 | binding.callLibraryWithoutUriCameraOnly.setOnClickListener { 84 | startCameraWithoutUri(includeCamera = true, includeGallery = false) 85 | } 86 | binding.callLibraryWithoutUriGalleryOnly.setOnClickListener { 87 | startCameraWithoutUri(includeCamera = false, includeGallery = true) 88 | } 89 | } 90 | 91 | private fun startCameraWithoutUri(includeCamera: Boolean, includeGallery: Boolean) { 92 | customCropImage.launch( 93 | CropImageContractOptions( 94 | uri = null, 95 | cropImageOptions = CropImageOptions( 96 | imageSourceIncludeCamera = includeCamera, 97 | imageSourceIncludeGallery = includeGallery, 98 | ), 99 | ), 100 | ) 101 | } 102 | 103 | private fun startCameraWithUri() { 104 | cropImage.launch( 105 | CropImageContractOptions( 106 | uri = outputUri, 107 | cropImageOptions = CropImageOptions(), 108 | ), 109 | ) 110 | } 111 | 112 | private fun showErrorMessage(message: String) { 113 | Timber.tag("AIC-Sample").e("Camera error: $message") 114 | Toast.makeText(activity, "Crop failed: $message", Toast.LENGTH_SHORT).show() 115 | } 116 | 117 | private fun handleCropImageResult(uri: String) { 118 | SampleResultScreen.start(this, null, Uri.parse(uri.replace("file:", "")), null) 119 | } 120 | 121 | private fun setupOutputUri() { 122 | if (outputUri == null) { 123 | context?.let { ctx -> 124 | val authorities = "${ctx.applicationContext?.packageName}$AUTHORITY_SUFFIX" 125 | outputUri = FileProvider.getUriForFile(ctx, authorities, createImageFile()) 126 | } 127 | } 128 | } 129 | 130 | private fun createImageFile(): File { 131 | val timeStamp = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(Date()) 132 | val storageDir: File? = activity?.getExternalFilesDir(Environment.DIRECTORY_PICTURES) 133 | return File.createTempFile( 134 | "$FILE_NAMING_PREFIX${timeStamp}$FILE_NAMING_SUFFIX", 135 | FILE_FORMAT, 136 | storageDir, 137 | ) 138 | } 139 | 140 | companion object { 141 | const val DATE_FORMAT = "yyyyMMdd_HHmmss" 142 | const val FILE_NAMING_PREFIX = "JPEG_" 143 | const val FILE_NAMING_SUFFIX = "_" 144 | const val FILE_FORMAT = ".jpg" 145 | const val AUTHORITY_SUFFIX = ".cropper.fileprovider" 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/SampleCustomActivity.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION") 2 | 3 | package com.canhub.cropper.sample 4 | 5 | import android.app.Activity 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import android.view.Menu 10 | import android.view.View 11 | import androidx.core.app.ActivityCompat 12 | import com.canhub.cropper.CropImage.ActivityResult 13 | import com.canhub.cropper.CropImageActivity 14 | import com.canhub.cropper.CropImageView 15 | import com.example.croppersample.R 16 | import com.example.croppersample.databinding.ExtendedActivityBinding 17 | import timber.log.Timber 18 | 19 | internal class SampleCustomActivity : CropImageActivity() { 20 | 21 | companion object { 22 | fun start(activity: Activity) { 23 | ActivityCompat.startActivity( 24 | activity, 25 | Intent(activity, SampleCustomActivity::class.java), 26 | null, 27 | ) 28 | } 29 | } 30 | 31 | private lateinit var binding: ExtendedActivityBinding 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | binding = ExtendedActivityBinding.inflate(layoutInflater) 35 | 36 | super.onCreate(savedInstanceState) 37 | 38 | binding.saveBtn.setOnClickListener { cropImage() } 39 | binding.backBtn.setOnClickListener { onBackPressedDispatcher.onBackPressed() } 40 | binding.rotateText.setOnClickListener { onRotateClick() } 41 | 42 | binding.cropImageView.setOnCropWindowChangedListener { 43 | updateExpectedImageSize() 44 | } 45 | 46 | setCropImageView(binding.cropImageView) 47 | } 48 | 49 | override fun onSetImageUriComplete( 50 | view: CropImageView, 51 | uri: Uri, 52 | error: Exception?, 53 | ) { 54 | super.onSetImageUriComplete(view, uri, error) 55 | 56 | updateRotationCounter() 57 | updateExpectedImageSize() 58 | } 59 | 60 | private fun updateExpectedImageSize() { 61 | binding.expectedImageSize.text = binding.cropImageView.expectedImageSize().toString() 62 | } 63 | 64 | override fun setContentView(view: View?) { 65 | super.setContentView(binding.root) 66 | } 67 | 68 | private fun updateRotationCounter() { 69 | binding.rotateText.text = getString(R.string.rotation_value, binding.cropImageView.rotatedDegrees.toString()) 70 | } 71 | 72 | override fun onPickImageResult(resultUri: Uri?) { 73 | super.onPickImageResult(resultUri) 74 | 75 | if (resultUri != null) { 76 | binding.cropImageView.setImageUriAsync(resultUri) 77 | } 78 | } 79 | 80 | override fun getResultIntent(uri: Uri?, error: java.lang.Exception?, sampleSize: Int): Intent { 81 | val result = super.getResultIntent(uri, error, sampleSize) 82 | // Adding some more information. 83 | return result.putExtra("EXTRA_KEY", "Extra data") 84 | } 85 | 86 | override fun setResult(uri: Uri?, error: Exception?, sampleSize: Int) { 87 | val result = ActivityResult( 88 | originalUri = binding.cropImageView.imageUri, 89 | uriContent = uri, 90 | error = error, 91 | cropPoints = binding.cropImageView.cropPoints, 92 | cropRect = binding.cropImageView.cropRect, 93 | rotation = binding.cropImageView.rotatedDegrees, 94 | wholeImageRect = binding.cropImageView.wholeImageRect, 95 | sampleSize = sampleSize, 96 | ) 97 | 98 | Timber.tag("AIC-Sample").i("Original bitmap: ${result.originalBitmap}") 99 | Timber.tag("AIC-Sample").i("Original uri: ${result.originalUri}") 100 | Timber.tag("AIC-Sample").i("Output bitmap: ${result.bitmap}") 101 | Timber.tag("AIC-Sample").i("Output uri: ${result.getUriFilePath(this)}") 102 | binding.cropImageView.setImageUriAsync(result.uriContent) 103 | } 104 | 105 | override fun setResultCancel() { 106 | Timber.tag("AIC-Sample").i("User this override to change behaviour when cancel") 107 | super.setResultCancel() 108 | } 109 | 110 | override fun updateMenuItemIconColor(menu: Menu, itemId: Int, color: Int) { 111 | Timber.tag("AIC-Sample").i("If not using your layout, this can be one option to change colours") 112 | super.updateMenuItemIconColor(menu, itemId, color) 113 | } 114 | 115 | private fun onRotateClick() { 116 | binding.cropImageView.rotateImage(90) 117 | updateRotationCounter() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/SampleResultScreen.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.sample 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.Window 9 | import android.widget.Toast 10 | import androidx.fragment.app.Fragment 11 | import com.canhub.cropper.parcelable 12 | import com.example.croppersample.R 13 | import com.example.croppersample.databinding.ActivityCropResultBinding 14 | 15 | class SampleResultScreen : Activity() { 16 | private lateinit var binding: ActivityCropResultBinding 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | requestWindowFeature(Window.FEATURE_NO_TITLE) 21 | binding = ActivityCropResultBinding.inflate(layoutInflater) 22 | setContentView(binding.root) 23 | 24 | binding.resultImageView.setBackgroundResource(R.drawable.backdrop) 25 | binding.resultImageView.setOnClickListener { 26 | releaseBitmap() 27 | finish() 28 | } 29 | 30 | image?.let { 31 | binding.resultImageView.setImageBitmap(it) 32 | val sampleSize = intent.getIntExtra(SAMPLE_SIZE, 1) 33 | val ratio = (10 * it.width / it.height.toDouble()).toInt() / 10.0 34 | val byteCount: Int = it.byteCount / 1024 35 | val desc = 36 | "(${it.width}, ${it.height}), Sample: $sampleSize, Ratio: $ratio, Bytes: $byteCount K" 37 | 38 | binding.resultImageText.text = desc 39 | } ?: run { 40 | val imageUri = intent.parcelable(URI) 41 | 42 | if (imageUri != null) { 43 | binding.resultImageView.setImageURI(imageUri) 44 | } else { 45 | Toast.makeText(this, "No image is set to show", Toast.LENGTH_LONG).show() 46 | } 47 | } 48 | } 49 | 50 | override fun onDestroy() { 51 | super.onDestroy() 52 | releaseBitmap() 53 | } 54 | 55 | private fun releaseBitmap() { 56 | image?.let { 57 | it.recycle() 58 | image = null 59 | } 60 | } 61 | 62 | companion object { 63 | fun start(fragment: Fragment, imageBitmap: Bitmap?, uri: Uri?, sampleSize: Int?) { 64 | image = imageBitmap 65 | 66 | fragment.startActivity( 67 | Intent(fragment.context, SampleResultScreen::class.java) 68 | .putExtra(SAMPLE_SIZE, sampleSize) 69 | .putExtra(URI, uri), 70 | ) 71 | } 72 | 73 | // This is used, because bitmap is huge and cannot be passed in Intent without throw and exception 74 | private var image: Bitmap? = null 75 | 76 | private const val SAMPLE_SIZE = "SAMPLE_SIZE" 77 | private const val URI = "URI" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/canhub/cropper/sample/SampleUsingImageViewFragment.kt: -------------------------------------------------------------------------------- 1 | package com.canhub.cropper.sample 2 | 3 | import android.graphics.Rect 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.Menu 8 | import android.view.MenuInflater 9 | import android.view.MenuItem 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.Toast 13 | import androidx.activity.result.contract.ActivityResultContracts 14 | import androidx.core.view.MenuProvider 15 | import androidx.fragment.app.Fragment 16 | import com.canhub.cropper.CropImage 17 | import com.canhub.cropper.CropImageOptions 18 | import com.canhub.cropper.CropImageView 19 | import com.canhub.cropper.CropImageView.CropResult 20 | import com.canhub.cropper.CropImageView.OnCropImageCompleteListener 21 | import com.canhub.cropper.CropImageView.OnSetImageUriCompleteListener 22 | import com.canhub.cropper.sample.optionsdialog.SampleOptionsBottomSheet 23 | import com.example.croppersample.R 24 | import com.example.croppersample.databinding.FragmentCropImageViewBinding 25 | import timber.log.Timber 26 | 27 | internal class SampleUsingImageViewFragment : 28 | Fragment(), 29 | MenuProvider, 30 | SampleOptionsBottomSheet.Listener, 31 | OnSetImageUriCompleteListener, 32 | OnCropImageCompleteListener { 33 | private var _binding: FragmentCropImageViewBinding? = null 34 | private val binding get() = _binding!! 35 | 36 | private var options: CropImageOptions? = null 37 | private val openPicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> 38 | binding.cropImageView.setImageUriAsync(uri) 39 | } 40 | 41 | override fun onCreateView( 42 | inflater: LayoutInflater, 43 | container: ViewGroup?, 44 | savedInstanceState: Bundle?, 45 | ): View { 46 | activity?.addMenuProvider(this) 47 | _binding = FragmentCropImageViewBinding.inflate(layoutInflater, container, false) 48 | return binding.root 49 | } 50 | 51 | override fun onDestroyView() { 52 | super.onDestroyView() 53 | binding.cropImageView.setOnSetImageUriCompleteListener(null) 54 | binding.cropImageView.setOnCropImageCompleteListener(null) 55 | _binding = null 56 | } 57 | 58 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 59 | super.onViewCreated(view, savedInstanceState) 60 | 61 | setOptions() 62 | 63 | binding.cropImageView.setOnSetImageUriCompleteListener(this) 64 | binding.cropImageView.setOnCropImageCompleteListener(this) 65 | 66 | if (savedInstanceState == null) { 67 | binding.cropImageView.imageResource = R.drawable.cat 68 | } 69 | 70 | binding.settings.setOnClickListener { 71 | SampleOptionsBottomSheet.show(childFragmentManager, options, this) 72 | } 73 | 74 | binding.searchImage.setOnClickListener { 75 | openPicker.launch("image/*") 76 | } 77 | 78 | binding.reset.setOnClickListener { 79 | binding.cropImageView.resetCropRect() 80 | binding.cropImageView.imageResource = R.drawable.cat 81 | onOptionsApplySelected(CropImageOptions()) 82 | } 83 | } 84 | 85 | override fun onOptionsApplySelected(options: CropImageOptions) { 86 | this.options = options 87 | 88 | binding.cropImageView.setImageCropOptions(options) 89 | } 90 | 91 | override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { 92 | menuInflater.inflate(R.menu.main, menu) 93 | } 94 | 95 | override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { 96 | R.id.main_action_crop -> { 97 | binding.cropImageView.croppedImageAsync() 98 | true 99 | } 100 | R.id.main_action_rotate -> { 101 | binding.cropImageView.rotateImage(90) 102 | true 103 | } 104 | R.id.main_action_flip_horizontally -> { 105 | binding.cropImageView.flipImageHorizontally() 106 | true 107 | } 108 | R.id.main_action_flip_vertically -> { 109 | binding.cropImageView.flipImageVertically() 110 | true 111 | } 112 | else -> false 113 | } 114 | 115 | override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) { 116 | if (error != null) { 117 | Timber.tag("AIC-Sample").e(error, "Failed to load image by URI") 118 | Toast.makeText(activity, "Image load failed: " + error.message, Toast.LENGTH_LONG) 119 | .show() 120 | } 121 | } 122 | 123 | override fun onCropImageComplete(view: CropImageView, result: CropResult) { 124 | if (result.error == null) { 125 | val imageBitmap = if (binding.cropImageView.cropShape == CropImageView.CropShape.OVAL) { 126 | result.bitmap?.let(CropImage::toOvalBitmap) 127 | } else { 128 | result.bitmap 129 | } 130 | Timber.tag("AIC-Sample").i("Original bitmap: ${result.originalBitmap}") 131 | Timber.tag("AIC-Sample").i("Original uri: ${result.originalUri}") 132 | Timber.tag("AIC-Sample").i("Output bitmap: $imageBitmap") 133 | Timber.tag("AIC-Sample").i("Output uri: ${result.getUriFilePath(view.context)}") 134 | SampleResultScreen.start(this, imageBitmap, result.uriContent, result.sampleSize) 135 | } else { 136 | Timber.tag("AIC-Sample").e(result.error, "Failed to crop image") 137 | Toast 138 | .makeText(activity, "Crop failed: ${result.error?.message}", Toast.LENGTH_SHORT) 139 | .show() 140 | } 141 | } 142 | 143 | private fun setOptions() { 144 | binding.cropImageView.cropRect = Rect(100, 300, 500, 1200) 145 | onOptionsApplySelected(CropImageOptions()) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /sample/src/main/res/color/chip_bg_states.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/color/chip_text_states.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/color/switch_thumb_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/color/switch_track_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/canhub_logo_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/sample/src/main/res/drawable-nodpi/canhub_logo_purple.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/canhub_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/sample/src/main/res/drawable-nodpi/canhub_logo_white.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/sample/src/main/res/drawable-nodpi/cat.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/checktile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CanHub/Android-Image-Cropper/e8cbcd386005fc10b8c923440bf0580345eeddd9/sample/src/main/res/drawable-nodpi/checktile.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/backdrop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/bg_bottom_sheet_rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/bg_draggable_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/bg_purple_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/checkerboard.xml: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_arrow_back_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_gear_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_mage_search_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/muted.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_crop_result.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 20 | 27 | 38 |