Skip to content

Commit 1ca8d74

Browse files
committed
feat: add swift image compress native module
1 parent 93c5f75 commit 1ca8d74

File tree

7 files changed

+178
-25
lines changed

7 files changed

+178
-25
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import SDWebImageWebPCoder
2+
3+
internal enum ImageFormat: String {
4+
case jpeg
5+
case jpg
6+
case png
7+
case webp
8+
9+
var fileExtension: String {
10+
switch self {
11+
case .jpeg, .jpg:
12+
return "jpeg"
13+
case .png:
14+
return "png"
15+
case .webp:
16+
return "webp"
17+
}
18+
}
19+
}
20+
21+
internal struct SaveImageOptions {
22+
var base64: Bool = false
23+
var compressImageQuality: Double = 1.0
24+
var format: ImageFormat = .jpeg
25+
}
26+
27+
internal typealias SaveImageResult = (url: URL, data: Data)
28+
29+
func imageData(from image: UIImage, options: SaveImageOptions) -> Data? {
30+
switch options.format {
31+
case .jpeg, .jpg:
32+
return image.jpegData(compressionQuality: options.compressImageQuality)
33+
case .png:
34+
return image.pngData()
35+
case .webp:
36+
return SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeCompressionQuality: options.compressImageQuality])
37+
}
38+
}
39+
40+
41+
@objc(StreamChatImageCompress)
42+
class StreamChatImageCompress: NSObject {
43+
private func saveImage(image: UIImage, options: SaveImageOptions) -> SaveImageResult? {
44+
// First create a file path
45+
let fileManager = FileManager.default
46+
let tempDirectory = fileManager.temporaryDirectory
47+
let fileName = UUID().uuidString
48+
let filePath = tempDirectory.appendingPathComponent(fileName).appendingPathExtension(options.format.fileExtension)
49+
50+
// Then save the image to the file path
51+
guard let data = imageData(from: image, options: options) else {
52+
print("Failed to get image data")
53+
return nil
54+
}
55+
56+
do {
57+
try data.write(to: filePath, options: .atomic)
58+
} catch {
59+
// Log the error
60+
print("Error saving image: \(error)")
61+
return nil
62+
}
63+
64+
return SaveImageResult(url: filePath, data: data)
65+
}
66+
67+
private func parseSaveImageOptions(from dict: NSDictionary) -> SaveImageOptions {
68+
var options = SaveImageOptions()
69+
70+
if let quality = dict["compressImageQuality"] as? Double {
71+
options.compressImageQuality = quality
72+
}
73+
74+
if let formatString = dict["format"] as? String,
75+
let format = ImageFormat(rawValue: formatString.lowercased()) {
76+
options.format = format
77+
}
78+
79+
if let base64 = dict["base64"] as? Bool {
80+
options.base64 = base64
81+
}
82+
83+
return options
84+
}
85+
86+
@objc
87+
func compressImage(_ imageURL: String,
88+
options: NSDictionary,
89+
resolver resolve: @escaping RCTPromiseResolveBlock,
90+
rejecter reject: @escaping RCTPromiseRejectBlock) {
91+
let savedOptions = parseSaveImageOptions(from: options)
92+
93+
print("Compressing image at \(imageURL)")
94+
95+
// Convert URL string to path
96+
var imagePath = imageURL
97+
if imageURL.hasPrefix("file://") {
98+
if let url = URL(string: imageURL) {
99+
imagePath = url.path
100+
}
101+
}
102+
103+
print("Compressing image at \(imagePath)")
104+
105+
106+
// First load the image
107+
guard let image = UIImage(contentsOfFile: imagePath) else {
108+
reject("Failed to load image", "Failed to load image", nil)
109+
return
110+
}
111+
112+
// Then compress the image
113+
guard let result = saveImage(image: image, options: savedOptions) else {
114+
reject("IMAGE_SAVE_ERROR", "Failed to save compressed image", nil)
115+
return
116+
}
117+
118+
resolve(result.url.absoluteString)
119+
}
120+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// StreamChatImageCompressBridge.m
2+
3+
#import <React/RCTBridgeModule.h>
4+
5+
@interface RCT_EXTERN_MODULE(StreamChatImageCompress, NSObject)
6+
7+
RCT_EXTERN_METHOD(compressImage:(NSString *)imageURL
8+
options:(NSDictionary *)options
9+
resolver:(RCTPromiseResolveBlock)resolve
10+
rejecter:(RCTPromiseRejectBlock)reject)
11+
12+
@end
Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,19 @@
1-
import StreamChatReactNative from '../native';
2-
3-
type CompressImageParams = {
4-
compressImageQuality: number;
5-
height: number;
6-
uri: string;
7-
width: number;
8-
};
1+
import { compressImage as compressImageNative } from '../native';
92

103
export const compressImage = async ({
11-
compressImageQuality = 1,
12-
height,
134
uri,
14-
width,
15-
}: CompressImageParams) => {
5+
compressImageQuality = 1,
6+
}: {
7+
uri: string;
8+
compressImageQuality?: number;
9+
}) => {
1610
try {
17-
const { uri: compressedUri } = await StreamChatReactNative.createResizedImage(
18-
uri,
19-
width,
20-
height,
21-
'JPEG',
22-
Math.min(Math.max(0, compressImageQuality), 1) * 100,
23-
0,
24-
undefined,
25-
false,
26-
{ mode: 'cover' },
27-
);
28-
return compressedUri;
11+
const result = await compressImageNative(uri, {
12+
compressImageQuality: Math.min(Math.max(0, compressImageQuality), 1),
13+
});
14+
return result;
2915
} catch (error) {
3016
console.log('Error resizing image:', error);
31-
return uri;
17+
return null;
3218
}
3319
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { TurboModule } from 'react-native';
2+
3+
import { TurboModuleRegistry } from 'react-native';
4+
5+
export interface Spec extends TurboModule {
6+
compressImage(
7+
imageURL: string,
8+
options: {
9+
base64: boolean;
10+
compressImageQuality: number;
11+
format: 'JPEG' | 'PNG' | 'WEBP';
12+
},
13+
): Promise<string>;
14+
}
15+
16+
export default TurboModuleRegistry.getEnforcing<Spec>('StreamChatImageCompress');

package/native-package/src/native/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
export type { ResizeFormat, ResizeMode, Response } from './types';
1111
import NativeStreamChatHapticsModule from './NativeStreamChatHapticsModule';
1212
import NativeStreamChatReactNative from './NativeStreamChatReactNative';
13+
import NativeStreamChatImageCompress from './NativeStreamChatImageCompress';
14+
import { CompressImageOptions } from './types/compressImage';
1315

1416
// @ts-ignore
1517
// eslint-disable-next-line no-underscore-dangle
@@ -23,6 +25,10 @@ const Haptics = isTurboModuleEnabled
2325
? NativeStreamChatHapticsModule
2426
: NativeModules.StreamChatHapticsModule;
2527

28+
const ImageCompress = isTurboModuleEnabled
29+
? NativeStreamChatImageCompress
30+
: NativeModules.StreamChatImageCompress;
31+
2632
const defaultOptions: Options = {
2733
mode: 'contain',
2834
onlyScaleDown: false,
@@ -67,6 +73,10 @@ export async function selectionFeedback() {
6773
await Haptics.selectionFeedback();
6874
}
6975

76+
export async function compressImage(imageURL: string, options: CompressImageOptions) {
77+
return await ImageCompress.compressImage(imageURL, options);
78+
}
79+
7080
export default {
7181
createResizedImage,
7282
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type ImageFormat = 'JPEG' | 'PNG' | 'WEBP';
2+
3+
export type CompressImageOptions = {
4+
base64?: boolean;
5+
compressImageQuality?: number;
6+
format?: ImageFormat;
7+
};

package/native-package/stream-chat-react-native.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Pod::Spec.new do |s|
1818

1919
s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
2020

21+
s.dependency "SDWebImageWebPCoder"
22+
2123
if respond_to?(:install_modules_dependencies, true)
2224
install_modules_dependencies(s)
2325
else

0 commit comments

Comments
 (0)