Skip to content

Feature support avifs animation #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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