Skip to content

Commit

Permalink
read and handle frame durations (delays) from GIF data
Browse files Browse the repository at this point in the history
Also add comments to the header file, clean up the implementation generally, and add another test image with variable frame durations.
  • Loading branch information
Rob Mayoff committed Feb 20, 2013
1 parent 94bf07c commit 1b2cb8f
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 81 deletions.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
## Summary

This project defines a category `animatedGIF` on `UIImage`. The category defines two methods. This method creates an animated `UIImage` using the frames of the GIF in `data`:

+[UIImage animatedImageWithAnimatedGIFData:(NSData *)data duration:(NSTimeInterval)duration]
+[UIImage animatedImageWithAnimatedGIFData:(NSData *)data]

This method creates an animated `UIImage` using the frames of the GIF loaded from `url`:

+[UIImage animatedImageWithAnimatedGIFURL:(NSURL *)url duration:(NSTimeInterval)duration]
+[UIImage animatedImageWithAnimatedGIFURL:(NSURL *)url]

Look at the comments in `UIImage+animatedGIF.h` for details.

You can build and run the project to see a trivial demo app.

## To use this category in your own project

1. Copy `UIImage+animatedGIF.h` and `UIImage+animatedGIF.m` to your project.
2. Add `UIImage+animatedGIF.m` to your target's “Compile Sources” build phase, if you didn't tell Xcode to do that when you performed step 1.
3. Add `ImageIO.framework` to your target's "Link Binary With Libraries" build phase.

## Implementation notes

