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
import ImageResizer from 'react-native-image-resizer';
ImageResizer.createResizedImage(imageUri, newWidth, newHeight, compressFormat, quality, rotation, outputPath).then((resizedImageUri) => {
// resizeImageUri is the URI of the new image that can now be displayed, uploaded...
ImageResizer.createResizedImage(imageUri, newWidth, newHeight, compressFormat, quality, rotation, outputPath).then((response) => {
// 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) => {
// Oops, something went wrong. Check that the filename is correct and
// 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)`
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.
> :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.
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.
Option | Description
------ | -----------

View File

@ -5,21 +5,15 @@ import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Base64;
import android.util.Pair;
import java.io.Closeable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Date;
@ -28,10 +22,11 @@ import java.util.Date;
* Provide methods to resize and rotate an image file.
*/
class ImageResizer {
public final static String BASE64_PREFIX = "data:image/";
public final static String CONTENT_PREFIX = "content://";
public final static String FILE_PREFIX = "file:";
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";
/**
* 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.
*/
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)
throws IOException {
if (bitmap == null) {
@ -96,7 +91,7 @@ class ImageResizer {
fos.flush();
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
* 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;
if (!imagePath.startsWith(CONTENT_PREFIX)) {
String imageUriScheme = imageUri.getScheme();
if (imageUriScheme == null || !imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
try {
sourceImage = BitmapFactory.decodeFile(imagePath, options);
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(Uri.parse(imagePath));
InputStream input = cr.openInputStream(imageUri);
if (input != null) {
sourceImage = BitmapFactory.decodeStream(input, null, options);
input.close();
@ -218,18 +214,18 @@ class ImageResizer {
/**
* 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 {
// Decode the image bounds to find the size of the source image.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
loadBitmap(context, imagePath, options);
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, imagePath, options);
return loadBitmap(context, imageUri, options);
}
@ -239,21 +235,21 @@ class ImageResizer {
* png: 'data:image/png;base64,iVBORw0KGgoAA...'
* jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...'
*/
private static Bitmap loadBitmapFromBase64(String imagePath) {
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);
// base64 image. Convert to a bitmap.
final int prefixLen = BASE64_PREFIX.length();
final boolean isJpeg = (imagePath.indexOf("jpeg") == prefixLen);
final boolean isPng = (!isJpeg) && (imagePath.indexOf("png") == prefixLen);
int commaLocation = -1;
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);
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;
@ -262,17 +258,15 @@ class ImageResizer {
/**
* 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 quality, int rotation, String outputPath) throws IOException {
Bitmap sourceImage = null;
// If the BASE64_PREFIX is absent, load bitmap from a file. Otherwise, load from base64.
if (!imagePath.startsWith(BASE64_PREFIX)) {
sourceImage = ImageResizer.loadBitmapFromFile(context, imagePath, newWidth, newHeight);
}
else {
sourceImage = ImageResizer.loadBitmapFromBase64(imagePath);
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_DATA)) {
sourceImage = ImageResizer.loadBitmapFromBase64(imageUri);
}
if (sourceImage == null) {
@ -287,7 +281,7 @@ class ImageResizer {
// Rotate if necessary
Bitmap rotatedImage = scaledImage;
int orientation = getOrientation(context, Uri.parse(imagePath));
int orientation = getOrientation(context, imageUri);
rotation = orientation + rotation;
rotatedImage = ImageResizer.rotateImage(scaledImage, rotation);
@ -301,12 +295,12 @@ class ImageResizer {
path = new File(outputPath);
}
String resizedImagePath = ImageResizer.saveImage(rotatedImage, path,
File newFile = ImageResizer.saveImage(rotatedImage, path,
Long.toString(new Date().getTime()), compressFormat, quality);
// Clean up remaining image
rotatedImage.recycle();
return resizedImagePath;
return newFile;
}
}

View File

@ -2,12 +2,16 @@ package fr.bamlab.rnimageresizer;
import android.content.Context;
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.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import java.io.File;
import java.io.IOException;
/**
@ -45,19 +49,22 @@ class ImageResizerModule extends ReactContextBaseJavaModule {
String compressFormatString, int quality, int rotation, String outputPath,
final Callback successCb, final Callback failureCb) throws IOException {
Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
if (imagePath.startsWith(ImageResizer.FILE_PREFIX)) {
imagePath = imagePath.replaceFirst(ImageResizer.FILE_PREFIX, "");
}
Uri imageUri = Uri.parse(imagePath);
String resizedImagePath = ImageResizer.createResizedImage(this.context, imagePath, newWidth,
File resizedImage = ImageResizer.createResizedImage(this.context, imageUri, newWidth,
newHeight, compressFormat, quality, rotation, outputPath);
// If resizedImagePath is empty and this wasn't caught earlier, throw.
if (resizedImagePath == null || resizedImagePath.isEmpty()) {
throw new IOException("Error getting resized image path");
if (resizedImage.isFile()) {
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" {
export interface Response {
path: string;
uri: string;
size?: number;
name?: string;
}
export function createResizedImage(
uri: string, width: number, height: number,
format: "PNG" | "JPEG" | "WEBP", quality: number,
rotation?: number, outputPath?: string
): Promise<string>;
): Promise<Response>;
}

View File

@ -9,12 +9,12 @@ export default {
}
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) {
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.", @""]);
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]);
}];
}