diff --git a/ios/ReactNativeBlobUtil/ReactNativeBlobUtil.mm b/ios/ReactNativeBlobUtil/ReactNativeBlobUtil.mm index 60f006a0..79c7720f 100644 --- a/ios/ReactNativeBlobUtil/ReactNativeBlobUtil.mm +++ b/ios/ReactNativeBlobUtil/ReactNativeBlobUtil.mm @@ -512,7 +512,7 @@ - (void)ls:(NSString *)path RCT_EXPORT_METHOD(stat:(NSString *)target callback:(RCTResponseSenderBlock) callback) { - [ReactNativeBlobUtilFS getPathFromUri:target completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [ReactNativeBlobUtilFS getPathFromUri:target completionHandler:^(NSString *path, PHAsset *asset) { __block NSMutableDictionary * result; if(path != nil) { @@ -536,10 +536,29 @@ - (void)ls:(NSString *)path } else if(asset != nil) { - __block NSNumber * size = [NSNumber numberWithLong:[asset size]]; - result = [asset metadata]; - [result setValue:size forKey:@"size"]; - callback(@[[NSNull null], result]); + // For PHAsset, get basic metadata + NSMutableDictionary *assetInfo = [NSMutableDictionary dictionary]; + [assetInfo setValue:[NSNumber numberWithLong:asset.pixelWidth] forKey:@"width"]; + [assetInfo setValue:[NSNumber numberWithLong:asset.pixelHeight] forKey:@"height"]; + [assetInfo setValue:[NSNumber numberWithLong:asset.duration] forKey:@"duration"]; + [assetInfo setValue:[NSDate date] forKey:@"lastModified"]; + [assetInfo setValue:@"asset" forKey:@"type"]; + [assetInfo setValue:@"PHAsset" forKey:@"filename"]; + + // Get actual file size by requesting image data + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + [assetInfo setValue:[NSNumber numberWithLong:imageData.length] forKey:@"size"]; + } else { + [assetInfo setValue:[NSNumber numberWithLong:0] forKey:@"size"]; + } + callback(@[[NSNull null], assetInfo]); + }]; } else { @@ -597,7 +616,7 @@ - (void)cp:(NSString *)src callback:(RCTResponseSenderBlock)callback { // path = [ReactNativeBlobUtilFS getPathOfAsset:path]; - [ReactNativeBlobUtilFS getPathFromUri:src completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [ReactNativeBlobUtilFS getPathFromUri:src completionHandler:^(NSString *path, PHAsset *asset) { NSError * error = nil; if(path == nil) { diff --git a/ios/ReactNativeBlobUtilFS.h b/ios/ReactNativeBlobUtilFS.h index f5267e03..872e9858 100644 --- a/ios/ReactNativeBlobUtilFS.h +++ b/ios/ReactNativeBlobUtilFS.h @@ -18,7 +18,7 @@ #import "RCTBridgeModule.h" #endif -#import +#import @interface ReactNativeBlobUtilFS : NSObject { NSOutputStream * outStream; @@ -57,7 +57,7 @@ + (NSString *) getTempPath:(NSString*)taskId withExtension:(NSString *)ext; + (NSString *) getPathOfAsset:(NSString *)assetURI; + (NSString *) getPathForAppGroup:(NSString *)groupName; -+ (void) getPathFromUri:(NSString *)uri completionHandler:(void(^)(NSString * path, ALAssetRepresentation *asset)) onComplete; ++ (void) getPathFromUri:(NSString *)uri completionHandler:(void(^)(NSString * path, PHAsset *asset)) onComplete; // fs methods + (ReactNativeBlobUtilFS *) getFileStreams; @@ -82,7 +82,7 @@ resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; //+ (void) writeFileFromFile:(NSString *)src toFile:(NSString *)dest append:(BOOL)append; -+ (void) writeAssetToPath:(ALAssetRepresentation * )rep dest:(NSString *)dest; ++ (void) writeAssetToPath:(PHAsset * )asset dest:(NSString *)dest; + (void) readStream:(NSString *)uri encoding:(NSString * )encoding bufferSize:(int)bufferSize tick:(int)tick streamId:(NSString *)streamId baseModule:(ReactNativeBlobUtil *)baseModule; + (void) df:(RCTResponseSenderBlock)callback; diff --git a/ios/ReactNativeBlobUtilFS.mm b/ios/ReactNativeBlobUtilFS.mm index b7f59726..4751180d 100644 --- a/ios/ReactNativeBlobUtilFS.mm +++ b/ios/ReactNativeBlobUtilFS.mm @@ -11,7 +11,7 @@ #import "ReactNativeBlobUtilFS.h" #import "ReactNativeBlobUtilConst.h" #import "ReactNativeBlobUtilFileTransformer.h" -#import +#import #import @@ -154,7 +154,7 @@ + (void) readStream:(NSString *)uri streamId:(NSString *)streamId baseModule:(ReactNativeBlobUtil*)baseModule { - [[self class] getPathFromUri:uri completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [[self class] getPathFromUri:uri completionHandler:^(NSString *path, PHAsset *asset) { __block int read = 0; __block int backoff = tick *1000; @@ -188,17 +188,34 @@ + (void) readStream:(NSString *)uri } else if (asset != nil) { - int cursor = 0; - NSError * err; - while((read = [asset getBytes:buffer fromOffset:cursor length:bufferSize error:&err]) > 0) - { - cursor += read; - [[self class] emitDataChunks:[NSData dataWithBytes:buffer length:read] encoding:encoding streamId:streamId baseModule:baseModule]; - if(tick > 0) - { - usleep(backoff); - } - } + // For PHAsset, we need to get the full image data first + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + // Write to temporary file and then read it back in chunks + NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"temp_asset_%@.tmp", [[NSUUID UUID] UUIDString]]]; + [imageData writeToFile:tempPath atomically:YES]; + + NSInputStream *stream = [[NSInputStream alloc] initWithFileAtPath:tempPath]; + [stream open]; + while((read = [stream read:buffer maxLength:bufferSize]) > 0) + { + [[self class] emitDataChunks:[NSData dataWithBytes:buffer length:read] encoding:encoding streamId:streamId baseModule:baseModule]; + if(tick > 0) + { + usleep(backoff); + } + } + [stream close]; + + // Clean up temp file + [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil]; + } + }]; } else { @@ -290,7 +307,7 @@ + (void) emitDataChunks:(NSData *)data encoding:(NSString *) encoding streamId:( + (NSNumber *) writeFileFromFile:(NSString *)src toFile:(NSString *)dest append:(BOOL)append callback:(void(^)(NSString * errMsg, NSNumber *size))callback { - [[self class] getPathFromUri:src completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [[self class] getPathFromUri:src completionHandler:^(NSString *path, PHAsset *asset) { if(path != nil) { __block NSInputStream * is = [[NSInputStream alloc] initWithFileAtPath:path]; @@ -313,21 +330,24 @@ + (NSNumber *) writeFileFromFile:(NSString *)src toFile:(NSString *)dest append: } else if(asset != nil) { - - __block NSOutputStream * os = [[NSOutputStream alloc] initToFileAtPath:dest append:append]; - int read = 0; - int cursor = 0; - __block long written = 0; - uint8_t buffer[10240]; - [os open]; - while((read = [asset getBytes:buffer fromOffset:cursor length:10240 error:nil]) > 0) - { - cursor += read; - [os write:buffer maxLength:read]; - } - __block NSNumber * size = [NSNumber numberWithLong:written]; - [os close]; - callback(nil, size); + // For PHAsset, get the full image data and write it + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + NSOutputStream *os = [[NSOutputStream alloc] initToFileAtPath:dest append:append]; + [os open]; + [imageData writeToFile:dest atomically:YES]; + NSNumber *size = [NSNumber numberWithLong:imageData.length]; + [os close]; + callback(nil, size); + } else { + callback(@"Failed to get image data from asset", nil); + } + }]; } else callback(@"failed to resolve path", nil); @@ -496,23 +516,26 @@ + (void) readFile:(NSString *)path transformFile:(BOOL) transformFile onComplete:(void (^)(NSData * content, NSString * codeStr, NSString * errMsg))onComplete { - [[self class] getPathFromUri:path completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [[self class] getPathFromUri:path completionHandler:^(NSString *path, PHAsset *asset) { __block NSData * fileContent; NSError * err; __block Byte * buffer; if(asset != nil) { - int size = asset.size; - buffer = (Byte *)malloc(size); - [asset getBytes:buffer fromOffset:0 length:asset.size error:&err]; - if(err != nil) - { - onComplete(nil, @"EUNSPECIFIED", [err description]); - free(buffer); - return; - } - fileContent = [NSData dataWithBytes:buffer length:size]; - free(buffer); + // For PHAsset, get the full image data + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + onComplete(imageData, nil, nil); + } else { + onComplete(nil, @"EUNSPECIFIED", @"Failed to get image data from asset"); + } + }]; + return; } else { @@ -795,7 +818,7 @@ + (NSDictionary *) stat:(NSString *) path error:(NSError **) error { + (void) exists:(NSString *) path callback:(RCTResponseSenderBlock)callback { - [[self class] getPathFromUri:path completionHandler:^(NSString *path, ALAssetRepresentation *asset) { + [[self class] getPathFromUri:path completionHandler:^(NSString *path, PHAsset *asset) { if(path != nil) { BOOL isDir = NO; @@ -886,7 +909,7 @@ + (void)slice:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { - [[self class] getPathFromUri:path completionHandler:^(NSString *path, ALAssetRepresentation *asset) + [[self class] getPathFromUri:path completionHandler:^(NSString *path, PHAsset *asset) { if(path != nil) { @@ -945,39 +968,34 @@ + (void)slice:(NSString *)path } else if (asset != nil) { - long expected = [end longValue] - [start longValue]; - long read = 0; - long chunkRead = 0; - NSOutputStream * os = [[NSOutputStream alloc] initToFileAtPath:dest append:NO]; - [os open]; - long size = asset.size; - long max = MIN(size, [end longValue]); - - while(read < expected) { - uint8_t chunk[10240]; - uint8_t * pointerToChunk = &chunk[0]; - long chunkSize = 0; - if([start longValue] + read + 10240 > max) - { - NSLog(@"read chunk %lu", max - read - [start longValue]); - chunkSize = max - read - [start longValue]; - chunkRead = [asset getBytes:pointerToChunk fromOffset:[start longValue] + read length:chunkSize error:nil]; - } - else - { - NSLog(@"read chunk %lu", 10240); - chunkSize = 10240; - chunkRead = [asset getBytes:pointerToChunk fromOffset:[start longValue] + read length:chunkSize error:nil]; + // For PHAsset, get the full image data and then slice it + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + long startPos = [start longValue]; + long endPos = [end longValue]; + long dataLength = imageData.length; + + // Clamp the range to the actual data length + startPos = MAX(0, MIN(startPos, dataLength)); + endPos = MAX(startPos, MIN(endPos, dataLength)); + + NSRange range = NSMakeRange(startPos, endPos - startPos); + NSData *sliceData = [imageData subdataWithRange:range]; + + if ([sliceData writeToFile:dest atomically:YES]) { + resolve(dest); + } else { + reject(@"EUNSPECIFIED", @"Failed to write slice to file", nil); + } + } else { + reject(@"EUNSPECIFIED", @"Failed to get image data from asset", nil); } - if( chunkRead <= 0) - break; - long remain = expected - read; - - [os write:chunk maxLength:chunkSize]; - read += chunkRead; - } - [os close]; - resolve(dest); + }]; } else { reject(@"EINVAL", [NSString stringWithFormat: @"Could not resolve URI %@", path ], nil); @@ -1001,20 +1019,18 @@ - (void)closeInStream # pragma mark - get absolute path of resource -+ (void) getPathFromUri:(NSString *)uri completionHandler:(void(^)(NSString * path, ALAssetRepresentation *asset)) onComplete ++ (void) getPathFromUri:(NSString *)uri completionHandler:(void(^)(NSString * path, PHAsset *asset)) onComplete { if([uri hasPrefix:AL_PREFIX]) { NSURL *asseturl = [NSURL URLWithString:uri]; - __block ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init]; - [assetslibrary assetForURL:asseturl - resultBlock:^(ALAsset *asset) { - __block ALAssetRepresentation * present = [asset defaultRepresentation]; - onComplete(nil, present); - } - failureBlock:^(NSError *error) { - onComplete(nil, nil); - }]; + PHFetchResult *assets = [PHAsset fetchAssetsWithALAssetURLs:@[asseturl] options:nil]; + if (assets.count > 0) { + PHAsset *asset = assets.firstObject; + onComplete(nil, asset); + } else { + onComplete(nil, nil); + } } else { @@ -1044,20 +1060,20 @@ +(void) df:(RCTResponseSenderBlock)callback } -+ (void) writeAssetToPath:(ALAssetRepresentation * )rep dest:(NSString *)dest ++ (void) writeAssetToPath:(PHAsset * )asset dest:(NSString *)dest { - int read = 0; - int cursor = 0; - Byte * buffer = (Byte *)malloc(10240); - NSOutputStream * ostream = [[NSOutputStream alloc] initToFileAtPath:dest append:NO]; - [ostream open]; - while((read = [rep getBytes:buffer fromOffset:cursor length:10240 error:nil]) > 0) - { - cursor+=10240; - [ostream write:buffer maxLength:read]; + if (asset.mediaType == PHAssetMediaTypeImage) { + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = YES; + options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + options.resizeMode = PHImageRequestOptionsResizeModeNone; + + [[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (imageData) { + [imageData writeToFile:dest atomically:YES]; + } + }]; } - [ostream close]; - free(buffer); return; } diff --git a/react-native-blob-util.podspec b/react-native-blob-util.podspec index 98040b9f..f7361a0b 100644 --- a/react-native-blob-util.podspec +++ b/react-native-blob-util.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| 'ReactNativeBlobUtilPrivacyInfo' => ['ios/PrivacyInfo.xcprivacy'], } s.platforms = { :ios => "11.0" } - s.framework = 'AssetsLibrary' + s.framework = 'Photos' if fabric_enabled # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.