Skip to content

Commit 9649357

Browse files
authored
Merge pull request #2 from SDWebImage/feature/encode
Feature: Support JPEG-XL Encoding (HDR/Animated Image as well)
2 parents f302e50 + bea3ab8 commit 9649357

File tree

4 files changed

+693
-44
lines changed

4 files changed

+693
-44
lines changed

Example/SDWebImageJPEGXLCoder/SDViewController.m

+60-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
#import "SDViewController.h"
1010
#import <SDWebImageJPEGXLCoder/SDImageJPEGXLCoder.h>
1111
#import <SDWebImage/SDWebImage.h>
12+
#import <libjxl/jxl/encode.h>
13+
14+
static void WriteTempJXLFile(NSData *data, NSString *filename) {
15+
NSString *tempOutputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
16+
[data writeToFile:tempOutputPath atomically:YES];
17+
NSLog(@"Written encoded JXL to : %@", tempOutputPath);
18+
}
1219

1320
@interface SDViewController ()
1421
@property (nonatomic, strong) UIImageView *imageView1;
@@ -34,27 +41,40 @@ - (void)viewDidLoad {
3441
self.imageView2.contentMode = UIViewContentModeScaleAspectFit;
3542
[self.view addSubview:self.imageView2];
3643

37-
NSURL *staticURL = [NSURL URLWithString:@"https://jpegxl.info/logo.jxl"];
38-
NSURL *animatedURL = [NSURL URLWithString:@"https://jpegxl.info/anim_jxl_logo.jxl"];
44+
NSURL *staticURL = [NSURL URLWithString:@"https://jpegxl.info/images/dice.jxl"];
45+
NSURL *animatedURL = [NSURL URLWithString:@"https://jpegxl.info/images/anim-icos.jxl"];
3946

4047
[self.imageView1 sd_setImageWithURL:staticURL placeholderImage:nil options:0 context:nil progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
4148
if (image) {
4249
NSLog(@"%@", @"Static JPEG-XL load success");
4350
}
44-
// TODO, JXL encoding
45-
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
46-
// NSUInteger maxFileSize = 4096;
47-
// NSData *jxlData = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{SDImageCoderEncodeMaxFileSize : @(maxFileSize)}];
48-
// if (jxlData) {
49-
// NSLog(@"%@", @"JPEG-XL encoding success");
50-
// }
51-
// });
51+
// static JXL encoding
52+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
53+
NSUInteger maxFileSize = 4096;
54+
NSData *jxlData = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{SDImageCoderEncodeMaxFileSize : @(maxFileSize)}];
55+
if (jxlData) {
56+
NSLog(@"Static JPEG-XL encode success, bytes: %lu", (unsigned long)jxlData.length);
57+
WriteTempJXLFile(jxlData, imageURL.lastPathComponent);
58+
}
59+
});
5260
}];
5361
[self.imageView2 sd_setImageWithURL:animatedURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
5462
if (image) {
5563
NSLog(@"%@", @"Animated JPEG-XL load success");
5664
}
65+
// animated JXL encoding
66+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
67+
NSData *jxlData = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{
68+
SDImageCoderEncodeJXLDistance : @(3.0),
69+
}];
70+
if (jxlData) {
71+
NSLog(@"Animated JPEG-XL encode success, bytes: %lu", (unsigned long)jxlData.length);
72+
WriteTempJXLFile(jxlData, imageURL.lastPathComponent);
73+
}
74+
});
5775
}];
76+
77+
[self testHDREncoding];
5878
}
5979

6080
- (void)viewWillLayoutSubviews {
@@ -63,6 +83,36 @@ - (void)viewWillLayoutSubviews {
6383
self.imageView2.frame = CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height / 2);
6484
}
6585

