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
const
NIM_PNG_VERSION = "0.1.9"
NIM_PNG_VERSION = "0.2.0"
type
PNGChunkType = distinct int32
@ -171,15 +171,15 @@ type
APNGFrameChunk = ref object of PNGChunk
sequenceNumber: int
APNGFrameControl = ref object of APNGFrameChunk
width: int
height: int
xOffset: int
yOffset: int
delayNum: int
delayDen: int
disposeOp: APNG_DISPOSE_OP
blendOp: APNG_BLEND_OP
APNGFrameControl* = ref object of APNGFrameChunk
width*: int
height*: int
xOffset*: int
yOffset*: int
delayNum*: int
delayDen*: int
disposeOp*: APNG_DISPOSE_OP
blendOp*: APNG_BLEND_OP
APNGFrameData = ref object of APNGFrameChunk
frameDataPos: int
@ -220,10 +220,11 @@ type
pixels*: string
apngChunks*: seq[APNGFrameChunk]
firstFrameIsDefaultImage*: bool
isAPNG*: bool
apngPixels*: seq[string]
APNGFrame* = ref object
width*: int
height*: int
ctl*: APNGFramecontrol
data*: string
PNGResult* = ref object
@ -404,6 +405,10 @@ proc getChunk*(png: PNG, chunkType: PNGChunkType): PNGChunk =
for c in png.chunks:
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 =
case colorType
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 =
chunk.numFrames = chunk.readInt32()
chunk.numPlays = chunk.readInt32()
result = true
method parseChunk(chunk: APNGFrameControl, png: PNG): bool =
chunk.sequenceNumber = chunk.readInt32()
@ -788,6 +794,7 @@ method parseChunk(chunk: APNGFrameControl, png: PNG): bool =
chunk.delayDen = chunk.readInt16()
chunk.disposeOp = chunk.readByte().APNG_DISPOSE_OP
chunk.blendOp = chunk.readByte().APNG_BLEND_OP
result = true
method validateChunk(chunk: APNGFrameControl, png: PNG): bool =
let header = PNGHEader(png.getChunk(IHDR))
@ -802,6 +809,7 @@ method validateChunk(chunk: APNGFrameControl, png: PNG): bool =
method parseChunk(chunk: APNGFrameData, png: PNG): bool =
chunk.sequenceNumber = chunk.readInt32()
chunk.frameDataPos = chunk.pos
result = true
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 hIST: result = make[PNGHist]()
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 fdAT: result = make[APNGFrameData]()
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
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
# into pure 2D image buffer with the PNG's colorType.
# Steps:
@ -1122,23 +1134,15 @@ proc postProcessScanLines(png: PNG) =
# *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace
# NOTE: the input buffer will be overwritten with intermediate data!
var header = PNGHeader(png.getChunk(IHDR))
let bpp = header.getBPP()
let w = header.width
let h = header.height
let bitsPerLine = w * bpp
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(bpp < 8) and (bitsPerLine != bitsPerPaddedLine):
unfilter(input, input, w, h, bpp)
removePaddingBits(output, input, bitsPerLine, bitsPerPaddedLine, h)
# we can immediatly filter into the out buffer, no other steps needed
# we can immediatly filter into the out buffer, no other steps needed
else: unfilter(output, input, w, h, bpp)
else: # interlace_method is 1 (Adam7)
var pass: PNGPass
@ -1161,6 +1165,29 @@ proc postProcessScanLines(png: PNG) =
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 =
var header = PNGHeader(png.getChunk(IHDR))
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)
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 =
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)
let header = PNGHeader(png.getChunk(IHDR))
# shadowing the settings
let settings = PNGDecoder(png.settings)
png.postProcessScanLines()
if PNGDecoder(png.settings).colorConvert:
if settings.colorConvert:
result = png.convert(colorType, bitDepth)
else:
let header = PNGHeader(png.getChunk(IHDR))
new(result)
result.width = header.width
result.height = header.height
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 =
var png = s.parsePNG(settings)
png.postProcessScanLines()
@ -1917,6 +2030,13 @@ proc decodePNG24*(input: string, settings = PNGDecoder(nil)): PNGResult =
debugEcho getCurrentExceptionMsg()
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-----------------------------
type

View File

@ -1,5 +1,5 @@
# Package
version = "0.1.9"
version = "0.2.0"
author = "Andri Lim"
description = "PNG encoder and decoder"
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 |
## 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
[badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master