Skip to content

Commit e0e153c

Browse files
authored
Merge pull request #146 from callstack/feature/retyui/headers
Allow pass `headers`
2 parents bc7a5fb + 9ccc5c1 commit e0e153c

File tree

7 files changed

+134
-61
lines changed

7 files changed

+134
-61
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ImageEditor.cropImage(uri, cropData).then((result) => {
5757
| `quality`<br>_(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) <br/>**Default value**: `0.9` |
5858
| `format`<br>_(optional)_ | `'jpeg' \| 'png' \| 'webp'` | The format of the resulting image.<br/> **Default value**: based on the provided image;<br>if value determination is not possible, `'jpeg'` will be used as a fallback.<br/>`'webp'` isn't supported by iOS. |
5959
| `includeBase64`<br>_(optional)_ | `boolean` | Indicates if Base64 formatted picture data should also be included in the [`CropResult`](#result-cropresult). <br/>**Default value**: `false` |
60+
| `headers`<br>_(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. |
6061

6162
### `result: CropResult`
6263

android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException
2828
import com.facebook.react.bridge.Promise
2929
import com.facebook.react.bridge.ReactApplicationContext
3030
import com.facebook.react.bridge.ReadableMap
31+
import com.facebook.react.bridge.ReadableType
3132
import com.facebook.react.bridge.WritableMap
3233
import com.facebook.react.common.ReactConstants
3334
import java.io.ByteArrayInputStream
@@ -99,6 +100,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
99100
* is passed to this is the file:// URI of the new image
100101
*/
101102
fun cropImage(uri: String?, options: ReadableMap, promise: Promise) {
103+
val headers =
104+
if (options.hasKey("headers") && options.getType("headers") == ReadableType.Map)
105+
options.getMap("headers")?.toHashMap()
106+
else null
102107
val format = if (options.hasKey("format")) options.getString("format") else null
103108
val offset = if (options.hasKey("offset")) options.getMap("offset") else null
104109
val size = if (options.hasKey("size")) options.getMap("size") else null
@@ -152,10 +157,11 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
152157
width,
153158
height,
154159
targetWidth,
155-
targetHeight
160+
targetHeight,
161+
headers
156162
)
157163
} else {
158-
cropTask(outOptions, uri, x, y, width, height)
164+
cropTask(outOptions, uri, x, y, width, height, headers)
159165
}
160166
if (cropped == null) {
161167
throw IOException("Cannot decode bitmap: $uri")
@@ -189,9 +195,10 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
189195
x: Int,
190196
y: Int,
191197
width: Int,
192-
height: Int
198+
height: Int,
199+
headers: HashMap<String, Any?>?
193200
): Bitmap? {
194-
return openBitmapInputStream(uri)?.use {
201+
return openBitmapInputStream(uri, headers)?.use {
195202
// Efficiently crops image without loading full resolution into memory
196203
// https://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html
197204
val decoder =
@@ -251,6 +258,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
251258
rectHeight: Int,
252259
outputWidth: Int,
253260
outputHeight: Int,
261+
headers: HashMap<String, Any?>?
254262
): Bitmap? {
255263
Assertions.assertNotNull(outOptions)
256264

@@ -262,7 +270,7 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
262270
// Where would the crop rect end up within the scaled bitmap?
263271

264272
val bitmap =
265-
openBitmapInputStream(uri)?.use {
273+
openBitmapInputStream(uri, headers)?.use {
266274
// This can use significantly less memory than decoding the full-resolution bitmap
267275
BitmapFactory.decodeStream(it, null, outOptions)
268276
} ?: return null
@@ -318,14 +326,19 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
318326
return Bitmap.createBitmap(bitmap, cropX, cropY, cropWidth, cropHeight, scaleMatrix, filter)
319327
}
320328

321-
private fun openBitmapInputStream(uri: String): InputStream? {
329+
private fun openBitmapInputStream(uri: String, headers: HashMap<String, Any?>?): InputStream? {
322330
return if (uri.startsWith("data:")) {
323331
val src = uri.substring(uri.indexOf(",") + 1)
324332
ByteArrayInputStream(Base64.decode(src, Base64.DEFAULT))
325333
} else if (isLocalUri(uri)) {
326334
reactContext.contentResolver.openInputStream(Uri.parse(uri))
327335
} else {
328336
val connection = URL(uri).openConnection()
337+
headers?.forEach { (key, value) ->
338+
if (value is String) {
339+
connection.setRequestProperty(key, value)
340+
}
341+
}
329342
connection.getInputStream()
330343
}
331344
}

ios/RNCImageEditor.mm

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
CGFloat quality;
3737
NSString *format;
3838
BOOL includeBase64;
39+
NSDictionary *headers;
3940
};
4041

4142
@implementation RNCImageEditor
@@ -54,6 +55,7 @@ - (Params)adaptParamsWithFormat:(id)format
5455
displayHeight:(id)displayHeight
5556
quality:(id)quality
5657
includeBase64:(id)includeBase64
58+
headers:(id)headers
5759
{
5860
return Params{
5961
.offset = {[RCTConvert double:offsetX], [RCTConvert double:offsetY]},
@@ -62,7 +64,8 @@ - (Params)adaptParamsWithFormat:(id)format
6264
.resizeMode = [RCTConvert RCTResizeMode:resizeMode ?: @(DEFAULT_RESIZE_MODE)],
6365
.quality = [RCTConvert CGFloat:quality],
6466
.format = [RCTConvert NSString:format],
65-
.includeBase64 = [RCTConvert BOOL:includeBase64]
67+
.includeBase64 = [RCTConvert BOOL:includeBase64],
68+
.headers = [RCTConvert NSDictionary:RCTNilIfNull(headers)]
6669
};
6770
}
6871

@@ -83,7 +86,6 @@ - (void) cropImage:(NSString *)uri
8386
resolve:(RCTPromiseResolveBlock)resolve
8487
reject:(RCTPromiseRejectBlock)reject
8588
{
86-
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString: uri]];
8789
auto params = [self adaptParamsWithFormat:data.format()
8890
width:@(data.size().width())
8991
height:@(data.size().height())
@@ -93,9 +95,10 @@ - (void) cropImage:(NSString *)uri
9395
displayWidth:@(data.displaySize().has_value() ? data.displaySize()->width() : DEFAULT_DISPLAY_SIZE)
9496
displayHeight:@(data.displaySize().has_value() ? data.displaySize()->height() : DEFAULT_DISPLAY_SIZE)
9597
quality:@(data.quality().has_value() ? *data.quality() : DEFAULT_COMPRESSION_QUALITY)
96-
includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)];
98+
includeBase64:@(data.includeBase64().has_value() ? *data.includeBase64() : NO)
99+
headers: data.headers()];
97100
#else
98-
RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest
101+
RCT_EXPORT_METHOD(cropImage:(NSString *)uri
99102
cropData:(NSDictionary *)cropData
100103
resolve:(RCTPromiseResolveBlock)resolve
101104
reject:(RCTPromiseRejectBlock)reject)
@@ -110,9 +113,16 @@ - (void) cropImage:(NSString *)uri
110113
displayHeight:cropData[@"displaySize"] ? cropData[@"displaySize"][@"height"] : @(DEFAULT_DISPLAY_SIZE)
111114
quality:cropData[@"quality"] ? cropData[@"quality"] : @(DEFAULT_COMPRESSION_QUALITY)
112115
includeBase64:cropData[@"includeBase64"]
113-
];
116+
headers:cropData[@"headers"]];
114117

115118
#endif
119+
NSMutableURLRequest *imageRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: uri]];
120+
[params.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
121+
if (value) {
122+
[imageRequest addValue:[RCTConvert NSString:value] forHTTPHeaderField:key];
123+
}
124+
}];
125+
116126
NSURL *url = [imageRequest URL];
117127
NSString *urlPath = [url path];
118128
NSString *extension = [urlPath pathExtension];

