Skip to content

Instantly share code, notes, and snippets.

@keefo
Last active November 1, 2019 22:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keefo/5344890 to your computer and use it in GitHub Desktop.
Save keefo/5344890 to your computer and use it in GitHub Desktop.
@interface NSImage (GIF)
- (BOOL)isGifImage;
- (BOOL)saveAnimatedGIFToFile:(NSString*)filepath;
@end
@implementation NSImage (GIF)
- (BOOL)isGifImage
{
@try {
NSArray * reps = [self representations];
for (NSImageRep * rep in reps)
{
if ([rep isKindOfClass:[NSBitmapImageRep class]] == YES)
{
NSBitmapImageRep * bitmapRep = (NSBitmapImageRep *)rep;
int numFrame = [[bitmapRep valueForProperty:NSImageFrameCount] intValue];
return numFrame > 1;
}
}
}@catch (NSException * e) {
}
@finally {
}
return NO;
}
- (BOOL)saveAnimatedGIFToFile:(NSString*)filepath
{
//(NSBitmapImageRep *firstFrame, NSBitmapImageRep *secondFrame,
// NSString *animatedGIFPath)
// options to set the delay on each frame of 2 seconds
//
// options to turn on looping for the GIF
@try {
NSArray * reps = [self representations];
for (NSImageRep * rep in reps)
{
if ([rep isKindOfClass:[NSBitmapImageRep class]] == YES)
{
NSBitmapImageRep * bitmapRep = (NSBitmapImageRep *)rep;
int numFrame = [[bitmapRep valueForProperty:NSImageFrameCount] intValue];
if (numFrame == 0){
NSData *data = [bitmapRep representationUsingType:NSGIFFileType properties: nil];
return [data writeToFile:filepath atomically:NO];
}
// set the place to save the GIF to
#if __has_feature(objc_arc)
CGImageDestinationRef animatedGIF = CGImageDestinationCreateWithURL((__bridge CFURLRef) [NSURL fileURLWithPath:filepath],
kUTTypeGIF,
numFrame,
NULL
);
#else
CGImageDestinationRef animatedGIF = CGImageDestinationCreateWithURL((CFURLRef) [NSURL fileURLWithPath:filepath],
kUTTypeGIF,
numFrame,
NULL
);
#endif
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast; //kCGImageAlphaNoneSkipFirst
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
int bitsPerComponent = 8;
for (int i = 0; i < numFrame; ++i)
{
[bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)];
CGDataProviderRef frameProvider = CGDataProviderCreateWithData(NULL,
[bitmapRep bitmapData],
[bitmapRep bytesPerRow] * [bitmapRep pixelsHigh],
NULL
);
CGImageRef cgFrame = CGImageCreate ([bitmapRep pixelsWide],
[bitmapRep pixelsHigh],
bitsPerComponent,
[bitmapRep bitsPerPixel],
[bitmapRep bytesPerRow],
colorSpaceRef,
bitmapInfo,
frameProvider,
NULL,
NO,
kCGRenderingIntentDefault
);
if (cgFrame) {
float duration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
/*
NSDictionary *frameProperties = [NSDictionary dictionaryWithObject:
[NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:duration]
forKey:(NSString *)kCGImagePropertyGIFDelayTime]
forKey:(NSString *)kCGImagePropertyGIFDictionary];
*/
NSDictionary *frameProperties = @{ (NSString *)kCGImagePropertyGIFDictionary: @{ (NSString *)kCGImagePropertyGIFDelayTime : @(duration) } };
#if __has_feature(objc_arc)
CGImageDestinationAddImage(animatedGIF, cgFrame, (__bridge CFDictionaryRef)frameProperties);
#else
CGImageDestinationAddImage(animatedGIF, cgFrame, (CFDictionaryRef)frameProperties);
#endif
CGImageRelease(cgFrame);
}
CGDataProviderRelease(frameProvider);
}
CGColorSpaceRelease(colorSpaceRef);
/*
NSDictionary *gifProperties = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:0]
forKey:(NSString *) kCGImagePropertyGIFLoopCount]
forKey:(NSString *) kCGImagePropertyGIFDictionary];
*/
NSDictionary *gifProperties = @{ (NSString *)kCGImagePropertyGIFDictionary: @{ (NSString *)kCGImagePropertyGIFLoopCount : @0 } };
#if __has_feature(objc_arc)
CGImageDestinationSetProperties(animatedGIF, (__bridge CFDictionaryRef) gifProperties);
#else
CGImageDestinationSetProperties(animatedGIF, (CFDictionaryRef) gifProperties);
#endif
CGImageDestinationFinalize(animatedGIF);
CFRelease(animatedGIF);
}
}
return YES;
}
@catch (NSException * e) {
NSLog(@"save gif e=%@",e);
return NO;
}
@finally {
}
}
@end
@ericdke
Copy link

