Last active
November 1, 2019 22:57
-
-
Save keefo/5344890 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
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
Thank you for this! It's perfect for my project.