react-native-capture-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +86 -0
  3. package/android/build.gradle +139 -0
  4. package/android/generated/java/com/capturestudio/NativeCaptureStudioSpec.java +48 -0
  5. package/android/generated/jni/CMakeLists.txt +31 -0
  6. package/android/generated/jni/RNCaptureStudioSpec-generated.cpp +44 -0
  7. package/android/generated/jni/RNCaptureStudioSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNCaptureStudioSpec/RNCaptureStudioSpecJSI.h +56 -0
  9. package/android/gradle.properties +5 -0
  10. package/android/src/main/AndroidManifest.xml +13 -0
  11. package/android/src/main/AndroidManifestNew.xml +2 -0
  12. package/android/src/main/java/com/capturestudio/CaptureStudioModule.kt +177 -0
  13. package/android/src/main/java/com/capturestudio/CaptureStudioPackage.kt +33 -0
  14. package/android/src/main/java/com/capturestudio/data/CameraRepository.kt +43 -0
  15. package/android/src/main/java/com/capturestudio/data/processing/ImageProcessingWorker.kt +126 -0
  16. package/android/src/main/java/com/capturestudio/data/processing/ImageProcessor.kt +244 -0
  17. package/android/src/main/java/com/capturestudio/domain/CaptureOptions.kt +0 -0
  18. package/android/src/main/java/com/capturestudio/domain/model/ImageProcessingItem.kt +18 -0
  19. package/android/src/main/java/com/capturestudio/domain/model/ProcessingResult.kt +14 -0
  20. package/android/src/main/java/com/capturestudio/ui/camera/CameraActivity.kt +55 -0
  21. package/android/src/main/java/com/capturestudio/ui/camera/CameraUiState.kt +7 -0
  22. package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModel.kt +34 -0
  23. package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModelFactory.kt +17 -0
  24. package/android/src/main/res/layout/activity_camera.xml +10 -0
  25. package/ios/CaptureStudio.h +7 -0
  26. package/ios/CaptureStudio.mm +186 -0
  27. package/ios/ImageProcessor.h +22 -0
  28. package/ios/ImageProcessor.mm +383 -0
  29. package/ios/generated/Package.swift +59 -0
  30. package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.h +25 -0
  31. package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.mm +40 -0
  32. package/ios/generated/ReactAppDependencyProvider/ReactAppDependencyProvider.podspec +34 -0
  33. package/ios/generated/ReactCodegen/RCTModuleProviders.h +16 -0
  34. package/ios/generated/ReactCodegen/RCTModuleProviders.mm +51 -0
  35. package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.h +18 -0
  36. package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.mm +54 -0
  37. package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.h +16 -0
  38. package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.mm +30 -0
  39. package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
  40. package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
  41. package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec-generated.mm +53 -0
  42. package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec.h +70 -0
  43. package/ios/generated/ReactCodegen/RNCaptureStudioSpecJSI.h +56 -0
  44. package/ios/generated/ReactCodegen/ReactCodegen.podspec +110 -0
  45. package/lib/commonjs/NativeCaptureStudio.js +9 -0
  46. package/lib/commonjs/NativeCaptureStudio.js.map +1 -0
  47. package/lib/commonjs/index.js +20 -0
  48. package/lib/commonjs/index.js.map +1 -0
  49. package/lib/module/NativeCaptureStudio.js +5 -0
  50. package/lib/module/NativeCaptureStudio.js.map +1 -0
  51. package/lib/module/index.js +13 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/typescript/commonjs/package.json +1 -0
  54. package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts +9 -0
  55. package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/index.d.ts +19 -0
  57. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  58. package/lib/typescript/module/package.json +1 -0
  59. package/lib/typescript/module/src/NativeCaptureStudio.d.ts +9 -0
  60. package/lib/typescript/module/src/NativeCaptureStudio.d.ts.map +1 -0
  61. package/lib/typescript/module/src/index.d.ts +19 -0
  62. package/lib/typescript/module/src/index.d.ts.map +1 -0
  63. package/package.json +202 -0
  64. package/react-native-capture-studio.podspec +48 -0
  65. package/react-native.config.js +12 -0
  66. package/src/NativeCaptureStudio.ts +11 -0
  67. package/src/index.tsx +30 -0
