diff --git a/examples/TypeScriptMessaging/ios/Podfile.lock b/examples/TypeScriptMessaging/ios/Podfile.lock
index c354b991db..7ce5d73f0b 100644
--- a/examples/TypeScriptMessaging/ios/Podfile.lock
+++ b/examples/TypeScriptMessaging/ios/Podfile.lock
@@ -1285,8 +1285,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- - react-native-image-resizer (3.0.10):
- - React-Core
- react-native-netinfo (11.3.2):
- React-Core
- react-native-safe-area-context (4.11.1):
@@ -1735,6 +1733,8 @@ PODS:
- RNSVG (15.8.0):
- React-Core
- SocketRocket (0.7.0)
+ - stream-chat-react-native (5.39.5):
+ - React-Core
- Yoga (0.0.0)
DEPENDENCIES:
@@ -1779,7 +1779,6 @@ DEPENDENCIES:
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- - "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)"
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-video (from `../node_modules/react-native-video`)
@@ -1817,6 +1816,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`)
- RNShare (from `../node_modules/react-native-share`)
- RNSVG (from `../node_modules/react-native-svg`)
+ - stream-chat-react-native (from `../node_modules/stream-chat-react-native`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -1903,8 +1903,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-document-picker"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
- react-native-image-resizer:
- :path: "../node_modules/@bam.tech/react-native-image-resizer"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-safe-area-context:
@@ -1979,6 +1977,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-share"
RNSVG:
:path: "../node_modules/react-native-svg"
+ stream-chat-react-native:
+ :path: "../node_modules/stream-chat-react-native"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -1990,7 +1990,7 @@ SPEC CHECKSUMS:
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
hermes-engine: ea92f60f37dba025e293cbe4b4a548fd26b610a0
op-sqlite: 5688336af53053aa37f0ec3496487dc2734c91cc
- RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47
+ RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
RCTTypeSafety: 28e24a6e44f5cbf912c66dde6ab7e07d1059a205
@@ -2022,7 +2022,6 @@ SPEC CHECKSUMS:
react-native-blob-util: 18b510205c080a453574a7d2344d64673d0ad9af
react-native-document-picker: 7343222102ece8aec51390717f47ad7119c7921f
react-native-image-picker: 2fbbafdae7a7c6db9d25df2f2b1db4442d2ca2ad
- react-native-image-resizer: fd0c333eca55147bd55c5e054cac95dcd0da6814
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
react-native-safe-area-context: 5141f11858b033636f1788b14f32eaba92cee810
react-native-video: b0584a6d2271cb163f817c7412708263f9893ed5
@@ -2061,8 +2060,9 @@ SPEC CHECKSUMS:
RNShare: 0fad69ae2d71de9d1f7b9a43acf876886a6cb99c
RNSVG: 8b1a777d54096b8c2a0fd38fc9d5a454332bbb4d
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
+ stream-chat-react-native: 489a6a053480ab8556883de05a28df2c7387ede6
Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6
PODFILE CHECKSUM: 6b7a4b74915b42bfe4ffddaf67cbf5e7a2bfeab3
-COCOAPODS: 1.15.2
+COCOAPODS: 1.16.2
diff --git a/examples/TypeScriptMessaging/package.json b/examples/TypeScriptMessaging/package.json
index 29c6e4d94a..24d18db561 100644
--- a/examples/TypeScriptMessaging/package.json
+++ b/examples/TypeScriptMessaging/package.json
@@ -12,7 +12,6 @@
"clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -"
},
"dependencies": {
- "@bam.tech/react-native-image-resizer": "^3.0.10",
"@op-engineering/op-sqlite": "^6.0.4",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/netinfo": "^11.3.2",
diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock
index b27dd13b91..18d16bc5f4 100644
--- a/examples/TypeScriptMessaging/yarn.lock
+++ b/examples/TypeScriptMessaging/yarn.lock
@@ -1777,11 +1777,6 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
-"@bam.tech/react-native-image-resizer@^3.0.10":
- version "3.0.10"
- resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.10.tgz#03395a29cb61cd819ce1e7730fb137ab6e75618a"
- integrity sha512-IVIBRkgy8eq4g51RjAzh7zk8KpGhiQH6GqLC7SgAUJ0plh2bdqG2l8+D+Q/A0uFe85YutUmHyFioyDEsRGXaCQ==
-
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
diff --git a/package/native-package/android/build.gradle b/package/native-package/android/build.gradle
new file mode 100644
index 0000000000..876b08a539
--- /dev/null
+++ b/package/native-package/android/build.gradle
@@ -0,0 +1,103 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.2.1"
+
+ }
+}
+
+def isNewArchitectureEnabled() {
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
+}
+
+apply plugin: "com.android.library"
+
+
+def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
+
+if (isNewArchitectureEnabled()) {
+ apply plugin: "com.facebook.react"
+}
+
+def getExtOrDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ImageResizer_" + name]
+}
+
+def getExtOrIntegerDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
+}
+
+android {
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+ def agpMajorVersion = agpVersion.tokenize('.')[0].toInteger()
+ def agpMinorVersion = agpVersion.tokenize('.')[1].toInteger()
+ /**
+ * Namespace should be declared here starting from AGP 8.x, Starting AGP 7.3 it is also supported.
+ * For AGP < 7.3, namespace should be declared in AndroidManifest.
+ * See: https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes#namespace-dsl
+ */
+ if (agpMajorVersion >= 7 && agpMinorVersion >= 3) {
+ namespace "com.reactnativeimageresizer"
+ }
+
+ defaultConfig {
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ lintOptions {
+ disable "GradleCompatible"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ if (isNewArchitectureEnabled()) {
+ java.srcDirs += [
+ "src/newarch",
+ // This is needed to build Kotlin project with NewArch enabled
+ "${project.buildDir}/generated/source/codegen/java"
+ ]
+ } else {
+ java.srcDirs += ["src/oldarch"]
+ }
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+
+dependencies {
+ // For < 0.71, this will be from the local maven repo
+ // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
+ //noinspection GradleDynamicVersion
+ implementation "com.facebook.react:react-native:+"
+ implementation "androidx.exifinterface:exifinterface:1.3.2"
+}
+
+if (isNewArchitectureEnabled()) {
+ react {
+ jsRootDir = file("../src/")
+ libraryName = "ImageResizer"
+ codegenJavaPackageName = "com.reactnativeimageresizer"
+ }
+}
diff --git a/package/native-package/android/gradle.properties b/package/native-package/android/gradle.properties
new file mode 100644
index 0000000000..e684c75fff
--- /dev/null
+++ b/package/native-package/android/gradle.properties
@@ -0,0 +1,5 @@
+ImageResizer_kotlinVersion=1.7.0
+ImageResizer_minSdkVersion=21
+ImageResizer_targetSdkVersion=31
+ImageResizer_compileSdkVersion=31
+ImageResizer_ndkversion=21.4.7075529
diff --git a/package/native-package/android/gradle/wrapper/gradle-wrapper.jar b/package/native-package/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..e708b1c023
Binary files /dev/null and b/package/native-package/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/package/native-package/android/src/main/AndroidManifest.xml b/package/native-package/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..5c6a27faa9
--- /dev/null
+++ b/package/native-package/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizer.java b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizer.java
new file mode 100644
index 0000000000..d1482655df
--- /dev/null
+++ b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizer.java
@@ -0,0 +1,594 @@
+package com.reactnativeimageresizer;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import androidx.exifinterface.media.ExifInterface;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Date;
+
+/**
+ * Provide methods to resize and rotate an image file.
+ */
+public class ImageResizer {
+ private final static String IMAGE_JPEG = "image/jpeg";
+ private final static String IMAGE_PNG = "image/png";
+ private final static String SCHEME_DATA = "data";
+ private final static String SCHEME_CONTENT = "content";
+ private final static String SCHEME_FILE = "file";
+ private final static String SCHEME_HTTP = "http";
+ private final static String SCHEME_HTTPS = "https";
+
+
+ // List of known EXIF tags we will be copying.
+ // Orientation, width, height, and some others are ignored
+ // TODO: Find any missing tag that might be useful
+ private final static String[] EXIF_TO_COPY_ROTATED = new String[]
+ {
+ ExifInterface.TAG_APERTURE_VALUE,
+ ExifInterface.TAG_MAX_APERTURE_VALUE,
+ ExifInterface.TAG_METERING_MODE,
+ ExifInterface.TAG_ARTIST,
+ ExifInterface.TAG_BITS_PER_SAMPLE,
+ ExifInterface.TAG_COMPRESSION,
+ ExifInterface.TAG_BODY_SERIAL_NUMBER,
+ ExifInterface.TAG_BRIGHTNESS_VALUE,
+ ExifInterface.TAG_CONTRAST,
+ ExifInterface.TAG_CAMERA_OWNER_NAME,
+ ExifInterface.TAG_COLOR_SPACE,
+ ExifInterface.TAG_COPYRIGHT,
+ ExifInterface.TAG_DATETIME,
+ ExifInterface.TAG_DATETIME_DIGITIZED,
+ ExifInterface.TAG_DATETIME_ORIGINAL,
+ ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
+ ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
+ ExifInterface.TAG_EXIF_VERSION,
+ ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
+ ExifInterface.TAG_EXPOSURE_INDEX,
+ ExifInterface.TAG_EXPOSURE_MODE,
+ ExifInterface.TAG_EXPOSURE_TIME,
+ ExifInterface.TAG_EXPOSURE_PROGRAM,
+ ExifInterface.TAG_FLASH,
+ ExifInterface.TAG_FLASH_ENERGY,
+ ExifInterface.TAG_FOCAL_LENGTH,
+ ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
+ ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+ ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
+ ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
+ ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
+ ExifInterface.TAG_PLANAR_CONFIGURATION,
+ ExifInterface.TAG_F_NUMBER,
+ ExifInterface.TAG_GAIN_CONTROL,
+ ExifInterface.TAG_GAMMA,
+ ExifInterface.TAG_GPS_ALTITUDE,
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
+ ExifInterface.TAG_GPS_AREA_INFORMATION,
+ ExifInterface.TAG_GPS_DATESTAMP,
+ ExifInterface.TAG_GPS_DOP,
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_STATUS,
+ ExifInterface.TAG_GPS_DEST_BEARING,
+ ExifInterface.TAG_GPS_DEST_BEARING_REF,
+ ExifInterface.TAG_GPS_DEST_DISTANCE,
+ ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
+ ExifInterface.TAG_GPS_DEST_LATITUDE,
+ ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
+ ExifInterface.TAG_GPS_DEST_LONGITUDE,
+ ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_DIFFERENTIAL,
+ ExifInterface.TAG_GPS_IMG_DIRECTION,
+ ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+ ExifInterface.TAG_GPS_MAP_DATUM,
+ ExifInterface.TAG_GPS_MEASURE_MODE,
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ ExifInterface.TAG_GPS_SATELLITES,
+ ExifInterface.TAG_GPS_SPEED,
+ ExifInterface.TAG_GPS_SPEED_REF,
+ ExifInterface.TAG_GPS_STATUS,
+ ExifInterface.TAG_GPS_TIMESTAMP,
+ ExifInterface.TAG_GPS_TRACK,
+ ExifInterface.TAG_GPS_TRACK_REF,
+ ExifInterface.TAG_GPS_VERSION_ID,
+ ExifInterface.TAG_IMAGE_DESCRIPTION,
+ ExifInterface.TAG_IMAGE_UNIQUE_ID,
+ ExifInterface.TAG_ISO_SPEED,
+ ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ExifInterface.TAG_LENS_MAKE,
+ ExifInterface.TAG_LENS_MODEL,
+ ExifInterface.TAG_LENS_SERIAL_NUMBER,
+ ExifInterface.TAG_LENS_SPECIFICATION,
+ ExifInterface.TAG_LIGHT_SOURCE,
+ ExifInterface.TAG_MAKE,
+ ExifInterface.TAG_MAKER_NOTE,
+ ExifInterface.TAG_MODEL,
+ // ExifInterface.TAG_ORIENTATION, // removed
+ ExifInterface.TAG_SATURATION,
+ ExifInterface.TAG_SHARPNESS,
+ ExifInterface.TAG_SHUTTER_SPEED_VALUE,
+ ExifInterface.TAG_SOFTWARE,
+ ExifInterface.TAG_SUBJECT_DISTANCE,
+ ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
+ ExifInterface.TAG_SUBJECT_LOCATION,
+ ExifInterface.TAG_USER_COMMENT,
+ ExifInterface.TAG_WHITE_BALANCE
+ };
+
+
+
+ /**
+ * Resize the specified bitmap.
+ */
+ private static Bitmap resizeImage(Bitmap image, int newWidth, int newHeight,
+ String mode, boolean onlyScaleDown) {
+ Bitmap newImage = null;
+ if (image == null) {
+ return null; // Can't load the image from the given path.
+ }
+
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ if (newHeight > 0 && newWidth > 0) {
+ int finalWidth;
+ int finalHeight;
+
+ if (mode.equals("stretch")) {
+ // Distort aspect ratio
+ finalWidth = newWidth;
+ finalHeight = newHeight;
+
+ if (onlyScaleDown) {
+ finalWidth = Math.min(width, finalWidth);
+ finalHeight = Math.min(height, finalHeight);
+ }
+ } else {
+ // "contain" (default) or "cover": keep its aspect ratio
+ float widthRatio = (float) newWidth / width;
+ float heightRatio = (float) newHeight / height;
+
+ float ratio = mode.equals("cover") ?
+ Math.max(widthRatio, heightRatio) :
+ Math.min(widthRatio, heightRatio);
+
+ if (onlyScaleDown) ratio = Math.min(ratio, 1);
+
+ finalWidth = (int) Math.round(width * ratio);
+ finalHeight = (int) Math.round(height * ratio);
+ }
+
+ try {
+ newImage = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
+ } catch (OutOfMemoryError e) {
+ return null;
+ }
+ }
+
+ return newImage;
+ }
+
+ /**
+ * Rotate the specified bitmap with the given angle, in degrees.
+ */
+ public static Bitmap rotateImage(Bitmap source, Matrix matrix, float angle)
+ {
+ Bitmap retVal;
+ matrix.postRotate(angle);
+
+ try {
+ retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
+ } catch (OutOfMemoryError e) {
+ return null;
+ }
+ return retVal;
+ }
+
+ /**
+ * Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
+ */
+ public static File saveImage(Bitmap bitmap, File saveDirectory, String fileName,
+ Bitmap.CompressFormat compressFormat, int quality)
+ throws IOException {
+ if (bitmap == null) {
+ throw new IOException("The bitmap couldn't be resized");
+ }
+
+ File newFile = new File(saveDirectory, fileName + "." + compressFormat.name());
+ if(!newFile.createNewFile()) {
+ throw new IOException("The file already exists");
+ }
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ bitmap.compress(compressFormat, quality, outputStream);
+ byte[] bitmapData = outputStream.toByteArray();
+
+ outputStream.flush();
+ outputStream.close();
+
+ FileOutputStream fos = new FileOutputStream(newFile);
+ fos.write(bitmapData);
+ fos.flush();
+ fos.close();
+
+ return newFile;
+ }
+
+ /**
+ * Get {@link File} object for the given Android URI.
+ * Use content resolver to get real path if direct path doesn't return valid file.
+ */
+ private static File getFileFromUri(Context context, Uri uri) {
+
+ // first try by direct path
+ File file = new File(uri.getPath());
+ if (file.exists()) {
+ return file;
+ }
+
+ // try reading real path from content resolver (gallery images)
+ Cursor cursor = null;
+ try {
+ String[] proj = {MediaStore.Images.Media.DATA};
+ cursor = context.getContentResolver().query(uri, proj, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ cursor.moveToFirst();
+ String realPath = cursor.getString(column_index);
+ file = new File(realPath);
+ } catch (Exception ignored) {
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return file;
+ }
+
+ /**
+ * Attempts to copy exif info from one file to another. Note: orientation, width, and height
+ exif attributes are not copied since those are lost after image rotation.
+
+ * imageUri: original image URI as provided from JS
+ * dstPath: final image output path
+ * Returns true if copy was successful, false otherwise.
+ */
+ public static boolean copyExif(Context context, Uri imageUri, String dstPath){
+ ExifInterface src = null;
+ ExifInterface dst = null;
+
+ try {
+
+ File file = getFileFromUri(context, imageUri);
+ if (!file.exists()) {
+ return false;
+ }
+
+ src = new ExifInterface(file.getAbsolutePath());
+ dst = new ExifInterface(dstPath);
+
+ } catch (Exception ignored) {
+ Log.e("ImageResizer::copyExif", "EXIF read failed", ignored);
+ }
+
+ if(src == null || dst == null){
+ return false;
+ }
+
+ try{
+
+ for (String attr : EXIF_TO_COPY_ROTATED)
+ {
+ String value = src.getAttribute(attr);
+ if (value != null){
+ dst.setAttribute(attr, value);
+ }
+ }
+ dst.saveAttributes();
+
+ } catch (Exception ignored) {
+ Log.e("ImageResizer::copyExif", "EXIF copy failed", ignored);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get orientation by reading Image metadata
+ */
+ public static Matrix getOrientationMatrix(Context context, Uri uri) {
+ try {
+ // ExifInterface(InputStream) only exists since Android N (r24)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ InputStream input = context.getContentResolver().openInputStream(uri);
+ ExifInterface ei = new ExifInterface(input);
+ return getOrientationMatrix(ei);
+ }
+ File file = getFileFromUri(context, uri);
+ if (file.exists()) {
+ ExifInterface ei = new ExifInterface(file.getAbsolutePath());
+ return getOrientationMatrix(ei);
+ }
+ } catch (Exception ignored) { }
+
+ return new Matrix();
+ }
+
+ /**
+ * Convert metadata to degrees
+ */
+ public static Matrix getOrientationMatrix(ExifInterface exif) {
+ Matrix matrix = new Matrix();
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+ matrix.setScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_TRANSPOSE:
+ matrix.setRotate(90);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ matrix.setRotate(90);
+ break;
+ case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+ matrix.setRotate(180);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ matrix.setRotate(180);
+ break;
+ case ExifInterface.ORIENTATION_TRANSVERSE:
+ matrix.setRotate(270);
+ matrix.postScale(-1, 1);
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ matrix.setRotate(270);
+ break;
+ }
+ return matrix;
+ }
+
+ /**
+ * Compute the inSampleSize value to use to load a bitmap.
+ * Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
+ */
+ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ final int halfHeight = height / 2;
+ final int halfWidth = width / 2;
+
+ // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+ // height and width larger than the requested height and width.
+ while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
+ inSampleSize *= 2;
+ }
+ }
+
+ return inSampleSize;
+ }
+
+ /**
+ * Load a bitmap either from a real file or using the {@link ContentResolver} of the current
+ * {@link Context} (to read gallery images for example).
+ *
+ * Note that, when options.inJustDecodeBounds = true, we actually expect sourceImage to remain
+ * as null (see https://developer.android.com/training/displaying-bitmaps/load-bitmap.html), so
+ * getting null sourceImage at the completion of this method is not always worthy of an error.
+ */
+ private static Bitmap loadBitmap(Context context, Uri imageUri, BitmapFactory.Options options) throws IOException {
+ Bitmap sourceImage = null;
+ String imageUriScheme = imageUri.getScheme();
+ if (imageUriScheme == null || !imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
+ try {
+ sourceImage = BitmapFactory.decodeFile(imageUri.getPath(), options);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new IOException("Error decoding image file");
+ }
+ } else {
+ ContentResolver cr = context.getContentResolver();
+ InputStream input = cr.openInputStream(imageUri);
+ if (input != null) {
+ sourceImage = BitmapFactory.decodeStream(input, null, options);
+ input.close();
+ }
+ }
+ return sourceImage;
+ }
+
+ /**
+ * Loads the bitmap resource from the file specified in imagePath.
+ */
+ private static Bitmap loadBitmapFromFile(Context context, Uri imageUri, int newWidth,
+ int newHeight) throws IOException {
+ // Decode the image bounds to find the size of the source image.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ loadBitmap(context, imageUri, options);
+
+ // Set a sample size according to the image size to lower memory usage.
+ options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
+ options.inJustDecodeBounds = false;
+ //System.out.println(options.inSampleSize);
+ return loadBitmap(context, imageUri, options);
+
+ }
+
+ /**
+ * Loads the bitmap resource from an URL
+ */
+ private static Bitmap loadBitmapFromURL(Uri imageUri, int newWidth,
+ int newHeight) throws IOException {
+
+ InputStream input = null;
+ Bitmap sourceImage = null;
+
+ try{
+ URL url = new URL(imageUri.toString());
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.connect();
+ input = connection.getInputStream();
+
+ if (input != null) {
+
+ // need to load into memory since inputstream is not seekable
+ // we still won't load the whole bitmap into memory
+ // Also need this ugly code since we are on Java8...
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ int nRead;
+ byte[] data = new byte[1024];
+ byte[] imageData = null;
+
+ try{
+ while ((nRead = input.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+ imageData = buffer.toByteArray();
+ }
+ finally{
+ buffer.close();
+ }
+
+
+ // Decode the image bounds to find the size of the source image.
+ // Do it here so we only do one request
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);
+
+ // Set a sample size according to the image size to lower memory usage.
+ options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
+ options.inJustDecodeBounds = false;
+
+ sourceImage = BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ throw new IOException("Error fetching remote image file.");
+ }
+ finally{
+ try {
+ if(input != null){
+ input.close();
+ }
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ return sourceImage;
+
+ }
+
+ /**
+ * Loads the bitmap resource from a base64 encoded jpg or png.
+ * Format is as such:
+ * png: 'data:image/png;base64,iVBORw0KGgoAA...'
+ * jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...'
+ */
+ private static Bitmap loadBitmapFromBase64(Uri imageUri) {
+ Bitmap sourceImage = null;
+ String imagePath = imageUri.getSchemeSpecificPart();
+ int commaLocation = imagePath.indexOf(',');
+ if (commaLocation != -1) {
+ final String mimeType = imagePath.substring(0, commaLocation).replace('\\','/').toLowerCase();
+ final boolean isJpeg = mimeType.startsWith(IMAGE_JPEG);
+ final boolean isPng = !isJpeg && mimeType.startsWith(IMAGE_PNG);
+
+ if (isJpeg || isPng) {
+ // base64 image. Convert to a bitmap.
+ final String encodedImage = imagePath.substring(commaLocation + 1);
+ final byte[] decodedString = Base64.decode(encodedImage, Base64.DEFAULT);
+ sourceImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
+ }
+ }
+
+ return sourceImage;
+ }
+
+ /**
+ * Create a resized version of the given image and returns a Bitmap object
+ * ready to be saved or converted. Ensure that the result is cleaned up after use
+ * by using recycle
+ */
+ public static Bitmap createResizedImage(Context context, Uri imageUri, int newWidth,
+ int newHeight, int quality, int rotation,
+ String mode, boolean onlyScaleDown) throws IOException {
+ Bitmap sourceImage = null;
+ String imageUriScheme = imageUri.getScheme();
+
+ if (imageUriScheme == null ||
+ imageUriScheme.equalsIgnoreCase(SCHEME_FILE) ||
+ imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)
+ ) {
+ sourceImage = ImageResizer.loadBitmapFromFile(context, imageUri, newWidth, newHeight);
+ } else if (imageUriScheme.equalsIgnoreCase(SCHEME_HTTP) || imageUriScheme.equalsIgnoreCase(SCHEME_HTTPS)){
+ sourceImage = ImageResizer.loadBitmapFromURL(imageUri, newWidth, newHeight);
+ } else if (imageUriScheme.equalsIgnoreCase(SCHEME_DATA)) {
+ sourceImage = ImageResizer.loadBitmapFromBase64(imageUri);
+ }
+
+ if (sourceImage == null) {
+ throw new IOException("Unable to load source image from path");
+ }
+
+
+ // Rotate if necessary. Rotate first because we will otherwise
+ // get wrong dimensions if we want the new dimensions to be after rotation.
+ // NOTE: This will "fix" the image using it's exif info if it is rotated as well.
+ Bitmap rotatedImage = sourceImage;
+ Matrix matrix = getOrientationMatrix(context, imageUri);
+ rotatedImage = ImageResizer.rotateImage(sourceImage, matrix, rotation);
+
+ if(rotatedImage == null){
+ throw new IOException("Unable to rotate image. Most likely due to not enough memory.");
+ }
+
+ if (rotatedImage != sourceImage) {
+ sourceImage.recycle();
+ }
+
+ // Scale image
+ Bitmap scaledImage = ImageResizer.resizeImage(rotatedImage, newWidth, newHeight, mode, onlyScaleDown);
+
+ if(scaledImage == null){
+ throw new IOException("Unable to resize image. Most likely due to not enough memory.");
+ }
+
+ if (scaledImage != rotatedImage) {
+ rotatedImage.recycle();
+ }
+
+ return scaledImage;
+ }
+}
+
diff --git a/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerModule.java b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerModule.java
new file mode 100644
index 0000000000..d3405f2eb3
--- /dev/null
+++ b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerModule.java
@@ -0,0 +1,111 @@
+package com.reactnativeimageresizer;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.GuardedAsyncTask;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.bridge.ReactMethod;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+public class ImageResizerModule extends ImageResizerSpec {
+ public static final String NAME = "ImageResizer";
+
+ ImageResizerModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ @NonNull
+ public String getName() {
+ return NAME;
+ }
+
+ @ReactMethod
+ public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Boolean keepMeta, Promise promise) {
+ WritableMap options = Arguments.createMap();
+ options.putString("mode", mode);
+ options.putBoolean("onlyScaleDown", onlyScaleDown);
+
+ // Run in guarded async task to prevent blocking the React bridge
+ new GuardedAsyncTask(this.getReactApplicationContext()) {
+ @Override
+ protected void doInBackgroundGuarded(Void... params) {
+ try {
+ Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, keepMeta, options);
+ promise.resolve(response);
+ }
+ catch (IOException e) {
+ promise.reject(e);
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @SuppressLint("LongLogTag")
+ private Object createResizedImageWithExceptions(String imagePath, int newWidth, int newHeight,
+ String compressFormatString, int quality, int rotation, String outputPath,
+ final boolean keepMeta,
+ final ReadableMap options) throws IOException {
+
+ Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
+ Uri imageUri = Uri.parse(imagePath);
+
+ Bitmap scaledImage = ImageResizer.createResizedImage(this.getReactApplicationContext(), imageUri, newWidth, newHeight, quality, rotation,
+ options.getString("mode"), options.getBoolean("onlyScaleDown"));
+
+ if (scaledImage == null) {
+ throw new IOException("The image failed to be resized; invalid Bitmap result.");
+ }
+
+ // Save the resulting image
+ File path = this.getReactApplicationContext().getCacheDir();
+ if (outputPath != null) {
+ path = new File(outputPath);
+ }
+
+ File resizedImage = ImageResizer.saveImage(scaledImage, path, UUID.randomUUID().toString(), compressFormat, quality);
+ WritableMap response = Arguments.createMap();
+
+ // If resizedImagePath is empty and this wasn't caught earlier, throw.
+ if (resizedImage.isFile()) {
+ response.putString("path", resizedImage.getAbsolutePath());
+ response.putString("uri", Uri.fromFile(resizedImage).toString());
+ response.putString("name", resizedImage.getName());
+ response.putDouble("size", resizedImage.length());
+ response.putDouble("width", scaledImage.getWidth());
+ response.putDouble("height", scaledImage.getHeight());
+
+ // Copy file's metadata/exif info if required
+ if(keepMeta){
+ try{
+ ImageResizer.copyExif(this.getReactApplicationContext(), imageUri, resizedImage.getAbsolutePath());
+ }
+ catch(Exception ignored){
+ Log.e("ImageResizer::createResizedImageWithExceptions", "EXIF copy failed", ignored);
+ }
+ }
+ } else {
+ throw new IOException("Error getting resized image path");
+ }
+
+
+ // Clean up bitmap
+ scaledImage.recycle();
+ return response;
+ }
+}
diff --git a/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerPackage.java b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerPackage.java
new file mode 100644
index 0000000000..3f52ebc5d8
--- /dev/null
+++ b/package/native-package/android/src/main/java/com/reactnativeimageresizer/ImageResizerPackage.java
@@ -0,0 +1,45 @@
+package com.reactnativeimageresizer;
+
+import androidx.annotation.Nullable;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.module.model.ReactModuleInfo;
+import com.facebook.react.module.model.ReactModuleInfoProvider;
+import com.facebook.react.TurboReactPackage;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ImageResizerPackage extends TurboReactPackage {
+
+ @Nullable
+ @Override
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
+ if (name.equals(ImageResizerModule.NAME)) {
+ return new ImageResizerModule(reactContext);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
+ return () -> {
+ final Map moduleInfos = new HashMap<>();
+ boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
+ moduleInfos.put(
+ ImageResizerModule.NAME,
+ new ReactModuleInfo(
+ ImageResizerModule.NAME,
+ ImageResizerModule.NAME,
+ false, // canOverrideExistingModule
+ false, // needsEagerInit
+ true, // hasConstants
+ false, // isCxxModule
+ isTurboModule // isTurboModule
+ ));
+ return moduleInfos;
+ };
+ }
+}
diff --git a/package/native-package/android/src/newarch/com/reactnativeimageresizer/ImageResizerSpec.java b/package/native-package/android/src/newarch/com/reactnativeimageresizer/ImageResizerSpec.java
new file mode 100644
index 0000000000..85f922d7ec
--- /dev/null
+++ b/package/native-package/android/src/newarch/com/reactnativeimageresizer/ImageResizerSpec.java
@@ -0,0 +1,9 @@
+package com.reactnativeimageresizer;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+
+abstract class ImageResizerSpec extends NativeImageResizerSpec {
+ ImageResizerSpec(ReactApplicationContext context) {
+ super(context);
+ }
+}
diff --git a/package/native-package/android/src/oldarch/com/reactnativeimageresizer/ImageResizerSpec.java b/package/native-package/android/src/oldarch/com/reactnativeimageresizer/ImageResizerSpec.java
new file mode 100644
index 0000000000..a031fa161b
--- /dev/null
+++ b/package/native-package/android/src/oldarch/com/reactnativeimageresizer/ImageResizerSpec.java
@@ -0,0 +1,16 @@
+package com.reactnativeimageresizer;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+
+abstract class ImageResizerSpec extends ReactContextBaseJavaModule {
+
+ ImageResizerSpec(ReactApplicationContext context) {
+ super(context);
+ }
+
+ public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Boolean keepMeta, Promise promise);
+}
diff --git a/package/native-package/ios/ImageHelpers.h b/package/native-package/ios/ImageHelpers.h
new file mode 100644
index 0000000000..467c6143e2
--- /dev/null
+++ b/package/native-package/ios/ImageHelpers.h
@@ -0,0 +1,57 @@
+/*
+ File: ImageHelpers.h
+
+ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
+ Inc. ("Apple") in consideration of your agreement to the following
+ terms, and your use, installation, modification or redistribution of
+ this Apple software constitutes acceptance of these terms. If you do
+ not agree with these terms, please do not use, install, modify or
+ redistribute this Apple software.
+
+ In consideration of your agreement to abide by the following terms, and
+ subject to these terms, Apple grants you a personal, non-exclusive
+ license, under Apple's copyrights in this original Apple software (the
+ "Apple Software"), to use, reproduce, modify and redistribute the Apple
+ Software, with or without modifications, in source and/or binary forms;
+ provided that if you redistribute the Apple Software in its entirety and
+ without modifications, you must retain this notice and the following
+ text and disclaimers in all such redistributions of the Apple Software.
+ Neither the name, trademarks, service marks or logos of Apple Inc. may
+ be used to endorse or promote products derived from the Apple Software
+ without specific prior written permission from Apple. Except as
+ expressly stated in this notice, no other rights or licenses, express or
+ implied, are granted by Apple herein, including but not limited to any
+ patent rights that may be infringed by your derivative works or by other
+ works in which the Apple Software may be incorporated.
+
+ The Apple Software is provided by Apple on an "AS IS" basis. APPLE
+ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
+ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
+ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
+ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
+
+ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
+ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
+ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
+ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ Copyright (C) 2009 Apple Inc. All Rights Reserved.
+ */
+
+#include
+
+extern const CGBitmapInfo kDefaultCGBitmapInfo;
+extern const CGBitmapInfo kDefaultCGBitmapInfoNoAlpha;
+
+float GetScaleForProportionalResize( CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize );
+CGContextRef CreateCGBitmapContextForWidthAndHeight( unsigned int width, unsigned int height, CGColorSpaceRef optionalColorSpace, CGBitmapInfo optionalInfo );
+
+CGImageRef CreateCGImageFromUIImageScaled( UIImage* inImage, float scaleFactor );
+
+@interface UIImage (scale)
+-(UIImage*)scaleToSize:(CGSize)toSize;
+@end
diff --git a/package/native-package/ios/ImageHelpers.m b/package/native-package/ios/ImageHelpers.m
new file mode 100644
index 0000000000..d9e5a19945
--- /dev/null
+++ b/package/native-package/ios/ImageHelpers.m
@@ -0,0 +1,179 @@
+/*
+ File: ImageHelpers.m
+
+ Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
+ Inc. ("Apple") in consideration of your agreement to the following
+ terms, and your use, installation, modification or redistribution of
+ this Apple software constitutes acceptance of these terms. If you do
+ not agree with these terms, please do not use, install, modify or
+ redistribute this Apple software.
+
+ In consideration of your agreement to abide by the following terms, and
+ subject to these terms, Apple grants you a personal, non-exclusive
+ license, under Apple's copyrights in this original Apple software (the
+ "Apple Software"), to use, reproduce, modify and redistribute the Apple
+ Software, with or without modifications, in source and/or binary forms;
+ provided that if you redistribute the Apple Software in its entirety and
+ without modifications, you must retain this notice and the following
+ text and disclaimers in all such redistributions of the Apple Software.
+ Neither the name, trademarks, service marks or logos of Apple Inc. may
+ be used to endorse or promote products derived from the Apple Software
+ without specific prior written permission from Apple. Except as
+ expressly stated in this notice, no other rights or licenses, express or
+ implied, are granted by Apple herein, including but not limited to any
+ patent rights that may be infringed by your derivative works or by other
+ works in which the Apple Software may be incorporated.
+
+ The Apple Software is provided by Apple on an "AS IS" basis. APPLE
+ MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
+ THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
+ FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
+ OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
+
+ IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
+ MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
+ AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
+ STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ Copyright (C) 2009 Apple Inc. All Rights Reserved.
+ */
+
+#include "ImageHelpers.h"
+
+const CGBitmapInfo kDefaultCGBitmapInfo = (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host);
+const CGBitmapInfo kDefaultCGBitmapInfoNoAlpha = (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host);
+
+CGColorSpaceRef GetDeviceRGBColorSpace() {
+ static CGColorSpaceRef deviceRGBSpace = NULL;
+ if( deviceRGBSpace == NULL )
+ deviceRGBSpace = CGColorSpaceCreateDeviceRGB();
+ return deviceRGBSpace;
+}
+
+float GetScaleForProportionalResize( CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize )
+{
+ float sx = theSize.width;
+ float sy = theSize.height;
+ float dx = intoSize.width;
+ float dy = intoSize.height;
+ float scale = 1;
+
+ if( sx != 0 && sy != 0 )
+ {
+ dx = dx / sx;
+ dy = dy / sy;
+
+ // if maximize is true, take LARGER of the scales, else smaller
+ if( maximize ) scale = (dx > dy) ? dx : dy;
+ else scale = (dx < dy) ? dx : dy;
+
+ if( scale > 1 && onlyScaleDown ) // reset scale
+ scale = 1;
+ }
+ else
+ {
+ scale = 0;
+ }
+ return scale;
+}
+
+CGContextRef CreateCGBitmapContextForWidthAndHeight( unsigned int width, unsigned int height,
+ CGColorSpaceRef optionalColorSpace, CGBitmapInfo optionalInfo )
+{
+ CGColorSpaceRef colorSpace = (optionalColorSpace == NULL) ? GetDeviceRGBColorSpace() : optionalColorSpace;
+ CGBitmapInfo alphaInfo = ( (int32_t)optionalInfo < 0 ) ? kDefaultCGBitmapInfo : optionalInfo;
+ return CGBitmapContextCreate( NULL, width, height, 8, 0, colorSpace, alphaInfo );
+}
+
+CGImageRef CreateCGImageFromUIImageScaled( UIImage* image, float scaleFactor )
+{
+ CGImageRef newImage = NULL;
+ CGContextRef bmContext = NULL;
+ BOOL mustTransform = YES;
+ CGAffineTransform transform = CGAffineTransformIdentity;
+ UIImageOrientation orientation = image.imageOrientation;
+
+ CGImageRef srcCGImage = CGImageRetain( image.CGImage );
+
+ size_t width = CGImageGetWidth(srcCGImage) * scaleFactor;
+ size_t height = CGImageGetHeight(srcCGImage) * scaleFactor;
+
+ // These Orientations are rotated 0 or 180 degrees, so they retain the width/height of the image
+ if( (orientation == UIImageOrientationUp) || (orientation == UIImageOrientationDown) || (orientation == UIImageOrientationUpMirrored) || (orientation == UIImageOrientationDownMirrored) )
+ {
+ bmContext = CreateCGBitmapContextForWidthAndHeight( width, height, NULL, kDefaultCGBitmapInfo );
+ }
+ else // The other Orientations are rotated ±90 degrees, so they swap width & height.
+ {
+ bmContext = CreateCGBitmapContextForWidthAndHeight( height, width, NULL, kDefaultCGBitmapInfo );
+ }
+
+ //CGContextSetInterpolationQuality( bmContext, kCGInterpolationLow );
+ CGContextSetBlendMode( bmContext, kCGBlendModeCopy ); // we just want to copy the data
+
+ switch(orientation)
+ {
+ case UIImageOrientationDown: // 0th row is at the bottom, and 0th column is on the right - Rotate 180 degrees
+ transform = CGAffineTransformMake(-1.0, 0.0, 0.0, -1.0, width, height);
+ break;
+
+ case UIImageOrientationLeft: // 0th row is on the left, and 0th column is the bottom - Rotate -90 degrees
+ transform = CGAffineTransformMake(0.0, 1.0, -1.0, 0.0, height, 0.0);
+ break;
+
+ case UIImageOrientationRight: // 0th row is on the right, and 0th column is the top - Rotate 90 degrees
+ transform = CGAffineTransformMake(0.0, -1.0, 1.0, 0.0, 0.0, width);
+ break;
+
+ case UIImageOrientationUpMirrored: // 0th row is at the top, and 0th column is on the right - Flip Horizontal
+ transform = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, width, 0.0);
+ break;
+
+ case UIImageOrientationDownMirrored: // 0th row is at the bottom, and 0th column is on the left - Flip Vertical
+ transform = CGAffineTransformMake(1.0, 0.0, 0, -1.0, 0.0, height);
+ break;
+
+ case UIImageOrientationLeftMirrored: // 0th row is on the left, and 0th column is the top - Rotate -90 degrees and Flip Vertical
+ transform = CGAffineTransformMake(0.0, -1.0, -1.0, 0.0, height, width);
+ break;
+
+ case UIImageOrientationRightMirrored: // 0th row is on the right, and 0th column is the bottom - Rotate 90 degrees and Flip Vertical
+ transform = CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0);
+ break;
+
+ default:
+ mustTransform = NO;
+ break;
+ }
+
+ if( mustTransform ) CGContextConcatCTM( bmContext, transform );
+
+ CGContextDrawImage( bmContext, CGRectMake(0.0, 0.0, width, height), srcCGImage );
+ CGImageRelease( srcCGImage );
+ newImage = CGBitmapContextCreateImage( bmContext );
+ CFRelease( bmContext );
+
+ return newImage;
+}
+
+@implementation UIImage (scale)
+
+-(UIImage*) scaleToSize:(CGSize)toSize
+{
+ UIImage *scaledImg = nil;
+ float scale = GetScaleForProportionalResize( self.size, toSize, false, false );
+ CGImageRef cgImage = CreateCGImageFromUIImageScaled( self, scale );
+
+ if( cgImage )
+ {
+ scaledImg = [UIImage imageWithCGImage:cgImage]; // autoreleased
+ CGImageRelease( cgImage );
+ }
+ return scaledImg;
+}
+
+@end
diff --git a/package/native-package/ios/ImageResizer.h b/package/native-package/ios/ImageResizer.h
new file mode 100644
index 0000000000..496c1ef8ce
--- /dev/null
+++ b/package/native-package/ios/ImageResizer.h
@@ -0,0 +1,13 @@
+#ifdef RCT_NEW_ARCH_ENABLED
+#import "RNImageResizerSpec.h"
+#else
+#import
+#endif
+
+#ifdef RCT_NEW_ARCH_ENABLED
+@interface ImageResizer : NSObject
+#else
+@interface ImageResizer : NSObject
+#endif
+
+@end
diff --git a/package/native-package/ios/ImageResizer.mm b/package/native-package/ios/ImageResizer.mm
new file mode 100644
index 0000000000..ac23dd88bd
--- /dev/null
+++ b/package/native-package/ios/ImageResizer.mm
@@ -0,0 +1,417 @@
+#import "ImageResizer.h"
+#import
+#import
+#import
+
+#if __has_include()
+#import
+#import
+#else
+#import "RCTLog.h"
+#import "RCTImageLoader.h"
+#endif
+
+NSString *moduleName = @"ImageResizer";
+
+@implementation ImageResizer
+
+@synthesize bridge = _bridge;
+
+RCT_EXPORT_MODULE()
+
+RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
+{
+ [self createResizedImage:uri width:width height:height format:format quality:quality mode:mode onlyScaleDown:onlyScaleDown rotation:rotation outputPath:outputPath keepMeta:keepMeta resolve:resolve reject:reject];
+}
+
+- (void)createResizedImage:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ @try {
+ CGSize newSize = CGSizeMake(width, height);
+
+ //Set image extension
+ NSString *extension = @"jpg";
+ if ([format isEqualToString:@"PNG"]) {
+ extension = @"png";
+ }
+
+ NSString* fullPath;
+ @try {
+ fullPath = generateFilePath(extension, outputPath);
+ } @catch (NSException *exception) {
+ [NSException raise:moduleName format:@"Invalid output path."];
+ }
+
+ RCTImageLoader *loader = [self.bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES];
+ NSURLRequest *request = [RCTConvert NSURLRequest:uri];
+ [loader loadImageWithURLRequest:request
+ size:newSize
+ scale:1
+ clipped:NO
+ resizeMode:RCTResizeModeContain
+ progressBlock:nil
+ partialLoadBlock:nil
+ completionBlock:^(NSError *error, UIImage *image) {
+ if (error) {
+ RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", [NSString stringWithFormat: @"%ld", (long)error.code], error.description]);
+ reject([NSString stringWithFormat: @"%ld", (long)error.code], error.description, nil);
+ return;
+ }
+ NSDictionary * response = transformImage(image, uri, [rotation integerValue], newSize, fullPath, format, (int)quality, [keepMeta boolValue], @{@"mode": mode, @"onlyScaleDown": [NSNumber numberWithBool:onlyScaleDown]});
+ resolve(response);
+ }];
+ } @catch (NSException *exception) {
+ RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", exception.name, exception.reason]);
+ reject(exception.name, exception.reason, nil);
+ }
+ });
+}
+
+
+
+bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality, NSMutableDictionary *metadata)
+{
+ if(metadata == nil){
+ NSData* data = nil;
+ if ([format isEqualToString:@"JPEG"]) {
+ data = UIImageJPEGRepresentation(image, quality / 100.0);
+ } else if ([format isEqualToString:@"PNG"]) {
+ data = UIImagePNGRepresentation(image);
+ }
+
+ if (data == nil) {
+ return NO;
+ }
+
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ return [fileManager createFileAtPath:fullPath contents:data attributes:nil];
+ }
+
+ // process / write metadata together with image data
+ else{
+
+ CFStringRef imgType = kUTTypeJPEG;
+
+ if ([format isEqualToString:@"JPEG"]) {
+ [metadata setObject:@(quality / 100.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
+ }
+ else if([format isEqualToString:@"PNG"]){
+ imgType = kUTTypePNG;
+ }
+ else{
+ return NO;
+ }
+
+ NSMutableData * destData = [NSMutableData data];
+
+ CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)destData, imgType, 1, NULL);
+
+ @try{
+ CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef) metadata);
+
+ // write final image data with metadata to our destination
+ if (CGImageDestinationFinalize(destination)){
+
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ return [fileManager createFileAtPath:fullPath contents:destData attributes:nil];
+ }
+ else{
+ return NO;
+ }
+ }
+ @finally{
+ @try{
+ CFRelease(destination);
+ }
+ @catch(NSException *exception){
+ NSLog(@"Failed to release CGImageDestinationRef: %@", exception);
+ }
+ }
+ }
+}
+
+NSString * generateFilePath(NSString * ext, NSString * outputPath)
+{
+ NSString* directory;
+
+ if ([outputPath length] == 0) {
+ NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+ directory = [paths firstObject];
+ } else {
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+ if ([outputPath hasPrefix:documentsDirectory]) {
+ directory = outputPath;
+ } else {
+ directory = [documentsDirectory stringByAppendingPathComponent:outputPath];
+ }
+
+ NSError *error;
+ [[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error];
+ if (error) {
+ NSLog(@"Error creating documents subdirectory: %@", error);
+ @throw [NSException exceptionWithName:@"InvalidPathException" reason:[NSString stringWithFormat:@"Error creating documents subdirectory: %@", error] userInfo:nil];
+ }
+ }
+
+ NSString* name = [[NSUUID UUID] UUIDString];
+ NSString* fullName = [NSString stringWithFormat:@"%@.%@", name, ext];
+ NSString* fullPath = [directory stringByAppendingPathComponent:fullName];
+
+ return fullPath;
+}
+
+UIImage * rotateImage(UIImage *inputImage, float rotationDegrees)
+{
+
+ // We want only fixed 0, 90, 180, 270 degree rotations.
+ const int rotDiv90 = (int)round(rotationDegrees / 90);
+ const int rotQuadrant = rotDiv90 % 4;
+ const int rotQuadrantAbs = (rotQuadrant < 0) ? rotQuadrant + 4 : rotQuadrant;
+
+ // Return the input image if no rotation specified.
+ if (0 == rotQuadrantAbs) {
+ return inputImage;
+ } else {
+ // Rotate the image by 80, 180, 270.
+ UIImageOrientation orientation = UIImageOrientationUp;
+
+ switch(rotQuadrantAbs) {
+ case 1:
+ orientation = UIImageOrientationRight; // 90 deg CW
+ break;
+ case 2:
+ orientation = UIImageOrientationDown; // 180 deg rotation
+ break;
+ default:
+ orientation = UIImageOrientationLeft; // 90 deg CCW
+ break;
+ }
+
+ return [[UIImage alloc] initWithCGImage: inputImage.CGImage
+ scale: 1.0
+ orientation: orientation];
+ }
+}
+
+float getScaleForProportionalResize(CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize)
+{
+ float sx = theSize.width;
+ float sy = theSize.height;
+ float dx = intoSize.width;
+ float dy = intoSize.height;
+ float scale = 1;
+
+ if( sx != 0 && sy != 0 )
+ {
+ dx = dx / sx;
+ dy = dy / sy;
+
+ // if maximize is true, take LARGER of the scales, else smaller
+ if (maximize) {
+ scale = MAX(dx, dy);
+ } else {
+ scale = MIN(dx, dy);
+ }
+
+ if (onlyScaleDown) {
+ scale = MIN(scale, 1);
+ }
+ }
+ else
+ {
+ scale = 0;
+ }
+ return scale;
+}
+
+
+// returns a resized image keeping aspect ratio and considering
+// any :image scale factor.
+// The returned image is an unscaled image (scale = 1.0)
+// so no additional scaling math needs to be done to get its pixel dimensions
+UIImage* scaleImage (UIImage* image, CGSize toSize, NSString* mode, bool onlyScaleDown)
+{
+
+ // Need to do scaling corrections
+ // based on scale, since UIImage width/height gives us
+ // a possibly scaled image (dimensions in points)
+ // Idea taken from RNCamera resize code
+ CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
+
+ // using this instead of ImageHelpers allows us to consider
+ // rotation variations
+ CGSize newSize;
+
+ if ([mode isEqualToString:@"stretch"]) {
+ // Distort aspect ratio
+ int width = toSize.width;
+ int height = toSize.height;
+
+ if (onlyScaleDown) {
+ width = MIN(width, imageSize.width);
+ height = MIN(height, imageSize.height);
+ }
+
+ newSize = CGSizeMake(width, height);
+ } else {
+ // Either "contain" (default) or "cover": preserve aspect ratio
+ bool maximize = [mode isEqualToString:@"cover"];
+ float scale = getScaleForProportionalResize(imageSize, toSize, onlyScaleDown, maximize);
+ newSize = CGSizeMake(roundf(imageSize.width * scale), roundf(imageSize.height * scale));
+ }
+
+ UIGraphicsBeginImageContextWithOptions(newSize, NO, 1.0);
+ [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
+ UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return newImage;
+}
+
+// Returns the image's metadata, or nil if failed to retrieve it.
+NSMutableDictionary * getImageMeta(NSString * path)
+{
+ if([path hasPrefix:@"assets-library"]) {
+
+ __block NSMutableDictionary* res = nil;
+
+ ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
+ {
+
+ NSDictionary *exif = [[myasset defaultRepresentation] metadata];
+ res = [exif mutableCopy];
+
+ };
+
+ ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
+ NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+
+ [assetslibrary assetForURL:url resultBlock:resultblock failureBlock:^(NSError *error) { NSLog(@"error couldn't image from assets library"); }];
+
+ return res;
+
+ } else {
+
+ NSData* imageData = nil;
+
+ if ([path hasPrefix:@"data:"] || [path hasPrefix:@"file:"]) {
+ NSURL *imageUrl = [[NSURL alloc] initWithString:path];
+ imageData = [NSData dataWithContentsOfURL:imageUrl];
+
+ } else {
+ imageData = [NSData dataWithContentsOfFile:path];
+ }
+
+ if(imageData == nil){
+ NSLog(@"Could not get image file data to extract metadata.");
+ return nil;
+ }
+
+ CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
+
+
+ if(source != nil){
+
+ CFDictionaryRef metaRef = CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
+
+ // release CF image
+ CFRelease(source);
+
+ CFMutableDictionaryRef metaRefMutable = CFDictionaryCreateMutableCopy(NULL, 0, metaRef);
+
+ // release the source meta ref now that we've copie it
+ CFRelease(metaRef);
+
+ // bridge CF object so it auto releases
+ NSMutableDictionary* res = (NSMutableDictionary *)CFBridgingRelease(metaRefMutable);
+
+ return res;
+
+ }
+ else{
+ return nil;
+ }
+
+ }
+}
+
+NSDictionary * transformImage(UIImage *image,
+ NSString * originalPath,
+ int rotation,
+ CGSize newSize,
+ NSString* fullPath,
+ NSString* format,
+ int quality,
+ BOOL keepMeta,
+ NSDictionary* options)
+{
+ if (image == nil) {
+ [NSException raise:moduleName format:@"Can't retrieve the file from the path."];
+ }
+
+ // Rotate image if rotation is specified.
+ if (0 != (int)rotation) {
+ image = rotateImage(image, rotation);
+ if (image == nil) {
+ [NSException raise:moduleName format:@"Can't rotate the image."];
+ }
+ }
+
+ // Do the resizing
+ UIImage * scaledImage = scaleImage(
+ image,
+ newSize,
+ options[@"mode"],
+ [[options objectForKey:@"onlyScaleDown"] boolValue]
+ );
+
+ if (scaledImage == nil) {
+ [NSException raise:moduleName format:@"Can't resize the image."];
+ }
+
+
+ NSMutableDictionary *metadata = nil;
+
+ // to be consistent with Android, we will only allow JPEG
+ // to do this.
+ if(keepMeta && [format isEqualToString:@"JPEG"]){
+
+ metadata = getImageMeta(originalPath);
+
+ // remove orientation (since we fix it)
+ // width/height meta is adjusted automatically
+ // NOTE: This might still leave some stale values due to resize
+ metadata[(NSString*)kCGImagePropertyOrientation] = @(1);
+
+ }
+
+ // Compress and save the image
+ if (!saveImage(fullPath, scaledImage, format, quality, metadata)) {
+ [NSException raise:moduleName format:@"Can't save the image. Check your compression format and your output path"];
+ }
+
+ NSURL *fileUrl = [[NSURL alloc] initFileURLWithPath:fullPath];
+ NSString *fileName = fileUrl.lastPathComponent;
+ NSError *attributesError = nil;
+ NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:&attributesError];
+ NSNumber *fileSize = fileAttributes == nil ? 0 : [fileAttributes objectForKey:NSFileSize];
+ NSDictionary *response = @{@"path": fullPath,
+ @"uri": fileUrl.absoluteString,
+ @"name": fileName,
+ @"size": fileSize == nil ? @(0) : fileSize,
+ @"width": @(scaledImage.size.width),
+ @"height": @(scaledImage.size.height),
+ };
+
+ return response;
+}
+
+// Don't compile this code when we build for the old architecture.
+#ifdef RCT_NEW_ARCH_ENABLED
+- (std::shared_ptr)getTurboModule:
+(const facebook::react::ObjCTurboModule::InitParams &)params
+{
+ return std::make_shared(params);
+}
+#endif
+@end
diff --git a/package/native-package/ios/ImageResizer.xcodeproj/project.pbxproj b/package/native-package/ios/ImageResizer.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..cfc6ebba37
--- /dev/null
+++ b/package/native-package/ios/ImageResizer.xcodeproj/project.pbxproj
@@ -0,0 +1,274 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 5E555C0D2413F4C50049A1A2 /* ImageResizer.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ImageResizer.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 58B511D91A9E6C8500147676 /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/$(PRODUCT_NAME)";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 134814201AA4EA6300B7C361 /* libImageResizer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libImageResizer.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ B3E7B5881CC2AC0600A0062D /* ImageResizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageResizer.h; sourceTree = ""; };
+ B3E7B5891CC2AC0600A0062D /* ImageResizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageResizer.m; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 58B511D81A9E6C8500147676 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 134814211AA4EA7D00B7C361 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 134814201AA4EA6300B7C361 /* libImageResizer.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 58B511D21A9E6C8500147676 = {
+ isa = PBXGroup;
+ children = (
+ B3E7B5881CC2AC0600A0062D /* ImageResizer.h */,
+ B3E7B5891CC2AC0600A0062D /* ImageResizer.m */,
+ 134814211AA4EA7D00B7C361 /* Products */,
+ );
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 58B511DA1A9E6C8500147676 /* ImageResizer */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ImageResizer" */;
+ buildPhases = (
+ 58B511D71A9E6C8500147676 /* Sources */,
+ 58B511D81A9E6C8500147676 /* Frameworks */,
+ 58B511D91A9E6C8500147676 /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = ImageResizer;
+ productName = RCTDataManager;
+ productReference = 134814201AA4EA6300B7C361 /* libImageResizer.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 58B511D31A9E6C8500147676 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0920;
+ ORGANIZATIONNAME = Facebook;
+ TargetAttributes = {
+ 58B511DA1A9E6C8500147676 = {
+ CreatedOnToolsVersion = 6.1.1;
+ };
+ };
+ };
+ buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ImageResizer" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ English,
+ en,
+ );
+ mainGroup = 58B511D21A9E6C8500147676;
+ productRefGroup = 58B511D21A9E6C8500147676;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 58B511DA1A9E6C8500147676 /* ImageResizer */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 58B511D71A9E6C8500147676 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ B3E7B58A1CC2AC0600A0062D /* ImageResizer.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 58B511ED1A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ "EXCLUDED_ARCHS[sdk=*]" = arm64;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 58B511EE1A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ "EXCLUDED_ARCHS[sdk=*]" = arm64;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 58B511F01A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = ImageResizer;
+ SKIP_INSTALL = YES;
+ };
+ name = Debug;
+ };
+ 58B511F11A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = ImageResizer;
+ SKIP_INSTALL = YES;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "ImageResizer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511ED1A9E6C8500147676 /* Debug */,
+ 58B511EE1A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "ImageResizer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511F01A9E6C8500147676 /* Debug */,
+ 58B511F11A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 58B511D31A9E6C8500147676 /* Project object */;
+}
diff --git a/package/native-package/package.json b/package/native-package/package.json
index aad9e34599..e4d2f23f30 100644
--- a/package/native-package/package.json
+++ b/package/native-package/package.json
@@ -2,10 +2,19 @@
"name": "stream-chat-react-native",
"description": "The official React Native SDK for Stream Chat, a service for building chat applications",
"version": "5.39.5",
+ "homepage": "https://www.npmjs.com/package/stream-chat-react-native",
"author": {
"company": "Stream.io Inc",
"name": "Stream.io Inc"
},
+ "files": [
+ "src",
+ "types",
+ "android",
+ "ios",
+ "*.podspec",
+ "package.json"
+ ],
"license": "SEE LICENSE IN LICENSE",
"main": "src/index.js",
"types": "types/index.d.ts",
@@ -14,7 +23,6 @@
"stream-chat-react-native-core": "5.39.5"
},
"peerDependencies": {
- "@bam.tech/react-native-image-resizer": ">=3.0.10",
"@react-native-camera-roll/camera-roll": ">=7.8.0",
"@react-native-clipboard/clipboard": ">=1.14.1",
"@stream-io/flat-list-mvcp": ">=0.10.3",
@@ -64,7 +72,11 @@
"postpack": "rm README.md"
},
"devDependencies": {
- "@bam.tech/react-native-image-resizer": ">=3.0.10",
"react-native": ">=0.67.0"
+ },
+ "codegenConfig": {
+ "name": "RNImageResizerSpec",
+ "type": "modules",
+ "jsSrcsDir": "src/ImageResizer"
}
}
diff --git a/package/native-package/src/ImageResizer/NativeImageResizer.ts b/package/native-package/src/ImageResizer/NativeImageResizer.ts
new file mode 100644
index 0000000000..61d0ef6051
--- /dev/null
+++ b/package/native-package/src/ImageResizer/NativeImageResizer.ts
@@ -0,0 +1,28 @@
+import type { TurboModule } from 'react-native';
+
+import { TurboModuleRegistry } from 'react-native';
+
+export interface Spec extends TurboModule {
+ createResizedImage(
+ uri: string,
+ width: number,
+ height: number,
+ format: string,
+ quality: number,
+ mode: string,
+ onlyScaleDown: boolean,
+ rotation?: number,
+ outputPath?: string | null,
+ keepMeta?: boolean,
+ ): Promise<{
+ base64: string;
+ height: number;
+ name: string;
+ path: string;
+ size: number;
+ uri: string;
+ width: number;
+ }>;
+}
+
+export default TurboModuleRegistry.getEnforcing('ImageResizer');
diff --git a/package/native-package/src/ImageResizer/index.tsx b/package/native-package/src/ImageResizer/index.tsx
new file mode 100644
index 0000000000..8e837c1829
--- /dev/null
+++ b/package/native-package/src/ImageResizer/index.tsx
@@ -0,0 +1,47 @@
+import { NativeModules } from 'react-native';
+
+import type { Options, ResizeFormat, Response } from './types';
+export type { ResizeFormat, ResizeMode, Response } from './types';
+
+// eslint-disable-next-line no-underscore-dangle
+const isTurboModuleEnabled = global.__turboModuleProxy != null;
+
+const ImageResizer = isTurboModuleEnabled
+ ? require('./NativeImageResizer').default
+ : NativeModules.ImageResizer;
+
+const defaultOptions: Options = {
+ mode: 'contain',
+ onlyScaleDown: false,
+};
+
+function createResizedImage(
+ uri: string,
+ width: number,
+ height: number,
+ format: ResizeFormat,
+ quality: number,
+ rotation: number = 0,
+ outputPath?: string | null,
+ keepMeta = false,
+ options: Options = defaultOptions,
+): Promise {
+ const { mode, onlyScaleDown } = { ...defaultOptions, ...options };
+
+ return ImageResizer.createResizedImage(
+ uri,
+ width,
+ height,
+ format,
+ quality,
+ mode,
+ onlyScaleDown,
+ rotation,
+ outputPath,
+ keepMeta,
+ );
+}
+
+export default {
+ createResizedImage,
+};
diff --git a/package/native-package/src/ImageResizer/types.ts b/package/native-package/src/ImageResizer/types.ts
new file mode 100644
index 0000000000..ec74640b14
--- /dev/null
+++ b/package/native-package/src/ImageResizer/types.ts
@@ -0,0 +1,32 @@
+export interface Response {
+ height: number;
+ name: string;
+ path: string;
+ size: number;
+ uri: string;
+ width: number;
+}
+
+export type ResizeFormat = 'PNG' | 'JPEG' | 'WEBP';
+export type ResizeMode = 'contain' | 'cover' | 'stretch';
+
+export type Options = {
+ /**
+ * Either `contain` (the default), `cover`, or `stretch`. Similar to
+ * [react-native 's resizeMode](https://reactnative.dev/docs/image#resizemode)
+ *
+ * - `contain` will fit the image within `width` and `height`,
+ * preserving its ratio
+ * - `cover` will make sure at least one dimension fits `width` or
+ * `height`, and the other is larger, also preserving its ratio.
+ * - `stretch` will resize the image to exactly `width` and `height`.
+ *
+ * (Default: 'contain')
+ */
+ mode?: ResizeMode;
+ /**
+ * Whether to avoid resizing the image to be larger than the original.
+ * (Default: false)
+ */
+ onlyScaleDown?: boolean;
+};
diff --git a/package/native-package/src/handlers/compressImage.ts b/package/native-package/src/handlers/compressImage.ts
index 2a85c0686a..67b0bd9500 100644
--- a/package/native-package/src/handlers/compressImage.ts
+++ b/package/native-package/src/handlers/compressImage.ts
@@ -1,5 +1,5 @@
// @ts-ignore this module does not have a type declaration
-import ImageResizer from '@bam.tech/react-native-image-resizer';
+import ImageResizer from '../ImageResizer';
type CompressImageParams = {
compressImageQuality: number;
@@ -28,7 +28,7 @@ export const compressImage = async ({
);
return compressedUri;
} catch (error) {
- console.log(error);
+ console.log('Error resizing image:', error);
return uri;
}
};
diff --git a/package/native-package/stream-chat-react-native.podspec b/package/native-package/stream-chat-react-native.podspec
new file mode 100644
index 0000000000..ee2b2dceb3
--- /dev/null
+++ b/package/native-package/stream-chat-react-native.podspec
@@ -0,0 +1,37 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
+
+Pod::Spec.new do |s|
+ s.name = "stream-chat-react-native"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.homepage = package["homepage"]
+ s.license = package["license"]
+ s.authors = package["author"]
+
+ s.platforms = { :ios => "10.0" }
+ s.source = { :git => "./ios", :tag => "#{s.version}" }
+
+ s.source_files = "ios/**/*.{h,m,mm}"
+
+ s.dependency "React-Core"
+ s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
+
+ # Don't install the dependencies when we run `pod install` in the old architecture.
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
+ s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
+ s.pod_target_xcconfig = {
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
+ "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
+ }
+ s.dependency "React-Codegen"
+ s.dependency "RCT-Folly"
+ s.dependency "RCTRequired"
+ s.dependency "RCTTypeSafety"
+ s.dependency "ReactCommon/turbomodule/core"
+ end
+end
+
diff --git a/package/native-package/yarn.lock b/package/native-package/yarn.lock
index 8da048dab5..edbc21049c 100644
--- a/package/native-package/yarn.lock
+++ b/package/native-package/yarn.lock
@@ -1038,11 +1038,6 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
-"@bam.tech/react-native-image-resizer@>=3.0.10":
- version "3.0.10"
- resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.10.tgz#03395a29cb61cd819ce1e7730fb137ab6e75618a"
- integrity sha512-IVIBRkgy8eq4g51RjAzh7zk8KpGhiQH6GqLC7SgAUJ0plh2bdqG2l8+D+Q/A0uFe85YutUmHyFioyDEsRGXaCQ==
-
"@gorhom/bottom-sheet@^4.6.4":
version "4.6.4"
resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.6.4.tgz#387d0f0f21e3470eb8575498cb81ce96f5108e79"
@@ -4247,10 +4242,10 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
-stream-chat-react-native-core@5.39.4:
- version "5.39.4"
- resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.4.tgz#39db32070ccf95aa32ceb2fec96f777c27f0a8d2"
- integrity sha512-XKrPnIKVVOz9mi4/oVkMX8zD8JnqDIxjiqhYZelT4Hb2qMrhvpmCmxq60Fz++JZf7lFgtnoAx+oec1ns4g0VWg==
+stream-chat-react-native-core@5.39.5:
+ version "5.39.5"
+ resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.5.tgz#8a2f587404bc84f1761e180602358a60b46f724b"
+ integrity sha512-H0wSnF7PgwepvozXRZs5K9TR9A3mOKMc6hyV+sLtLKW13rqyN11dSuSKorOYzCCkrnasYi+2sXM1dd7Orunbww==
dependencies:
"@gorhom/bottom-sheet" "^4.6.4"
dayjs "1.10.5"