The implementation of this category uses the [Image I/O Framework](http://developer.apple.com/library/ios/#documentation/GraphicsImaging/Conceptual/ImageIOGuide/imageio_intro/ikpg_intro.html) to extract the images and durations from the GIF data.

**To use this category in your own project,** copy `UIImage+animatedGIF.h` and `UIImage+animatedGIF.m` to your project, and add `ImageIO.framework` to the "Link Binary With Libraries" build phase of your target.
Diego Peinador provided the inspiration for handling variable-frame-rate animations, although I didn't end up using his code.

The implementation of this category is quite simple. It uses the [Image I/O Framework](http://developer.apple.com/library/ios/#documentation/GraphicsImaging/Conceptual/ImageIOGuide/imageio_intro/ikpg_intro.html) to do all of the real work.
## Copyright or lack thereof

The contents of this repository are dedicated to the public domain, in accordance with the [CC0 1.0 Universal Public Domain Dedication](http://creativecommons.org/publicdomain/zero/1.0/), which is reproduced in the file `COPYRIGHT`.

Expand Down
37 changes: 30 additions & 7 deletions uiimage-from-animated-gif.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
319268E116D44B4200B913B6 /* variableDuration.gif in Resources */ = {isa = PBXBuildFile; fileRef = 319268E016D44B4200B913B6 /* variableDuration.gif */; };
31C640E014D3AF2B007FB1F0 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31C640DF14D3AF2B007FB1F0 /* UIKit.framework */; };
31C640E214D3AF2B007FB1F0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31C640E114D3AF2B007FB1F0 /* Foundation.framework */; };
31C640E414D3AF2B007FB1F0 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31C640E314D3AF2B007FB1F0 /* CoreGraphics.framework */; };
Expand All @@ -22,6 +23,7 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
319268E016D44B4200B913B6 /* variableDuration.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = variableDuration.gif; sourceTree = "<group>"; };
31C640DB14D3AF2B007FB1F0 /* uiimage-from-animated-gif.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "uiimage-from-animated-gif.app"; sourceTree = BUILT_PRODUCTS_DIR; };
31C640DF14D3AF2B007FB1F0 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
31C640E114D3AF2B007FB1F0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
Expand All @@ -35,8 +37,8 @@
31C640F114D3AF2B007FB1F0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
31C640F214D3AF2B007FB1F0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
31C640F514D3AF2B007FB1F0 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ViewController.xib; sourceTree = "<group>"; };
31C640FC14D3AFB1007FB1F0 /* UIImage+animatedGIF.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+animatedGIF.h"; sourceTree = "<group>"; };
31C640FD14D3AFB1007FB1F0 /* UIImage+animatedGIF.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+animatedGIF.m"; sourceTree = "<group>"; };
31C640FC14D3AFB1007FB1F0 /* UIImage+animatedGIF.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+animatedGIF.h"; path = "uiimage-from-animated-gif/UIImage+animatedGIF.h"; sourceTree = "<group>"; };
31C640FD14D3AFB1007FB1F0 /* UIImage+animatedGIF.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+animatedGIF.m"; path = "uiimage-from-animated-gif/UIImage+animatedGIF.m"; sourceTree = "<group>"; };
31C6410314D3B277007FB1F0 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
31C6410614D3B731007FB1F0 /* test.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test.gif; sourceTree = "<group>"; };
31C6410814D3B790007FB1F0 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
Expand All @@ -58,12 +60,22 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
319268E216D44C8200B913B6 /* COPY THESE FILES TO YOUR PROJECT */ = {
isa = PBXGroup;
children = (
31C640FC14D3AFB1007FB1F0 /* UIImage+animatedGIF.h */,
31C640FD14D3AFB1007FB1F0 /* UIImage+animatedGIF.m */,
);
name = "COPY THESE FILES TO YOUR PROJECT";
sourceTree = "<group>";
};
31C640D014D3AF2B007FB1F0 = {
isa = PBXGroup;
children = (
31C6410814D3B790007FB1F0 /* README.md */,
31C6410A14D3B9D1007FB1F0 /* COPYRIGHT */,
31C640E514D3AF2B007FB1F0 /* uiimage-from-animated-gif */,
319268E216D44C8200B913B6 /* COPY THESE FILES TO YOUR PROJECT */,
31C640E514D3AF2B007FB1F0 /* test app */,
31C640DE14D3AF2B007FB1F0 /* Frameworks */,
31C640DC14D3AF2B007FB1F0 /* Products */,
);
Expand All @@ -88,19 +100,19 @@
name = Frameworks;
sourceTree = "<group>";
};
31C640E514D3AF2B007FB1F0 /* uiimage-from-animated-gif */ = {
31C640E514D3AF2B007FB1F0 /* test app */ = {
isa = PBXGroup;
children = (
31C6410614D3B731007FB1F0 /* test.gif */,
319268E016D44B4200B913B6 /* variableDuration.gif */,
31C640EE14D3AF2B007FB1F0 /* AppDelegate.h */,
31C640EF14D3AF2B007FB1F0 /* AppDelegate.m */,
31C640F114D3AF2B007FB1F0 /* ViewController.h */,
31C640F214D3AF2B007FB1F0 /* ViewController.m */,
31C640F414D3AF2B007FB1F0 /* ViewController.xib */,
31C640E614D3AF2B007FB1F0 /* Supporting Files */,
31C640FC14D3AFB1007FB1F0 /* UIImage+animatedGIF.h */,
31C640FD14D3AFB1007FB1F0 /* UIImage+animatedGIF.m */,
);
name = "test app";
path = "uiimage-from-animated-gif";
sourceTree = "<group>";
};
Expand Down Expand Up @@ -141,7 +153,7 @@
31C640D214D3AF2B007FB1F0 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0420;
LastUpgradeCheck = 0460;
};
buildConfigurationList = 31C640D514D3AF2B007FB1F0 /* Build configuration list for PBXProject "uiimage-from-animated-gif" */;
compatibilityVersion = "Xcode 3.2";
Expand Down Expand Up @@ -169,6 +181,7 @@
31C640F614D3AF2B007FB1F0 /* ViewController.xib in Resources */,
31C6410714D3B731007FB1F0 /* test.gif in Resources */,
31C6410914D3B790007FB1F0 /* README.md in Resources */,
319268E116D44B4200B913B6 /* variableDuration.gif in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -214,6 +227,10 @@
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD_32_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
Expand All @@ -227,6 +244,7 @@
GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
SDKROOT = iphoneos;
Expand All @@ -239,12 +257,17 @@
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD_32_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 5.0;
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
Expand Down
28 changes: 26 additions & 2 deletions uiimage-from-animated-gif/UIImage+animatedGIF.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
#import <UIKit/UIKit.h>

/**
UIImage (animatedGIF)
This category adds class methods to `UIImage` to create an animated `UIImage` from an animated GIF.
*/
@interface UIImage (animatedGIF)

