Skip to content

Commit

Permalink
Merge pull request #31 from SDWebImage/feature_support_avifs_animation
Browse files Browse the repository at this point in the history
Feature support avifs animation
  • Loading branch information
dreampiggy authored Jul 31, 2021
2 parents e4718c6 + d66afa7 commit 91ebf46
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 38 deletions.
10 changes: 5 additions & 5 deletions Example/SDWebImageAVIFCoder/SDViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ - (void)viewDidLoad
SDImageAVIFCoder *AVIFCoder = [SDImageAVIFCoder sharedCoder];
[[SDImageCodersManager sharedManager] addCoder:AVIFCoder];
NSURL *AVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/fox.profile0.8bpc.yuv420.avif"];
NSURL *HDRAVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/hato.profile2.12bpc.yuv422.avif"];
// NSURL *HDRAVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/hato.profile2.12bpc.yuv422.avif"];
NSURL *animatedAVIFSURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/star-12bpc-with-alpha.avifs"];
CGSize screenSize = [UIScreen mainScreen].bounds.size;

UIImageView *imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, screenSize.width, screenSize.height / 2)];
UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(0, screenSize.height / 2, screenSize.width, screenSize.height / 2)];
SDAnimatedImageView *imageView2 = [[SDAnimatedImageView alloc] initWithFrame:CGRectMake(0, screenSize.height / 2, screenSize.width, screenSize.height / 2)];

[self.view addSubview:imageView1];
[self.view addSubview:imageView2];
Expand All @@ -43,10 +44,9 @@ - (void)viewDidLoad
});
}
}];
[imageView2 sd_setImageWithURL:HDRAVIFURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
// 10-bit HDR
[imageView2 sd_setImageWithURL:animatedAVIFSURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
NSLog(@"HDR AVIF load success");
NSLog(@"Animated AVIFS load success");
}
}];
// Do any additional setup after loading the view, typically from a nib.
Expand Down
12 changes: 6 additions & 6 deletions Example/SDWebImageAVIFCoder_Example macOS/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ - (void)viewDidLoad {

SDImageAVIFCoder *AVIFCoder = [SDImageAVIFCoder sharedCoder];
[[SDImageCodersManager sharedManager] addCoder:AVIFCoder];
NSURL *AVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/AOMediaCodec/av1-avif/master/testFiles/Microsoft/kids_720p.avif"];
NSURL *HDRAVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/AOMediaCodec/av1-avif/master/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008.avif"];
NSURL *AVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/fox.profile0.8bpc.yuv420.avif"];
// NSURL *HDRAVIFURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/hato.profile2.12bpc.yuv422.avif"];
NSURL *animatedAVIFSURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/link-u/avif-sample-images/master/star-12bpc-with-alpha.avifs"];

CGSize screenSize = self.view.bounds.size;

UIImageView *imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, screenSize.width / 2, screenSize.height)];
imageView1.imageScaling = NSImageScaleProportionallyUpOrDown;

UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(screenSize.width / 2, 0, screenSize.width / 2, screenSize.height)];
SDAnimatedImageView *imageView2 = [[SDAnimatedImageView alloc] initWithFrame:CGRectMake(screenSize.width / 2, 0, screenSize.width / 2, screenSize.height)];
imageView2.imageScaling = NSImageScaleProportionallyUpOrDown;

[self.view addSubview:imageView1];
Expand All @@ -42,10 +43,9 @@ - (void)viewDidLoad {
});
}
}];
[imageView2 sd_setImageWithURL:HDRAVIFURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
// 10-bit HDR
[imageView2 sd_setImageWithURL:animatedAVIFSURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
NSLog(@"HDR AVIF load success");
NSLog(@"Animated AVIFS load success");
}
}];
}
Expand Down
Binary file modified Example/Screenshot/AVIFDemo-iOS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Example/Screenshot/AVIFDemo-macOS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@

