diff --git a/README.md b/README.md index 295fd23..578d543 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ async function savePicture() { * [`deletePhotos`](#deletephotos) * [`iosGetImageDataById`](#iosgetimagedatabyid) * [`useCameraRoll`](#usecameraroll) +* [`getPhotoThumbnail`](#getphotothumbnail) **iOS only** --- @@ -527,6 +528,82 @@ function Example() { }; ``` + +### `getPhotoThumbnail()` + +**iOS only** + +Returns a Promise with thumbnail photo. + +**Parameters:** + +| Name | Type | Required | Description | +| ------------ | --------------------- | -------- | -------------------------------------------------- | +| internalID | string | Yes | Ios internal ID 'PH://xxxx'. | +| options | PhotoThumbnailOptions | Yes | Expects an options with the shape described below. | + +* `allowNetworkAccess` : {boolean} : **default = false** : Specifies whether the requested image can be downloaded from iCloud. **iOS only** +* `targetSize` : {ThumbnailSize} : Expects a targetSize with the shape desribed below: + * `height` : {number} : **default = 400** + * `width` : {number} : **default = 400** +* `quality` : {number} : **default = 1.0** : jpeg quality used for compression (a value from 0.0 to 1.0). A value of 0.0 is maximum compression (or lowest quality). A value of 1.0 is least compression (or best quality). + +**Returns:** + +| Type | Description | +| ------------------------- | ------------------------------------------------------------- | +| Promise\ | A Promise with PhotoThumbnail with the shape described below. | + +* `thumbnailBase64` : {string} + +#### Example + +Loading a thumbnail: + +```javascript +export default function Thumbnail(props) { + const [base64Image, setBase64Image] = useState(null); + + useEffect(() => { + const getThumbnail = async () => { + const options = { + allowNetworkAccess: true, + targetSize: { + height: 80, + width: 80 + }, + quality: 1.0 + }; + + const thumbnailResponse = await CameraRoll.getPhotoThumbnail(props.image.uri, options); + + setBase64Image(thumbnailResponse.thumbnailBase64); + }; + + getThumbnail(); + }, []); + + const extension = props.image.extension; + let prefix; + + switch (extension) { + case 'png': + prefix = 'data:image/png;base64,'; + break; + default: + //all others can use jpeg + prefix = 'data:image/jpeg;base64,'; + break; + } + + return ( + + ); +} +``` + ### Known issues #### IOS diff --git a/ios/RNCCameraRoll.mm b/ios/RNCCameraRoll.mm index 10c53a3..5f717f9 100644 --- a/ios/RNCCameraRoll.mm +++ b/ios/RNCCameraRoll.mm @@ -693,6 +693,70 @@ RCT_EXPORT_METHOD(getPhotoByInternalID:(NSString *)internalId }, false); } +RCT_EXPORT_METHOD(getPhotoThumbnail:(NSString *)internalId + options:(NSDictionary *)options + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + checkPhotoLibraryConfig(); + + BOOL const allowNetworkAccess = options[@"allowNetworkAccess"] == nil ? NO : [RCTConvert BOOL:options[@"allowNetworkAccess"]]; + + NSDictionary *const targetSize = [RCTConvert NSDictionary:options[@"targetSize"]]; + CGFloat const targetHeight = targetSize[@"height"] == nil ? 400 : [RCTConvert CGFloat:targetSize[@"height"]]; + CGFloat const targetWidth = targetSize[@"width"] == nil ? 400 : [RCTConvert CGFloat:targetSize[@"width"]]; + + CGFloat quality = options[@"quality"] == nil ? 1.0 : [RCTConvert CGFloat:options[@"quality"]]; + + requestPhotoLibraryAccess(reject, ^(bool isLimited){ + + PHFetchResult *fetchResult; + PHAsset *asset; + NSString *mediaIdentifier = internalId; + + if ([internalId rangeOfString:@"ph://"].location != NSNotFound) { + mediaIdentifier = [internalId stringByReplacingOccurrencesOfString:@"ph://" + withString:@""]; + } + + fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[mediaIdentifier] options:nil]; + if(fetchResult){ + asset = fetchResult.firstObject;//only object in the array. + } + + if(asset){ + PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new]; + requestOptions.networkAccessAllowed = allowNetworkAccess; + requestOptions.version = PHImageRequestOptionsVersionUnadjusted; + requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + + CGSize const thumbnailSize = CGSizeMake(targetWidth, targetHeight); + [[PHImageManager defaultManager] requestImageForAsset:asset + targetSize:thumbnailSize + contentMode:PHImageContentModeAspectFill + options:requestOptions + resultHandler:^(UIImage * _Nullable image, + NSDictionary * _Nullable info) { + NSError *const error = [info objectForKey:PHImageErrorKey]; + if (error) { + reject(@"Error while getting thumbnail image",@"Error while getting thumbnail image",error); + } + + NSString *thumbnailBase64 = [UIImageJPEGRepresentation(image, quality) base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + + resolve(@{ + @"thumbnailBase64": thumbnailBase64 + }); + }]; + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" + " with localIdentifier %@ with no error message.", internalId]; + NSError *error = RCTErrorWithMessage(errorMessage); + reject(@"No asset found",@"No asset found",error); + } + }, false); +} + NSString *subTypeLabelForCollection(PHAssetCollection *assetCollection) { PHAssetCollectionSubtype subtype = assetCollection.assetCollectionSubtype; diff --git a/src/CameraRoll.ts b/src/CameraRoll.ts index 50a773e..141bd54 100644 --- a/src/CameraRoll.ts +++ b/src/CameraRoll.ts @@ -179,6 +179,21 @@ export type Album = { subtype?: AlbumSubType; }; +export type ThumbnailSize = { + height: number, + width: number +}; + +export type PhotoThumbnailOptions = { + allowNetworkAccess: boolean, //iOS only + targetSize: ThumbnailSize, + quality: number +}; + +export type PhotoThumbnail = { + thumbnailBase64: string, +}; + /** * `CameraRoll` provides access to the local camera roll or photo library. * @@ -272,4 +287,15 @@ export class CameraRoll { }; return RNCCameraRoll.getPhotoByInternalID(internalID, conversionOption); } + + /** + * Returns a Promise with thumbnail photo. + * + * @param internalID - PH photo internal ID. + * @param options - thumbnail photo options. + * @returns Promise + */ + static getPhotoThumbnail(internalID: string, options: PhotoThumbnailOptions): Promise { + return RNCCameraRoll.getPhotoThumbnail(internalID, options); + } } diff --git a/src/NativeCameraRollModule.ts b/src/NativeCameraRollModule.ts index c6d5e76..81c7036 100644 --- a/src/NativeCameraRollModule.ts +++ b/src/NativeCameraRollModule.ts @@ -2,6 +2,7 @@ // we use Object type because methods on the native side use NSDictionary and ReadableMap // and we want to stay compatible with those import {TurboModuleRegistry, TurboModule} from 'react-native'; +import type { PhotoThumbnail } from './CameraRoll'; export type AlbumSubType = | 'AlbumRegular' @@ -76,6 +77,10 @@ export interface Spec extends TurboModule { internalID: string, options: Object, ): Promise; + getPhotoThumbnail( + internalID: string, + options: Object + ): Promise } export default TurboModuleRegistry.getEnforcing('RNCCameraRoll');