ericdke commented Apr 18, 2018

Thank you for this! It's perfect for my project.

@helje5
Copy link

helje5 commented Apr 27, 2019

Naive, untested Swift (5, probably 4.2 OK) port:

import CoreGraphics

//CGDataProviderReleaseDataCallback
fileprivate func NoReleaseFunc(info: UnsafeMutableRawPointer?,
                              data: UnsafeRawPointer, count: Int)
{}


public extension NSImage {
  
  var zzBitmapRepresentation : NSBitmapImageRep? {
    return representations.first(where: {$0 is NSBitmapImageRep })
           as? NSBitmapImageRep
  }
  
  /// Returns 0 if missing, hm.
  var zzBitmapImageFrameCount : Int {
    guard let bm = zzBitmapRepresentation else { return 0 }
    guard let frameCount = bm.value(forProperty: .frameCount) else { return 0 }
    guard let count = frameCount as? Int else {
      assertionFailure("cannot grab framecount as Int?")
      return 0
    }
    return count
  }
  
  func zzMakeGIF() -> Data? {
    guard let bitmap = zzBitmapRepresentation else { return nil }

    let frameCount = zzBitmapImageFrameCount
    if frameCount < 2 {
      // This does not work with multiframe
      return bitmap.representation(using: .gif, properties: [:])
    }

    // https://gist.github.com/keefo/5344890
    let md   = NSMutableData()
    guard let dest = CGImageDestinationCreateWithData(md, kUTTypeGIF,
                                                      frameCount, nil) else {
      assertionFailure("could not create image dest for gif \(self) ...")
      return nil
    }
    let bitmapInfo : CGBitmapInfo = []
          // [ .byteOrderDefault, .alphaInfoMask ]
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitsPerComponent = 8
  
    for frame in 0..<frameCount {
      bitmap.setProperty(.currentFrame, withValue: frame)
      
      guard let frameProvider = CGDataProvider(
        dataInfo: nil, data: bitmap.bitmapData!,
        size: bitmap.bytesPerRow * bitmap.pixelsHigh,
        releaseData: NoReleaseFunc
      ) else {
        assertionFailure("got not data provider \(self) ...")
        return nil // TBD
      }

      guard let cgFrame = CGImage(width             : bitmap.pixelsWide,
                                  height            : bitmap.pixelsHigh,
                                  bitsPerComponent  : bitsPerComponent,
                                  bitsPerPixel      : bitmap.bitsPerPixel,
                                  bytesPerRow       : bitmap.bytesPerRow,
                                  space             : colorSpace,
                                  bitmapInfo        : bitmapInfo,
                                  provider          : frameProvider,
                                  decode            : nil,
                                  shouldInterpolate : false,
                                  intent            : .defaultIntent) else {
        assertionFailure("got not image for frame provider \(self) ...")
        return nil // TBD
      }
      
      let duration = bitmap.value(forProperty: .currentFrameDuration)
        // .gifDelayTime, .gifDictionary
      
      let frameProperties : [ String : Any ] = [
        kCGImagePropertyGIFDictionary as String: [
          kCGImagePropertyGIFDelayTime as String: duration
        ]
      ]
      
      CGImageDestinationAddImage(dest, cgFrame, frameProperties as CFDictionary)
    }
    
    let gifProperties : [ String : Any ] = [
      kCGImagePropertyGIFDictionary as String: [
        kCGImagePropertyGIFLoopCount as String: 0 // TBD
      ]
    ]
    CGImageDestinationSetProperties(dest, gifProperties as CFDictionary)
    CGImageDestinationFinalize(dest)
    
    return md as Data
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment