Unify returned value between Android and iOS implementation, (#83)

previously Android implementation worked differently and to the returned
absolute path a 'file:' is prepended.

This different behaviour may causes issues/bugs when handling the response from an
unified code base point of view and also the returned uri on the Android native code
is not generated properly using native functions to do so.

This patch fixes this incoherence, but also extends the returned value to add:
absolute path, file name, file size and the uri.
Refactored Android code to make it easier to read and more robust to
case sensitive file names.
This commit is contained in:
Marco Cimmino 2017-07-18 14:05:26 -07:00 committed by Florian Rival
parent 711527e2b4
commit 544dd9d473
6 changed files with 79 additions and 61 deletions

View File

@ -33,8 +33,11 @@ Note: on latest versions of React Native, you may have an error during the Gradl
```javascript ```javascript
import ImageResizer from 'react-native-image-resizer'; import ImageResizer from 'react-native-image-resizer';
ImageResizer.createResizedImage(imageUri, newWidth, newHeight, compressFormat, quality, rotation, outputPath).then((resizedImageUri) => { ImageResizer.createResizedImage(imageUri, newWidth, newHeight, compressFormat, quality, rotation, outputPath).then((response) => {
// resizeImageUri is the URI of the new image that can now be displayed, uploaded... // response.uri is the URI of the new image that can now be displayed, uploaded...
// response.path is the path of the new image
// response.name is the name of the new image with the extension
// response.size is the size of the new image
}).catch((err) => { }).catch((err) => {
// Oops, something went wrong. Check that the filename is correct and // Oops, something went wrong. Check that the filename is correct and
// inspect err to get more details. // inspect err to get more details.
@ -49,9 +52,7 @@ A basic, sample app is available in [the `example` folder](https://github.com/ba
### `promise createResizedImage(path, maxWidth, maxHeight, compressFormat, quality, rotation = 0, outputPath)` ### `promise createResizedImage(path, maxWidth, maxHeight, compressFormat, quality, rotation = 0, outputPath)`
The promise resolves with a string containing the URI of the new file. This URI can be used directly as the `source` of an [`<Image>`](https://facebook.github.io/react-native/docs/image.html) component. The promise resolves with an object containing: path, uri, name and size of the new file. The URI can be used directly as the `source` of an [`<Image>`](https://facebook.github.io/react-native/docs/image.html) component.
> :warning: On Android, `file:` will be prepended to the returned string. This allows it to be displayed as an image, but it also means you [won't be able to access the file directly](https://github.com/bamlab/react-native-image-resizer/issues/50) when using a utility like `fs`. To do so, you can simply use `rawPath = originalPath.replace('file:', '')` to get the raw path.
Option | Description Option | Description
------ | ----------- ------ | -----------

View File

@ -5,21 +5,15 @@ import android.content.ContentResolver;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Base64; import android.util.Base64;
import android.util.Pair;
import java.io.Closeable;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Date; import java.util.Date;
@ -28,10 +22,11 @@ import java.util.Date;
* Provide methods to resize and rotate an image file. * Provide methods to resize and rotate an image file.
*/ */
class ImageResizer { class ImageResizer {
private final static String IMAGE_JPEG = "image/jpeg";
public final static String BASE64_PREFIX = "data:image/"; private final static String IMAGE_PNG = "image/png";
public final static String CONTENT_PREFIX = "content://"; private final static String SCHEME_DATA = "data";
public final static String FILE_PREFIX = "file:"; private final static String SCHEME_CONTENT = "content";
private final static String SCHEME_FILE = "file";
/** /**
* Resize the specified bitmap, keeping its aspect ratio. * Resize the specified bitmap, keeping its aspect ratio.
@ -72,7 +67,7 @@ class ImageResizer {
/** /**
* Save the given bitmap in a directory. Extension is automatically generated using the bitmap format. * Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
*/ */
private static String saveImage(Bitmap bitmap, File saveDirectory, String fileName, private static File saveImage(Bitmap bitmap, File saveDirectory, String fileName,
Bitmap.CompressFormat compressFormat, int quality) Bitmap.CompressFormat compressFormat, int quality)
throws IOException { throws IOException {
if (bitmap == null) { if (bitmap == null) {
@ -96,7 +91,7 @@ class ImageResizer {
fos.flush(); fos.flush();
fos.close(); fos.close();
return newFile.getAbsolutePath(); return newFile;
} }
/** /**
@ -195,18 +190,19 @@ class ImageResizer {
* as null (see https://developer.android.com/training/displaying-bitmaps/load-bitmap.html), so * 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. * getting null sourceImage at the completion of this method is not always worthy of an error.
*/ */
private static Bitmap loadBitmap(Context context, String imagePath, BitmapFactory.Options options) throws IOException { private static Bitmap loadBitmap(Context context, Uri imageUri, BitmapFactory.Options options) throws IOException {
Bitmap sourceImage = null; Bitmap sourceImage = null;
if (!imagePath.startsWith(CONTENT_PREFIX)) { String imageUriScheme = imageUri.getScheme();
if (imageUriScheme == null || !imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
try { try {
sourceImage = BitmapFactory.decodeFile(imagePath, options); sourceImage = BitmapFactory.decodeFile(imageUri.getPath(), options);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
throw new IOException("Error decoding image file"); throw new IOException("Error decoding image file");
} }
} else { } else {
ContentResolver cr = context.getContentResolver(); ContentResolver cr = context.getContentResolver();
InputStream input = cr.openInputStream(Uri.parse(imagePath)); InputStream input = cr.openInputStream(imageUri);
if (input != null) { if (input != null) {
sourceImage = BitmapFactory.decodeStream(input, null, options); sourceImage = BitmapFactory.decodeStream(input, null, options);
input.close(); input.close();
@ -218,18 +214,18 @@ class ImageResizer {
/** /**
* Loads the bitmap resource from the file specified in imagePath. * Loads the bitmap resource from the file specified in imagePath.
*/ */
private static Bitmap loadBitmapFromFile(Context context, String imagePath, int newWidth, private static Bitmap loadBitmapFromFile(Context context, Uri imageUri, int newWidth,
int newHeight) throws IOException { int newHeight) throws IOException {
// Decode the image bounds to find the size of the source image. // Decode the image bounds to find the size of the source image.
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
loadBitmap(context, imagePath, options); loadBitmap(context, imageUri, options);
// Set a sample size according to the image size to lower memory usage. // Set a sample size according to the image size to lower memory usage.
options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight); options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
System.out.println(options.inSampleSize); System.out.println(options.inSampleSize);
return loadBitmap(context, imagePath, options); return loadBitmap(context, imageUri, options);
} }
@ -239,21 +235,21 @@ class ImageResizer {
* png: 'data:image/png;base64,iVBORw0KGgoAA...' * png: 'data:image/png;base64,iVBORw0KGgoAA...'
* jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...' * jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...'
*/ */
private static Bitmap loadBitmapFromBase64(String imagePath) { private static Bitmap loadBitmapFromBase64(Uri imageUri) {
Bitmap sourceImage = null; 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);
// base64 image. Convert to a bitmap. if (isJpeg || isPng) {
final int prefixLen = BASE64_PREFIX.length(); // base64 image. Convert to a bitmap.
final boolean isJpeg = (imagePath.indexOf("jpeg") == prefixLen); final String encodedImage = imagePath.substring(commaLocation + 1);
final boolean isPng = (!isJpeg) && (imagePath.indexOf("png") == prefixLen); final byte[] decodedString = Base64.decode(encodedImage, Base64.DEFAULT);
int commaLocation = -1; sourceImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
if (isJpeg || isPng){ }
commaLocation = imagePath.indexOf(',');
}
if (commaLocation > 0) {
final String encodedImage = imagePath.substring(commaLocation+1);
final byte[] decodedString = Base64.decode(encodedImage, Base64.DEFAULT);
sourceImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
} }
return sourceImage; return sourceImage;
@ -262,17 +258,15 @@ class ImageResizer {
/** /**
* Create a resized version of the given image. * Create a resized version of the given image.
*/ */
public static String createResizedImage(Context context, String imagePath, int newWidth, public static File createResizedImage(Context context, Uri imageUri, int newWidth,
int newHeight, Bitmap.CompressFormat compressFormat, int newHeight, Bitmap.CompressFormat compressFormat,
int quality, int rotation, String outputPath) throws IOException { int quality, int rotation, String outputPath) throws IOException {
Bitmap sourceImage = null; Bitmap sourceImage = null;
String imageUriScheme = imageUri.getScheme();
// If the BASE64_PREFIX is absent, load bitmap from a file. Otherwise, load from base64. if (imageUriScheme == null || imageUriScheme.equalsIgnoreCase(SCHEME_FILE) || imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
if (!imagePath.startsWith(BASE64_PREFIX)) { sourceImage = ImageResizer.loadBitmapFromFile(context, imageUri, newWidth, newHeight);
sourceImage = ImageResizer.loadBitmapFromFile(context, imagePath, newWidth, newHeight); } else if (imageUriScheme.equalsIgnoreCase(SCHEME_DATA)) {
} sourceImage = ImageResizer.loadBitmapFromBase64(imageUri);
else {
sourceImage = ImageResizer.loadBitmapFromBase64(imagePath);
} }
if (sourceImage == null) { if (sourceImage == null) {
@ -287,7 +281,7 @@ class ImageResizer {
// Rotate if necessary // Rotate if necessary
Bitmap rotatedImage = scaledImage; Bitmap rotatedImage = scaledImage;
int orientation = getOrientation(context, Uri.parse(imagePath)); int orientation = getOrientation(context, imageUri);
rotation = orientation + rotation; rotation = orientation + rotation;
rotatedImage = ImageResizer.rotateImage(scaledImage, rotation); rotatedImage = ImageResizer.rotateImage(scaledImage, rotation);
@ -301,12 +295,12 @@ class ImageResizer {
path = new File(outputPath); path = new File(outputPath);
} }
String resizedImagePath = ImageResizer.saveImage(rotatedImage, path, File newFile = ImageResizer.saveImage(rotatedImage, path,
Long.toString(new Date().getTime()), compressFormat, quality); Long.toString(new Date().getTime()), compressFormat, quality);
// Clean up remaining image // Clean up remaining image
rotatedImage.recycle(); rotatedImage.recycle();
return resizedImagePath; return newFile;
} }
} }

View File

@ -2,12 +2,16 @@ package fr.bamlab.rnimageresizer;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import java.io.File;
import java.io.IOException; import java.io.IOException;
/** /**
@ -45,19 +49,22 @@ class ImageResizerModule extends ReactContextBaseJavaModule {
String compressFormatString, int quality, int rotation, String outputPath, String compressFormatString, int quality, int rotation, String outputPath,
final Callback successCb, final Callback failureCb) throws IOException { final Callback successCb, final Callback failureCb) throws IOException {
Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString); Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
if (imagePath.startsWith(ImageResizer.FILE_PREFIX)) { Uri imageUri = Uri.parse(imagePath);
imagePath = imagePath.replaceFirst(ImageResizer.FILE_PREFIX, "");
}
String resizedImagePath = ImageResizer.createResizedImage(this.context, imagePath, newWidth, File resizedImage = ImageResizer.createResizedImage(this.context, imageUri, newWidth,
newHeight, compressFormat, quality, rotation, outputPath); newHeight, compressFormat, quality, rotation, outputPath);
// If resizedImagePath is empty and this wasn't caught earlier, throw. // If resizedImagePath is empty and this wasn't caught earlier, throw.
if (resizedImagePath == null || resizedImagePath.isEmpty()) { if (resizedImage.isFile()) {
throw new IOException("Error getting resized image path"); WritableMap response = Arguments.createMap();
response.putString("path", resizedImage.getAbsolutePath());
response.putString("uri", Uri.fromFile(resizedImage).toString());
response.putString("name", resizedImage.getName());
response.putDouble("size", resizedImage.length());
// Invoke success
successCb.invoke(response);
} else {
failureCb.invoke("Error getting resized image path");
} }
// Invoke success, prepending file prefix.
successCb.invoke(ImageResizer.FILE_PREFIX + resizedImagePath);
} }
} }

8
index.d.ts vendored
View File

@ -1,7 +1,13 @@
declare module "react-native-image-resizer" { declare module "react-native-image-resizer" {
export interface Response {
path: string;
uri: string;
size?: number;
name?: string;
}
export function createResizedImage( export function createResizedImage(
uri: string, width: number, height: number, uri: string, width: number, height: number,
format: "PNG" | "JPEG" | "WEBP", quality: number, format: "PNG" | "JPEG" | "WEBP", quality: number,
rotation?: number, outputPath?: string rotation?: number, outputPath?: string
): Promise<string>; ): Promise<Response>;
} }

View File

@ -9,12 +9,12 @@ export default {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
NativeModules.ImageResizer.createResizedImage(path, width, height, format, quality, rotation, outputPath, (err, resizedPath) => { NativeModules.ImageResizer.createResizedImage(path, width, height, format, quality, rotation, outputPath, (err, response) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
resolve(resizedPath); resolve(response);
}); });
}); });
}, },

View File

@ -131,8 +131,18 @@ RCT_EXPORT_METHOD(createResizedImage:(NSString *)path
callback(@[@"Can't save the image. Check your compression format.", @""]); callback(@[@"Can't save the image. Check your compression format.", @""]);
return; return;
} }
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
};
callback(@[[NSNull null], fullPath]); callback(@[[NSNull null], response]);
}]; }];
} }