I am having this very strange issue. I am creating animated gifs from UIImages and most of the time they come out correct. However when I start to get into larger size images my colors start to disappear. For example if I do a 4 frame 32 x 32 pixel image with no more than 10 colors no issue. If I scale the same image up to 832 x 832 I lose a pink color and my brown turns green.
@1x 32 x 32
@10x 320 x 320
@26x 832 x 832
Here is the code I use to create the gif...
var kFrameCount = 0
for smdLayer in drawingToUse!.layers{
if !smdLayer.hidden {
kFrameCount += 1
}
}
let loopingProperty = [String(kCGImagePropertyGIFLoopCount): 0]
let fileProperties: [String: AnyObject] = [String(kCGImagePropertyGIFDictionary): loopingProperty as AnyObject];
let frameProperty = [String(kCGImagePropertyGIFDelayTime): Float(speedLabel.text!)!]
let frameProperties: [String: AnyObject] = [String(kCGImagePropertyGIFDictionary): frameProperty as AnyObject];
let documentsDirectoryPath = "file://\(NSTemporaryDirectory())"
if let documentsDirectoryURL = URL(string: documentsDirectoryPath){
let fileURL = documentsDirectoryURL.appendingPathComponent("\(drawing.name)\(getScaleString()).gif")
let destination = CGImageDestinationCreateWithURL(fileURL as CFURL, kUTTypeGIF, kFrameCount, nil)!
CGImageDestinationSetProperties(destination, fileProperties as CFDictionary);
for smdLayer in drawingToUse!.layers{
if !smdLayer.hidden{
let image = UIImage(smdLayer: smdLayer, alphaBlend: useAlphaLayers, backgroundColor: backgroundColorButton.backgroundColor!, scale: scale)
CGImageDestinationAddImage(destination, image.cgImage!, frameProperties as CFDictionary)
}
}
if (!CGImageDestinationFinalize(destination)) {
print("failed to finalize image destination")
}
}
I have put in a break point right before I call CGImageDestinationAddImage(destination, image.cgImage!, frameProperties as CFDictionary)
and the image is perfectly fine with the correct colors. I hope someone out there knows what I am missing.
Update
Here is a sample project. Note that although it isn't animated in the preview it is saving an animated gif and I log out the location of the image in the console.
It seems that turning off the global color map fixes the problem:
let loopingProperty: [String: AnyObject] = [
kCGImagePropertyGIFLoopCount as String: 0 as NSNumber,
kCGImagePropertyGIFHasGlobalColorMap as String: false as NSNumber
]
Note that unlike PNGs, GIFs can use only a 256 color map, without transparency. For animated GIFs there can be either a global or a per-frame color map.
Unfortunately, Core Graphics does not allow us to work with color maps directly, therefore there is some automatic color conversion when the GIF is encoded.
It seems that turning off the global color map is all what is needed. Also setting up color map explicitly for every frame using kCGImagePropertyGIFImageColorMap
would probably work too.
Since this seems not to work reliably, let's create our own color map for every frame:
struct Color : Hashable {
let red: UInt8
let green: UInt8
let blue: UInt8
var hashValue: Int {
return Int(red) + Int(green) + Int(blue)
}
public static func ==(lhs: Color, rhs: Color) -> Bool {
return [lhs.red, lhs.green, lhs.blue] == [rhs.red, rhs.green, rhs.blue]
}
}
struct ColorMap {
var colors = Set<Color>()
var exported: Data {
let data = Array(colors)
.map { [$0.red, $0.green, $0.blue] }
.joined()
return Data(bytes: Array(data))
}
}
Now let's update our methods:
func getScaledImages(_ scale: Int) -> [(CGImage, ColorMap)] {
var sourceImages = [UIImage]()
var result: [(CGImage, ColorMap)] = []
...
var colorMap = ColorMap()
let pixelData = imageRef.dataProvider!.data
let rawData: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
for y in 0 ..< imageRef.height{
for _ in 0 ..< scale {
for x in 0 ..< imageRef.width{
let offset = y * imageRef.width * 4 + x * 4
let color = Color(red: rawData[offset], green: rawData[offset + 1], blue: rawData[offset + 2])
colorMap.colors.insert(color)
for _ in 0 ..< scale {
pixelPointer[byteIndex] = rawData[offset]
pixelPointer[byteIndex+1] = rawData[offset+1]
pixelPointer[byteIndex+2] = rawData[offset+2]
pixelPointer[byteIndex+3] = rawData[offset+3]
byteIndex += 4
}
}
}
}
let cgImage = context.makeImage()!
result.append((cgImage, colorMap))
and
func createAnimatedGifFromImages(_ images: [(CGImage, ColorMap)]) -> URL {
...
for (image, colorMap) in images {
let frameProperties: [String: AnyObject] = [
String(kCGImagePropertyGIFDelayTime): 0.2 as NSNumber,
String(kCGImagePropertyGIFImageColorMap): colorMap.exported as NSData
]
let properties: [String: AnyObject] = [
String(kCGImagePropertyGIFDictionary): frameProperties as AnyObject
];
CGImageDestinationAddImage(destination, image, properties as CFDictionary);
}
Of course, this will work only if the number of colors is less than 256. I would really recommend a custom GIF library that can handle the color conversion correctly.