Skip to content

Commit b5ab684

Browse files
authored
Merge pull request #20 from Shobbak/feat/whatsapp-image-compression
feat: add compression like whatsapp by enabling autoCompress props
2 parents cadcc11 + 51c5a01 commit b5ab684

File tree

11 files changed

+337
-56
lines changed

11 files changed

+337
-56
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ react-native link react-native-compressor
5858

5959
### Image
6060

61+
##### For Whatsapp Image Compression
62+
63+
```js
64+
import { Image } from 'react-native-compressor';
65+
66+
const result = await Image.compress('file://path_of_file/image.jpg', {
67+
autoCompress: true,
68+
});
69+
```
70+
71+
[Here is this package comparison of compression with WhatsApp](https://docs.google.com/spreadsheets/d/13TsnC1c7NOC9aCjzN6wkKurJQPeGRNwDhWsQOkXQskU/edit?usp=sharing)
72+
73+
##### For manual Compression
74+
6175
```js
6276
import { Image } from 'react-native-compressor';
6377

@@ -108,6 +122,10 @@ const result = await Video.compress(
108122

109123
### CompressorOptions
110124

125+
- ###### `autoCompress: boolean` (default: false)
126+
127+
if you want to compress images like whatsapp then make this prop `true`. by enable this option other option will not effect in compression
128+
111129
- ###### `maxWidth: number` (default: 1024)
112130

113131
The maximum width boundary used as the main boundary in resizing a landscape image.

android/src/main/java/com/reactnativecompressor/CompressorModule.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,22 @@ private void sendEvent(ReactContext reactContext,
5555
//Image
5656
@ReactMethod
5757
public void image_compress(
58-
String value,
58+
String imagePath,
5959
ReadableMap optionMap,
6060
Promise promise) {
6161
try {
6262
final ImageCompressorOptions options = ImageCompressorOptions.fromMap(optionMap);
63-
final Bitmap image = options.input == ImageCompressorOptions.InputType.base64
64-
? ImageCompressor.decodeImage(value)
65-
: ImageCompressor.loadImage(value);
6663

67-
final Bitmap resizedImage = ImageCompressor.resize(image, options.maxWidth, options.maxHeight);
68-
final ByteArrayOutputStream imageDataByteArrayOutputStream = ImageCompressor.compress(resizedImage, options.output, options.quality);
69-
Boolean isBase64=options.returnableOutputType==ImageCompressorOptions.ReturnableOutputType.base64;
70-
71-
final String returnableResult = ImageCompressor.encodeImage(imageDataByteArrayOutputStream,isBase64,image,options.output.toString(),this.reactContext);
72-
73-
promise.resolve(returnableResult);
64+
if(options.autoCompress)
65+
{
66+
String returnableResult=ImageCompressor.autoCompressImage(imagePath,options.output.toString(),reactContext);
67+
promise.resolve(returnableResult);
68+
}
69+
else
70+
{
71+
String returnableResult=ImageCompressor.manualCompressImage(imagePath,options,reactContext);
72+
promise.resolve(returnableResult);
73+
}
7474
} catch (Exception ex) {
7575
promise.reject(ex);
7676
}

android/src/main/java/com/reactnativecompressor/Image/ImageCompressor.java

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,38 @@
55
import android.graphics.Canvas;
66
import android.graphics.Matrix;
77
import android.graphics.Paint;
8+
import android.media.ExifInterface;
9+
import android.net.Uri;
810
import android.util.Base64;
911

1012
import com.facebook.react.bridge.ReactApplicationContext;
1113
import com.reactnativecompressor.Image.utils.ImageCompressorOptions;
1214
import com.reactnativecompressor.Image.utils.ImageSize;
1315

1416
import java.io.ByteArrayOutputStream;
17+
import java.io.File;
18+
import java.io.FileNotFoundException;
1519
import java.io.FileOutputStream;
20+
import java.io.IOException;
21+
import java.net.MalformedURLException;
22+
1623
import static com.reactnativecompressor.Utils.Utils.generateCacheFilePath;
1724

1825

1926
public class ImageCompressor {
27+
private static final float autoCompressMaxHeight = 1280.0f;
28+
private static final float audoCompressMaxWidth = 1280.0f;
29+
30+
public static String getRNFileUrl(String filePath) {
31+
File returnAbleFile= new File(filePath);
32+
try {
33+
filePath = returnAbleFile.toURL().toString();
34+
} catch (MalformedURLException e) {
35+
e.printStackTrace();
36+
}
37+
return filePath;
38+
}
39+
2040
public static ImageSize findActualSize(Bitmap image, int maxWidth, int maxHeight) {
2141
final float width = (float) image.getWidth();
2242
final float height = (float) image.getHeight();
@@ -40,11 +60,8 @@ public static Bitmap decodeImage(String value) {
4060
}
4161

4262
public static Bitmap loadImage(String value) {
43-
String filePath=value;
44-
if(value.indexOf("file:/")>-1)
45-
{
46-
filePath=value.substring( value.indexOf( ':' ) + 1 );
47-
}
63+
Uri uri= Uri.parse(value);
64+
String filePath = uri.getPath();
4865
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
4966
return bitmap;
5067
}
@@ -63,7 +80,7 @@ public static String encodeImage(ByteArrayOutputStream imageDataByteArrayOutputS
6380
try {
6481
FileOutputStream fos=new FileOutputStream(outputUri);
6582
imageDataByteArrayOutputStream.writeTo(fos);
66-
return "file:/"+outputUri;
83+
return getRNFileUrl(outputUri);
6784
} catch (Exception e) {
6885
e.printStackTrace();
6986
}
@@ -99,4 +116,136 @@ public static ByteArrayOutputStream compress(Bitmap image, ImageCompressorOption
99116
image.compress(format, Math.round(100 * quality), stream);
100117
return stream;
101118
}
119+
120+
public static String manualCompressImage(String imagePath,ImageCompressorOptions options, ReactApplicationContext reactContext) {
121+
final Bitmap image = options.input == ImageCompressorOptions.InputType.base64
122+
? ImageCompressor.decodeImage(imagePath)
123+
: ImageCompressor.loadImage(imagePath);
124+
125+
final Bitmap resizedImage = ImageCompressor.resize(image, options.maxWidth, options.maxHeight);
126+
final ByteArrayOutputStream imageDataByteArrayOutputStream = ImageCompressor.compress(resizedImage, options.output, options.quality);
127+
Boolean isBase64=options.returnableOutputType==ImageCompressorOptions.ReturnableOutputType.base64;
128+
129+
String returnableResult = ImageCompressor.encodeImage(imageDataByteArrayOutputStream,isBase64,image,options.output.toString(),reactContext);
130+
return returnableResult;
131+
}
132+
133+
134+
public static String autoCompressImage(String imagePath,String outputExtension, ReactApplicationContext reactContext) {
135+
136+
Uri uri= Uri.parse(imagePath);
137+
imagePath = uri.getPath();
138+
Bitmap scaledBitmap = null;
139+
140+
BitmapFactory.Options options = new BitmapFactory.Options();
141+
options.inJustDecodeBounds = true;
142+
Bitmap bmp = BitmapFactory.decodeFile(imagePath, options);
143+
144+
int actualHeight = options.outHeight;
145+
int actualWidth = options.outWidth;
146+
147+
float imgRatio = (float) actualWidth / (float) actualHeight;
148+
float maxRatio = audoCompressMaxWidth / autoCompressMaxHeight;
149+
150+
if (actualHeight > autoCompressMaxHeight || actualWidth > audoCompressMaxWidth) {
151+
if (imgRatio < maxRatio) {
152+
imgRatio = autoCompressMaxHeight / actualHeight;
153+
actualWidth = (int) (imgRatio * actualWidth);
154+
actualHeight = (int) autoCompressMaxHeight;
155+
} else if (imgRatio > maxRatio) {
156+
imgRatio = audoCompressMaxWidth / actualWidth;
157+
actualHeight = (int) (imgRatio * actualHeight);
158+
actualWidth = (int) audoCompressMaxWidth;
159+
} else {
160+
actualHeight = (int) autoCompressMaxHeight;
161+
actualWidth = (int) audoCompressMaxWidth;
162+
163+
}
164+
}
165+
166+
options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);
167+
options.inJustDecodeBounds = false;
168+
options.inDither = false;
169+
options.inPurgeable = true;
170+
options.inInputShareable = true;
171+
options.inTempStorage = new byte[16 * 1024];
172+
173+
try {
174+
bmp = BitmapFactory.decodeFile(imagePath, options);
175+
} catch (OutOfMemoryError exception) {
176+
exception.printStackTrace();
177+
178+
}
179+
try {
180+
scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.RGB_565);
181+
} catch (OutOfMemoryError exception) {
182+
exception.printStackTrace();
183+
}
184+
185+
float ratioX = actualWidth / (float) options.outWidth;
186+
float ratioY = actualHeight / (float) options.outHeight;
187+
float middleX = actualWidth / 2.0f;
188+
float middleY = actualHeight / 2.0f;
189+
190+
Matrix scaleMatrix = new Matrix();
191+
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
192+
193+
Canvas canvas = new Canvas(scaledBitmap);
194+
canvas.setMatrix(scaleMatrix);
195+
canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
196+
197+
if(bmp!=null)
198+
{
199+
bmp.recycle();
200+
}
201+
202+
ExifInterface exif;
203+
try {
204+
exif = new ExifInterface(imagePath);
205+
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
206+
Matrix matrix = new Matrix();
207+
if (orientation == 6) {
208+
matrix.postRotate(90);
209+
} else if (orientation == 3) {
210+
matrix.postRotate(180);
211+
} else if (orientation == 8) {
212+
matrix.postRotate(270);
213+
}
214+
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
215+
} catch (IOException e) {
216+
e.printStackTrace();
217+
}
218+
FileOutputStream out = null;
219+
String filepath = generateCacheFilePath(outputExtension,reactContext);;
220+
try {
221+
out = new FileOutputStream(filepath);
222+
223+
//write the compressed bitmap at the destination specified by filename.
224+
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
225+
226+
} catch (FileNotFoundException e) {
227+
e.printStackTrace();
228+
}
229+
return getRNFileUrl(filepath);
230+
}
231+
232+
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
233+
final int height = options.outHeight;
234+
final int width = options.outWidth;
235+
int inSampleSize = 1;
236+
237+
if (height > reqHeight || width > reqWidth) {
238+
final int heightRatio = Math.round((float) height / (float) reqHeight);
239+
final int widthRatio = Math.round((float) width / (float) reqWidth);
240+
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
241+
}
242+
final float totalPixels = width * height;
243+
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
244+
245+
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
246+
inSampleSize++;
247+
}
248+
249+
return inSampleSize;
250+
}
102251
}

android/src/main/java/com/reactnativecompressor/Image/utils/ImageCompressorOptions.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public static ImageCompressorOptions fromMap(ReadableMap map) {
1212
final String key = iterator.nextKey();
1313

1414
switch (key) {
15+
case "autoCompress":
16+
options.autoCompress = map.getBoolean(key);
17+
break;
1518
case "maxWidth":
1619
options.maxWidth = map.getInt(key);
1720
break;
@@ -48,10 +51,11 @@ public enum ReturnableOutputType {
4851
base64, uri
4952
}
5053

54+
public boolean autoCompress = false;
5155
public int maxWidth = 640;
5256
public int maxHeight = 480;
5357
public float quality = 1.0f;
5458
public InputType input = InputType.uri;
5559
public OutputType output = OutputType.jpg;
56-
public ReturnableOutputType returnableOutputType = ReturnableOutputType.uri;
60+
public ReturnableOutputType returnableOutputType = ReturnableOutputType.uri;
5761
}

example/src/Screens/Image/index.tsx

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import React, { useState } from 'react';
2-
import { View, StyleSheet, Alert } from 'react-native';
2+
import {
3+
View,
4+
StyleSheet,
5+
Alert,
6+
useWindowDimensions,
7+
Image as RNImage,
8+
} from 'react-native';
39
import Button from '../../Components/Button';
410
import Row from '../../Components/Row';
511
import * as ImagePicker from 'react-native-image-picker';
612
const prettyBytes = require('pretty-bytes');
713
import { Image, getFileInfo } from 'react-native-compressor';
814
const Index = () => {
15+
const dimension = useWindowDimensions();
16+
const [orignalUri, setOrignalUri] = useState<string>();
17+
const [commpressedUri, setCommpressedUri] = useState<string>();
918
const [fileName, setFileName] = useState<any>('');
1019
const [mimeType, setMimeType] = useState<any>('');
1120
const [orignalSize, setOrignalSize] = useState(0);
@@ -27,19 +36,16 @@ const Index = () => {
2736

2837
setFileName(source.fileName);
2938
setMimeType(source.type);
39+
setOrignalUri(source.uri);
3040
}
3141

3242
Image.compress(source.uri, {
33-
maxWidth: 100,
34-
input: 'uri',
35-
output: 'jpg',
36-
quality: 0.5,
37-
returnableOutputType: 'uri',
43+
autoCompress: true,
3844
})
3945
.then(async (compressedFileUri) => {
46+
setCommpressedUri(compressedFileUri);
4047
const detail: any = await getFileInfo(compressedFileUri);
4148
setCompressedSize(prettyBytes(parseInt(detail.size)));
42-
console.log(compressedFileUri, 'compressed');
4349
})
4450
.catch((e) => {
4551
console.log(e, 'error');
@@ -49,14 +55,35 @@ const Index = () => {
4955
);
5056
} catch (err) {}
5157
};
52-
5358
return (
5459
<View style={styles.container}>
55-
<Row label="File Name" value={fileName} />
56-
<Row label="Mime Type" value={mimeType} />
57-
<Row label="Orignal Size" value={orignalSize} />
58-
<Row label="Compressed Size" value={compressedSize} />
59-
<Button onPress={chooseAudioHandler} title="Choose Image" />
60+
<View style={styles.imageContainer}>
61+
{orignalUri && (
62+
<RNImage
63+
resizeMode="contain"
64+
source={{ uri: orignalUri }}
65+
style={{
66+
width: dimension.width / 3,
67+
}}
68+
/>
69+
)}
70+
{commpressedUri && (
71+
<RNImage
72+
resizeMode="contain"
73+
source={{ uri: commpressedUri }}
74+
style={{
75+
width: dimension.width / 3,
76+
}}
77+
/>
78+
)}
79+
</View>
80+
<View style={styles.container}>
81+
<Row label="File Name" value={fileName} />
82+
<Row label="Mime Type" value={mimeType} />
83+
<Row label="Orignal Size" value={orignalSize} />
84+
<Row label="Compressed Size" value={compressedSize} />
85+
<Button onPress={chooseAudioHandler} title="Choose Image" />
86+
</View>
6087
</View>
6188
);
6289
};
@@ -69,4 +96,10 @@ const styles = StyleSheet.create({
6996
justifyContent: 'center',
7097
alignItems: 'center',
7198
},
99+
imageContainer: {
100+
flex: 1,
101+
width: '100%',
102+
flexDirection: 'row',
103+
justifyContent: 'space-around',
104+
},
72105
});

0 commit comments

Comments
 (0)