src/NativeRNCImageEditor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export interface Spec extends TurboModule {
5454
* (Optional) Indicates if Base64 formatted picture data should also be included in the result.
5555
*/
5656
includeBase64?: boolean;
57+
58+
/**
59+
* (Optional) An object representing the HTTP headers to send along with the request for a remote image.
60+
*/
61+
headers?: {
62+
[key: string]: string;
63+
};
5764
}
5865
): Promise<{
5966
/**

src/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ const RNCImageEditor: Spec = NativeRNCImageEditor
2727
type CropResultWithoutBase64 = Omit<CropResult, 'base64'>;
2828
type ImageCropDataWithoutBase64 = Omit<ImageCropData, 'includeBase64'>;
2929

30+
function toHeadersObject(
31+
headers: ImageCropData['headers']
32+
): Record<string, string> | undefined {
33+
return headers instanceof Headers
34+
? Object.fromEntries(
35+
// @ts-expect-error: Headers.entries isn't added yet in TS but exists in Runtime
36+
headers.entries()
37+
)
38+
: headers;
39+
}
40+
3041
class ImageEditor {
3142
/**
3243
* Crop the image specified by the URI param. If URI points to a remote
@@ -56,7 +67,10 @@ class ImageEditor {
5667
): Promise<CropResultWithoutBase64 & { base64: string }>;
5768

5869
static cropImage(uri: string, cropData: ImageCropData): Promise<CropResult> {
59-
return RNCImageEditor.cropImage(uri, cropData) as Promise<CropResult>;
70+
return RNCImageEditor.cropImage(uri, {
71+
...cropData,
72+
headers: toHeadersObject(cropData.headers),
73+
}) as Promise<CropResult>;
6074
}
6175
}
6276

src/index.web.ts

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { ImageCropData, CropResult } from './types.ts';
22

3+
const ERROR_PREFIX = 'ImageEditor: ';
4+
35
function drawImage(
4-
img: HTMLImageElement,
6+
img: HTMLImageElement | ImageBitmap,
57
{ offset, size, displaySize }: ImageCropData
68
): HTMLCanvasElement {
79
const canvas = document.createElement('canvas');
810
const context = canvas.getContext('2d');
911

1012
if (!context) {
11-
throw new Error('Failed to get canvas context');
13+
throw new Error(ERROR_PREFIX + 'Failed to get canvas context');
1214
}
1315

1416
const sx = offset.x,
@@ -28,7 +30,30 @@ function drawImage(
2830
return canvas;
2931
}
3032

31-
function fetchImage(imgSrc: string): Promise<HTMLImageElement> {
33+
function fetchImage(
34+
imgSrc: string,
35+
headers: ImageCropData['headers']
36+
): Promise<HTMLImageElement | ImageBitmap> {
37+
if (headers) {
38+
return fetch(imgSrc, {
39+
method: 'GET',
40+
headers: new Headers(headers),
41+
})
42+
.then((response) => {
43+
if (!response.ok) {
44+
throw new Error(
45+
ERROR_PREFIX +
46+
'Failed to fetch the image: ' +
47+
imgSrc +
48+
'. Request failed with status: ' +
49+
response.status
50+
);
51+
}
52+
return response.blob();
53+
})
54+
.then((blob) => createImageBitmap(blob));
55+
}
56+
3257
return new Promise<HTMLImageElement>((resolve, reject) => {
3358
const onceOptions = { once: true };
3459
const img = new Image();
@@ -58,51 +83,53 @@ class ImageEditor {
5883
/**
5984
* Returns a promise that resolves with the base64 encoded string of the cropped image
6085
*/
61-
return fetchImage(imgSrc).then(function onfulfilledImgToCanvas(image) {
62-
const ext = cropData.format ?? 'jpeg';
63-
const type = `image/${ext}`;
64-
const quality = cropData.quality ?? DEFAULT_COMPRESSION_QUALITY;
65-
const canvas = drawImage(image, cropData);
66-
67-
return new Promise<Blob | null>(function onfulfilledCanvasToBlob(
68-
resolve
69-
) {
70-
canvas.toBlob(resolve, type, quality);
71-
}).then((blob) => {
72-
if (!blob) {
73-
throw new Error('Image cannot be created from canvas');
74-
}
75-
76-
let _path: string, _uri: string;
77-
78-
const result: CropResult = {
79-
width: canvas.width,
80-
height: canvas.height,
81-
name: 'ReactNative_cropped_image.' + ext,
82-
type: ('image/' + ext) as CropResult['type'],
83-
size: blob.size,
84-
// Lazy getters to avoid unnecessary memory usage
85-
get path() {
86-
if (!_path) {
87-
_path = URL.createObjectURL(blob);
88-
}
89-
return _path;
90-
},
91-
get uri() {
92-
return result.base64 as string;
93-
},
94-
get base64() {
95-
if (!_uri) {
96-
_uri = canvas.toDataURL(type, quality);
97-
}
98-
return _uri.split(',')[1];
99-
// ^^^ remove `data:image/xxx;base64,` prefix (to align with iOS/Android platform behavior)
100-
},
101-
};
102-
103-
return result;
104-
});
105-
});
86+
return fetchImage(imgSrc, cropData.headers).then(
87+
function onfulfilledImgToCanvas(image) {
88+
const ext = cropData.format ?? 'jpeg';
89+
const type = `image/${ext}`;
90+
const quality = cropData.quality ?? DEFAULT_COMPRESSION_QUALITY;
91+
const canvas = drawImage(image, cropData);
92+
93+
return new Promise<Blob | null>(function onfulfilledCanvasToBlob(
94+
resolve
95+
) {
96+
canvas.toBlob(resolve, type, quality);
97+
}).then((blob) => {
98+
if (!blob) {
99+
throw new Error('Image cannot be created from canvas');
100+
}
101+
102+
let _path: string, _uri: string;
103+
104+
const result: CropResult = {
105+
width: canvas.width,
106+
height: canvas.height,
107+
name: 'ReactNative_cropped_image.' + ext,
108+
type: ('image/' + ext) as CropResult['type'],
109+
size: blob.size,
110+
// Lazy getters to avoid unnecessary memory usage
111+
get path() {
112+
if (!_path) {
113+
_path = URL.createObjectURL(blob);
114+
}
115+
return _path;
116+
},
117+
get uri() {
118+
return result.base64 as string;
119+
},
120+
get base64() {
121+
if (!_uri) {
122+
_uri = canvas.toDataURL(type, quality);
123+
}
124+
return _uri.split(',')[1];
125+
// ^^^ remove `data:image/xxx;base64,` prefix (to align with iOS/Android platform behavior)
126+
},
127+
};
128+
129+
return result;
130+
});
131+
}
132+
);
106133
}
107134
}
108135

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { Spec } from './NativeRNCImageEditor.ts';
33
type ImageCropDataFromSpec = Parameters<Spec['cropImage']>[1];
44

55
export interface ImageCropData
6-
extends Omit<ImageCropDataFromSpec, 'resizeMode' | 'format'> {
6+
extends Omit<ImageCropDataFromSpec, 'headers' | 'resizeMode' | 'format'> {
7+
headers?: Record<string, string> | Headers;
78
format?: 'png' | 'jpeg' | 'webp';
89
resizeMode?: 'contain' | 'cover' | 'stretch' | 'center';
910
// ^^^ codegen doesn't support union types yet

0 commit comments

Comments
 (0)