#import "StreamChatReactNative.h"
#import <React/RCTLog.h>
#if __has_include(<React/RCTLog.h>)
#import <React/RCTLog.h>
#import <React/RCTImageLoader.h>
#else
#import "RCTLog.h"
#import "RCTImageLoader.h"
#endif
static NSString *const moduleName = @"StreamChatReactNative";
// These helpers are declared `static` so their symbols keep internal linkage and
// are not exported from the framework. Otherwise they collide (duplicate symbol
// link error) with libraries that vendor the same image-resizer sources, e.g.
// @bam.tech/react-native-image-resizer. See issue #3227.
static bool saveImage(NSString *fullPath, UIImage *image, NSString *format, float quality);
static NSString *generateFilePath(NSString *ext, NSString *outputPath);
static UIImage *rotateImage(UIImage *inputImage, float rotationDegrees);
static float getScaleForProportionalResize(CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize);
static UIImage *scaleImage(UIImage *image, CGSize toSize, NSString *mode, bool onlyScaleDown, UIColor *backgroundColor);
static NSDictionary *transformImage(UIImage *image, int rotation, CGSize newSize, NSString *fullPath, NSString *format, int quality, NSDictionary *options);
@implementation StreamChatReactNative
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath backgroundColor:(NSNumber *)backgroundColor resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
[self createResizedImage:uri width:width height:height format:format quality:quality mode:mode onlyScaleDown:onlyScaleDown rotation:rotation outputPath:outputPath backgroundColor:backgroundColor resolve:resolve reject:reject];
}
- (void)createResizedImage:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath backgroundColor:(NSNumber *)backgroundColor resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@try {
CGSize newSize = CGSizeMake(width, height);
//Set image extension
NSString *extension = @"jpg";
if ([format isEqualToString:@"PNG"]) {
extension = @"png";
}
NSString* fullPath;
@try {
fullPath = generateFilePath(extension, outputPath);
} @catch (NSException *exception) {
[NSException raise:moduleName format:@"Invalid output path."];
}
// Nil unless the caller asked for a backdrop. Converted here (on the JS-side
// argument) rather than in scaleImage so the ARGB decoding lives in one place.
UIColor *fillColor = backgroundColor == nil ? nil : [RCTConvert UIColor:backgroundColor];
NSMutableDictionary *options = [@{@"mode": mode, @"onlyScaleDown": [NSNumber numberWithBool:onlyScaleDown]} mutableCopy];
if (fillColor != nil) {
options[@"backgroundColor"] = fillColor;
}
RCTImageLoader *loader = [self.bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES];
NSURLRequest *request = [RCTConvert NSURLRequest:uri];
[loader loadImageWithURLRequest:request
size:newSize
scale:1
clipped:NO
resizeMode:RCTResizeModeContain
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", [NSString stringWithFormat: @"%ld", (long)error.code], error.description]);
reject([NSString stringWithFormat: @"%ld", (long)error.code], error.description, nil);
return;
}
NSDictionary * response = transformImage(image, [rotation integerValue], newSize, fullPath, format, (int)quality, options);
resolve(response);
}];
} @catch (NSException *exception) {
RCTLogError(@"%@", [NSString stringWithFormat:@"Code : %@ / Message : %@", exception.name, exception.reason]);
reject(exception.name, exception.reason, nil);
}
});
}
static bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality)
{
NSData* data = nil;
if ([format isEqualToString:@"JPEG"]) {
data = UIImageJPEGRepresentation(image, quality / 100.0);
} else if ([format isEqualToString:@"PNG"]) {
data = UIImagePNGRepresentation(image);
}
if (data == nil) {
return NO;
}
NSFileManager* fileManager = [NSFileManager defaultManager];
return [fileManager createFileAtPath:fullPath contents:data attributes:nil];
}
static NSString * generateFilePath(NSString * ext, NSString * outputPath)
{
NSString* directory;
if ([outputPath length] == 0) {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
directory = [paths firstObject];
} else {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
if ([outputPath hasPrefix:documentsDirectory]) {
directory = outputPath;
} else {
directory = [documentsDirectory stringByAppendingPathComponent:outputPath];
}
NSError *error;
[[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"Error creating documents subdirectory: %@", error);
@throw [NSException exceptionWithName:@"InvalidPathException" reason:[NSString stringWithFormat:@"Error creating documents subdirectory: %@", error] userInfo:nil];
}
}
NSString* name = [[NSUUID UUID] UUIDString];
NSString* fullName = [NSString stringWithFormat:@"%@.%@", name, ext];
NSString* fullPath = [directory stringByAppendingPathComponent:fullName];
return fullPath;
}
static UIImage * rotateImage(UIImage *inputImage, float rotationDegrees)
{
// We want only fixed 0, 90, 180, 270 degree rotations.
const int rotDiv90 = (int)round(rotationDegrees / 90);
const int rotQuadrant = rotDiv90 % 4;
const int rotQuadrantAbs = (rotQuadrant < 0) ? rotQuadrant + 4 : rotQuadrant;
// Return the input image if no rotation specified.
if (0 == rotQuadrantAbs) {
return inputImage;
} else {
// Rotate the image by 80, 180, 270.
UIImageOrientation orientation = UIImageOrientationUp;
switch(rotQuadrantAbs) {
case 1:
orientation = UIImageOrientationRight; // 90 deg CW
break;
case 2:
orientation = UIImageOrientationDown; // 180 deg rotation
break;
default:
orientation = UIImageOrientationLeft; // 90 deg CCW
break;
}
return [[UIImage alloc] initWithCGImage: inputImage.CGImage
scale: 1.0
orientation: orientation];
}
}
static float getScaleForProportionalResize(CGSize theSize, CGSize intoSize, bool onlyScaleDown, bool maximize)
{
float sx = theSize.width;
float sy = theSize.height;
float dx = intoSize.width;
float dy = intoSize.height;
float scale = 1;
if( sx != 0 && sy != 0 )
{
dx = dx / sx;
dy = dy / sy;
// if maximize is true, take LARGER of the scales, else smaller
if (maximize) {
scale = MAX(dx, dy);
} else {
scale = MIN(dx, dy);
}
if (onlyScaleDown) {
scale = MIN(scale, 1);
}
}
else
{
scale = 0;
}
return scale;
}
// returns a resized image keeping aspect ratio and considering
// any :image scale factor.
// The returned image is an unscaled image (scale = 1.0)
// so no additional scaling math needs to be done to get its pixel dimensions
static UIImage* scaleImage (UIImage* image, CGSize toSize, NSString* mode, bool onlyScaleDown, UIColor* backgroundColor)
{
// Need to do scaling corrections
// based on scale, since UIImage width/height gives us
// a possibly scaled image (dimensions in points)
// Idea taken from RNCamera resize code
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
// using this instead of ImageHelpers allows us to consider
// rotation variations
CGSize newSize;
if ([mode isEqualToString:@"stretch"]) {
// Distort aspect ratio
int width = toSize.width;
int height = toSize.height;
if (onlyScaleDown) {
width = MIN(width, imageSize.width);
height = MIN(height, imageSize.height);
}
newSize = CGSizeMake(width, height);
} else {
// Either "contain" (default) or "cover": preserve aspect ratio
bool maximize = [mode isEqualToString:@"cover"];
float scale = getScaleForProportionalResize(imageSize, toSize, onlyScaleDown, maximize);
newSize = CGSizeMake(roundf(imageSize.width * scale), roundf(imageSize.height * scale));
}
// A non-opaque context initialises to white (tested on iOS 26). Encoders
// without an alpha channel (JPEG) then drop the alpha and keep the RGB, which
// is why transparent areas come out white. When a backdrop is requested we
// make the context opaque and fill it first, so the image composites onto the
// colour in the same render pass - no extra bitmap, and an opaque context is
// cheaper than an alpha one.
BOOL opaque = (backgroundColor != nil);
UIGraphicsBeginImageContextWithOptions(newSize, opaque, 1.0);
if (opaque) {
[backgroundColor setFill];
UIRectFill(CGRectMake(0, 0, newSize.width, newSize.height));
}
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
static NSDictionary * transformImage(UIImage *image,
int rotation,
CGSize newSize,
NSString* fullPath,
NSString* format,
int quality,
NSDictionary* options)
{
if (image == nil) {
[NSException raise:moduleName format:@"Can't retrieve the file from the path."];
}
// Rotate image if rotation is specified.
if (0 != (int)rotation) {
image = rotateImage(image, rotation);
if (image == nil) {
[NSException raise:moduleName format:@"Can't rotate the image."];
}
}
// Do the resizing
UIImage * scaledImage = scaleImage(
image,
newSize,
options[@"mode"],
[[options objectForKey:@"onlyScaleDown"] boolValue],
options[@"backgroundColor"]
);
if (scaledImage == nil) {
[NSException raise:moduleName format:@"Can't resize the image."];
}
// Compress and save the image
if (!saveImage(fullPath, scaledImage, format, quality)) {
[NSException raise:moduleName format:@"Can't save the image. Check your compression format and your output path"];
}
NSURL *fileUrl = [[NSURL alloc] initFileURLWithPath:fullPath];
NSString *fileName = fileUrl.lastPathComponent;
NSError *attributesError = nil;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:&attributesError];
NSNumber *fileSize = fileAttributes == nil ? 0 : [fileAttributes objectForKey:NSFileSize];
NSDictionary *response = @{@"path": fullPath,
@"uri": fileUrl.absoluteString,
@"name": fileName,
@"size": fileSize == nil ? @(0) : fileSize,
@"width": @(scaledImage.size.width),
@"height": @(scaledImage.size.height),
};
return response;
}
// Don't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeStreamChatReactNativeSpecJSI>(params);
}
#endif
@end