Skip to content

Commit 2a6eba2

Browse files
committed
Added the JXL related encoding options, like effort, distance, etc
Also allows to control the thread count to avoid huge CPU usage
1 parent cb7d7de commit 2a6eba2

File tree

3 files changed

+164
-63
lines changed

3 files changed

+164
-63
lines changed

Example/SDWebImageJPEGXLCoder/SDViewController.m

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "SDViewController.h"
1010
#import <SDWebImageJPEGXLCoder/SDImageJPEGXLCoder.h>
1111
#import <SDWebImage/SDWebImage.h>
12+
#import <libjxl/jxl/encode.h>
1213

1314
@interface SDViewController ()
1415
@property (nonatomic, strong) UIImageView *imageView1;
@@ -37,29 +38,26 @@ - (void)viewDidLoad {
3738
NSURL *staticURL = [NSURL URLWithString:@"https://jpegxl.info/logo.jxl"];
3839
NSURL *animatedURL = [NSURL URLWithString:@"https://jpegxl.info/anim_jxl_logo.jxl"];
3940

40-
// [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) {
41-
// if (image) {
42-
// NSLog(@"%@", @"Static JPEG-XL load success");
43-
// }
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-
// });
52-
// }];
53-
// [self.imageView2 sd_setImageWithURL:animatedURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
54-
// if (image) {
55-
// NSLog(@"%@", @"Animated JPEG-XL load success");
56-
// }
57-
// }];
41+
[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) {
42+
if (image) {
43+
NSLog(@"%@", @"Static JPEG-XL load success");
44+
}
45+
// TODO, JXL encoding
46+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
47+
NSUInteger maxFileSize = 4096;
48+
NSData *jxlData = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{SDImageCoderEncodeMaxFileSize : @(maxFileSize)}];
49+
if (jxlData) {
50+
NSLog(@"%@", @"JPEG-XL encoding success");
51+
}
52+
});
53+
}];
54+
[self.imageView2 sd_setImageWithURL:animatedURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
55+
if (image) {
56+
NSLog(@"%@", @"Animated JPEG-XL load success");
57+
}
58+
}];
5859

59-
// Test JXL Encode
60-
NSData *HDRData = [NSData dataWithContentsOfFile:@"/Users/lizhuoli/Desktop/iso-hdr-demo.jxl"];
61-
UIImage *image = [UIImage imageWithData:HDRData];
62-
[self encodeJXLWithImage:image];
60+
[self testHDREncoding];
6361
}
6462

6563
- (void)viewWillLayoutSubviews {
@@ -68,16 +66,36 @@ - (void)viewWillLayoutSubviews {
6866
self.imageView2.frame = CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height / 2);
6967
}
7068

69+
- (void)testHDREncoding {
70+
// Test JXL Encode
71+
NSURL *HDRURL = [NSURL URLWithString:@"https://ncdn.camarts.cn/iso-hdr-demo.jxl"];
72+
NSURLSessionTask *task = [NSURLSession.sharedSession dataTaskWithURL:HDRURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
73+
UIImage *image = [UIImage imageWithData:data];
74+
[self encodeJXLWithImage:image];
75+
}];
76+
[task resume];
77+
}
78+
7179
- (void)encodeJXLWithImage:(UIImage *)image {
80+
NSCParameterAssert(image);
81+
NSDictionary *frameSetting = @{
82+
@(JXL_ENC_FRAME_SETTING_EFFORT) : @(1),
83+
@(JXL_ENC_FRAME_SETTING_BROTLI_EFFORT) : @(0)
84+
};
85+
// fastest encoding speed but largest compressed size, you can adjust options here
7286
NSData *data = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{
73-
SDImageCoderEncodeCompressionQuality : @0.68
87+
// SDImageCoderEncodeCompressionQuality : @0.68,
88+
SDImageCoderEncodeJXLDistance : @(1.0),
89+
SDImageCoderEncodeJXLFrameSetting : frameSetting,
7490
}];
7591
NSCParameterAssert(data);
76-
[data writeToFile:@"/tmp/a.jxl" atomically:YES];
92+
NSString *tempOutputPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"iso-hdr-demo.jxl"];
93+
[data writeToFile:tempOutputPath atomically:YES];
94+
NSLog(@"Written encoded JXL to : %@", tempOutputPath);
7795

7896
CIImage *ciimage = [CIImage imageWithData:data];
7997
NSString *desc = [ciimage description];
80-
NSLog(@"Encoded JXL CIImage description: %@", desc);
98+
NSLog(@"Re-decoded JXL CIImage description: %@", desc);
8199
}
82100