+ (UIImage *)animatedImageWithAnimatedGIFData:(NSData *)data;
+ (UIImage *)animatedImageWithAnimatedGIFURL:(NSURL *)url;
/*
UIImage *animation = [UIImage animatedImageWithAnimatedGIFData:theData];
I interpret `theData` as a GIF. I create an animated `UIImage` using the source images in the GIF.
The GIF stores a separate duration for each frame, in units of centiseconds (hundredths of a second). However, a `UIImage` only has a single, total `duration` property, which is a floating-point number.
To handle this mismatch, I add each source image (from the GIF) to `animation` a varying number of times to match the ratios between the frame durations in the GIF.
For example, suppose the GIF contains three frames. Frame 0 has duration 3. Frame 1 has duration 9. Frame 2 has duration 15. I divide each duration by the greatest common denominator of all the durations, which is 3, and add each frame the resulting number of times. Thus `animation` will contain frame 0 3/3 = 1 time, then frame 1 9/3 = 3 times, then frame 2 15/3 = 5 times. I set `animation.duration` to (3+9+15)/100 = 0.27 seconds.
*/
+ (UIImage *)animatedImageWithAnimatedGIFData:(NSData *)theData;

/*
UIImage *image = [UIImage animatedImageWithAnimatedGIFURL:theURL];
I interpret the contents of `theURL` as a GIF. I create an animated `UIImage` using the source images in the GIF.
I operate exactly like `+[UIImage animatedImageWithAnimatedGIFData:]`, except that I read the data from `theURL`. If `theURL` is not a `file:` URL, you probably want to call me on a background thread or GCD queue to avoid blocking the main thread.
*/
+ (UIImage *)animatedImageWithAnimatedGIFURL:(NSURL *)theURL;

@end
126 changes: 81 additions & 45 deletions uiimage-from-animated-gif/UIImage+animatedGIF.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,104 @@

#if __has_feature(objc_arc)
#define toCF (__bridge CFTypeRef)
#define toNSString (__bridge NSString*)
#define toNSDictionary (__bridge NSDictionary *)
#define fromCF (__bridge id)
#else
#define toCF (CFTypeRef)
#define toNSString (NSString*)
#define toNSDictionary (NSDictionary *)
#define fromCF (id)
#endif

@implementation UIImage (animatedGIF)