@@ -0,0 +1,186 @@
1
+ #import "CaptureStudio.h"
2
+ #import "ImageProcessor.h"
3
+ #import <ImageIO/ImageIO.h>
4
+ #import <CoreGraphics/CoreGraphics.h>
5
+ #import <UIKit/UIKit.h>
6
+
7
+ // Store active operations and results
8
+ static NSMutableDictionary<NSString *, NSOperationQueue *> *operationQueues;
9
+ static NSMutableDictionary<NSString *, NSDictionary *> *operationResults;
10
+
11
+ @implementation CaptureStudio
12
+
13
+ RCT_EXPORT_MODULE()
14
+
15
+ + (void)initialize {
16
+ if (self == [CaptureStudio class]) {
17
+ operationQueues = [NSMutableDictionary new];
18
+ operationResults = [NSMutableDictionary new];
19
+ }
20
+ }
21
+
22
+ #pragma mark - openCaptureStudio
23
+
24
+ - (void)openCaptureStudio:(NSDictionary *)options
25
+ resolve:(RCTPromiseResolveBlock)resolve
26
+ reject:(RCTPromiseRejectBlock)reject
27
+ {
28
+ // TODO: Implement camera capture UI
29
+ resolve(@{@"status": @"not_implemented"});
30
+ }
31
+
32
+ #pragma mark - processImages
33
+
34
+ - (void)processImages:(NSArray *)images
35
+ resolve:(RCTPromiseResolveBlock)resolve
36
+ reject:(RCTPromiseRejectBlock)reject
37
+ {
38
+ if (!images || images.count == 0) {
39
+ reject(@"INVALID_INPUT", @"No images provided", nil);
40
+ return;
41
+ }
42
+
43
+ NSString *operationId = [[NSUUID UUID] UUIDString];
44
+
45
+ // Create operation queue for background processing
46
+ NSOperationQueue *queue = [[NSOperationQueue alloc] init];
47
+ queue.name = [NSString stringWithFormat:@"ImageProcessing-%@", operationId];
48
+ queue.maxConcurrentOperationCount = 1; // Sequential processing
49
+ queue.qualityOfService = NSQualityOfServiceUserInitiated;
50
+
51
+ @synchronized (operationQueues) {
52
+ operationQueues[operationId] = queue;
53
+ }
54
+
55
+ // Create operation for processing
56
+ // Copy operationId for use in block
57
+ NSString *opId = [operationId copy];
58
+
59
+ NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
60
+ NSMutableArray *results = [NSMutableArray new];
61
+
62
+ for (NSDictionary *imageInfo in images) {
63
+ @autoreleasepool {
64
+ NSString *localPath = imageInfo[@"localPath"];
65
+ NSString *timeStamp = imageInfo[@"timeStamp"];
66
+ BOOL isForOnlyWatermark = [imageInfo[@"isForOnlyWatermark"] boolValue];
67
+ BOOL compressJpeg = [imageInfo[@"compressJpegImage"] boolValue];
68
+ // Default to YES (replace original) if not specified
69
+ BOOL replaceOriginal = imageInfo[@"replaceOriginal"] != nil ?
70
+ [imageInfo[@"replaceOriginal"] boolValue] : YES;
71
+
72
+ // Skip invalid entries
73
+ if (!localPath || localPath.length == 0 ||
74
+ [localPath isEqualToString:@"undefined"]) {
75
+ continue;
76
+ }
77
+
78
+ // Remove file:// prefix if present
79
+ NSString *cleanPath = [localPath stringByReplacingOccurrencesOfString:@"file://"
80
+ withString:@""];
81
+
82
+ // Check if file exists
83
+ if (![[NSFileManager defaultManager] fileExistsAtPath:cleanPath]) {
84
+ [results addObject:@{
85
+ @"localPath": localPath,
86
+ @"outputPath": localPath,
87
+ @"success": @(NO),
88
+ @"error": @"File not found"
89
+ }];
90
+ continue;
91
+ }
92
+
93
+ ImageProcessorResult *processorResult = [ImageProcessor processImageAtPath:cleanPath
94
+ timeStamp:timeStamp ?: @""
95
+ isForOnlyWatermark:isForOnlyWatermark
96
+ compressJpeg:compressJpeg
97
+ replaceOriginal:replaceOriginal];
98
+
99
+ // Add file:// prefix back to output path for consistency
100
+ NSString *outputPathWithPrefix = [NSString stringWithFormat:@"file://%@",
101
+ processorResult.outputPath];
102
+
103
+ [results addObject:@{
104
+ @"localPath": localPath,
105
+ @"outputPath": outputPathWithPrefix,
106
+ @"success": @(processorResult.success),
107
+ @"error": processorResult.error ? processorResult.error.localizedDescription : [NSNull null]
108
+ }];
109
+ }
110
+ }
111
+
112
+ // Store results
113
+ @synchronized (operationResults) {
114
+ operationResults[opId] = @{
115
+ @"status": @"completed",
116
+ @"processedImages": results
117
+ };
118
+ }
119
+ }];
120
+
121
+ [queue addOperation:operation];
122
+
123
+ // Return operation ID immediately
124
+ resolve(operationId);
125
+ }
126
+
127
+ #pragma mark - fetchProcessingResult
128
+
129
+ - (void)fetchProcessingResult:(NSString *)operationId
130
+ resolve:(RCTPromiseResolveBlock)resolve
131
+ reject:(RCTPromiseRejectBlock)reject
132
+ {
133
+ if (!operationId || operationId.length == 0) {
134
+ reject(@"INVALID_INPUT", @"Operation ID is required", nil);
135
+ return;
136
+ }
137
+
138
+ NSDictionary *result = nil;
139
+ NSOperationQueue *queue = nil;
140
+
141
+ @synchronized (operationResults) {
142
+ result = operationResults[operationId];
143
+ }
144
+
145
+ if (result) {
146
+ NSError *jsonError;
147
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:result
148
+ options:0
149
+ error:&jsonError];
150
+ if (jsonData) {
151
+ NSString *jsonString = [[NSString alloc] initWithData:jsonData
152
+ encoding:NSUTF8StringEncoding];
153
+ // Cleanup
154
+ @synchronized (operationQueues) {
155
+ [operationQueues removeObjectForKey:operationId];
156
+ }
157
+ @synchronized (operationResults) {
158
+ [operationResults removeObjectForKey:operationId];
159
+ }
160
+ resolve(jsonString);
161
+ } else {
162
+ reject(@"JSON_ERROR", @"Failed to serialize result", jsonError);
163
+ }
164
+ } else {
165
+ // Check if operation is still running
166
+ @synchronized (operationQueues) {
167
+ queue = operationQueues[operationId];
168
+ }
169
+
170
+ if (queue && queue.operationCount > 0) {
171
+ resolve(@"{\"status\":\"processing\"}");
172
+ } else {
173
+ reject(@"NOT_FOUND", @"Operation not found", nil);
174
+ }
175
+ }
176
+ }
177
+
178
+ #pragma mark - TurboModule
179
+
180
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
181
+ (const facebook::react::ObjCTurboModule::InitParams &)params
182
+ {
183
+ return std::make_shared<facebook::react::NativeCaptureStudioSpecJSI>(params);
184
+ }
185
+
186
+ @end
@@ -0,0 +1,22 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <CoreGraphics/CoreGraphics.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface ImageProcessorResult : NSObject
7
+ @property (nonatomic, strong) NSString *outputPath;
8
+ @property (nonatomic, assign) BOOL success;
9
+ @property (nonatomic, strong, nullable) NSError *error;
10
+ @end
11
+
12
+ @interface ImageProcessor : NSObject
13
+
14
+ + (ImageProcessorResult *)processImageAtPath:(NSString *)path
15
+ timeStamp:(NSString *)timeStamp
16
+ isForOnlyWatermark:(BOOL)isForOnlyWatermark
17
+ compressJpeg:(BOOL)compressJpeg
18
+ replaceOriginal:(BOOL)replaceOriginal;
19
+
20
+ @end
21
+
22
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,383 @@
1
+ #import "ImageProcessor.h"
2
+ #import <ImageIO/ImageIO.h>
3
+ #import <UIKit/UIKit.h>
4
+ #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
5
+ #import <CoreImage/CoreImage.h>
6
+
7
+ @implementation ImageProcessorResult
8
+ @end
9
+
10
+ @implementation ImageProcessor
11
+
12
+ #pragma mark - Public API
13
+
14
+ + (ImageProcessorResult *)processImageAtPath:(NSString *)path
15
+ timeStamp:(NSString *)timeStamp
16
+ isForOnlyWatermark:(BOOL)isForOnlyWatermark
17
+ compressJpeg:(BOOL)compressJpeg
18
+ replaceOriginal:(BOOL)replaceOriginal
19
+ {
20
+ ImageProcessorResult *result = [[ImageProcessorResult alloc] init];
21
+ result.success = NO;
22
+ result.outputPath = path; // Always return original path (like Android)
23
+
24
+ @autoreleasepool {
25
+ // 1. Load image
26
+ NSURL *fileURL = [NSURL fileURLWithPath:path];
27
+ CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL);
28
+
29
+ if (!source) {
30
+ result.error = [NSError errorWithDomain:@"ImageProcessor"
31
+ code:1
32
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to load image"}];
33
+ return result;
34
+ }
35
+
36
+ CGImageRef originalImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
37
+ CFRelease(source);
38
+
39
+ if (!originalImage) {
40
+ result.error = [NSError errorWithDomain:@"ImageProcessor"
41
+ code:2
42
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to decode image"}];
43
+ return result;
44
+ }
45
+
46
+ // 2. Get EXIF orientation and rotate if needed
47
+ CGImageRef rotatedImage = [self createRotatedImage:originalImage fromPath:path];
48
+ CGImageRelease(originalImage);
49
+
50
+ if (!rotatedImage) {
51
+ result.error = [NSError errorWithDomain:@"ImageProcessor"
52
+ code:5
53
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to rotate image"}];
54
+ return result;
55
+ }
56
+
57
+ // 3. Add watermark
58
+ CGImageRef watermarkedImage = [self createWatermarkedImage:rotatedImage withText:timeStamp];
59
+ CGImageRelease(rotatedImage);
60
+
61
+ if (!watermarkedImage) {
62
+ result.error = [NSError errorWithDomain:@"ImageProcessor"
63
+ code:6
64
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to add watermark"}];
65
+ return result;
66
+ }
67
+
68
+ // 4. Output path - ALWAYS use original path (like Android)
69
+ // This overwrites the original file with WebP/JPEG data
70
+ // The path stays the same regardless of format change
71
+ NSString *outputPath = path;
72
+ result.outputPath = outputPath;
73
+
74
+ // 5. Compress to target size (300-500KB)
75
+ // Use WebP by default (better quality at smaller sizes), JPEG only if explicitly requested
76
+ NSInteger minSizeKB = isForOnlyWatermark ? 400 : 300;
77
+ NSInteger maxSizeKB = 500;
78
+
79
+ NSError *compressError = nil;
80
+ BOOL success = [self compressImage:watermarkedImage
81
+ toPath:outputPath
82
+ minSizeKB:minSizeKB
83
+ maxSizeKB:maxSizeKB
84
+ compressJpeg:compressJpeg
85
+ error:&compressError];
86
+
87
+ CGImageRelease(watermarkedImage);
88
+
89
+ result.success = success;
90
+ result.error = compressError;
91
+
92
+ return result;
93
+ }
94
+ }
95
+
96
+ #pragma mark - Generate New Path
97
+
98
+ + (NSString *)generateNewPath:(NSString *)originalPath compressJpeg:(BOOL)compressJpeg
99
+ {
100
+ NSString *directory = [originalPath stringByDeletingLastPathComponent];
101
+ NSString *filename = [originalPath lastPathComponent];
102
+ NSString *nameWithoutExt = [filename stringByDeletingPathExtension];
103
+
104
+ // Generate timestamp for unique filename
105
+ NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
106
+ [formatter setDateFormat:@"yyyyMMdd_HHmmss"];
107
+ NSString *timestamp = [formatter stringFromDate:[NSDate date]];
108
+
109
+ // Determine extension based on format
110
+ NSString *extension = compressJpeg ? @"jpg" : @"webp";
111
+
112
+ // Check if WebP is supported, fallback to jpg
113
+ if (!compressJpeg) {
114
+ CFArrayRef supportedTypes = CGImageDestinationCopyTypeIdentifiers();
115
+ BOOL webpSupported = NO;
116
+ CFStringRef webpType = (__bridge CFStringRef)UTTypeWebP.identifier;
117
+
118
+ for (CFIndex i = 0; i < CFArrayGetCount(supportedTypes); i++) {
119
+ CFStringRef type = (CFStringRef)CFArrayGetValueAtIndex(supportedTypes, i);
120
+ if (CFStringCompare(type, webpType, 0) == kCFCompareEqualTo) {
121
+ webpSupported = YES;
122
+ break;
123
+ }
124
+ }
125
+ CFRelease(supportedTypes);
126
+
127
+ if (!webpSupported) {
128
+ extension = @"jpg";
129
+ }
130
+ }
131
+
132
+ // Create new filename: originalName_compressed_timestamp.ext
133
+ NSString *newFilename = [NSString stringWithFormat:@"%@_compressed_%@.%@",
134
+ nameWithoutExt, timestamp, extension];
135
+
136
+ return [directory stringByAppendingPathComponent:newFilename];
137
+ }
138
+
139
+ #pragma mark - Image Rotation (EXIF)
140
+
141
+ + (CGImageRef)createRotatedImage:(CGImageRef)image fromPath:(NSString *)path
142
+ {
143
+ // Read EXIF orientation
144
+ NSURL *fileURL = [NSURL fileURLWithPath:path];
145
+ CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)fileURL, NULL);
146
+
147
+ if (!source) {
148
+ CGImageRetain(image);
149
+ return image;
150
+ }
151
+
152
+ NSDictionary *properties = (__bridge_transfer NSDictionary *)
153
+ CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
154
+ CFRelease(source);
155
+
156
+ NSNumber *orientationValue = properties[(__bridge NSString *)kCGImagePropertyOrientation];
157
+ CGImagePropertyOrientation orientation = orientationValue ?
158
+ (CGImagePropertyOrientation)[orientationValue intValue] :
159
+ kCGImagePropertyOrientationUp;
160
+
161
+ if (orientation == kCGImagePropertyOrientationUp) {
162
+ CGImageRetain(image);
163
+ return image;
164
+ }
165
+
166
+ // Create rotated image using CIImage (handles all EXIF orientations)
167
+ CIImage *ciImage = [CIImage imageWithCGImage:image];
168
+ ciImage = [ciImage imageByApplyingOrientation:orientation];
169
+
170
+ CIContext *context = [CIContext contextWithOptions:nil];
171
+ CGImageRef rotated = [context createCGImage:ciImage fromRect:ciImage.extent];
172
+
173
+ return rotated;
174
+ }
175
+
176
+ #pragma mark - Watermark
177
+
178
+ + (CGImageRef)createWatermarkedImage:(CGImageRef)image withText:(NSString *)text
179
+ {
180
+ if (!text || text.length == 0) {
181
+ CGImageRetain(image);
182
+ return image;
183
+ }
184
+
185
+ size_t width = CGImageGetWidth(image);
186
+ size_t height = CGImageGetHeight(image);
187
+
188
+ // Create bitmap context
189
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
190
+ CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8,
191
+ width * 4, colorSpace,
192
+ kCGImageAlphaPremultipliedLast);
193
+ CGColorSpaceRelease(colorSpace);
194
+
195
+ if (!context) {
196
+ CGImageRetain(image);
197
+ return image;
198
+ }
199
+
200
+ // Draw original image
201
+ CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
202
+
203
+ // Calculate sizes relative to image (matching Android)
204
+ CGFloat textSize = width / 40.0;
205
+ CGFloat padding = width / 50.0;
206
+
207
+ // Create attributed string for measurement
208
+ UIFont *font = [UIFont systemFontOfSize:textSize weight:UIFontWeightRegular];
209
+ NSDictionary *attributes = @{
210
+ NSFontAttributeName: font,
211
+ NSForegroundColorAttributeName: [UIColor yellowColor]
212
+ };
213
+
214
+ CGSize textSizeRect = [text sizeWithAttributes:attributes];
215
+
216
+ // Calculate position (bottom-right, matching Android)
217
+ CGFloat bgWidth = textSizeRect.width + 2 * padding;
218
+ CGFloat bgHeight = textSizeRect.height + 2 * padding;
219
+ CGFloat x = width - bgWidth - padding;
220
+ CGFloat y = padding; // CoreGraphics origin is bottom-left
221
+
222
+ // Draw black background rectangle
223
+ CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
224
+ CGContextFillRect(context, CGRectMake(x, y, bgWidth, bgHeight));
225
+
226
+ // Draw text using UIGraphics (push context)
227
+ UIGraphicsPushContext(context);
228
+
229
+ // Flip coordinate system for text
230
+ CGContextTranslateCTM(context, 0, height);
231
+ CGContextScaleCTM(context, 1.0, -1.0);
232
+
233
+ // Recalculate Y for flipped coordinates
234
+ CGFloat textY = height - y - bgHeight + padding;
235
+
236
+ [text drawAtPoint:CGPointMake(x + padding, textY) withAttributes:attributes];
237
+
238
+ UIGraphicsPopContext();
239
+
240
+ // Create final image
241
+ CGImageRef result = CGBitmapContextCreateImage(context);
242
+ CGContextRelease(context);
243
+
244
+ return result;
245
+ }
246
+
247
+ #pragma mark - Compression (Binary Search for Target Size)
248
+
249
+ + (BOOL)compressImage:(CGImageRef)image
250
+ toPath:(NSString *)path
251
+ minSizeKB:(NSInteger)minSizeKB
252
+ maxSizeKB:(NSInteger)maxSizeKB
253
+ compressJpeg:(BOOL)compressJpeg
254
+ error:(NSError **)error
255
+ {
256
+ // Use UTType for iOS 14+
257
+ CFStringRef imageType;
258
+
259
+ if (compressJpeg) {
260
+ imageType = (__bridge CFStringRef)UTTypeJPEG.identifier;
261
+ } else {
262
+ // Check if WebP is supported
263
+ CFArrayRef supportedTypes = CGImageDestinationCopyTypeIdentifiers();
264
+ BOOL webpSupported = NO;
265
+ CFStringRef webpType = (__bridge CFStringRef)UTTypeWebP.identifier;
266
+
267
+ for (CFIndex i = 0; i < CFArrayGetCount(supportedTypes); i++) {
268
+ CFStringRef type = (CFStringRef)CFArrayGetValueAtIndex(supportedTypes, i);
269
+ if (CFStringCompare(type, webpType, 0) == kCFCompareEqualTo) {
270
+ webpSupported = YES;
271
+ break;
272
+ }
273
+ }
274
+ CFRelease(supportedTypes);
275
+
276
+ if (webpSupported) {
277
+ imageType = webpType;
278
+ } else {
279
+ // Fallback to JPEG
280
+ imageType = (__bridge CFStringRef)UTTypeJPEG.identifier;
281
+ }
282
+ }
283
+
284
+ // Binary search for optimal quality (matching Android algorithm)
285
+ // Start with quality=100 and find optimal quality for 300-500KB range
286
+ NSInteger low = 0;
287
+ NSInteger high = 100;
288
+ NSInteger bestQuality = 100;
289
+ NSData *bestData = nil;
290
+ NSData *highestQualityValidData = nil; // Highest quality within range
291
+ NSInteger highestQualityFound = 0;
292
+
293
+ while (low <= high) {
294
+ NSInteger quality = (low + high) / 2;
295
+ CGFloat qualityFloat = quality / 100.0;
296
+
297
+ NSMutableData *imageData = [NSMutableData data];
298
+ CGImageDestinationRef destination = CGImageDestinationCreateWithData(
299
+ (__bridge CFMutableDataRef)imageData,
300
+ imageType,
301
+ 1,
302
+ NULL
303
+ );
304
+
305
+ if (!destination) {
306
+ if (error) {
307
+ *error = [NSError errorWithDomain:@"ImageProcessor"
308
+ code:3
309
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to create image destination"}];
310
+ }
311
+ return NO;
312
+ }
313
+
314
+ NSDictionary *options = @{
315
+ (__bridge NSString *)kCGImageDestinationLossyCompressionQuality: @(qualityFloat)
316
+ };
317
+
318
+ CGImageDestinationAddImage(destination, image, (__bridge CFDictionaryRef)options);
319
+ BOOL finalized = CGImageDestinationFinalize(destination);
320
+ CFRelease(destination);
321
+
322
+ if (!finalized) {
323
+ if (error) {
324
+ *error = [NSError errorWithDomain:@"ImageProcessor"
325
+ code:7
326
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to finalize image"}];
327
+ }
328
+ return NO;
329
+ }
330
+
331
+ NSInteger fileSizeKB = imageData.length / 1024;
332
+
333
+ if (fileSizeKB < minSizeKB) {
334
+ // Size too small, increase quality
335
+ low = quality + 1;
336
+ bestData = imageData;
337
+ bestQuality = quality;
338
+ } else if (fileSizeKB > maxSizeKB) {
339
+ // Size too large, decrease quality
340
+ high = quality - 1;
341
+ bestData = imageData;
342
+ bestQuality = quality;
343
+ } else {
344
+ // Within range - keep track of highest quality within range
345
+ if (quality > highestQualityFound) {
346
+ highestQualityFound = quality;
347
+ highestQualityValidData = imageData;
348
+ }
349
+ // Try to find higher quality within range
350
+ low = quality + 1;
351
+ bestData = imageData;
352
+ bestQuality = quality;
353
+ }
354
+ }
355
+
356
+ // Prefer highest quality data that was within the valid range
357
+ if (highestQualityValidData) {
358
+ bestData = highestQualityValidData;
359
+ }
360
+
361
+ if (!bestData) {
362
+ if (error) {
363
+ *error = [NSError errorWithDomain:@"ImageProcessor"
364
+ code:8
365
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to compress image"}];
366
+ }
367
+ return NO;
368
+ }
369
+
370
+ // Write to file
371
+ NSError *writeError = nil;
372
+ BOOL success = [bestData writeToFile:path options:NSDataWritingAtomic error:&writeError];
373
+
374
+ if (!success && error) {
375
+ *error = writeError ?: [NSError errorWithDomain:@"ImageProcessor"
376
+ code:4
377
+ userInfo:@{NSLocalizedDescriptionKey: @"Failed to write image"}];
378
+ }
379
+
380
+ return success;
381
+ }
382
+
383
+ @end
@@ -0,0 +1,59 @@
1
+ // swift-tools-version: 6.1
2
+ // The swift-tools-version declares the minimum version of Swift required to build this package.
3
+
4
+ import PackageDescription
5
+
6
+ let package = Package(
7
+ name: "React-GeneratedCode",
8
+ platforms: [.iOS(.v15), .macCatalyst(SupportedPlatform.MacCatalystVersion.v13)],
9
+ products: [
10
+ // Products define the executables and libraries a package produces, making them visible to other packages.
11
+ .library(
12
+ name: "ReactCodegen",
13
+ targets: ["ReactCodegen"]),
14
+ .library(
15
+ name: "ReactAppDependencyProvider",
16
+ targets: ["ReactAppDependencyProvider"]),
17
+ ],
18
+ dependencies: [
19
+ .package(name: "React", path: "../../../../../../../node_modules/react-native")
20
+ ],
21
+ targets: [
22
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
23
+ // Targets can depend on other targets in this package and products from dependencies.
24
+ .target(
25
+ name: "ReactCodegen",
26
+ dependencies: ["React"],
27
+ path: "ReactCodegen",
28
+ exclude: ["ReactCodegen.podspec"],
29
+ publicHeadersPath: ".",
30
+ cSettings: [
31
+ .headerSearchPath("headers")
32
+ ],
33
+ cxxSettings: [
34
+ .headerSearchPath("headers"),
35
+ .unsafeFlags(["-std=c++20"]),
36
+ ],
37
+ linkerSettings: [
38
+ .linkedFramework("Foundation")
39
+ ]
40
+ ),
41
+ .target(
42
+ name: "ReactAppDependencyProvider",
43
+ dependencies: ["ReactCodegen"],
44
+ path: "ReactAppDependencyProvider",
45
+ exclude: ["ReactAppDependencyProvider.podspec"],
46
+ publicHeadersPath: ".",
47
+ cSettings: [
48
+ .headerSearchPath("headers"),
49
+ ],
50
+ cxxSettings: [
51
+ .headerSearchPath("headers"),
52
+ .unsafeFlags(["-std=c++20"]),
53
+ ],
54
+ linkerSettings: [
55
+ .linkedFramework("Foundation")
56
+ ]
57
+ )
58
+ ]
59
+ )
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+
9
+ #import <Foundation/Foundation.h>
10
+
11
+ #if __has_include(<React-RCTAppDelegate/RCTDependencyProvider.h>)
12
+ #import <React-RCTAppDelegate/RCTDependencyProvider.h>
13
+ #elif __has_include(<React_RCTAppDelegate/RCTDependencyProvider.h>)
14
+ #import <React_RCTAppDelegate/RCTDependencyProvider.h>
15
+ #else
16
+ #import "RCTDependencyProvider.h"
17
+ #endif
18
+
19
+ NS_ASSUME_NONNULL_BEGIN
20
+
21
+ @interface RCTAppDependencyProvider : NSObject <RCTDependencyProvider>
22
+
23
+ @end
24
+
25
+ NS_ASSUME_NONNULL_END