diff --git a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt index cd51dbc..8fd6014 100644 --- a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt @@ -206,10 +206,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { BitmapRegionDecoder.newInstance(it) } else { @Suppress("DEPRECATION") BitmapRegionDecoder.newInstance(it, false) - } + } ?: throw Error("Could not create bitmap decoder. Uri: $uri") - val imageHeight: Int = decoder!!.height - val imageWidth: Int = decoder!!.width + val imageHeight: Int = decoder.height + val imageWidth: Int = decoder.width val orientation = getOrientation(reactContext, Uri.parse(uri)) val (left, top) = @@ -229,9 +229,9 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { return@use try { val rect = Rect(left, top, right, bottom) - decoder!!.decodeRegion(rect, outOptions) + decoder.decodeRegion(rect, outOptions) } finally { - decoder!!.recycle() + decoder.recycle() } } } @@ -262,68 +262,79 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) { ): Bitmap? { Assertions.assertNotNull(outOptions) - // Loading large bitmaps efficiently: - // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html - - // This uses scaling mode COVER - - // Where would the crop rect end up within the scaled bitmap? - - val bitmap = - openBitmapInputStream(uri, headers)?.use { - // This can use significantly less memory than decoding the full-resolution bitmap - BitmapFactory.decodeStream(it, null, outOptions) - } ?: return null + return openBitmapInputStream(uri, headers)?.use { + // Efficiently crops image without loading full resolution into memory + // https://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html + val decoder = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(it) + } else { + @Suppress("DEPRECATION") BitmapRegionDecoder.newInstance(it, false) + } ?: throw Error("Could not create bitmap decoder. Uri: $uri") - val orientation = getOrientation(reactContext, Uri.parse(uri)) - val (x, y) = - when (orientation) { - 90 -> yPos to bitmap.height - rectWidth - xPos - 270 -> bitmap.width - rectHeight - yPos to xPos - 180 -> bitmap.width - rectWidth - xPos to bitmap.height - rectHeight - yPos - else -> xPos to yPos - } + val orientation = getOrientation(reactContext, Uri.parse(uri)) + val (x, y) = + when (orientation) { + 90 -> yPos to decoder.height - rectWidth - xPos + 270 -> decoder.width - rectHeight - yPos to xPos + 180 -> decoder.width - rectWidth - xPos to decoder.height - rectHeight - yPos + else -> xPos to yPos + } - val (width, height) = - when (orientation) { - 90, - 270 -> rectHeight to rectWidth - else -> rectWidth to rectHeight - } - val (targetWidth, targetHeight) = - when (orientation) { - 90, - 270 -> outputHeight to outputWidth - else -> outputWidth to outputHeight - } + val (width, height) = + when (orientation) { + 90, + 270 -> rectHeight to rectWidth + else -> rectWidth to rectHeight + } + val (targetWidth, targetHeight) = + when (orientation) { + 90, + 270 -> outputHeight to outputWidth + else -> outputWidth to outputHeight + } - val cropRectRatio = width / height.toFloat() - val targetRatio = targetWidth / targetHeight.toFloat() - val isCropRatioLargerThanTargetRatio = cropRectRatio > targetRatio - val newWidth = - if (isCropRatioLargerThanTargetRatio) height * targetRatio else width.toFloat() - val newHeight = - if (isCropRatioLargerThanTargetRatio) height.toFloat() else width / targetRatio - val newX = if (isCropRatioLargerThanTargetRatio) x + (width - newWidth) / 2 else x.toFloat() - val newY = - if (isCropRatioLargerThanTargetRatio) y.toFloat() else y + (height - newHeight) / 2 - val scale = - if (isCropRatioLargerThanTargetRatio) targetHeight / height.toFloat() - else targetWidth / width.toFloat() - - // Decode the bitmap. We have to open the stream again, like in the example linked above. - // Is there a way to just continue reading from the stream? - outOptions.inSampleSize = getDecodeSampleSize(width, height, targetWidth, targetHeight) - - val cropX = (newX / outOptions.inSampleSize.toFloat()).roundToInt() - val cropY = (newY / outOptions.inSampleSize.toFloat()).roundToInt() - val cropWidth = (newWidth / outOptions.inSampleSize.toFloat()).roundToInt() - val cropHeight = (newHeight / outOptions.inSampleSize.toFloat()).roundToInt() - val cropScale = scale * outOptions.inSampleSize - val scaleMatrix = Matrix().apply { setScale(cropScale, cropScale) } - val filter = true - - return Bitmap.createBitmap(bitmap, cropX, cropY, cropWidth, cropHeight, scaleMatrix, filter) + val cropRectRatio = width / height.toFloat() + val targetRatio = targetWidth / targetHeight.toFloat() + val isCropRatioLargerThanTargetRatio = cropRectRatio > targetRatio + val newWidth = + if (isCropRatioLargerThanTargetRatio) height * targetRatio else width.toFloat() + val newHeight = + if (isCropRatioLargerThanTargetRatio) height.toFloat() else width / targetRatio + val newX = + if (isCropRatioLargerThanTargetRatio) x + (width - newWidth) / 2 else x.toFloat() + val newY = + if (isCropRatioLargerThanTargetRatio) y.toFloat() else y + (height - newHeight) / 2 + val scale = + if (isCropRatioLargerThanTargetRatio) targetHeight / height.toFloat() + else targetWidth / width.toFloat() + + // Decode the bitmap. We have to open the stream again, like in the example linked + // above. + // Is there a way to just continue reading from the stream? + outOptions.inSampleSize = getDecodeSampleSize(width, height, targetWidth, targetHeight) + + val cropX = (newX / outOptions.inSampleSize.toFloat()).roundToInt() + val cropY = (newY / outOptions.inSampleSize.toFloat()).roundToInt() + val cropWidth = (newWidth / outOptions.inSampleSize.toFloat()).roundToInt() + val cropHeight = (newHeight / outOptions.inSampleSize.toFloat()).roundToInt() + val cropScale = scale * outOptions.inSampleSize + val scaleMatrix = Matrix().apply { setScale(cropScale, cropScale) } + val filter = true + + val rect = Rect(0, 0, decoder.width, decoder.height) + val bitmap = decoder.decodeRegion(rect, outOptions) + + return Bitmap.createBitmap( + bitmap, + cropX, + cropY, + cropWidth, + cropHeight, + scaleMatrix, + filter + ) + } } private fun openBitmapInputStream(uri: String, headers: HashMap?): InputStream? { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 47b06f4..9a71e63 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -824,6 +824,22 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-slider (4.5.0): + - RCT-Folly (= 2021.07.22.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-NativeModulesApple (0.72.6): - React-callinvoker - React-Core @@ -985,6 +1001,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - "react-native-image-editor (from `../node_modules/@react-native-community/image-editor`)" + - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -1060,6 +1077,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-image-editor: :path: "../node_modules/@react-native-community/image-editor" + react-native-slider: + :path: "../node_modules/@react-native-community/slider" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-perflogger: @@ -1130,11 +1149,12 @@ SPEC CHECKSUMS: React-jsinspector: 194e32c6aab382d88713ad3dd0025c5f5c4ee072 React-logger: cebf22b6cf43434e471dc561e5911b40ac01d289 react-native-image-editor: 6491eca6c084de724a9a144056323cb00848b68c + react-native-slider: 69ccddffd41798b325247b9c4c09a0927e3b7cec React-NativeModulesApple: 63505fb94b71e2469cab35bdaf36cca813cb5bfd React-perflogger: e3596db7e753f51766bceadc061936ef1472edc3 React-RCTActionSheet: 17ab132c748b4471012abbcdcf5befe860660485 React-RCTAnimation: c8bbaab62be5817d2a31c36d5f2571e3f7dcf099 - React-RCTAppDelegate: 100a4f479c664e4f63d4cfd3ff8acbd3915e31e7 + React-RCTAppDelegate: 16245b0c3a216dd600db53209812f8d470643a57 React-RCTBlob: 86ab788db3fcc1af0d186a6625e7d0956ffeea5b React-RCTFabric: 87e15f0ad21f7bf2642d4e78afaf162d349eb221 React-RCTImage: 670a3486b532292649b1aef3ffddd0b495a5cee4 @@ -1156,4 +1176,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c464aabec59351f1e87d676c093bc6f524e21681 -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.3 diff --git a/example/package.json b/example/package.json index e24b294..48059cd 100644 --- a/example/package.json +++ b/example/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@react-native-community/image-editor": "link:..", + "@react-native-community/slider": "^4.5.0", "react": "18.2.0", "react-native": "0.72.6" }, diff --git a/example/src/SquareImageCropper.tsx b/example/src/SquareImageCropper.tsx index 253cb02..c72aa8e 100644 --- a/example/src/SquareImageCropper.tsx +++ b/example/src/SquareImageCropper.tsx @@ -8,6 +8,7 @@ import { SafeAreaView, } from 'react-native'; import ImageEditor from '@react-native-community/image-editor'; +import Slider from '@react-native-community/slider'; import type { LayoutChangeEvent } from 'react-native'; import { DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT } from './constants'; @@ -19,6 +20,7 @@ interface State { croppedImageURI: string | null; cropError: Error | null; measuredSize: ImageSize | null; + cropScale: number; } interface Props { // noop @@ -41,6 +43,7 @@ export class SquareImageCropper extends Component { measuredSize: null, croppedImageURI: null, cropError: null, + cropScale: 1, }; } @@ -89,6 +92,17 @@ export class SquareImageCropper extends Component { style={[styles.imageCropper, measuredSize]} onTransformDataChange={this._onTransformDataChange} /> + + Scale {this.state.cropScale.toFixed(2)} + this.setState({ cropScale })} + value={this.state.cropScale} + /> + + { if (!this._transformData) { return; } + const displaySize = + this.state.cropScale !== 1 + ? { + width: this._transformData?.size.width * this.state.cropScale, + height: this._transformData?.size.height * this.state.cropScale, + } + : undefined; + const cropData: ImageCropData = { + ...this._transformData, + displaySize, + }; const { uri } = await ImageEditor.cropImage( this.state.photo.uri, - this._transformData + cropData ); if (uri) { this.setState({ croppedImageURI: uri }); @@ -146,7 +171,7 @@ export class SquareImageCropper extends Component { }; _reset = () => { - this.setState({ croppedImageURI: null, cropError: null }); + this.setState({ croppedImageURI: null, cropError: null, cropScale: 1 }); }; } @@ -186,4 +211,12 @@ const styles = StyleSheet.create({ fontSize: 16, marginBottom: 10, }, + scaleSlider: { + width: '100%', + }, + scaleSliderContainer: { + width: '80%', + alignSelf: 'center', + alignItems: 'center', + }, }); diff --git a/example/yarn.lock b/example/yarn.lock index abf275b..0835da9 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1687,6 +1687,11 @@ version "0.0.0" uid "" +"@react-native-community/slider@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-4.5.0.tgz#5c55488ee30060cd87100fb746b9d8655dbab04e" + integrity sha512-pyUvNTvu5IfCI5abzqRfO/dd3A009RC66RXZE6t0gyOwI/j0QDlq9VZRv3rjkpuIvNTnsYj+m5BHlh0DkSYUyA== + "@react-native/assets-registry@^0.72.0": version "0.72.0" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.72.0.tgz#c82a76a1d86ec0c3907be76f7faf97a32bbed05d"