86+
- (void)testHDREncoding {
87+
// Test JXL Encode
88+
NSURL *HDRURL = [NSURL URLWithString:@"https://ncdn.camarts.cn/iso-hdr-demo.jxl"];
89+
NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:HDRURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
90+
UIImage *image = [UIImage imageWithData:data];
91+
[self encodeJXLWithImage:image];
92+
}];
93+
[task resume];
94+
}
95+
96+
- (void)encodeJXLWithImage:(UIImage *)image {
97+
NSCParameterAssert(image);
98+
NSDictionary *frameSetting = @{
99+
@(JXL_ENC_FRAME_SETTING_EFFORT) : @(7),
100+
// @(JXL_ENC_FRAME_SETTING_BROTLI_EFFORT) : @(11)
101+
};
102+
// fastest encoding speed but largest compressed size, you can adjust options here
103+
NSData *data = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{
104+
// SDImageCoderEncodeCompressionQuality : @0.68,
105+
SDImageCoderEncodeJXLDistance : @(1.0),
106+
SDImageCoderEncodeJXLFrameSetting : frameSetting,
107+
}];
108+
NSCParameterAssert(data);
109+
WriteTempJXLFile(data, @"iso-hdr-demo.jxl");
110+
111+
CIImage *ciimage = [CIImage imageWithData:data];
112+
NSString *desc = [ciimage description];
113+
NSLog(@"Re-decoded JXL CIImage description: %@", desc);
114+
}
115+
66116
- (void)didReceiveMemoryWarning {
67117
[super didReceiveMemoryWarning];
68118
// Dispose of any resources that can be recreated.

README.md

+98-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ This coder supports the HDR/SDR decoding, as well as JPEG-XL aniamted image.
1515
## Notes
1616

1717
1. This coder supports animation via UIImageView/NSImageView, no SDAnimatedImageView currently (Because the current coder API need codec supports non-sequential frame decoding, but libjxl does not have. Will remove this limit in SDWebImage 6.0)
18-
2. This coder does not supports JPEG-XL encoding (Because I have no time :))
19-
3. Apple's ImageIO supports JPEGXL decoding from iOS 17/tvOS 17/watchOS 10/macOS 14 (via: [WWDC2023](https://developer.apple.com/videos/play/wwdc2023/10122/)), so SDWebImage on those platform can also decode JPEGXL images using `SDImageIOCoder` (but no animated JPEG-XL support)
18+
2. Apple's ImageIO supports JPEGXL decoding from iOS 17/tvOS 17/watchOS 10/macOS 14 (via: [WWDC2023](https://developer.apple.com/videos/play/wwdc2023/10122/)), so SDWebImage on those platform can also decode JPEGXL images using `SDImageIOCoder` (but no animated JPEG-XL support)
19+
3. From v0.2.0, this coder support JXL encoding, including HDR, static JXL, animated JXL encoding as well (a huge work...)
2020

2121
## Requirements
2222

@@ -102,6 +102,102 @@ imageView.sd_setImage(with: JPEGXLURL)
102102

103103
Note: You can also test animated JPEG-XL on UIImageView/NSImageView and WebImage (via SwiftUI port)
104104

105+
### Decoding
106+
107+
+ Objective-C
108+
109+
```objective-c
110+
// JPEGXL image decoding
111+
NSData *JPEGXLData;
112+
UIImage *image = [[SDImageJPEGXLCoder sharedCoder] decodedImageWithData:JPEGXLData options:nil];
113+
```
114+
115+
+ Swift
116+
117+
```swift
118+
// JPEGXL image decoding
119+
let JPEGXLData: Data
120+
let image = SDImageJPEGXLCoder.shared.decodedImage(with: data, options: nil)
121+
```
122+
123+
### Encoding
124+
125+
+ Objective-c
126+
127+
```objective-c
128+
// JPEGXL image encoding
129+
UIImage *image;
130+
NSData *JPEGXLData = [[SDImageJPEGXLCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatJPEGXL options:nil];
131+
// Encode Quality
132+
NSData *lossyJPEGXLData = [[SDImageJPEGXLCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{SDImageCoderEncodeCompressionQuality : @(0.1)}]; // [0, 1] compression quality
133+
```
134+
135+
+ Swift
136+
137+
```swift
138+
// JPEGXL image encoding
139+
let image: UIImage
140+
let JPEGXLData = SDImageJPEGXLCoder.shared.encodedData(with: image, format: .jpegxl, options: nil)
141+
// Encode Quality
142+
let lossyJPEGXLData = SDImageJPEGXLCoder.shared.encodedData(with: image, format: .jpegxl, options: [.encodeCompressionQuality: 0.1]) // [0, 1] compression quality
143+
```
144+
145+
### Animated JPEG-XL Encoding
146+
147+
+ Objective-c
148+
149+
```objective-c
150+
// Animated encoding
151+
NSMutableArray<SDImageFrames *> *frames = [NSMutableArray array];
152+
for (size_t i = 0; i < images.count; i++) {
153+
SDImageFrame *frame = [SDImageFrame frameWithImage:images[i] duration:0.1];
154+
[frames appendObject:frame];
155+
}
156+
NSData *animatedData = [[SDImageJPEGXLCoder sharedCoder] encodedDataWithFrames:frames loopCount:0 format:SDImageFormatJPEGXL options:nil];
157+
```
158+
159+
+ Swift
160+
161+
```swift
162+
// Animated encoding
163+
var frames: [SDImageFrame] = []
164+
for i in 0..<images.count {
165+
let frame = SDImageFrame(image: images[i], duration: 0.1)
166+
frames.append(frame)
167+
}
168+
let animatedData = SDImageJPEGXLCoder.shared.encodedData(with: frames, loopCount: 0, format: .jpegxl, options: nil)
169+
```
170+
171+
### Advanced jxl codec options
172+
173+
For advanced user who want detailed control like `cjxl` command line tool, you can pass the underlying encode options in
174+
175+
+ Objective-C
176+
177+
```objective-c
178+
NSDictionary *frameSetting = @{
179+
@(JXL_ENC_FRAME_SETTING_EFFORT) : @(10),
180+
@(JXL_ENC_FRAME_SETTING_BROTLI_EFFORT) : @(11)
181+
};
182+
NSData *data = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{
183+
SDImageCoderEncodeJXLDistance : @(3.0), // jxl -distance
184+
SDImageCoderEncodeJXLFrameSetting : frameSetting, // jxl -effort
185+
}];
186+
```
187+
188+
+ Swift
189+
190+
```swift
191+
let frameSetting = [
192+
JXL_ENC_FRAME_SETTING_EFFORT.rawValue : 10,
193+
JXL_ENC_FRAME_SETTING_BROTLI_EFFORT.rawValue : 11
194+
]
195+
let data = SDImageJPEGXLCoder.shared.encodedData(with: image, format: .jpegxl, options: [
196+
.encodeJXLDistance : 3.0, // jxl -distance
197+
.encodeJXLFrameSetting : frameSetting, // jxl -effort
198+
]);
199+
```
200+
105201
## Example
106202

