從零開始實現(xiàn)一個APNG Decoder(Swift, iOS)
本文基于Swift,開源項目地址:https://github.com/czqasngit/BerryPlant
參考:
https://en.wikipedia.org/wiki/Portable_Network_Graphics
https://en.wikipedia.org/wiki/APNG
更好的方案是使用C實現(xiàn)解碼部分.
要做好APNG的解碼,首先得了解APNG的格式
1.什么是APNG ?
不同的圖片格式的文件有不同的壓縮算法,不同的數(shù)據(jù)組織結(jié)構(gòu).
APNG也是一種文件格式,他是基于PNG擴(kuò)展出來的一種類似GIF的動態(tài)圖片格式.
不同于GIF的是他是存在Alpha通道的,解析后就是一幀一幀的PNG圖片.
既然APNG是PNG的擴(kuò)展,那我們首先得搞清楚PNG的文件結(jié)構(gòu).

圖中所畫就是PNG的數(shù)據(jù)結(jié)構(gòu)
PNG signature: PNG圖片的簽名,32字節(jié),值是固定的: '.PNG'(89 50 4E 47 0D 0A 1A 0A)
IHDR、Other Chunks、IDAT、Other Chunks、IEND都被統(tǒng)一成一種結(jié)構(gòu),稱為Chunk
圖中所示Chunk的結(jié)構(gòu)
4個字節(jié)表示長度,4個字節(jié)表示Chunk的類型,length個字節(jié)表示chunk 的數(shù)據(jù),CRC4個字節(jié)用于校驗數(shù)據(jù)
IHDR: 圖片的元數(shù)據(jù)
Other Chunks: 在這里我暫且這樣稱呼,這里表示有一個或者多個連續(xù)的Chunk


圖中所示就是Other Chunks可能的類型值, chunk 有很多種類型,使用的比較少,主要使用的有(IHDR,IDAT,IEND)
IDAT:圖片數(shù)據(jù)
IEND:結(jié)束Chunk
IDAT:存儲壓縮后的PNG圖片數(shù)據(jù)
有了以上的完整的PNG數(shù)據(jù)就可以被標(biāo)準(zhǔn)的PNG Decoder解碼,iOS中我使用
CGDataProvider
CGImageSourceCreateWithDataProvider
來解析并得到數(shù)據(jù)
PNG的數(shù)據(jù)結(jié)構(gòu)搞清楚了,我們就可以來擼一擼APNG的數(shù)據(jù)結(jié)構(gòu)了

