diff --git a/nimPNG.nim b/nimPNG.nim index abec445..75c2362 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -155,6 +155,35 @@ type PNGSbit = ref object of PNGChunk + APNGAnimationControl = ref object of PNGChunk + numFrames: int + numPlays: int + + APNG_DISPOSE_OP* = enum + APNG_DISPOSE_OP_NONE + APNG_DISPOSE_OP_BACKGROUND + APNG_DISPOSE_OP_PREVIOUS + + APNG_BLEND_OP* = enum + APNG_BLEND_OP_SOURCE + APNG_BLEND_OP_OVER + + 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 + + APNGFrameData = ref object of APNGFrameChunk + frameDataPos: int + PNGPass = object w, h: array[0..6, int] filterStart, paddedStart, start: array[0..7, int] @@ -189,11 +218,19 @@ type settings*: PNGSettings chunks*: seq[PNGChunk] pixels*: string + apngChunks*: seq[APNGFrameChunk] + firstFrameIsDefaultImage*: bool + + APNGFrame* = ref object + width*: int + height*: int + data*: string PNGResult* = ref object width*: int height*: int data*: string + frames*: seq[APNGFrame] DataBuf = Buffer[string] @@ -252,6 +289,11 @@ const sPLT = makeChunkType("sPLT") hIST = makeChunkType("hIST") + # APNG chunks + acTL = makeChunkType("acTL") + fcTL = makeChunkType("fcTL") + fdAT = makeChunkType("fdAT") + # shared values used by multiple Adam7 related functions ADAM7_IX = [ 0, 4, 0, 2, 0, 1, 0 ] # x start values ADAM7_IY = [ 0, 0, 4, 0, 2, 0, 1 ] # y start values @@ -353,6 +395,11 @@ proc hasChunk*(png: PNG, chunkType: PNGChunkType): bool = if c.chunkType == chunkType: return true result = false +proc apngHasChunk*(png: PNG, chunkType: PNGChunkType): bool = + for c in png.apngChunks: + if c.chunkType == chunkType: return true + result = false + proc getChunk*(png: PNG, chunkType: PNGChunkType): PNGChunk = for c in png.chunks: if c.chunkType == chunkType: return c @@ -727,43 +774,77 @@ method parseChunk(chunk: PNGSbit, png: PNG): bool = result = true +method parseChunk(chunk: APNGAnimationControl, png: PNG): bool = + chunk.numFrames = chunk.readInt32() + chunk.numPlays = chunk.readInt32() + +method parseChunk(chunk: APNGFrameControl, png: PNG): bool = + chunk.sequenceNumber = chunk.readInt32() + chunk.width = chunk.readInt32() + chunk.height = chunk.readInt32() + chunk.xOffset = chunk.readInt32() + chunk.yOffset = chunk.readInt32() + chunk.delayNum = chunk.readInt16() + chunk.delayDen = chunk.readInt16() + chunk.disposeOp = chunk.readByte().APNG_DISPOSE_OP + chunk.blendOp = chunk.readByte().APNG_BLEND_OP + +method validateChunk(chunk: APNGFrameControl, png: PNG): bool = + let header = PNGHEader(png.getChunk(IHDR)) + result = true + result = result and (chunk.xOffset >= 0) + result = result and (chunk.yOffset >= 0) + result = result and (chunk.width > 0) + result = result and (chunk.height > 0) + result = result and (chunk.xOffset + chunk.width <= header.width) + result = result and (chunk.yOffset + chunk.height <= header.height) + +method parseChunk(chunk: APNGFrameData, png: PNG): bool = + chunk.sequenceNumber = chunk.readInt32() + chunk.frameDataPos = chunk.pos + proc make[T](): T = new(result) proc createChunk(png: PNG, chunkType: PNGChunkType, data: string, crc: uint32): PNGChunk = var settings = PNGDecoder(png.settings) result = nil - if chunkType == IHDR: result = make[PNGHeader]() - elif chunkType == PLTE: result = make[PNGPalette]() - elif chunkType == IDAT: + case chunkType + of IHDR: result = make[PNGHeader]() + of PLTE: result = make[PNGPalette]() + of IDAT: + if png.apngHasChunk(fcTL): png.firstFrameIsDefaultImage = true if not png.hasChunk(IDAT): result = make[PNGData]() else: var idat = PNGData(png.getChunk(IDAT)) idat.data.add data return idat - elif chunkType == tRNS: result = make[PNGTrans]() - elif chunkType == bKGD: result = make[PNGBackground]() - elif chunkType == tIME: result = make[PNGTime]() - elif chunkType == pHYs: result = make[PNGPhys]() - elif chunkType == tEXt: + of tRNS: result = make[PNGTrans]() + of bKGD: result = make[PNGBackground]() + of tIME: result = make[PNGTime]() + of pHYs: result = make[PNGPhys]() + of tEXt: if settings.readTextChunks: result = make[PNGTExt]() else: if settings.rememberUnknownChunks: new(result) - elif chunkType == zTXt: + of zTXt: if settings.readTextChunks: result = make[PNGZtxt]() else: if settings.rememberUnknownChunks: new(result) - elif chunkType == iTXt: + of iTXt: if settings.readTextChunks: result = make[PNGItxt]() else: if settings.rememberUnknownChunks: new(result) - elif chunkType == gAMA: result = make[PNGGamma]() - elif chunkType == cHRM: result = make[PNGChroma]() - elif chunkType == iCCP: result = make[PNGICCProfile]() - elif chunkType == sRGB: result = make[PNGStandarRGB]() - elif chunkType == sPLT: result = make[PNGSPalette]() - elif chunkType == hIST: result = make[PNGHist]() - elif chunkType == sBIT: result = make[PNGSbit]() + of gAMA: result = make[PNGGamma]() + of cHRM: result = make[PNGChroma]() + of iCCP: result = make[PNGICCProfile]() + of sRGB: result = make[PNGStandarRGB]() + of sPLT: result = make[PNGSPalette]() + of hIST: result = make[PNGHist]() + of sBIT: result = make[PNGSbit]() + of acTL: result = make[APNGAnimationControl]() + of fcTL: result = make[APNGFrameControl]() + of fdAT: result = make[APNGFrameData]() else: if settings.rememberUnknownChunks: new(result) @@ -784,6 +865,7 @@ proc parsePNG(s: Stream, settings: PNGDecoder): PNG = var png: PNG new(png) png.chunks = @[] + png.apngChunks = @[] if settings == nil: png.settings = makePNGDecoder() else: png.settings = settings @@ -805,7 +887,10 @@ proc parsePNG(s: Stream, settings: PNGDecoder): PNG = if chunkType != IDAT and chunk != nil: if not chunk.parseChunk(png): raise PNGError("error parse chunk: " & $chunkType) if not chunk.validateChunk(png): raise PNGError("invalid chunk: " & $chunkType) - if chunk != nil: png.chunks.add chunk + if chunk != nil: + if chunkType == fcTL or chunkType == fdAT: + png.apngChunks.add APNGFrameChunk(chunk) + else: png.chunks.add chunk if chunkType == IEND: break if not png.hasChunk(IHDR): raise PNGError("no IHDR found") @@ -1029,7 +1114,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) = # This function converts the filtered-padded-interlaced data # into pure 2D image buffer with the PNG's colorType. # Steps: @@ -1781,7 +1866,7 @@ proc decodePNG*(s: Stream, colorType: PNGcolorType, bitDepth: int, settings = PN if not bitDepthAllowed(colorType, bitDepth): raise PNGError("colorType and bitDepth combination not allowed") var png = s.parsePNG(settings) - png.postProcessscanLines() + png.postProcessScanLines() if PNGDecoder(png.settings).colorConvert: result = png.convert(colorType, bitDepth) @@ -1794,7 +1879,7 @@ proc decodePNG*(s: Stream, colorType: PNGcolorType, bitDepth: int, settings = PN proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG = var png = s.parsePNG(settings) - png.postProcessscanLines() + png.postProcessScanLines() result = png when not defined(js):