保存GIF动画时iOS颜色不正确

时间:2017-07-07 00:23:59

标签: ios swift uiimage core-graphics animated-gif

我有这个非常奇怪的问题。我正在用UIImages创建GIF动画,大多数时候它们都是正确的。然而,当我开始进入更大尺寸的图像时,我的颜色开始消失。例如,如果我做一个4帧32 x 32像素图像,不超过10种颜色没有问题。如果我将相同的图像缩放到832 x 832,我会失去粉红色,而我的棕色会变成绿色。

@ 1x 32 x 32

enter image description here

@ 10x 320 x 320

enter image description here

@ 26x 832 x 832

enter image description here

这是我用来创建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")
    }        
}

在我致电CGImageDestinationAddImage(destination, image.cgImage!, frameProperties as CFDictionary)之前,我已经提出了一个断点,并且正确的颜色图像非常精细。我希望那里有人知道我错过了什么。

更新

这是一个示例项目。请注意,虽然它在预览中没有动画,但它保存了动画gif,我在控制台中注销了图像的位置。

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

2 个答案:

答案 0 :(得分:7)

似乎关闭全局色彩图可以解决问题:

let loopingProperty: [String: AnyObject] = [
    kCGImagePropertyGIFLoopCount as String: 0 as NSNumber,
    kCGImagePropertyGIFHasGlobalColorMap as String: false as NSNumber
]

请注意,与PNG不同,GIF只能使用256色地图,没有透明度。对于动画GIF,可以是全局或每帧彩色图。

不幸的是,Core Graphics不允许我们直接使用色彩映射,因此在编码GIF时会有一些自动颜色转换。

似乎关闭全局色彩图就是所需要的。此外,使用kCGImagePropertyGIFImageColorMap为每个帧明确设置颜色映射也可能有效。

由于这似乎不能可靠地工作,让我们为每一帧创建我们自己的颜色图:

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))
    }
}

现在让我们更新我们的方法:

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))

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);
    }

当然,这只有在颜色数小于256时才有效。我真的推荐一个可以正确处理颜色转换的自定义GIF库。

答案 1 :(得分:2)

接下来,这里有关于正在发生的量化失败的更多背景知识。如果您通过imagemagick运行GIF输出以提取具有全局颜色映射与每帧颜色映射的版本的调色板,则可以深入了解问题的根源:

带有GLOBAL颜色贴图的版本: $ 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)

每帧彩色地图的版本: $ 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

所以第一个缺少棕色和粉红色,红色通道中的246113的颜色根本没有列出,这些在直方图中正确列出(可能是重复的每帧彩色地图版本中较长输出的每一帧。

这证明调色板在GIF中生成不正确,这是我们用眼睛很容易看到的。然而,令我惊讶的是,全局颜色映射版本具有多种颜色的重复条目。这指出了ImageIO中调色板量化的一个非常明显的错误。在有限的调色板中应该没有重复的条目。

简而言之:不要依赖Core Graphics来量化24位RGB图像。在将它们发送到ImageIO并关闭全局颜色映射之前,请事先对它们进行预量化。如果问题仍然存在,则ImageIO调色板写入被破坏,您应该使用不同的GIF输出库