APNG的第一幀就是一個PNG,只是Other Chunks里面多了兩種在PNG中沒有的Chuck,分別是acTL與fcTL。在標(biāo)準(zhǔn)的PNG解碼器中可以將APNG的第一幀解析出來并生成圖片,acTL與fcTL也并不會影響到解碼器,因為他們只是用于起控制作用的Chunk,并不會影響到IDAT里面的數(shù)據(jù).
在維基百科上面看這幅圖時可能會有些疑惑,因為他省略了一些細(xì)節(jié)的東西,這樣會對解碼APNG過程有一定的影響。
解碼APNG的流程大致是這樣的:
1.拿到文件數(shù)據(jù),分離簽名與Chunk
2.獲取APNG的元數(shù)據(jù)與acTL(總動畫控制)
3.提取第一幀PNG
4.以第一幀為參考,將后續(xù)fdAT解析出來替換第一幀IDAT的位置生成每一幀的圖片
5.用fcTL(幀動畫控制)的屬性渲染圖片
下面用Swift在iOS系統(tǒng)為例實現(xiàn)一下解析與渲染:
1.分離簽名與Chunk
2.提取元數(shù)據(jù)與acTL
func parseChunks() {
var offset = 8
var stop = false
var firstHalfEnd = false
while !stop {
let chunkDataLength = UInt32.from(of: data, from: offset, to: offset + 4).bigToHost()
let chunkLength = 4 + 4 + chunkDataLength + 4
let chunkType = String.from(source: data, from: offset + 4, to: offset + 8)
let chunk = BerryAPNGChunk(start: offset, end: offset + numericCast(chunkLength), length: chunkLength, type: chunkType)
if chunk.type == "" {
break
}
self.chunks.append(chunk)
if chunk.type == "IEND" {
stop = true
}
offset += numericCast(chunkLength)
if !firstHalfEnd && chunk.type == "fcTL" {
firstHalfEnd = true
}
if chunk.type != "fcTL" && chunk.type != "fdAT" && chunk.type != "IDAT", chunk.type != "acTL" {
if !firstHalfEnd { common.appendFirstHalf(chunk) }
else { common.appendSecondHalf(chunk) }
}
if chunk.type == "acTL" {
self.actl = BerryAPNGACTL(with: data.subdata(in: (chunk.start + 8)..<chunk.end - 4))
}
if chunk.type == "IHDR" {
self.ihdr = BerryAPNGIHDR(with: data.subdata(in: (chunk.start + 8)..<chunk.end - 4))
}
}
for var i in 0..<chunks.count {
if chunks[i].type == "fcTL" {
frames.append(BerryAPNGFrame(idat: chunks[i + 1],
fctlData: self.data.subdata(in: (chunks[i].start + 8)..<chunks[i].end)))
i += 1
}
}
}
以Chunk結(jié)構(gòu)為基礎(chǔ),分離出所有的Chunk
if !firstHalfEnd && chunk.type == "fcTL" {
firstHalfEnd = true
}
if chunk.type != "fcTL" && chunk.type != "fdAT" && chunk.type != "IDAT", chunk.type != "acTL" {
if !firstHalfEnd { common.appendFirstHalf(chunk) }
else { common.appendSecondHalf(chunk) }
}
這一部分是以提取第一幀為模板,以方便后續(xù)拼接每一幀為一個完整的PNG結(jié)構(gòu)
for var i in 0..<chunks.count {
if chunks[i].type == "fcTL" {
frames.append(BerryAPNGFrame(idat: chunks[i + 1],
fctlData: self.data.subdata(in: (chunks[i].start + 8)..<chunks[i].end)))
i += 1
}
}
從chunks中分離出圖片幀的原始數(shù)據(jù)每一幀由一個fcTL與一個IDAT或者fdAT組成
要組成完整的PNG結(jié)構(gòu)就要用到我們前面解析出來的PNG signature與other chunks還有IEND了
從第二幀開始: PNG signature + other chunks + (fdAT 轉(zhuǎn)成 IDAT) + other chunks + IEND
這樣就組成了一個完整的PNG,從第二幀開始將fdAT轉(zhuǎn)換成IDATA,替換第一幀的IDAT的位置即可。
轉(zhuǎn)換的代碼如下:
let dataLength = frame.idat.length - 16 //IDAT length
var resultData = Data()
var dataLengthLittleSequence = NSSwapHostIntToBig(dataLength)
withUnsafeBytes(of: &dataLengthLittleSequence) {
let buffer = [UInt8]($0)
resultData.append(contentsOf: buffer)
}
let idat = ["I", "D", "A", "T"]
resultData.append(contentsOf: idat.map { UInt8.init(ascii: Unicode.Scalar.init($0)! ) })
//chunk data
let chunkDataStart = 12
let chunkDataEnd: Int = numericCast(12 + dataLength)
resultData.append(fdatData.subdata(in: chunkDataStart..<chunkDataEnd))
let bytes = resultData.copyAllBytes()
let crcValue = crc32(0, bytes + 4, numericCast(resultData.count) - 4)
bytes.deallocate()
var crc = NSSwapHostIntToBig(numericCast(crcValue))
withUnsafeBytes(of: &crc) {
let buffer = [UInt8]($0)
resultData.append(contentsOf: buffer)
}
data.append(resultData)
其實就是把fdAT的chunk data部分提取出來,重新組合成一個新的chunk data,類型是 "IDAT"即可。
但這里需要注意的是CRC的生成.
CRC即循環(huán)冗余檢測, 一種校驗算法。僅僅用來校驗數(shù)據(jù)的正確性的。
CRC(cyclic redundancy check)域中的值是對Chunk Type Code域和Chunk Data域中的數(shù)據(jù)進(jìn)行計算得到的。CRC具體算法定義在ISO 3309和ITU-T V.42中,其值按下面的CRC碼生成多項式進(jìn)行計算:
x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
很幸運的是iOS已經(jīng)在zlib中提供了crc32的函數(shù),可以直接調(diào)用
到此所有的數(shù)據(jù)已經(jīng)準(zhǔn)備就緒,接下來就是針對不同的平臺實現(xiàn)不能的解碼渲染了。
解碼的第一步就是把每一幀組織成一個完整的PNG結(jié)構(gòu),然后使用iOS的Core Graphics框架來解碼并生成UIImage
func getPNGData(with frame: BerryAPNGFrame, fileData data: Data) -> Data {
var pngData = Data()
pngData.append(signature.data)
meger(with: frame, from: chunks1, from: data, to: &pngData)
appendIDAT(with: frame, from: data, to: &pngData)
meger(with: frame, from: chunks2, from: data, to: &pngData)
return pngData
}
這一部分就是將一幀原始的fcTL+IDAT/fdAT的數(shù)據(jù)結(jié)構(gòu)其它chunk與簽名組織成一個完整的PNG
渲染APNG拼接成的圖片與渲染一個單獨的PNG格式文件的圖片還是有些區(qū)別的,渲染PNG只有一幀只會考慮幀內(nèi)壓縮,并不會考慮幀間壓縮。而APNG為了達(dá)到更高的壓縮率在幀與幀之前使用了幀間壓縮,渲染每一幀的模式由fcTL控制。
1.創(chuàng)建一個完整的畫布
let fullRect = CGRect(x: 0, y: 0, width: CGFloat(self.ihdr.width), height: CGFloat(self.ihdr.height))
這里需要注意的就是 APNG中fcTL的y_offset是至下而上的,而我們畫圖是從上到下的,所有畫圖的offsetY需要單獨計算一下。
let offsetY = self.ihdr.height - frame.fctl.y_offset - frame.fctl.height
let drawRect = CGRect(x: CGFloat(frame.fctl.x_offset), y: CGFloat(offsetY), width: CGFloat(frame.fctl.width), height: CGFloat(frame.fctl.height))
fcTL中有兩個非常重要的屬性,用于控制下一幀畫圖的操作
var dispose_op: UInt8
var blend_op: UInt8
[圖片上傳失敗...(image-1f5371-1537493846908)]
dispose_op 指定了當(dāng)前幀渲染完成后應(yīng)該對緩存區(qū)做什么操作
NONE:什么也不做,直接把新的圖片數(shù)據(jù)渲染到畫布指定的區(qū)域
BACKGROUND:渲染完當(dāng)前幀將緩存區(qū)清空
PREVIOUS:在渲染下一幀前將緩存區(qū)中的圖片恢復(fù)到成上一幀
blend_op 指定是否完全替換緩沖區(qū)中的內(nèi)容,意思就是是否在渲染當(dāng)前幀之前清空緩存

