diff --git a/README.md b/README.md
index 10d5b10..258aa0f 100755
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ ImageEditor.cropImage(uri, cropData).then((result) => {
| `quality`
_(optional)_ | `number` | A value in range `0.0` - `1.0` specifying compression level of the result image. `1` means no compression (highest quality) and `0` the highest compression (lowest quality)
**Default value**: `0.9` |
| `format`
_(optional)_ | `'jpeg' \| 'png' \| 'webp'` | The format of the resulting image.
**Default value**: based on the provided image;
if value determination is not possible, `'jpeg'` will be used as a fallback.
`'webp'` isn't supported by iOS. |
| `includeBase64`
_(optional)_ | `boolean` | Indicates if Base64 formatted picture data should also be included in the [`CropResult`](#result-cropresult).
**Default value**: `false` |
+| `headers`
_(optional)_ | `object \| Headers` | An object or [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) interface representing the HTTP headers to send along with the request for a remote image. |
### `result: CropResult`
diff --git a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
index ff78c15..cd51dbc 100644
--- a/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
+++ b/android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt
@@ -28,6 +28,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.ReadableType
import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.ReactConstants
import java.io.ByteArrayInputStream
@@ -99,6 +100,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
* is passed to this is the file:// URI of the new image
*/
fun cropImage(uri: String?, options: ReadableMap, promise: Promise) {
+ val headers =
+ if (options.hasKey("headers") && options.getType("headers") == ReadableType.Map)
+ options.getMap("headers")?.toHashMap()
+ else null
val format = if (options.hasKey("format")) options.getString("format") else null
val offset = if (options.hasKey("offset")) options.getMap("offset") else null
val size = if (options.hasKey("size")) options.getMap("size") else null
@@ -152,10 +157,11 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
width,
height,
targetWidth,
- targetHeight
+ targetHeight,
+ headers
)
} else {
- cropTask(outOptions, uri, x, y, width, height)
+ cropTask(outOptions, uri, x, y, width, height, headers)
}
if (cropped == null) {
throw IOException("Cannot decode bitmap: $uri")
@@ -189,9 +195,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
x: Int,
y: Int,
width: Int,
- height: Int
+ height: Int,
+ headers: HashMap?
): Bitmap? {
- return openBitmapInputStream(uri)?.use {
+ return openBitmapInputStream(uri, headers)?.use {
// Efficiently crops image without loading full resolution into memory
// https://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html
val decoder =
@@ -251,6 +258,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
rectHeight: Int,
outputWidth: Int,
outputHeight: Int,
+ headers: HashMap?
): Bitmap? {
Assertions.assertNotNull(outOptions)
@@ -262,7 +270,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
// Where would the crop rect end up within the scaled bitmap?
val bitmap =
- openBitmapInputStream(uri)?.use {
+ openBitmapInputStream(uri, headers)?.use {
// This can use significantly less memory than decoding the full-resolution bitmap
BitmapFactory.decodeStream(it, null, outOptions)
} ?: return null
@@ -318,7 +326,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
return Bitmap.createBitmap(bitmap, cropX, cropY, cropWidth, cropHeight, scaleMatrix, filter)
}
- private fun openBitmapInputStream(uri: String): InputStream? {
+ private fun openBitmapInputStream(uri: String, headers: HashMap?): InputStream? {
return if (uri.startsWith("data:")) {
val src = uri.substring(uri.indexOf(",") + 1)
ByteArrayInputStream(Base64.decode(src, Base64.DEFAULT))
@@ -326,6 +334,11 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
reactContext.contentResolver.openInputStream(Uri.parse(uri))
} else {
val connection = URL(uri).openConnection()
+ headers?.forEach { (key, value) ->
+ if (value is String) {
+ connection.setRequestProperty(key, value)
+ }
+ }
connection.getInputStream()
}
}
diff --git a/ios/RNCImageEditor.mm b/ios/RNCImageEditor.mm
index 440ea6b..75d4971 100644
--- a/ios/RNCImageEditor.mm
+++ b/ios/RNCImageEditor.mm
@@ -36,6 +36,7 @@
CGFloat quality;
NSString *format;
BOOL includeBase64;
+ NSDictionary *headers;
};
@implementation RNCImageEditor
@@ -54,6 +55,7 @@ - (Params)adaptParamsWithFormat:(id)format
displayHeight:(id)displayHeight
quality:(id)quality
includeBase64:(id)includeBase64
+ headers:(id)headers
{
return Params{
.offset = {[RCTConvert double:offsetX], [RCTConvert double:offsetY]},
@@ -62,7 +64,8 @@ - (Params)adaptParamsWithFormat:(id)format
.resizeMode = [RCTConvert RCTResizeMode:resizeMode ?: @(DEFAULT_RESIZE_MODE)],
.quality = [RCTConvert CGFloat:quality],
.format = [RCTConvert NSString:format],
- .includeBase64 = [RCTConvert BOOL:includeBase64]
+ .includeBase64 = [RCTConvert BOOL:includeBase64],
+ .headers = [RCTConvert NSDictionary:RCTNilIfNull(headers)]
};
}
@@ -83,7 +86,6 @@ - (void) cropImage:(NSString *)uri
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
- NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString: uri]];
auto params = [self adaptParamsWithFormat:data.format()
width:@(data.size().width())
height:@(data.size().height())
@@ -93,9 +95,10 @@ - (void) cropImage:(NSString *)uri
displayWidth:@(data.displaySize().has_value() ? data.displaySize()->width() : DEFAULT_DISPLAY_SIZE)
displayHeight:@(data.displaySize().has_value() ? data.displaySize()->height() : DEFAULT_DISPLAY_SIZE)
quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)
- includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)];
+ includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)
+ headers: data.headers()];
#else
-RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest
+RCT_EXPORT_METHOD(cropImage:(NSString *)uri
cropData:(NSDictionary *)cropData
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
@@ -110,9 +113,16 @@ - (void) cropImage:(NSString *)uri
displayHeight:cropData[@"displaySize"] ? cropData[@"displaySize"][@"height"] : @(DEFAULT_DISPLAY_SIZE)
quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)
includeBase64:cropData[@"includeBase64"]
- ];
+ headers:cropData[@"headers"]];
#endif
+ NSMutableURLRequest *imageRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: uri]];
+ [params.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ if (value) {
+ [imageRequest addValue:[RCTConvert NSString:value] forHTTPHeaderField:key];
+ }
+ }];
+
NSURL *url = [imageRequest URL];
NSString *urlPath = [url path];
NSString *extension = [urlPath pathExtension];
diff --git a/src/NativeRNCImageEditor.ts b/src/NativeRNCImageEditor.ts
index c3c9a07..8d3d2db 100644
--- a/src/NativeRNCImageEditor.ts
+++ b/src/NativeRNCImageEditor.ts
@@ -54,6 +54,13 @@ export interface Spec extends TurboModule {
* (Optional) Indicates if Base64 formatted picture data should also be included in the result.
*/
includeBase64?: boolean;
+
+ /**
+ * (Optional) An object representing the HTTP headers to send along with the request for a remote image.
+ */
+ headers?: {
+ [key: string]: string;
+ };
}
): Promise<{
/**
diff --git a/src/index.ts b/src/index.ts
index acdafd3..402e936 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -27,6 +27,17 @@ const RNCImageEditor: Spec = NativeRNCImageEditor
type CropResultWithoutBase64 = Omit;
type ImageCropDataWithoutBase64 = Omit;
+function toHeadersObject(
+ headers: ImageCropData['headers']
+): Record | undefined {
+ return headers instanceof Headers
+ ? Object.fromEntries(
+ // @ts-expect-error: Headers.entries isn't added yet in TS but exists in Runtime
+ headers.entries()
+ )
+ : headers;
+}
+
class ImageEditor {
/**
* Crop the image specified by the URI param. If URI points to a remote
@@ -56,7 +67,10 @@ class ImageEditor {
): Promise;
static cropImage(uri: string, cropData: ImageCropData): Promise {
- return RNCImageEditor.cropImage(uri, cropData) as Promise;
+ return RNCImageEditor.cropImage(uri, {
+ ...cropData,
+ headers: toHeadersObject(cropData.headers),
+ }) as Promise;
}
}
diff --git a/src/index.web.ts b/src/index.web.ts
index 960233f..aba5ad9 100644
--- a/src/index.web.ts
+++ b/src/index.web.ts
@@ -1,14 +1,16 @@
import type { ImageCropData, CropResult } from './types.ts';
+const ERROR_PREFIX = 'ImageEditor: ';
+
function drawImage(
- img: HTMLImageElement,
+ img: HTMLImageElement | ImageBitmap,
{ offset, size, displaySize }: ImageCropData
): HTMLCanvasElement {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
- throw new Error('Failed to get canvas context');
+ throw new Error(ERROR_PREFIX + 'Failed to get canvas context');
}
const sx = offset.x,
@@ -28,7 +30,30 @@ function drawImage(
return canvas;
}
-function fetchImage(imgSrc: string): Promise {
+function fetchImage(
+ imgSrc: string,
+ headers: ImageCropData['headers']
+): Promise {
+ if (headers) {
+ return fetch(imgSrc, {
+ method: 'GET',
+ headers: new Headers(headers),
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error(
+ ERROR_PREFIX +
+ 'Failed to fetch the image: ' +
+ imgSrc +
+ '. Request failed with status: ' +
+ response.status
+ );
+ }
+ return response.blob();
+ })
+ .then((blob) => createImageBitmap(blob));
+ }
+
return new Promise((resolve, reject) => {
const onceOptions = { once: true };
const img = new Image();
@@ -58,51 +83,53 @@ class ImageEditor {
/**
* Returns a promise that resolves with the base64 encoded string of the cropped image
*/
- return fetchImage(imgSrc).then(function onfulfilledImgToCanvas(image) {
- const ext = cropData.format ?? 'jpeg';
- const type = `image/${ext}`;
- const quality = cropData.quality ?? DEFAULT_COMPRESSION_QUALITY;
- const canvas = drawImage(image, cropData);
-
- return new Promise(function onfulfilledCanvasToBlob(
- resolve
- ) {
- canvas.toBlob(resolve, type, quality);
- }).then((blob) => {
- if (!blob) {
- throw new Error('Image cannot be created from canvas');
- }
-
- let _path: string, _uri: string;
-
- const result: CropResult = {
- width: canvas.width,
- height: canvas.height,
- name: 'ReactNative_cropped_image.' + ext,
- type: ('image/' + ext) as CropResult['type'],
- size: blob.size,
- // Lazy getters to avoid unnecessary memory usage
- get path() {
- if (!_path) {
- _path = URL.createObjectURL(blob);
- }
- return _path;
- },
- get uri() {
- return result.base64 as string;
- },
- get base64() {
- if (!_uri) {
- _uri = canvas.toDataURL(type, quality);
- }
- return _uri.split(',')[1];
- // ^^^ remove `data:image/xxx;base64,` prefix (to align with iOS/Android platform behavior)
- },
- };
-
- return result;
- });
- });
+ return fetchImage(imgSrc, cropData.headers).then(
+ function onfulfilledImgToCanvas(image) {
+ const ext = cropData.format ?? 'jpeg';
+ const type = `image/${ext}`;
+ const quality = cropData.quality ?? DEFAULT_COMPRESSION_QUALITY;
+ const canvas = drawImage(image, cropData);
+
+ return new Promise(function onfulfilledCanvasToBlob(
+ resolve
+ ) {
+ canvas.toBlob(resolve, type, quality);
+ }).then((blob) => {
+ if (!blob) {
+ throw new Error('Image cannot be created from canvas');
+ }
+
+ let _path: string, _uri: string;
+
+ const result: CropResult = {
+ width: canvas.width,
+ height: canvas.height,
+ name: 'ReactNative_cropped_image.' + ext,
+ type: ('image/' + ext) as CropResult['type'],
+ size: blob.size,
+ // Lazy getters to avoid unnecessary memory usage
+ get path() {
+ if (!_path) {
+ _path = URL.createObjectURL(blob);
+ }
+ return _path;
+ },
+ get uri() {
+ return result.base64 as string;
+ },
+ get base64() {
+ if (!_uri) {
+ _uri = canvas.toDataURL(type, quality);
+ }
+ return _uri.split(',')[1];
+ // ^^^ remove `data:image/xxx;base64,` prefix (to align with iOS/Android platform behavior)
+ },
+ };
+
+ return result;
+ });
+ }
+ );
}
}
diff --git a/src/types.ts b/src/types.ts
index 40f1f05..53d4583 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -3,7 +3,8 @@ import type { Spec } from './NativeRNCImageEditor.ts';
type ImageCropDataFromSpec = Parameters[1];
export interface ImageCropData
- extends Omit {
+ extends Omit {
+ headers?: Record | Headers;
format?: 'png' | 'jpeg' | 'webp';
resizeMode?: 'contain' | 'cover' | 'stretch' | 'center';
// ^^^ codegen doesn't support union types yet