APNG decoder ok

This commit is contained in:
andri lim 2017-11-24 00:08:48 +07:00
parent 333122b9f3
commit 93c738e7e0
4 changed files with 166 additions and 27 deletions

BIN
apng/firefox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

View File

@ -30,7 +30,7 @@ import streams, endians, tables, hashes, math
import private.buffer, private.nimz import private.buffer, private.nimz
const const
NIM_PNG_VERSION = "0.1.9" NIM_PNG_VERSION = "0.2.0"
type type
PNGChunkType = distinct int32 PNGChunkType = distinct int32
@ -171,15 +171,15 @@ type
APNGFrameChunk = ref object of PNGChunk APNGFrameChunk = ref object of PNGChunk
sequenceNumber: int sequenceNumber: int
APNGFrameControl = ref object of APNGFrameChunk APNGFrameControl* = ref object of APNGFrameChunk
width: int width*: int
height: int height*: int
xOffset: int xOffset*: int
yOffset: int yOffset*: int
delayNum: int delayNum*: int
delayDen: int delayDen*: int
disposeOp: APNG_DISPOSE_OP disposeOp*: APNG_DISPOSE_OP
blendOp: APNG_BLEND_OP blendOp*: APNG_BLEND_OP
APNGFrameData = ref object of APNGFrameChunk APNGFrameData = ref object of APNGFrameChunk
frameDataPos: int frameDataPos: int
@ -220,10 +220,11 @@ type
pixels*: string pixels*: string
apngChunks*: seq[APNGFrameChunk] apngChunks*: seq[APNGFrameChunk]
firstFrameIsDefaultImage*: bool firstFrameIsDefaultImage*: bool
isAPNG*: bool
apngPixels*: seq[string]
APNGFrame* = ref object APNGFrame* = ref object
width*: int ctl*: APNGFramecontrol
height*: int
data*: string data*: string
PNGResult* = ref object PNGResult* = ref object
@ -404,6 +405,10 @@ proc getChunk*(png: PNG, chunkType: PNGChunkType): PNGChunk =
for c in png.chunks: for c in png.chunks:
if c.chunkType == chunkType: return c if c.chunkType == chunkType: return c
proc apngGetChunk*(png: PNG, chunkType: PNGChunkType): PNGChunk =
for c in png.apngChunks:
if c.chunkType == chunkType: return c
proc bitDepthAllowed(colorType: PNGcolorType, bitDepth: int): bool = proc bitDepthAllowed(colorType: PNGcolorType, bitDepth: int): bool =
case colorType case colorType
of LCT_GREY : result = bitDepth in {1, 2, 4, 8, 16} of LCT_GREY : result = bitDepth in {1, 2, 4, 8, 16}
@ -777,6 +782,7 @@ method parseChunk(chunk: PNGSbit, png: PNG): bool =
method parseChunk(chunk: APNGAnimationControl, png: PNG): bool = method parseChunk(chunk: APNGAnimationControl, png: PNG): bool =
chunk.numFrames = chunk.readInt32() chunk.numFrames = chunk.readInt32()
chunk.numPlays = chunk.readInt32() chunk.numPlays = chunk.readInt32()
result = true
method parseChunk(chunk: APNGFrameControl, png: PNG): bool = method parseChunk(chunk: APNGFrameControl, png: PNG): bool =
chunk.sequenceNumber = chunk.readInt32() chunk.sequenceNumber = chunk.readInt32()
@ -788,6 +794,7 @@ method parseChunk(chunk: APNGFrameControl, png: PNG): bool =
chunk.delayDen = chunk.readInt16() chunk.delayDen = chunk.readInt16()
chunk.disposeOp = chunk.readByte().APNG_DISPOSE_OP chunk.disposeOp = chunk.readByte().APNG_DISPOSE_OP
chunk.blendOp = chunk.readByte().APNG_BLEND_OP chunk.blendOp = chunk.readByte().APNG_BLEND_OP
result = true
method validateChunk(chunk: APNGFrameControl, png: PNG): bool = method validateChunk(chunk: APNGFrameControl, png: PNG): bool =
let header = PNGHEader(png.getChunk(IHDR)) let header = PNGHEader(png.getChunk(IHDR))
@ -802,6 +809,7 @@ method validateChunk(chunk: APNGFrameControl, png: PNG): bool =
method parseChunk(chunk: APNGFrameData, png: PNG): bool = method parseChunk(chunk: APNGFrameData, png: PNG): bool =
chunk.sequenceNumber = chunk.readInt32() chunk.sequenceNumber = chunk.readInt32()
chunk.frameDataPos = chunk.pos chunk.frameDataPos = chunk.pos
result = true
proc make[T](): T = new(result) proc make[T](): T = new(result)
@ -842,7 +850,11 @@ proc createChunk(png: PNG, chunkType: PNGChunkType, data: string, crc: uint32):
of sPLT: result = make[PNGSPalette]() of sPLT: result = make[PNGSPalette]()
of hIST: result = make[PNGHist]() of hIST: result = make[PNGHist]()
of sBIT: result = make[PNGSbit]() of sBIT: result = make[PNGSbit]()
of acTL: result = make[APNGAnimationControl]() of acTL:
# acTL chunk must precede IDAT chunk
# to be recognized as APNG
if not png.hasChunk(IDAT): png.isAPNG = true
result = make[APNGAnimationControl]()
of fcTL: result = make[APNGFrameControl]() of fcTL: result = make[APNGFrameControl]()
of fdAT: result = make[APNGFrameData]() of fdAT: result = make[APNGFrameData]()
else: else:
@ -1114,7 +1126,7 @@ proc Adam7Deinterlace(output: var DataBuf, input: DataBuf, w, h, bpp: int) =
# note that this function assumes the out buffer is completely 0, use setBitOfReversedStream otherwise # note that this function assumes the out buffer is completely 0, use setBitOfReversedStream otherwise
setBitOfReversedStream0(obp, output, bit) setBitOfReversedStream0(obp, output, bit)
proc postProcessScanLines(png: PNG) = proc postProcessScanLines(png: PNG; header: PNGHeader, w, h: int; input, output: var DataBuf) =
# This function converts the filtered-padded-interlaced data # This function converts the filtered-padded-interlaced data
# into pure 2D image buffer with the PNG's colorType. # into pure 2D image buffer with the PNG's colorType.
# Steps: # Steps:
@ -1122,17 +1134,9 @@ proc postProcessScanLines(png: PNG) =
# *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace # *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace
# NOTE: the input buffer will be overwritten with intermediate data! # NOTE: the input buffer will be overwritten with intermediate data!
var header = PNGHeader(png.getChunk(IHDR))
let bpp = header.getBPP() let bpp = header.getBPP()
let w = header.width
let h = header.height
let bitsPerLine = w * bpp let bitsPerLine = w * bpp
let bitsPerPaddedLine = ((w * bpp + 7) div 8) * 8 let bitsPerPaddedLine = ((w * bpp + 7) div 8) * 8
var idat = PNGData(png.getChunk(IDAT))
png.pixels = newString(idatRawSize(header.width, header.height, header))
var input = initBuffer(idat.idat)
var output = initBuffer(png.pixels)
zeroMem(output)
if header.interlaceMethod == IM_NONE: if header.interlaceMethod == IM_NONE:
if(bpp < 8) and (bitsPerLine != bitsPerPaddedLine): if(bpp < 8) and (bitsPerLine != bitsPerPaddedLine):
@ -1161,6 +1165,29 @@ proc postProcessScanLines(png: PNG) =
Adam7Deinterlace(output, input, w, h, bpp) Adam7Deinterlace(output, input, w, h, bpp)
proc postProcessScanLines(png: PNG) =
var header = PNGHeader(png.getChunk(IHDR))
let w = header.width
let h = header.height
var idat = PNGData(png.getChunk(IDAT))
png.pixels = newString(idatRawSize(header.width, header.height, header))
var input = initBuffer(idat.idat)
var output = initBuffer(png.pixels)
zeroMem(output)
png.postProcessScanLines(header, w, h, input, output)
proc postProcessScanLines(png: PNG, ctl: APNGFrameControl, data: string) =
var header = PNGHeader(png.getChunk(IHDR))
let w = ctl.width
let h = ctl.height
png.apngPixels.add newString(idatRawSize(ctl.width, ctl.height, header))
var input = initBuffer(data)
var output = initBuffer(png.apngPixels[^1])
zeroMem(output)
png.postProcessScanLines(header, w, h, input, output)
proc getColorMode(png: PNG): PNGColorMode = proc getColorMode(png: PNG): PNGColorMode =
var header = PNGHeader(png.getChunk(IHDR)) var header = PNGHeader(png.getChunk(IHDR))
var cm = newColorMode(header.colorType, header.bitDepth) var cm = newColorMode(header.colorType, header.bitDepth)
@ -1862,21 +1889,107 @@ proc convert*(png: PNG, colorType: PNGcolorType, bitDepth: int): PNGResult =
convert(output, input, modeOut, modeIn, numPixels) convert(output, input, modeOut, modeIn, numPixels)
proc convert*(png: PNG, colorType: PNGcolorType, bitDepth: int, ctl: APNGFrameControl, data: string): APNGFrame =
let modeIn = png.getColorMode()
let modeOut = newColorMode(colorType, bitDepth)
let size = getRawSize(ctl.width, ctl.height, modeOut)
let numPixels = ctl.width * ctl.height
let input = initBuffer(data)
new(result)
result.ctl = ctl
result.data = newString(size)
var output = initBuffer(result.data)
if modeOut == modeIn:
output.copyElements(input, size)
return result
convert(output, input, modeOut, modeIn, numPixels)
proc toString(chunk: APNGFrameData): string =
let fdatLen = chunk.data.len - chunk.frameDataPos
let fdatAddr = chunk.data[chunk.frameDataPos].addr
result = newString(fdatLen)
copyMem(result[0].addr, fdatAddr, fdatLen)
proc decodePNG*(s: Stream, colorType: PNGcolorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult = proc decodePNG*(s: Stream, colorType: PNGcolorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult =
if not bitDepthAllowed(colorType, bitDepth): if not bitDepthAllowed(colorType, bitDepth):
raise PNGError("colorType and bitDepth combination not allowed") raise PNGError("colorType and bitDepth combination not allowed")
var png = s.parsePNG(settings) var png = s.parsePNG(settings)
let header = PNGHeader(png.getChunk(IHDR))
# shadowing the settings
let settings = PNGDecoder(png.settings)
png.postProcessScanLines() png.postProcessScanLines()
if PNGDecoder(png.settings).colorConvert: if settings.colorConvert:
result = png.convert(colorType, bitDepth) result = png.convert(colorType, bitDepth)
else: else:
let header = PNGHeader(png.getChunk(IHDR))
new(result) new(result)
result.width = header.width result.width = header.width
result.height = header.height result.height = header.height
result.data = png.pixels result.data = png.pixels
if png.isAPNG:
var
actl = APNGAnimationControl(png.getChunk(acTL))
frameControl = newSeqOfCap[APNGFrameControl](actl.numFrames)
frameData = newSeqOfCap[string](actl.numFrames)
numFrames = 0
lastChunkType = PNGChunkType(0)
start = 0
if png.firstFrameIsDefaultImage:
start = 1
# IDAT already processed, so we add a dummy here
frameData.add string(nil)
for x in png.apngChunks:
if x.chunkType == fcTL:
frameControl.add APNGFrameControl(x)
inc numFrames
lastChunkType = fcTL
else:
let y = APNGFrameData(x)
if lastChunkType == fdAT:
frameData[^1].add y.toString()
else:
frameData.add y.toString()
lastChunkType = fdAT
if actl.numFrames == 0 or actl.numFrames != numFrames or actl.numFrames != frameData.len:
raise PNGError("animation numFrames error")
png.apngPixels = newSeqOfCap[string](numFrames)
result.frames = newSeqOfCap[APNGFrame](numFrames)
if png.firstFrameIsDefaultImage:
let ctl = frameControl[0]
if ctl.width != header.width or ctl.height != header.height:
raise PNGError("animation control error: dimension")
if ctl.xOffset != 0 or ctl.xOffset != 0:
raise PNGError("animation control error: offset")
var frame = new(APNGFrame)
frame.ctl = ctl
frame.data = result.data
result.frames.add frame
for i in start..<numFrames:
let ctl = frameControl[i]
var nz = nzInflateInit(frameData[i])
nz.ignoreAdler32 = PNGDecoder(png.settings).ignoreAdler32
let idat = zlib_decompress(nz)
png.postProcessScanLines(ctl, idat)
if PNGDecoder(png.settings).colorConvert:
result.frames.add png.convert(colorType, bitDepth, ctl, png.apngPixels[^1])
else:
var frame = new(APNGFrame)
frame.ctl = ctl
frame.data = png.apngPixels[^1]
result.frames.add frame
proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG = proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG =
var png = s.parsePNG(settings) var png = s.parsePNG(settings)
png.postProcessScanLines() png.postProcessScanLines()
@ -1917,6 +2030,13 @@ proc decodePNG24*(input: string, settings = PNGDecoder(nil)): PNGResult =
debugEcho getCurrentExceptionMsg() debugEcho getCurrentExceptionMsg()
result = nil result = nil
proc main() =
var png = loadPNG32("apng\\clock.png")
#var png2 = loadPNG32("apng\\firefox.png")
#var png3 = loadPNG32("tester\\sample.png")
main()
#Encoder/Decoder demarcation line----------------------------- #Encoder/Decoder demarcation line-----------------------------
type type

View File

@ -1,5 +1,5 @@
# Package # Package
version = "0.1.9" version = "0.2.0"
author = "Andri Lim" author = "Andri Lim"
description = "PNG encoder and decoder" description = "PNG encoder and decoder"
license = "MIT" license = "MIT"

View File

@ -92,6 +92,25 @@ pixels are stored as raw bytes using Nim's string as container:
| grey1,a1,grey2,a2,...,greyn,an | GREY ALPHA 8 bit | | grey1,a1,grey2,a2,...,greyn,an | GREY ALPHA 8 bit |
## Animated PNG (APNG)
Since version 0.2.0, nimPNG provides support for [Animated PNG](https://wiki.mozilla.org/APNG_Specification).
### Decoding
```Nim
#let png = loadPNG("image.png", LCT_RGBA, 8)
# or
#let png = decodePNG32(raw_bytes)
```
The usual loadPNG and decodePNG can decode both unanimated and animated PNG.
png.width, png.height, png.data works as usual. If the decoded PNG is an APNG, png.data will contains default frame.
Animation frames can be accessible via png.frames. If it is not an APNG, png.frames will be nil.
### Encoding
[nimpng-travisci]: https://travis-ci.org/jangko/nimPNG [nimpng-travisci]: https://travis-ci.org/jangko/nimPNG
[badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master [badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master