Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS Colors Incorrect When Saving Animated Gif

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

enter image description here

@10x 320 x 320

enter image description here

@26x 832 x 832

enter image description here

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.

https://www.dropbox.com/s/pb52awaj8w3amyz/gifTest.zip?dl=0

like image 931
Skyler Lauren Avatar asked Jul 07 '17 00:07

Skyler Lauren


People also ask

How do you color correct a GIF?

Click the "Recolor" button on the left side of the "Picture Tools" ribbon. Without clicking, hover the cursor over all of the recoloring options to see them reflected in the GIF. Click an actual option to commit the color change.

Why do GIFs have limited colors?

GIFs are suitable for sharp-edged line art with a limited number of colors, such as logos. This takes advantage of the format's lossless compression, which favors flat areas of uniform color with well defined edges.


2 Answers

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.

like image 102
Sulthan Avatar answered Sep 29 '22 16:09

Sulthan


Following on, here's some more background on the quantisation fail that is occurring. If you run the GIF output through imagemagick to extract the colour palettes for the version with a global colour map vs. a per-frame colour map, there is some insight into the root of the problem:

The version with GLOBAL colour map: $ convert test.gif -format %c -depth 8 histogram:info:- 28392: ( 0, 0, 0,255) #000000FF black 240656: ( 71,162, 58,255) #47A23AFF srgba(71,162,58,1) 422500: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 676: (255,255,255,255) #FFFFFFFF white 2704: ( 71,162, 58,255) #47A23AFF srgba(71,162,58,1) 676: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 2704: ( 71,162, 58,255) #47A23AFF srgba(71,162,58,1) 676: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 2704: ( 71,162, 58,255) #47A23AFF srgba(71,162,58,1) 676: (147,221,253,255) #93DDFDFF srgba(147,221,253,1)

The version with per-frame colour maps: $ convert test.gif -format %c -depth 8 histogram:info:- 28392: ( 0, 0, 0,255) #000000FF black 237952: ( 71,163, 59,255) #47A33BFF srgba(71,163,59,1) 2704: (113, 78, 0,255) #714E00FF srgba(113,78,0,1) 421824: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 676: (246, 81,249,255) #F651F9FF srgba(246,81,249,1) 676: (255,255,255,255) #FFFFFFFF white 28392: ( 0, 0, 0,255) #000000FF black 237952: ( 71,163, 59,255) #47A33BFF srgba(71,163,59,1) 2704: (113, 78, 0,255) #714E00FF srgba(113,78,0,1) 421824: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 676: (246, 81,249,255) #F651F9FF srgba(246,81,249,1) 676: (255,255,255,255) #FFFFFFFF white 28392: ( 0, 0, 0,255) #000000FF black 237952: ( 71,163, 59,255) #47A33BFF srgba(71,163,59,1) 2704: (113, 78, 0,255) #714E00FF srgba(113,78,0,1) 421824: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 676: (246, 81,249,255) #F651F9FF srgba(246,81,249,1) 676: (255,255,255,255) #FFFFFFFF white 28392: ( 0, 0, 0,255) #000000FF black 237952: ( 71,163, 59,255) #47A33BFF srgba(71,163,59,1) 2704: (113, 78, 0,255) #714E00FF srgba(113,78,0,1) 421824: (147,221,253,255) #93DDFDFF srgba(147,221,253,1) 676: (246, 81,249,255) #F651F9FF srgba(246,81,249,1) 676: (255,255,255,255) #FFFFFFFF white

So the first one is missing the brown and pink, the colours with 246 and 113 in the red channel are not listed at all, and these are listed correctly in the histogram (presumably repeated for every frame in the longer output) for the per-frame colour map version.

This is proof that the palette is generated incorrectly in the GIF, which is what we see easily with our eyes. However, what makes me wonder is that the global colour map version has duplicate entries for several colours. This points at a pretty clear bug in palette quantisation in ImageIO. There should be no duplicate entries in a limited colour palette.

In short: do not rely on Core Graphics to quantise your 24-bit RGB images. Pre-quantise them in advance before sending them to ImageIO and turn off global colour maps. If the problem still manifests then, ImageIO palette writing is broken and you should use a different GIF output library

like image 25
Marc Palmer Avatar answered Sep 29 '22 17:09

Marc Palmer