iOS 保存动画 Gif 时颜色不正确
iOS Colors Incorrect When Saving Animated Gif
我遇到了这个非常奇怪的问题。我正在从 UIImages 创建动画 gif,大多数时候它们都是正确的。然而,当我开始使用更大尺寸的图像时,我的颜色开始消失。例如,如果我使用不超过 10 种颜色的 4 帧 32 x 32 像素图像就没问题。如果我将同一图像缩放到 832 x 832,我会失去粉红色,棕色变成绿色。
@1x 32 x 32
@10x 320 x 320
@26x 832 x 832
这是我用来创建 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 并且我在控制台中注销了图像的位置。
关闭全局色图似乎可以解决问题:
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 { [[=11=].red, [=11=].green, [=11=].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 库。
接下来,这里是关于正在发生的量化失败的更多背景信息。如果您 运行 通过 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
所以第一个缺少棕色和粉红色,红色通道中带有 246
和 113
的颜色根本没有列出,这些在直方图中正确列出(大概对于每帧颜色图版本,对较长输出中的每一帧重复)。
这证明GIF中的调色板生成错误,这是我们肉眼很容易看到的。然而,让我感到奇怪的是,全球颜色地图版本有几种颜色的重复条目。这指出了 ImageIO 中调色板量化的一个非常明显的错误。有限的调色板中不应有重复的条目。
简而言之:不要依赖 Core Graphics 来量化您的 24 位 RGB 图像。在将它们发送到 ImageIO 之前预先量化它们并关闭全局颜色映射。如果问题仍然存在,则 ImageIO 调色板写入已损坏,您应该使用不同的 GIF 输出库
我遇到了这个非常奇怪的问题。我正在从 UIImages 创建动画 gif,大多数时候它们都是正确的。然而,当我开始使用更大尺寸的图像时,我的颜色开始消失。例如,如果我使用不超过 10 种颜色的 4 帧 32 x 32 像素图像就没问题。如果我将同一图像缩放到 832 x 832,我会失去粉红色,棕色变成绿色。
@1x 32 x 32
@10x 320 x 320
@26x 832 x 832
这是我用来创建 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 并且我在控制台中注销了图像的位置。
关闭全局色图似乎可以解决问题:
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 { [[=11=].red, [=11=].green, [=11=].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 库。
接下来,这里是关于正在发生的量化失败的更多背景信息。如果您 运行 通过 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
所以第一个缺少棕色和粉红色,红色通道中带有 246
和 113
的颜色根本没有列出,这些在直方图中正确列出(大概对于每帧颜色图版本,对较长输出中的每一帧重复)。
这证明GIF中的调色板生成错误,这是我们肉眼很容易看到的。然而,让我感到奇怪的是,全球颜色地图版本有几种颜色的重复条目。这指出了 ImageIO 中调色板量化的一个非常明显的错误。有限的调色板中不应有重复的条目。
简而言之:不要依赖 Core Graphics 来量化您的 24 位 RGB 图像。在将它们发送到 ImageIO 之前预先量化它们并关闭全局颜色映射。如果问题仍然存在,则 ImageIO 调色板写入已损坏,您应该使用不同的 GIF 输出库