SOURCE: 表示要清空
OVER: 表示不清空
public func decode(at index: Int) -> CGImage? {
guard frames.count > index else { return nil }
let frame = frames[index]
let data = common.getPNGData(with: frame, fileData: self.data)
guard let provider = CGDataProvider(data: data as CFData),
let source = CGImageSourceCreateWithDataProvider(provider, nil),
let originCGImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { return nil }
var image: CGImage? = nil
let offsetY = self.ihdr.height - frame.fctl.y_offset - frame.fctl.height
let fullRect = CGRect(x: 0, y: 0, width: CGFloat(self.ihdr.width), height: CGFloat(self.ihdr.height))
let drawRect = CGRect(x: CGFloat(frame.fctl.x_offset), y: CGFloat(offsetY), width: CGFloat(frame.fctl.width), height: CGFloat(frame.fctl.height))
if frame.fctl.dispose_op == APNG_DISPOSE_OP.none.rawValue {
if(frame.fctl.blend_op == APNG_BLEND_OP.source.rawValue) {
context.clear(drawRect)
}
context.draw(originCGImage, in: drawRect)
image = context.makeImage()
} else if frame.fctl.dispose_op == APNG_DISPOSE_OP.background.rawValue {
if(frame.fctl.blend_op == APNG_BLEND_OP.source.rawValue) {
context.clear(drawRect)
}
context.draw(originCGImage, in: drawRect)
image = context.makeImage()
context.clear(drawRect)
} else {
let previousCGImage = context.makeImage()
if(frame.fctl.blend_op == APNG_BLEND_OP.source.rawValue) {
context.clear(drawRect)
}
context.draw(originCGImage, in: drawRect)
image = context.makeImage()
if let previousCGImage = previousCGImage {
context.clear(fullRect)
context.draw(previousCGImage, in: fullRect)
}
}
return image
}
至此,解碼APNG的每一幀圖片已經(jīng)完成了,接下來就是怎么組織這些幀,應(yīng)該怎么播放,每一幀播放多久,
每一幀都由fcTL chunk控制
@objc func render(){
guard let link = self.link else { return }
let linkDuration = link.duration
self.currentDelay -= linkDuration
if self.currentDelay <= 0 {
self.showNextImage()
let delay = imageProvider.frameDuration(at: self.currentIndex)
self.currentDelay = (self.currentDelay + delay)
}
}
到此可以做一個小結(jié)了,文中只展示了重要的邏輯部分的代碼,完整代碼就從文首的Github上下載。
小結(jié):
APNG其實并不復(fù)雜,這里面涉及到了一些知識點
1.圖片格式的本質(zhì):本質(zhì)就是組織管理圖片數(shù)據(jù),每一種圖片格式都有它獨特的數(shù)據(jù)壓縮管理方式
2.APNG圖片的壓縮方式,幀內(nèi)與幀間,這與視頻壓縮類似
3.圖片渲染原理,本文以iOS平臺為例