107203
To run the example project, clone the repo, and run `pod install` from the root directory first. Then open `SDWebImageJPEGXLCoder.xcworkspace`.

SDWebImageJPEGXLCoder/Classes/SDImageJPEGXLCoder.h

+54
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,57 @@ static const SDImageFormat SDImageFormatJPEGXL = 17; // JPEG-XL
1919
@property (nonatomic, class, readonly, nonnull) SDImageJPEGXLCoder *sharedCoder;
2020

2121
@end
22+
23+
#pragma mark - JXL Encode Options
24+
25+
/*
26+
* Sets the distance level for lossy compression: target max butteraugli
27+
* distance, lower = higher quality. Range: 0 .. 25.
28+
* 0.0 = mathematically lossless (however, use @ref JxlEncoderSetFrameLossless
29+
* instead to use true lossless, as setting distance to 0 alone is not the only
30+
* requirement). 1.0 = visually lossless. Recommended range: 0.5 .. 3.0. Default
31+
* value: 1.0.
32+
* See more in upstream: https://libjxl.readthedocs.io/en/latest/api_encoder.html#_CPPv426JxlEncoderSetFrameDistanceP23JxlEncoderFrameSettingsf
33+
* A NSNumber value. The default value is nil.
34+
* @note: When you use both `SDImageCoderEncodeCompressionQuality` and this option, this option will override that one and takes effect.
35+
*/
36+
FOUNDATION_EXPORT _Nonnull SDImageCoderOption SDImageCoderEncodeJXLDistance;
37+
38+
/**
39+
* Enables lossless encoding.
40+
* See more in upstream: https://libjxl.readthedocs.io/en/latest/api_encoder.html#_CPPv426JxlEncoderSetFrameLosslessP23JxlEncoderFrameSettings8JXL_BOOL
41+
* A NSNumber value. The default value is NO.
42+
*/
43+
FOUNDATION_EXPORT _Nonnull SDImageCoderOption SDImageCoderEncodeJXLLoseless;
44+
45+
/**
46+
* Sets the feature level of the JPEG XL codestream. Valid values are 5 and
47+
* 10, or -1 (to choose automatically). Using the minimum required level, or
48+
* level 5 in most cases, is recommended for compatibility with all decoders.
49+
* See more in upstream: https://libjxl.readthedocs.io/en/latest/api_encoder.html#_CPPv428JxlEncoderSetCodestreamLevelP10JxlEncoderi
50+
* A NSNumber value. The default value is -1.
51+
*/
52+
FOUNDATION_EXPORT _Nonnull SDImageCoderOption SDImageCoderEncodeJXLCodeStreamLevel;
53+
54+
/* Pass extra underlying libjxl encoding frame setting. The Value is a NSDictionary, which each key-value pair use`JxlEncoderFrameSettingId` (NSNumber) as key, and NSNumber as value.
55+
* See more in upstream: https://libjxl.readthedocs.io/en/latest/api_encoder.html#_CPPv424JxlEncoderFrameSettingId
56+
* If you can not impoort the libjxl header, just pass the raw int number as `JxlEncoderFrameSettingId`
57+
58+
Objc code:
59+
~~~
60+
@{SDImageCoderEncodeJXLFrameSetting: @{@JXL_ENC_FRAME_SETTING_EFFORT: @(11)}
61+
~~~
62+
63+
Swift code:
64+
~~~
65+
[.encodeJXLFrameSetting : [JxlEncoderFrameSettingId.JXL_ENC_FRAME_SETTING_EFFORT : 11]
66+
~~~
67+
*/
68+
FOUNDATION_EXPORT _Nonnull SDImageCoderOption SDImageCoderEncodeJXLFrameSetting;
69+
70+
/**
71+
* Set the thread count for multithreading. 0 means using logical CPU core (hw.logicalcpu) to detect threads (like 8 core on M1 Mac/ 4 core on iPhone 16 Pro)
72+
* @warning If you're encoding huge or multiple JXL image at the same time, set this value to 1 to avoid huge CPU usage.
73+
* A NSNumber value. Defaults to 0, means logical CPU core count. Set to 1 if you want single-thread encoding.
74+
*/
75+
FOUNDATION_EXPORT _Nonnull SDImageCoderOption SDImageCoderEncodeJXLThreadCount;

0 commit comments

Comments
 (0)