83101
- (void)didReceiveMemoryWarning {

SDWebImageJPEGXLCoder/Classes/SDImageJPEGXLCoder.h

Lines changed: 54 additions & 0 deletions
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;

SDWebImageJPEGXLCoder/Classes/SDImageJPEGXLCoder.m

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131

3232
// Should moved to SDWebImage Core
3333
#include <sys/sysctl.h>
34-
static int computeHostNumPhysicalCores() {
34+
static int computeHostNumLogicalCores(void) {
3535
uint32_t count;
3636
size_t len = sizeof(count);
37-
sysctlbyname("hw.physicalcpu", &count, &len, NULL, 0);
37+
sysctlbyname("hw.logicalcpu", &count, &len, NULL, 0);
3838
if (count < 1) {
3939
int nm[2];
4040
nm[0] = CTL_HW;
@@ -373,20 +373,8 @@ - (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(N
373373
if (options[SDImageCoderEncodeCompressionQuality]) {
374374
compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
375375
}
376-
CGSize maxPixelSize = CGSizeZero;
377-
NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
378-
if (maxPixelSizeValue != nil) {
379-
#if SD_MAC
380-
maxPixelSize = maxPixelSizeValue.sizeValue;
381-
#else
382-
maxPixelSize = maxPixelSizeValue.CGSizeValue;
383-
#endif
384-
}
385-
NSUInteger maxFileSize = 0;
386-
if (options[SDImageCoderEncodeMaxFileSize]) {
387-
maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
388-
}
389376

377+
// TODO: Animated JPEG-XL Encoding support
390378
// BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
391379
// if (encodeFirstFrame || frames.count <= 1) {
392380
//
@@ -461,7 +449,7 @@ - (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(N
461449
#else
462450
CGImagePropertyOrientation orientation = kCGImagePropertyOrientationUp;
463451
#endif
464-
data = [self sd_encodedJXLDataWithImage:imageRef orientation:orientation quality:compressionQuality maxPixelSize:maxPixelSize maxFileSize:maxFileSize options:nil];
452+
data = [self sd_encodedJXLDataWithImage:imageRef orientation:orientation quality:compressionQuality options:options];
465453

466454
return data;
467455
}
@@ -494,11 +482,9 @@ JxlEncoderStatus EncodeWithEncoder(JxlEncoder* enc, NSMutableData *compressed) {
494482
}
495483

496484
- (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
497-
orientation:(CGImagePropertyOrientation)orientation
498-
quality:(double)quality
499-
maxPixelSize:(CGSize)maxPixelSize
500-
maxFileSize:(NSUInteger)maxFileSize
501-
options:(nullable SDImageCoderOptions *)options
485+
orientation:(CGImagePropertyOrientation)orientation
486+
quality:(double)quality
487+
options:(NSDictionary *)options
502488
{
503489
if (!imageRef) {
504490
return nil;
@@ -586,7 +572,7 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
586572
JxlBasicInfo info;
587573
JxlColorEncoding jxl_color;
588574
JxlPixelFormat jxl_fmt;
589-
JxlEncoderStatus jret;
575+
__block JxlEncoderStatus jret;
590576

591577
// encoder
592578
JxlEncoder* enc = JxlEncoderCreate(NULL);
@@ -597,12 +583,28 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
597583
/* populate the basic info settings */
598584
JxlEncoderInitBasicInfo(&info);
599585

586+
/* Parse the extra options */
587+
NSDictionary *frameSetting = options[SDImageCoderEncodeJXLFrameSetting];
588+
BOOL loseless = options[SDImageCoderEncodeJXLLoseless] ? [options[SDImageCoderEncodeJXLLoseless] boolValue] : NO;
589+
int codeStreamLevel = options[SDImageCoderEncodeJXLCodeStreamLevel] ? [options[SDImageCoderEncodeJXLCodeStreamLevel] intValue] : -1;
590+
591+
float distance;
592+
if (options[SDImageCoderEncodeJXLDistance] != nil) {
593+
// prefer JXL distance
594+
distance = [options[SDImageCoderEncodeJXLDistance] floatValue];
595+
} else {
596+
// convert JPEG quality (0-100) to JXL distance
597+
distance = JxlEncoderDistanceFromQuality(quality * 100.0);
598+
}
599+
/* bitexact lossless requires there to be no XYB transform */
600+
info.uses_original_profile = distance == 0.0;
601+
600602
jxl_fmt.num_channels = (uint32_t)components;
601603
info.xsize = (uint32_t)width;
602604
info.ysize = (uint32_t)height;
603605
info.num_extra_channels = (jxl_fmt.num_channels + 1) % 2;
604606
info.num_color_channels = jxl_fmt.num_channels - info.num_extra_channels;
605-
info.bits_per_sample = bitsPerPixel / jxl_fmt.num_channels;
607+
info.bits_per_sample = (uint32_t)bitsPerPixel / jxl_fmt.num_channels;
606608
info.alpha_bits = (info.num_extra_channels > 0) * info.bits_per_sample;
607609
// floating point
608610
if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) {
@@ -618,10 +620,6 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
618620
info.orientation = (JxlOrientation)orientation;
619621
// default endian (little)
620622
jxl_fmt.endianness = JXL_NATIVE_ENDIAN;
621-
// convert JPEG quality (0-100) to JXL distance
622-
float distance = JxlEncoderDistanceFromQuality(quality * 100.0);
623-
/* bitexact lossless requires there to be no XYB transform */
624-
info.uses_original_profile = distance == 0.0;
625623
/* rendering intent doesn't matter here
626624
* but libjxl will whine if we don't set it */
627625
JxlRenderingIntent render_indent;
@@ -662,23 +660,47 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
662660

663661
/* This needs to be set each time the decoder is reset */
664662
JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(enc, NULL);
665-
jret = JxlEncoderSetFrameDistance(frame_settings, distance);
666-
// JxlEncoderSetExtraChannelDistance(frame_settings, distance);
663+
jret |= JxlEncoderSetFrameDistance(frame_settings, distance);
664+
/* Set lossless */
665+
jret |= JxlEncoderSetFrameLossless(frame_settings, loseless ? 1 : 0);
666+
/* Set code steram level */
667+
jret |= JxlEncoderSetCodestreamLevel(enc, codeStreamLevel);
668+
669+
/* Set extra frame setting */
670+
[frameSetting enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, NSNumber * _Nonnull value, BOOL * _Nonnull stop) {
671+
JxlEncoderFrameSettingId frame_key = key.unsignedIntValue;
672+
// check the value is floating point or integer
673+
if ([[value stringValue] containsString:@"."]) {
674+
// floating point value
675+
double frame_value = value.doubleValue;
676+
jret |= JxlEncoderFrameSettingsSetFloatOption(frame_settings, frame_key, frame_value);
677+
} else {
678+
// integer value
679+
int64_t frame_value = value.integerValue;
680+
jret |= JxlEncoderFrameSettingsSetOption(frame_settings, frame_key, frame_value);
681+
}
682+
}];
683+
667684
if (jret != JXL_ENC_SUCCESS) {
668685
JxlEncoderDestroy(enc);
669686
return nil;
670687
}
671688

672-
/* This needs to be set each time the decoder is reset */
673-
size_t threadCount = computeHostNumPhysicalCores();
674-
void* runner = JxlThreadParallelRunnerCreate(NULL, threadCount);
675-
jret = JxlEncoderSetParallelRunner(enc, JxlThreadParallelRunner, runner);
676-
if (jret != JXL_ENC_SUCCESS) {
677-
JxlEncoderDestroy(enc);
678-
return nil;
689+
/* This needs to be set each time the decoder is reset */
690+
size_t threadCount = [options[SDImageCoderEncodeJXLThreadCount] unsignedIntValue];
691+
if (threadCount == 0) {
692+
threadCount = computeHostNumLogicalCores();
693+
}
694+
if (threadCount > 1) {
695+
void* runner = JxlThreadParallelRunnerCreate(NULL, threadCount);
696+
jret = JxlEncoderSetParallelRunner(enc, JxlThreadParallelRunner, runner);
697+
if (jret != JXL_ENC_SUCCESS) {
698+
JxlEncoderDestroy(enc);
699+
return nil;
700+
}
679701
}
680702

681-
// Add bitmap buffer
703+
/* Add frame bitmap buffer */
682704
jret = JxlEncoderAddImageFrame(frame_settings, &jxl_fmt,
683705
buffer.bytes,
684706
buffer.length);
@@ -688,7 +710,7 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
688710
}
689711
JxlEncoderCloseInput(enc);
690712

691-
// libjxp support incremental encoding, but we just wait it finished
713+
/* libjxp support incremental encoding, but we just wait it until finished */
692714
NSMutableData *output = [NSMutableData data];
693715
jret = EncodeWithEncoder(enc, output);
694716

@@ -701,3 +723,10 @@ - (nullable NSData *)sd_encodedJXLDataWithImage:(nullable CGImageRef)imageRef
701723
}
702724

703725
@end
726+
727+
#pragma mark - JXL Encode Options
728+
SDImageCoderOption SDImageCoderEncodeJXLDistance = @"encodeJXLDistance";
729+
SDImageCoderOption SDImageCoderEncodeJXLLoseless = @"encodeJXLLoseless";
730+
SDImageCoderOption SDImageCoderEncodeJXLCodeStreamLevel = @"encodeJXLCodeStreamLevel";
731+
SDImageCoderOption SDImageCoderEncodeJXLFrameSetting = @"encodeJXLFrameSetting";
732+
SDImageCoderOption SDImageCoderEncodeJXLThreadCount = @"encodeJXLThreadCount";

0 commit comments

Comments
 (0)