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.
- package/LICENSE +20 -0
- package/README.md +86 -0
- package/android/build.gradle +139 -0
- package/android/generated/java/com/capturestudio/NativeCaptureStudioSpec.java +48 -0
- package/android/generated/jni/CMakeLists.txt +31 -0
- package/android/generated/jni/RNCaptureStudioSpec-generated.cpp +44 -0
- package/android/generated/jni/RNCaptureStudioSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNCaptureStudioSpec/RNCaptureStudioSpecJSI.h +56 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/capturestudio/CaptureStudioModule.kt +177 -0
- package/android/src/main/java/com/capturestudio/CaptureStudioPackage.kt +33 -0
- package/android/src/main/java/com/capturestudio/data/CameraRepository.kt +43 -0
- package/android/src/main/java/com/capturestudio/data/processing/ImageProcessingWorker.kt +126 -0
- package/android/src/main/java/com/capturestudio/data/processing/ImageProcessor.kt +244 -0
- package/android/src/main/java/com/capturestudio/domain/CaptureOptions.kt +0 -0
- package/android/src/main/java/com/capturestudio/domain/model/ImageProcessingItem.kt +18 -0
- package/android/src/main/java/com/capturestudio/domain/model/ProcessingResult.kt +14 -0
- package/android/src/main/java/com/capturestudio/ui/camera/CameraActivity.kt +55 -0
- package/android/src/main/java/com/capturestudio/ui/camera/CameraUiState.kt +7 -0
- package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModel.kt +34 -0
- package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModelFactory.kt +17 -0
- package/android/src/main/res/layout/activity_camera.xml +10 -0
- package/ios/CaptureStudio.h +7 -0
- package/ios/CaptureStudio.mm +186 -0
- package/ios/ImageProcessor.h +22 -0
- package/ios/ImageProcessor.mm +383 -0
- package/ios/generated/Package.swift +59 -0
- package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.h +25 -0
- package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.mm +40 -0
- package/ios/generated/ReactAppDependencyProvider/ReactAppDependencyProvider.podspec +34 -0
- package/ios/generated/ReactCodegen/RCTModuleProviders.h +16 -0
- package/ios/generated/ReactCodegen/RCTModuleProviders.mm +51 -0
- package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.h +18 -0
- package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.mm +54 -0
- package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.h +16 -0
- package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.mm +30 -0
- package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
- package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
- package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec-generated.mm +53 -0
- package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec.h +70 -0
- package/ios/generated/ReactCodegen/RNCaptureStudioSpecJSI.h +56 -0
- package/ios/generated/ReactCodegen/ReactCodegen.podspec +110 -0
- package/lib/commonjs/NativeCaptureStudio.js +9 -0
- package/lib/commonjs/NativeCaptureStudio.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/NativeCaptureStudio.js +5 -0
- package/lib/module/NativeCaptureStudio.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts +9 -0
- package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +19 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/NativeCaptureStudio.d.ts +9 -0
- package/lib/typescript/module/src/NativeCaptureStudio.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +19 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/package.json +202 -0
- package/react-native-capture-studio.podspec +48 -0
- package/react-native.config.js +12 -0
- package/src/NativeCaptureStudio.ts +11 -0
- 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
|