从零开始实现一个APNG Decoder(Swift, iOS)

栏目: Swift · 发布时间: 6年前

内容简介:不同的图片格式的文件有不同的压缩算法,不同的数据组织结构.APNG也是一种文件格式,他是基于PNG扩展出来的一种类似GIF的动态图片格式.不同于GIF的是他是存在Alpha通道的,解析后就是一帧一帧的PNG图片.

从零开始实现一个APNG Decoder(Swift, iOS)

更好的方案是使用C实现解码部分.

要做好APNG的解码,首先得了解APNG的格式

1.什么是APNG ?

不同的图片格式的文件有不同的压缩算法,不同的数据组织结构.

APNG也是一种文件格式,他是基于PNG扩展出来的一种类似GIF的动态图片格式.

不同于GIF的是他是存在Alpha通道的,解析后就是一帧一帧的PNG图片.

既然APNG是PNG的扩展,那我们首先得搞清楚PNG的文件结构.

从零开始实现一个APNG Decoder(Swift, iOS)

图中所画就是PNG的数据结构

PNG signature: PNG图片的签名,32字节,值是固定的: '.PNG'(89 50 4E 47 0D 0A 1A 0A)

IHDR、Other Chunks、IDAT、Other Chunks、IEND都被统一成一种结构,称为Chunk

图中所示Chunk的结构

4个字节表示长度,4个字节表示Chunk的类型,length个字节表示chunk 的数据,CRC4个字节用于校验数据

IHDR: 图片的元数据

Other Chunks: 在这里我暂且这样称呼,这里表示有一个或者多个连续的Chunk

从零开始实现一个APNG Decoder(Swift, iOS) 从零开始实现一个APNG Decoder(Swift, iOS)

图中所示就是Other Chunks可能的类型值

IDAT:图片数据

IEND:结束Chunk

有了以上的完整的PNG数据就可以被标准的PNG Decoder解码,iOS中我使用

CGDataProvider

CGImageSourceCreateWithDataProvider

来解析并得到数据

PNG的数据结构搞清楚了,我们就可以来撸一撸APNG的数据结构了

从零开始实现一个APNG Decoder(Swift, iOS)

APNG的第一帧就是一个PNG,只是Other Chunks里面多了两种在PNG中没有的Chuck,分别是acTL与fcTL。在标准的PNG解码器中可以将APNG的第一帧解析出来并生成图片,acTL与fcTL也并不会影响到解码器,因为他们只是用于起控制作用的Chunk,并不会影响到IDAT里面的数据.

在维基百科上面看这幅图时可能会有些疑惑,因为他省略了一些细节的东西,这样会对解码APNG过程有一定的影响。

解码APNG的流程大致是这样的:

1.拿到文件数据,分离签名与Chunk

2.获取APNG的元数据与acTL(总动画控制)

3.提取第一帧PNG

4.以第一帧为参考,将后续fdAT解析出来替换第一帧IDAT的位置生成每一帧的图片

5.用fcTL(帧动画控制)的属性渲染图片

下面用Swift在iOS系统为例实现一下解析与渲染:

1.分离签名与Chunk

2.提取元数据与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="" nbsp="" -="" 4="" if="" chunk="" type="" self="" ihdr="">
 <chunk end="" nbsp="" -="" 4="" for="" var="" i="" in="" 0="">
  <chunks count="" nbsp="" if="" chunks="" i="" type="" frames="" append="" berryapngframe="" idat:="" 1="" fctldata:="" self="" data="" subdata="" in:="" start="" 8="">
   <chunks i="" end="" nbsp=""></chunks>
  </chunks>
 </chunk>
</chunk>

以Chunk结构为基础,分离出所有的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) }
            }

这一部分是以提取第一帧为模板,以方便后续拼接每一帧为一个完整的PNG结构

for var i in 0..
<chunks count="" nbsp="" if="" chunks="" i="" type="" frames="" append="" berryapngframe="" idat:="" 1="" fctldata:="" self="" data="" subdata="" in:="" start="" 8="">
 <chunks i="" end="" nbsp=""></chunks>
</chunks>

从chunks中分离出图片帧的原始数据每一帧由一个fcTL与一个IDAT或者fdAT组成

要组成完整的PNG结构就要用到我们前面解析出来的PNG signature与other chunks还有IEND了

解码的第一步就是把每一帧组织成一个完整的PNG结构,然后使用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的数据结构其它chunk与签名组织成一个完整的PNG

渲染APNG拼接成的图片与渲染一个单独的PNG格式文件的图片还是有些区别的,渲染PNG只有一帧只会考虑帧内压缩,并不会考虑帧间压缩。而APNG为了达到更高的压缩率在帧与帧之前使用了帧间压缩,渲染每一帧的模式由fcTL控制。

1.创建一个完整的画布

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
![](media/15374490859407/15374569731780.jpg)

dispose_op 指定了当前帧渲染完成后应该对缓存区做什么操作

NONE:什么也不做,直接把新的图片数据渲染到画布指定的区域

BACKGROUND:渲染完当前帧将缓存区清空

PREVIOUS:在渲染下一帧前将缓存区中的图片恢复到成上一帧

blend_op 指定是否完全替换缓冲区中的内容,意思就是是否在渲染当前帧之前清空缓存

从零开始实现一个APNG Decoder(Swift, iOS)

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的每一帧图片已经完成了,接下来就是怎么组织这些帧,应该怎么播放,每一帧播放多久,

每一帧都由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)
        }
    }

到此可以做一个小结了,文中只展示了重要的逻辑部分的代码,完整代码就从文首的Github上下载。

小结:

APNG其实并不复杂,这里面涉及到了一些知识点

1.图片格式的本质:本质就是组织管理图片数据,每一种图片格式都有它独特的数据压缩管理方式

2.APNG图片的压缩方式,帧内与帧间,这与视频压缩类似

3.图片渲染原理,本文以iOS平台为例


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C语言编程:一本全面的C语言入门教程(第三版)

C语言编程:一本全面的C语言入门教程(第三版)

(美)Stephen Kochan / 张小潘 / 电子社博文视点资讯有限公司 / 2006年 / 59.00元

本书是极负盛名的C语言入门经典教材,其第一版发行至今已有20年的历史。本书内容详实全面,由浅入深,示例丰富,并在每个章节后面附有部分习题,非常适合读者自学使用。除此之外,《C语言编程》一书对于C语言标准的最新进展、C语言常见开发工具以及管理C语言大型项目等重要方面,也进行了深入浅出的说明。一起来看看 《C语言编程:一本全面的C语言入门教程(第三版)》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试