This is a [SDWebImage](https://github.com/rs/SDWebImage) coder plugin to add [AV1 Image File Format (AVIF)](https://aomediacodec.github.io/av1-avif/) support. Which is built based on the open-sourced [libavif](https://github.com/AOMediaCodec/libavif) codec.

This AVIF coder plugin currently support AVIF still image **decoding**. Including alpha channel, as well as 10bit/12bit/16bit HDR images.
This AVIF coder plugin support AVIF still image. Including alpha channel, as well as 10bit/12bit/16bit HDR images.

And, the new 0.9.0+ version add the support for AVIF sequence animated image! Including alpha channel, as well as 10bit/12bit/16bit HDR images.

The AVIF encoding is also supported now. Which always encode as 8-bit depth images.

See the demo for the more showcase.

## Note

AVIF image spec is still in evolve. And the current upstream AVIF codec is a simple implementation. The encoding time may be long for large images.
Expand Down
3 changes: 2 additions & 1 deletion SDWebImageAVIFCoder/Classes/Public/SDImageAVIFCoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

static const SDImageFormat SDImageFormatAVIF = 15; // AV1-codec based HEIF

@interface SDImageAVIFCoder : NSObject <SDImageCoder>
/// Supports AVIF static image and AVIFS animated image
@interface SDImageAVIFCoder : NSObject <SDAnimatedImageCoder>

@property (nonatomic, class, readonly, nonnull) SDImageAVIFCoder *sharedCoder;

Expand Down
214 changes: 189 additions & 25 deletions SDWebImageAVIFCoder/Classes/SDImageAVIFCoder.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#import "SDImageAVIFCoder.h"
#import <Accelerate/Accelerate.h>
#import <os/lock.h>
#import <libkern/OSAtomic.h>
#if __has_include(<libavif/avif.h>)
#import <libavif/avif.h>
#import <libavif/internal.h>
Expand All @@ -17,7 +19,62 @@

#import "Private/Conversion.h"

@implementation SDImageAVIFCoder
#define SD_USE_OS_UNFAIR_LOCK TARGET_OS_MACCATALYST ||\
(__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0) ||\
(__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_12) ||\
(__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0) ||\
(__WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_3_0)

#ifndef SD_LOCK_DECLARE
#if SD_USE_OS_UNFAIR_LOCK
#define SD_LOCK_DECLARE(lock) os_unfair_lock lock
#else
#define SD_LOCK_DECLARE(lock) os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \
OSSpinLock lock##_deprecated;
#endif
#endif

#ifndef SD_LOCK_INIT
#if SD_USE_OS_UNFAIR_LOCK
#define SD_LOCK_INIT(lock) lock = OS_UNFAIR_LOCK_INIT
#else
#define SD_LOCK_INIT(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) lock = OS_UNFAIR_LOCK_INIT; \
else lock##_deprecated = OS_SPINLOCK_INIT;
#endif
#endif

#ifndef SD_LOCK
#if SD_USE_OS_UNFAIR_LOCK
#define SD_LOCK(lock) os_unfair_lock_lock(&lock)
#else
#define SD_LOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_lock(&lock); \
else OSSpinLockLock(&lock##_deprecated);
#endif
#endif

#ifndef SD_UNLOCK
#if SD_USE_OS_UNFAIR_LOCK
#define SD_UNLOCK(lock) os_unfair_lock_unlock(&lock)
#else
#define SD_UNLOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_unlock(&lock); \
else OSSpinLockUnlock(&lock##_deprecated);
#endif
#endif

@implementation SDImageAVIFCoder {
avifDecoder *_decoder;
NSData *_imageData;
CGFloat _scale;
NSUInteger _loopCount;
NSUInteger _frameCount;
SD_LOCK_DECLARE(_lock);
}

- (void)dealloc {
if (_decoder) {
avifDecoderDestroy(_decoder);
}
}

+ (instancetype)sharedCoder {
static SDImageAVIFCoder *coder;
Expand All @@ -44,23 +101,6 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)
}
}

// Currently only support primary image :)
CGImageRef imageRef = [self sd_createAVIFImageWithData:data];
if (!imageRef) {
return nil;
}

#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
#endif
CGImageRelease(imageRef);

return image;
}