static UIImage *animatedImageWithAnimatedGIFImageSource(CGImageSourceRef source) {
if (!source)
return nil;

size_t count = CGImageSourceGetCount(source);
NSMutableArray *images = [NSMutableArray arrayWithCapacity:count];
NSMutableArray *delays = [NSMutableArray arrayWithCapacity:count];
NSNumber *delay;
NSDictionary *dict, *gifDict;
double minDelay = INT_MAX, totalTime = 0;
int times;
// Properties
for (size_t i = 0; i < count; ++i) {
dict = toNSDictionary CGImageSourceCopyPropertiesAtIndex(source, i, NULL);

gifDict = [dict objectForKey:toNSString kCGImagePropertyGIFDictionary];
delay = [gifDict objectForKey:toNSString kCGImagePropertyGIFDelayTime];

// store delay for each frame
[delays addObject:delay];
if ([delay doubleValue]>0 && [delay doubleValue]<minDelay) {
minDelay = [delay doubleValue];
static int delayCentisecondsForImageAtIndex(CGImageSourceRef const source, size_t const i) {
int delayCentiseconds = 1;
CFDictionaryRef const properties = CGImageSourceCopyPropertiesAtIndex(source, i, NULL);
if (properties) {
CFDictionaryRef const gifProperties = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
CFRelease(properties);
if (gifProperties) {
CFNumberRef const number = CFDictionaryGetValue(gifProperties, kCGImagePropertyGIFDelayTime);
// Even though the GIF stores the delay as an integer number of centiseconds, ImageIO “helpfully” converts that to seconds for us.
delayCentiseconds = (int)lrint([fromCF number doubleValue] * 100);
}
}
if ([delays count]!=count) {
return nil;
return delayCentiseconds;
}

static void createImagesAndDelays(CGImageSourceRef source, size_t count, CGImageRef imagesOut[count], int delayCentisecondsOut[count]) {
for (size_t i = 0; i < count; ++i) {
imagesOut[i] = CGImageSourceCreateImageAtIndex(source, i, NULL);
delayCentisecondsOut[i] = delayCentisecondsForImageAtIndex(source, i);
}
// setup frame images
}

static int sum(size_t const count, int const *const values) {
int theSum = 0;
for (size_t i = 0; i < count; ++i) {
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!cgImage)
return nil;

delay = [delays objectAtIndex:i];
times = (int)round([delay doubleValue]/minDelay);
times = times>0?times:1;
theSum += values[i];
}
return theSum;
}

// a long delayed frame is represented by several similar images
for (int j=0; j<times; j++) {
[images addObject:[UIImage imageWithCGImage:cgImage]];
totalTime += minDelay;
static int pairGCD(int a, int b) {
if (a < b)
return pairGCD(b, a);
while (true) {
int const r = a % b;
if (r == 0)
return b;
a = b;
b = r;
}
}

static int vectorGCD(size_t const count, int const *const values) {
int gcd = values[0];
for (size_t i = 1; i < count; ++i) {
// Note that after I process the first few elements of the vector, `gcd` will probably be smaller than any remaining element. By passing the smaller value as the second argument to `pairGCD`, I avoid making it swap the arguments.
gcd = pairGCD(values[i], gcd);
}
return gcd;
}

static NSArray *frameArray(size_t const count, CGImageRef const images[count], int const delayCentiseconds[count], int const totalDurationCentiseconds) {
int const gcd = vectorGCD(count, delayCentiseconds);
size_t const frameCount = totalDurationCentiseconds / gcd;
UIImage *frames[frameCount];
for (size_t i = 0, f = 0; i < count; ++i) {
UIImage *const frame = [UIImage imageWithCGImage:images[i]];
for (size_t j = delayCentiseconds[i] / gcd; j > 0; --j) {
frames[f++] = frame;
}
CGImageRelease(cgImage);
}
return [NSArray arrayWithObjects:frames count:frameCount];
}

return [UIImage animatedImageWithImages:images duration:totalTime];
static void releaseImages(size_t const count, CGImageRef const images[count]) {
for (size_t i = 0; i < count; ++i) {
CGImageRelease(images[i]);
}
}

static UIImage *animatedImageWithAnimatedGIFImageSource(CGImageSourceRef const source) {
size_t const count = CGImageSourceGetCount(source);
CGImageRef images[count];
int delayCentiseconds[count]; // in centiseconds
createImagesAndDelays(source, count, images, delayCentiseconds);
int const totalDurationCentiseconds = sum(count, delayCentiseconds);
NSArray *const frames = frameArray(count, images, delayCentiseconds, totalDurationCentiseconds);
UIImage *const animation = [UIImage animatedImageWithImages:frames duration:(NSTimeInterval)totalDurationCentiseconds / 100.0];
releaseImages(count, images);
return animation;
}

static UIImage *animatedImageWithAnimatedGIFReleasingImageSource(CGImageSourceRef source) {
UIImage *image = animatedImageWithAnimatedGIFImageSource(source);
CFRelease(source);
return image;
if (source) {
UIImage *const image = animatedImageWithAnimatedGIFImageSource(source);
CFRelease(source);
return image;
} else {
return nil;
}
}

+ (UIImage *)animatedImageWithAnimatedGIFData:(NSData *)data {
Expand Down
5 changes: 3 additions & 2 deletions uiimage-from-animated-gif/ViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIImageView *dataImageView;
@property (weak, nonatomic) IBOutlet UIImageView *urlImageView;
@property (strong, nonatomic) IBOutlet UIImageView *dataImageView;
@property (strong, nonatomic) IBOutlet UIImageView *urlImageView;
@property (strong, nonatomic) IBOutlet UIImageView *variableDurationImageView;

@end
21 changes: 9 additions & 12 deletions uiimage-from-animated-gif/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ @implementation ViewController
@synthesize dataImageView;
@synthesize urlImageView;

- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];

NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"gif"];
self.dataImageView.image = [UIImage animatedImageWithAnimatedGIFData:[NSData dataWithContentsOfURL:url] duration:1];
self.urlImageView.image = [UIImage animatedImageWithAnimatedGIFURL:url duration:2];
}
self.dataImageView.image = [UIImage animatedImageWithAnimatedGIFData:[NSData dataWithContentsOfURL:url]];
self.urlImageView.image = [UIImage animatedImageWithAnimatedGIFURL:url];

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
url = [[NSBundle mainBundle] URLForResource:@"variableDuration" withExtension:@"gif"];
self.variableDurationImageView.image = [UIImage animatedImageWithAnimatedGIFURL:url];
}

- (void)viewDidUnload {
[self setDataImageView:nil];
[self setUrlImageView:nil];
[super viewDidUnload];
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return interfaceOrientation == UIInterfaceOrientationPortrait;
}

@end
Loading

0 comments on commit 1b2cb8f

Please sign in to comment.