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"