- (nullable CGImageRef)sd_createAVIFImageWithData:(nonnull NSData *)data CF_RETURNS_RETAINED {
// Decode it
avifDecoder * decoder = avifDecoderCreate();
avifDecoderSetIOMemory(decoder, data.bytes, data.length);
Expand All @@ -72,15 +112,54 @@ - (nullable CGImageRef)sd_createAVIFImageWithData:(nonnull NSData *)data CF_RETU
avifDecoderDestroy(decoder);
return nil;
}
avifResult nextImageResult = avifDecoderNextImage(decoder);
if (nextImageResult != AVIF_RESULT_OK || nextImageResult == AVIF_RESULT_NO_IMAGES_REMAINING) {
NSLog(@"Failed to decode image: %s", avifResultToString(nextImageResult));
avifDecoderDestroy(decoder);
return nil;

// Static image
if (decoder->imageCount <= 1) {
avifResult nextImageResult = avifDecoderNextImage(decoder);
if (nextImageResult != AVIF_RESULT_OK) {
NSLog(@"Failed to decode image: %s", avifResultToString(nextImageResult));
avifDecoderDestroy(decoder);
return nil;
}
CGImageRef imageRef = SDCreateCGImageFromAVIF(decoder->image);
if (!imageRef) {
return nil;
}
#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
#endif
CGImageRelease(imageRef);
return image;
}

// Animated image
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
while (avifDecoderNextImage(decoder) == AVIF_RESULT_OK) {
@autoreleasepool {
CGImageRef imageRef = SDCreateCGImageFromAVIF(decoder->image);
if (!imageRef) {
continue;
}
#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
#endif
NSTimeInterval duration = decoder->imageTiming.duration; // Should use `decoder->imageTiming`, not the `decoder->duration`, see libavif source code
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
}
}
CGImageRef const image = SDCreateCGImageFromAVIF(decoder->image);

avifDecoderDestroy(decoder);
return image;

UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
animatedImage.sd_imageLoopCount = 0;
animatedImage.sd_imageFormat = SDImageFormatAVIF;

return animatedImage;
}

// The AVIF encoding seems slow at the current time, but at least works
Expand Down Expand Up @@ -195,6 +274,91 @@ - (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image format:(SDIm
return imageData;
}

#pragma mark - Animation
- (instancetype)initWithAnimatedImageData:(NSData *)data options:(SDImageCoderOptions *)options {
self = [super init];
if (self) {
avifDecoder *decoder = avifDecoderCreate();
avifDecoderSetIOMemory(decoder, data.bytes, data.length);
// Disable strict mode to keep some AVIF image compatible
decoder->strictFlags = AVIF_STRICT_DISABLED;
avifResult decodeResult = avifDecoderParse(decoder);
if (decodeResult != AVIF_RESULT_OK) {
avifDecoderDestroy(decoder);
NSLog(@"Failed to decode image: %s", avifResultToString(decodeResult));
return nil;
}
// TODO: Optimize the performance like WebPCoder (frame meta cache, etc)
_frameCount = decoder->imageCount;
_loopCount = 0;
CGFloat scale = 1;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = [scaleFactor doubleValue];
if (scale < 1) {
scale = 1;
}
}
_scale = scale;
_decoder = decoder;
_imageData = data;
SD_LOCK_INIT(_lock);
}
return self;
}

- (NSData *)animatedImageData {
return _imageData;
}

- (NSUInteger)animatedImageLoopCount {
return _loopCount;
}

- (NSUInteger)animatedImageFrameCount {
return _frameCount;
}

- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
if (index >= _frameCount) {
return 0;
}
if (_frameCount <= 1) {
return 0;
}
SD_LOCK(_lock);
avifImageTiming timing;
avifResult decodeResult = avifDecoderNthImageTiming(_decoder, (uint32_t)index, &timing);
SD_UNLOCK(_lock);
if (decodeResult != AVIF_RESULT_OK) {
return 0;
}
return timing.duration;
}

- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= _frameCount) {
return nil;
}
SD_LOCK(_lock);
avifResult decodeResult = avifDecoderNthImage(_decoder, (uint32_t)index);
if (decodeResult != AVIF_RESULT_OK) {
return nil;
}
CGImageRef imageRef = SDCreateCGImageFromAVIF(_decoder->image);
SD_UNLOCK(_lock);
if (!imageRef) {
return nil;
}
#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp];
#endif
CGImageRelease(imageRef);
return image;
}


#pragma mark - Helper
+ (BOOL)isAVIFFormatForData:(NSData *)data
Expand Down

0 comments on commit 91ebf46

Please sign in to comment.