Skip to content

Commit 89126a9

Browse files
committed
Added the basic JXL animation support
This does not works on SDAnimatedImageView though
1 parent f11b34d commit 89126a9

File tree

3 files changed

+120
-37
lines changed

3 files changed

+120
-37
lines changed

Example/SDWebImageJPEGXLCoder/SDViewController.m

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
@interface SDViewController ()
1414
@property (nonatomic, strong) UIImageView *imageView1;
15-
@property (nonatomic, strong) SDAnimatedImageView *imageView2;
15+
@property (nonatomic, strong) UIImageView *imageView2;
1616

1717
@end
1818

@@ -22,15 +22,15 @@ - (void)viewDidLoad {
2222
[super viewDidLoad];
2323
// Do any additional setup after loading the view, typically from a nib.
2424

25-
[SDImageCache.sharedImageCache clearDiskOnCompletion:nil];
25+
[SDImageCache.sharedImageCache.diskCache removeAllData];
2626

2727
[[SDImageCodersManager sharedManager] addCoder:[SDImageJPEGXLCoder sharedCoder]];
2828

2929
self.imageView1 = [UIImageView new];
3030
self.imageView1.contentMode = UIViewContentModeScaleAspectFit;
3131
[self.view addSubview:self.imageView1];
3232

33-
self.imageView2 = [SDAnimatedImageView new];
33+
self.imageView2 = [UIImageView new];
3434
self.imageView2.contentMode = UIViewContentModeScaleAspectFit;
3535
[self.view addSubview:self.imageView2];
3636

@@ -41,15 +41,16 @@ - (void)viewDidLoad {
4141
if (image) {
4242
NSLog(@"%@", @"Static JPEG-XL load success");
4343
}
44-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
45-
NSUInteger maxFileSize = 4096;
46-
NSData *jxlData = [SDImageJPEGXLCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEGXL options:@{SDImageCoderEncodeMaxFileSize : @(maxFileSize)}];
47-
if (jxlData) {
48-
NSLog(@"%@", @"JPEG-XL encoding success");
49-
}
50-
});
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+
// });
5152
}];
52-
[self.imageView2 sd_setImageWithURL:animatedURL placeholderImage:nil options:SDWebImageProgressiveLoad completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
53+
[self.imageView2 sd_setImageWithURL:animatedURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
5354
if (image) {
5455
NSLog(@"%@", @"Animated JPEG-XL load success");
5556
}

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ See: [Why JPEG-XL](https://jpegxl.info/why-jxl.html)
1212

1313
This coder supports the HDR/SDR decoding, as well as JPEG-XL aniamted image.
1414

15-
Note: 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)
15+
## TODO
16+
17+
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)
1620

1721
## Requirements
1822

@@ -94,6 +98,8 @@ let imageView: UIImageView
9498
imageView.sd_setImage(with: JPEGXLURL)
9599
```
96100

101+
Note: You can also test animated JPEG-XL on UIImageView/NSImageView and WebImage (via SwiftUI port)
102+
97103
## Example
98104

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

SDWebImageJPEGXLCoder/Classes/SDImageJPEGXLCoder.m

Lines changed: 101 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@
1414
@import libjxl;
1515
#endif
1616

17-
#define SD_TWO_CC(c1,c2) ((uint16_t)(((c2) << 8) | (c1)))
18-
#define SD_FOUR_CC(c1,c2,c3,c4) ((uint32_t)(((c4) << 24) | ((c3) << 16) | ((c2) << 8) | (c1)))
17+
typedef void (^sd_cleanupBlock_t)(void);
18+
19+
#if defined(__cplusplus)
20+
extern "C" {
21+
#endif
22+
void sd_executeCleanupBlock (__strong sd_cleanupBlock_t *block);
23+
#if defined(__cplusplus)
24+
}
25+
#endif
26+
27+
//#define SD_TWO_CC(c1,c2) ((uint16_t)(((c2) << 8) | (c1)))
28+
//#define SD_FOUR_CC(c1,c2,c3,c4) ((uint32_t)(((c4) << 24) | ((c3) << 16) | ((c2) << 8) | (c1)))
1929

2030
static void FreeImageData(void *info, const void *data, size_t size) {
2131
free((void *)data);
@@ -108,6 +118,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)
108118
if (!data) {
109119
return nil;
110120
}
121+
BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
111122
CGFloat scale = 1;
112123
if ([options valueForKey:SDImageCoderDecodeScaleFactor]) {
113124
scale = [[options valueForKey:SDImageCoderDecodeScaleFactor] doubleValue];
@@ -132,25 +143,20 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)
132143
preserveAspectRatio = preserveAspectRatioValue.boolValue;
133144
}
134145

135-
CGImageRef imageRef = [self sd_createJXLImageWithData:data thumbnailSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
136-
if (!imageRef) {
137-
return nil;
138-
}
139-
140-
#if SD_MAC
141-
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
142-
#else
143-
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
144-
#endif
145-
CGImageRelease(imageRef);
146-
147-
return image;
148-
}
149-
150-
- (nullable CGImageRef)sd_createJXLImageWithData:(NSData *)data thumbnailSize:(CGSize)thumbnailSize preserveAspectRatio:(BOOL)preserveAspectRatio CF_RETURNS_RETAINED {
151-
// Learn example to render on screen, see: libjxl/tools/viewer/load_jxl.cc
146+
// cleanup
147+
__block JxlDecoder *dec;
148+
__block CGColorSpaceRef colorSpaceRef;
149+
__strong void(^cleanupBlock)(void) __attribute__((cleanup(sd_executeCleanupBlock), unused)) = ^{
150+
if (colorSpaceRef) {
151+
CGColorSpaceRelease(colorSpaceRef);
152+
}
153+
if (dec) {
154+
JxlDecoderDestroy(dec);
155+
}
156+
};
152157

153-
JxlDecoder *dec = JxlDecoderCreate(NULL);
158+
// Get basic info
159+
dec = JxlDecoderCreate(NULL);
154160
if (!dec) return nil;
155161

156162
// feed data
@@ -160,7 +166,7 @@ - (nullable CGImageRef)sd_createJXLImageWithData:(NSData *)data thumbnailSize:(C
160166
// note: when using `JxlDecoderSubscribeEvents` libjxl behaves likes incremental decoding
161167
// which need event loop to get latest status via `JxlDecoderProcessInput`
162168
// each status reports your next steps's info
163-
status = JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE);
169+
status = JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE);
164170
if (status != JXL_DEC_SUCCESS) return nil;
165171

166172
// decode it
@@ -171,9 +177,9 @@ - (nullable CGImageRef)sd_createJXLImageWithData:(NSData *)data thumbnailSize:(C
171177
JxlBasicInfo info;
172178
status = JxlDecoderGetBasicInfo(dec, &info);
173179
if (status != JXL_DEC_SUCCESS) return nil;
180+
CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)info.orientation;
174181

175182
// colorspace
176-
CGColorSpaceRef colorSpaceRef;
177183
size_t profileSize;
178184
status = JxlDecoderProcessInput(dec);
179185
if (status != JXL_DEC_COLOR_ENCODING) return nil;
@@ -196,6 +202,76 @@ - (nullable CGImageRef)sd_createJXLImageWithData:(NSData *)data thumbnailSize:(C
196202
CGColorSpaceRetain(colorSpaceRef);
197203
}
198204

205+
// animation check
206+
BOOL hasAnimation = info.have_animation;
207+
if (!hasAnimation || decodeFirstFrame) {
208+
status = JxlDecoderProcessInput(dec);
209+
if (status != JXL_DEC_FRAME) return nil;
210+
CGImageRef imageRef = [self sd_createJXLImageWithDec:dec info:info colorSpace:colorSpaceRef thumbnailSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
211+
if (!imageRef) {
212+
return nil;
213+
}
214+
#if SD_MAC
215+
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
216+
#else
217+
UIImageOrientation orientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
218+
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:orientation];
219+
#endif
220+
CGImageRelease(imageRef);
221+
222+
return image;
223+
}
224+
// loop frame
225+
NSUInteger loopCount = info.animation.num_loops;
226+
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
227+
JxlFrameHeader header;
228+
do {
229+
@autoreleasepool {
230+
status = JxlDecoderProcessInput(dec);
231+
if (status != JXL_DEC_FRAME) break;
232+
status = JxlDecoderGetFrameHeader(dec, &header);
233+
if (status != JXL_DEC_SUCCESS) continue;
234+
235+
// frame decode
236+
NSTimeInterval duration = [self sd_frameDurationWithInfo:info header:header];
237+
CGImageRef imageRef = [self sd_createJXLImageWithDec:dec info:info colorSpace:colorSpaceRef thumbnailSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
238+
if (!imageRef) continue;
239+
#if SD_MAC
240+
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
241+
#else
242+
UIImageOrientation orientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
243+
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:orientation];
244+
#endif
245+
CGImageRelease(imageRef);
246+
247+
// Assemble frame
248+
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
249+
[frames addObject:frame];
250+
}
251+
} while (!header.is_last);
252+
253+
UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
254+
animatedImage.sd_imageLoopCount = loopCount;
255+
animatedImage.sd_imageFormat = SDImageFormatJPEGXL;
256+
257+
return animatedImage;
258+
}
259+
260+
- (NSTimeInterval)sd_frameDurationWithInfo:(JxlBasicInfo)info header:(JxlFrameHeader)header {
261+
// Calculate duration, this is `tick`
262+
// We need tps (tick per second) to calculate
263+
NSTimeInterval duration = (double)header.duration * info.animation.tps_denominator / info.animation.tps_numerator;
264+
if (duration < 0.1) {
265+
// Should we still try to keep broswer behavior to limit 100ms ?
266+
// Like GIF/WebP ?
267+
return 0.1;
268+
}
269+
return duration;
270+
}
271+
272+
- (nullable CGImageRef)sd_createJXLImageWithDec:(JxlDecoder *)dec info:(JxlBasicInfo)info colorSpace:(CGColorSpaceRef)colorSpace thumbnailSize:(CGSize)thumbnailSize preserveAspectRatio:(BOOL)preserveAspectRatio CF_RETURNS_RETAINED {
273+
JxlDecoderStatus status;
274+
199275
// bitmap format
200276
BOOL hasAlpha = info.alpha_bits != 0;
201277
BOOL premultiplied = info.alpha_premultiplied;
@@ -255,13 +331,13 @@ - (nullable CGImageRef)sd_createJXLImageWithData:(NSData *)data thumbnailSize:(C
255331
status = JxlDecoderProcessInput(dec);
256332
if (status != JXL_DEC_FULL_IMAGE) return nil; // Final status
257333

258-
JxlDecoderDestroy(dec);
259-
260334
// create CGImage
261335
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, bufferSize, FreeImageData);
262336
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
263337
BOOL shouldInterpolate = YES;
264-
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);
338+
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);
339+
CGDataProviderRelease(provider);
340+
265341
if (!imageRef) {
266342
return nil;
267343
}

0 commit comments

Comments
 (0)