mirror of https://github.com/status-im/nimPNG.git
APNG decoder ok
This commit is contained in:
parent
333122b9f3
commit
93c738e7e0
Binary file not shown.
After Width: | Height: | Size: 622 KiB |
168
nimPNG.nim
168
nimPNG.nim
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
19
readme.md
19
readme.md
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue