From 75ff52a38f1f0f54054e3f44069fe20b9f5720fd Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 5 Jun 2020 13:38:31 +0700 Subject: [PATCH 1/5] update readme.md --- readme.md | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/readme.md b/readme.md index ff8f535..53dec38 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,10 @@ # nimPNG (PNG + APNG) Portable Network Graphics Encoder and Decoder written in Nim store lossless image with good compression. -Since version 0.2.0 also support Animated PNG! + +Notable releases: +- 0.2.0 support Animated PNG! +- 0.2.6 compile with --gc:arc. +- 0.3.0 [new set of API](apidoc.md) using seq[uint8] and new method to handle error. [![Build Status (Travis)](https://img.shields.io/travis/jangko/nimPNG/master.svg?label=Linux%20/%20macOS "Linux/macOS build status (Travis)")](https://travis-ci.org/jangko/nimPNG) [![Build status](https://ci.appveyor.com/api/projects/status/7ap5r5a41t7ea04p?svg=true)](https://ci.appveyor.com/project/jangko/nimpng) @@ -9,11 +13,11 @@ Since version 0.2.0 also support Animated PNG! all PNG standard color mode are supported: - - LCT_GREY = 0, # greyscale: 1,2,4,8,16 bit - - LCT_RGB = 2, # RGB: 8,16 bit - - LCT_PALETTE = 3, # palette: 1,2,4,8 bit - - LCT_GREY_ALPHA = 4, # greyscale with alpha: 8,16 bit - - LCT_RGBA = 6 # RGB with alpha: 8,16 bit +- LCT_GREY = 0, # greyscale: 1,2,4,8,16 bit +- LCT_RGB = 2, # RGB: 8,16 bit +- LCT_PALETTE = 3, # palette: 1,2,4,8 bit +- LCT_GREY_ALPHA = 4, # greyscale with alpha: 8,16 bit +- LCT_RGBA = 6 # RGB with alpha: 8,16 bit both interlaced and non-interlaced mode supported @@ -25,17 +29,17 @@ unknown chunks will be handled properly the following chunks are supported (generated/interpreted) by both encoder and decoder: -- IHDR: header information -- PLTE: color palette -- IDAT: pixel data -- IEND: the final chunk -- tRNS: transparency for palettized images -- tEXt: textual information -- zTXt: compressed textual information -- iTXt: international textual information -- bKGD: suggested background color -- pHYs: physical dimensions -- tIME: modification time +- IHDR: header information +- PLTE: color palette +- IDAT: pixel data +- IEND: the final chunk +- tRNS: transparency for palettized images +- tEXt: textual information +- zTXt: compressed textual information +- iTXt: international textual information +- bKGD: suggested background color +- pHYs: physical dimensions +- tIME: modification time the following chunks are parsed correctly, but not used by decoder: cHRM, gAMA, iCCP, sRGB, sBIT, hIST, sPLT @@ -52,7 +56,7 @@ Supported color conversions: - streaming for progressive loading ## Basic Usage -```nimrod +```Nim import nimPNG let png = loadPNG32("image.png") @@ -66,7 +70,7 @@ let png = loadPNG32("image.png") if you already have the whole file in memory: -```nimrod +```Nim let png = decodePNG32(raw_bytes) #will do the same as above ``` From 86355acd6201530fdfac14ad017110310b85224d Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 5 Jun 2020 13:43:01 +0700 Subject: [PATCH 2/5] update .nimble --- nimPNG.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimPNG.nimble b/nimPNG.nimble index c998f42..c6b642c 100644 --- a/nimPNG.nimble +++ b/nimPNG.nimble @@ -1,5 +1,5 @@ # Package -version = "0.2.7" +version = "0.3.0" author = "Andri Lim" description = "PNG encoder and decoder" license = "MIT" From a9eb82fbd4c5d04a1f12c7bbbe693d3a2e043c67 Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 5 Jun 2020 13:43:26 +0700 Subject: [PATCH 3/5] decodePNG new API --- apidoc.md | 61 ++++ nimPNG.nim | 167 +++++++---- nimPNG/results.nim | 642 +++++++++++++++++++++++++++++++++++++++++++ tests/test_apng.nim | 2 +- tests/test_codec.nim | 14 +- 5 files changed, 821 insertions(+), 65 deletions(-) create mode 100644 apidoc.md create mode 100644 nimPNG/results.nim diff --git a/apidoc.md b/apidoc.md new file mode 100644 index 0000000..ea3266a --- /dev/null +++ b/apidoc.md @@ -0,0 +1,61 @@ +## Legacy API + +```Nim +proc encodePNG*(input: string, colorType: PNGColorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG +proc encodePNG32*(input: string, w, h: int): PNG +proc encodePNG24*(input: string, w, h: int): PNG + +when not defined(js): + proc savePNG*(fileName, input: string, colorType: PNGColorType, bitDepth, w, h: int): bool + proc savePNG32*(fileName, input: string, w, h: int): bool + proc savePNG24*(fileName, input: string, w, h: int): bool + +proc prepareAPNG*(colorType: PNGColorType, bitDepth, numPlays: int, settings = PNGEncoder(nil)): PNG +proc prepareAPNG24*(numPlays = 0): PNG +proc prepareAPNG32*(numPlays = 0): PNG +proc addDefaultImage*(png: PNG, input: string, width, height: int, ctl = APNGFrameControl(nil)): bool +proc addFrame*(png: PNG, frame: string, ctl: APNGFrameControl): bool +proc encodeAPNG*(png: PNG): string + +when not defined(js): + proc saveAPNG*(png: PNG, fileName: string): bool + +proc decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult +proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG + +when not defined(js): + proc loadPNG*(fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder = nil): PNGResult + proc loadPNG32*(fileName: string, settings = PNGDecoder(nil)): PNGResult + proc loadPNG24*(fileName: string, settings = PNGDecoder(nil)): PNGResult + +proc decodePNG32*(input: string, settings = PNGDecoder(nil)): PNGResult +proc decodePNG24*(input: string, settings = PNGDecoder(nil)): PNGResult +``` + + +## New API + + +```Nim +proc decodePNG*(T: type, s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult[T] +proc decodePNG*(T: type, s: Stream, settings = PNGDecoder(nil)): PNG + +type + PNGRes*[T] = Result[PNGResult[T], string] + +when not defined(js): + proc loadPNG*(T: type, fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder = nil): PNGRes[T] + proc loadPNG32*(T: type, fileName: string, settings = PNGDecoder(nil)): PNGRes[T] + proc loadPNG24*(T: type, fileName: string, settings = PNGDecoder(nil)): PNGRes[T] + +proc decodePNG32*(T: type, input: T, settings = PNGDecoder(nil)): PNGRes[T] +proc decodePNG24*(T: type, input: T, settings = PNGDecoder(nil)): PNGRes[T] +``` + +## How to use PNGRes? + +```Nim + let res = loadPNG32(seq[uint8], fileName, settings) + if res.isOk: result = res.get() # get PNGResult[seq[uint8]] + else: debugEcho res.error() # get error string +``` diff --git a/nimPNG.nim b/nimPNG.nim index eda6627..1b44293 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -26,16 +26,18 @@ #------------------------------------- import streams, endians, tables, hashes, math, typetraits -import nimPNG/[buffer, nimz, filters] +import nimPNG/[buffer, nimz, filters, results] import strutils const - NIM_PNG_VERSION = "0.2.7" + NIM_PNG_VERSION = "0.3.0" type PNGChunkType = distinct int32 + Pixels* = seq[uint8] + PNGColorType* = enum LCT_GREY = 0, # greyscale: 1,2,4,8,16 bit LCT_RGB = 2, # RGB: 8,16 bit @@ -221,15 +223,15 @@ type isAPNG*: bool apngPixels*: seq[string] - APNGFrame* = ref object + APNGFrame*[T] = ref object ctl*: APNGFramecontrol - data*: string + data*: T - PNGResult* = ref object + PNGResult*[T] = ref object width*: int height*: int - data*: string - frames*: seq[APNGFrame] + data*: T + frames*: seq[APNGFrame[T]] DataBuf = Buffer[string] @@ -1618,7 +1620,7 @@ proc getConverterRGBA[T](mode: PNGColorMode): convertRGBA[T] = else: return RGBAFromRGBA16[T] else: raise PNGFatal("unsupported RGBA converter") -proc convert*[T](output: var openArray[T], input: openArray[T], modeOut, modeIn: PNGColorMode, numPixels: int) = +proc convertImpl*[T](output: var openArray[T], input: openArray[T], modeOut, modeIn: PNGColorMode, numPixels: int) = var tree: ColorTree8 if modeOut.colorType == LCT_PALETTE: var @@ -1660,7 +1662,7 @@ proc convert*[T](output: var openArray[T], input: openArray[T], modeOut, modeIn: cvt(p, input, px, modeIn) pxl(p, output, px, modeOut, tree) -proc convert*(png: PNG, colorType: PNGColorType, bitDepth: int): PNGResult = +proc convert*[T](png: PNG, colorType: PNGColorType, bitDepth: int): PNGResult[T] = # TODO: check if this works according to the statement in the documentation: "The converter can convert # from greyscale input color type, to 8-bit greyscale or greyscale with alpha" # if(colorType notin {LCT_RGB, LCT_RGBA}) and (bitDepth != 8): @@ -1675,17 +1677,20 @@ proc convert*(png: PNG, colorType: PNGColorType, bitDepth: int): PNGResult = new(result) result.width = header.width result.height = header.height - result.data = newString(size) + when T is string: + result.data = newString(size) + else: + result.data = newSeq[uint8](size) if modeOut == modeIn: result.data = png.pixels return - convert(result.data.toOpenArray(0, result.data.len-1), + convertImpl(result.data.toOpenArray(0, result.data.len-1), png.pixels.toOpenArray(0, png.pixels.len-1), modeOut, modeIn, numPixels) -proc convert*(png: PNG, colorType: PNGColorType, bitDepth: int, ctl: APNGFrameControl, data: string): APNGFrame = +proc convert*[T](png: PNG, colorType: PNGColorType, bitDepth: int, ctl: APNGFrameControl, data: string): APNGFrame[T] = let modeIn = png.getColorMode() let modeOut = newColorMode(colorType, bitDepth) let size = getRawSize(ctl.width, ctl.height, modeOut) @@ -1693,13 +1698,16 @@ proc convert*(png: PNG, colorType: PNGColorType, bitDepth: int, ctl: APNGFrameCo new(result) result.ctl = ctl - result.data = newString(size) + when T is string: + result.data = newString(size) + else: + result.data = newSeq[uint8](size) if modeOut == modeIn: result.data = data return result - convert(result.data.toOpenArray(0, result.data.len-1), + convertImpl(result.data.toOpenArray(0, result.data.len-1), data.toOpenArray(0, data.len-1), modeOut, modeIn, numPixels) @@ -1710,16 +1718,16 @@ proc toString(chunk: APNGFrameData): string = copyMem(result[0].addr, fdatAddr, fdatLen) type - APNG = ref object + APNG[T] = ref object png: PNG - result: PNGResult + result: PNGResult[T] -proc processingAPNG(apng: APNG, colorType: PNGColorType, bitDepth: int) = +proc processingAPNG[T](apng: APNG[T], colorType: PNGColorType, bitDepth: int) = let header = PNGHeader(apng.png.getChunk(IHDR)) var actl = APNGAnimationControl(apng.png.getChunk(acTL)) frameControl = newSeqOfCap[APNGFrameControl](actl.numFrames) - frameData = newSeqOfCap[string](actl.numFrames) + frameData = newSeqOfCap[T](actl.numFrames) numFrames = 0 lastChunkType = PNGChunkType(0) start = 0 @@ -1745,10 +1753,10 @@ proc processingAPNG(apng: APNG, colorType: PNGColorType, bitDepth: int) = if actl.numFrames == 0 or actl.numFrames != numFrames or actl.numFrames != frameData.len: raise PNGFatal("animation numFrames error") - apng.png.apngPixels = newSeqOfCap[string](numFrames) + apng.png.apngPixels = newSeqOfCap[T](numFrames) if apng.result != nil: - apng.result.frames = newSeqOfCap[APNGFrame](numFrames) + apng.result.frames = newSeqOfCap[APNGFrame[T]](numFrames) if apng.png.firstFrameIsDefaultImage: let ctl = frameControl[0] @@ -1758,7 +1766,7 @@ proc processingAPNG(apng: APNG, colorType: PNGColorType, bitDepth: int) = raise PNGFatal("animation control error: offset") if apng.result != nil: - var frame = new(APNGFrame) + var frame = new(APNGFrame[T]) frame.ctl = ctl frame.data = apng.result.data apng.result.frames.add frame @@ -1772,14 +1780,14 @@ proc processingAPNG(apng: APNG, colorType: PNGColorType, bitDepth: int) = if apng.result != nil: if PNGDecoder(apng.png.settings).colorConvert: - apng.result.frames.add apng.png.convert(colorType, bitDepth, ctl, apng.png.apngPixels[^1]) + apng.result.frames.add convert[T](apng.png, colorType, bitDepth, ctl, apng.png.apngPixels[^1]) else: - var frame = new(APNGFrame) + var frame = new(APNGFrame[T]) frame.ctl = ctl frame.data = apng.png.apngPixels[^1] apng.result.frames.add frame -proc decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult = +proc decodePNG*(T: type, s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult[T] = if not bitDepthAllowed(colorType, bitDepth): raise PNGFatal("colorType and bitDepth combination not allowed") @@ -1787,7 +1795,7 @@ proc decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PN png.postProcessScanLines() if PNGDecoder(png.settings).colorConvert: - result = png.convert(colorType, bitDepth) + result = convert[T](png, colorType, bitDepth) else: new(result) let header = PNGHeader(png.getChunk(IHDR)) @@ -1796,53 +1804,98 @@ proc decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PN result.data = png.pixels if png.isAPNG: - var apng = APNG(png: png, result: result) + var apng = APNG[T](png: png, result: result) apng.processingAPNG(colorType, bitDepth) -proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG = +proc decodePNG*(T: type, s: Stream, settings = PNGDecoder(nil)): PNG = var png = s.parsePNG(settings) png.postProcessScanLines() if png.isAPNG: - var apng = APNG(png: png, result: nil) + var apng = APNG[T](png: png, result: nil) apng.processingAPNG(PNGColorType(0), 0) result = png +type + PNGRes*[T] = Result[PNGResult[T], string] + when not defined(js): - proc loadPNG*(fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder = nil): PNGResult = + proc loadPNG*(T: type, fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder = nil): PNGRes[T] = try: var s = newFileStream(fileName, fmRead) - if s == nil: return nil - result = s.decodePNG(colorType, bitDepth, settings) + if s == nil: + result.err("cannot open input stream") + return + result.ok(s.decodePNG(colorType, bitDepth, settings)) s.close() except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() - result = nil + result.err(getCurrentExceptionMsg()) - proc loadPNG32*(fileName: string, settings = PNGDecoder(nil)): PNGResult = - result = loadPNG(fileName, LCT_RGBA, 8, settings) + proc loadPNG32*(T: type, fileName: string, settings = PNGDecoder(nil)): PNGRes[T] = + loadPNG(T, fileName, LCT_RGBA, 8, settings) - proc loadPNG24*(fileName: string, settings = PNGDecoder(nil)): PNGResult = - result = loadPNG(fileName, LCT_RGB, 8, settings) + proc loadPNG24*(T: type, fileName: string, settings = PNGDecoder(nil)): PNGRes[T] = + loadPNG(T, fileName, LCT_RGB, 8, settings) -proc decodePNG32*(input: string, settings = PNGDecoder(nil)): PNGResult = +proc decodePNG32*(T: type, input: T, settings = PNGDecoder(nil)): PNGRes[T] = try: - var s = newStringStream(input) - if s == nil: return nil - result = s.decodePNG(LCT_RGBA, 8, settings) + when T is string: + var s = newStringStream(input) + else: + var s = newStringStream(cast[string](input)) + if s == nil: + result.err("cannot open input stream") + return + result.ok(s.decodePNG(LCT_RGBA, 8, settings)) except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() - result = nil + result.err(getCurrentExceptionMsg()) -proc decodePNG24*(input: string, settings = PNGDecoder(nil)): PNGResult = +proc decodePNG24*(T: type, input: T, settings = PNGDecoder(nil)): PNGRes[T] = try: - var s = newStringStream(input) - if s == nil: return nil - result = s.decodePNG(LCT_RGB, 8, settings) + when T is string: + var s = newStringStream(input) + else: + var s = newStringStream(cast[string](input)) + if s == nil: + result.err("cannot open input stream") + return + result.ok(s.decodePNG(LCT_RGB, 8, settings)) except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() - result = nil + result.err(getCurrentExceptionMsg()) + +# these are legacy API +template decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): untyped = + decodePNG(string, s, colorType, bitDepth, settings) + +template decodePNG*(s: Stream, settings = PNGDecoder(nil)): untyped = + decodePNG(string, s, settings) + +when not defined(js): + proc loadPNG*(fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder = nil): PNGResult[string] = + let res = loadPNG(string, fileName, colorType, bitDepth, settings) + if res.isOk: result = res.get() + else: debugEcho res.error() + + proc loadPNG32*(fileName: string, settings = PNGDecoder(nil)): PNGResult[string] = + let res = loadPNG32(string, fileName, settings) + if res.isOk: result = res.get() + else: debugEcho res.error() + + proc loadPNG24*(fileName: string, settings = PNGDecoder(nil)): PNGResult[string] = + let res = loadPNG24(string, fileName, settings) + if res.isOk: result = res.get() + else: debugEcho res.error() + +proc decodePNG32*(input: string, settings = PNGDecoder(nil)): PNGResult[string] = + let res = decodePNG32(string, input, settings) + if res.isOk: result = res.get() + else: debugEcho res.error() + +proc decodePNG24*(input: string, settings = PNGDecoder(nil)): PNGResult[string] = + let res = decodePNG24(string, input, settings) + if res.isOk: result = res.get() + else: debugEcho res.error() #Encoder/Decoder demarcation line----------------------------- @@ -2672,7 +2725,7 @@ proc addChunkfdAT(png: PNG, sequenceNumber, frameDataPos: int) = chunk.frameDataPos = frameDataPos png.chunks.add chunk -proc frameConvert(png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) = +proc frameConvert[T](png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) = template input: untyped = png.apngPixels[frameNo] if modeIn != modeOut: @@ -2682,7 +2735,7 @@ proc frameConvert(png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, s var converted = newString(size) # although in preProcessScanLines png.pixels is reinitialized, it is ok # because initBuffer(png.pixels) share the ownership - convert(converted.toOpenArray(0, converted.len-1), + convertImpl(converted.toOpenArray(0, converted.len-1), input.toOpenArray(0, input.len-1), modeOut, modeIn, numPixels) @@ -2690,7 +2743,7 @@ proc frameConvert(png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, s else: preProcessScanLines(png, input.toOpenArray(0, input.len-1), frameNo, w, h, modeOut, state) -proc encoderCore(png: PNG) = +proc encoderCore[T](png: PNG) = let state = PNGEncoder(png.settings) var modeIn = newColorMode(state.modeIn) var modeOut = newColorMode(state.modeOut) @@ -2725,7 +2778,7 @@ proc encoderCore(png: PNG) = if not png.isAPNG: png.apngPixels = @[""] shallowCopy(png.apngPixels[0], png.pixels) - png.frameConvert(modeIn, modeOut, png.width, png.height, 0, state) + frameConvert[T](png, modeIn, modeOut, png.width, png.height, 0, state) shallowCopy(png.pixels, png.apngPixels[0]) png.addChunkIHDR(png.width, png.height, modeOut, state) @@ -2772,7 +2825,7 @@ proc encoderCore(png: PNG) = png.addChunkfcTL(ctl, sequenceNumber) inc sequenceNumber - png.frameConvert(modeIn, modeOut, ctl.width, ctl.height, i, state) + frameConvert[T](png, modeIn, modeOut, ctl.width, ctl.height, i, state) png.addChunkfdAT(sequenceNumber, i) inc sequenceNumber @@ -2806,7 +2859,7 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = png.width = w png.height = h shallowCopy(png.pixels, input) - png.encoderCore() + encoderCore[string](png) result = png proc encodePNG*(input: string, colorType: PNGColorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG = @@ -2925,7 +2978,7 @@ proc addFrame*(png: PNG, frame: string, ctl: APNGFrameControl): bool = proc encodeAPNG*(png: PNG): string = try: - png.encoderCore() + encoderCore[string](png) var s = newStringStream() png.writeChunks s result = s.data @@ -2936,7 +2989,7 @@ proc encodeAPNG*(png: PNG): string = when not defined(js): proc saveAPNG*(png: PNG, fileName: string): bool = try: - png.encoderCore() + encoderCore[string](png) var s = newFileStream(fileName, fmWrite) png.writeChunks s s.close() diff --git a/nimPNG/results.nim b/nimPNG/results.nim new file mode 100644 index 0000000..d48db9b --- /dev/null +++ b/nimPNG/results.nim @@ -0,0 +1,642 @@ +# nim-result is also available stand-alone from https://github.com/arnetheduck/nim-result/ + +# Copyright (c) 2019 Jacek Sieka +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +type + ResultError*[E] = object of ValueError + ## Error raised when using `tryGet` value of result when error is set + ## See also Exception bridge mode + error*: E + + ResultDefect* = object of Defect + ## Defect raised when accessing value when error is set and vice versa + ## See also Exception bridge mode + + Result*[T, E] = object + ## Result type that can hold either a value or an error, but not both + ## + ## # Example + ## + ## ``` + ## # It's convenient to create an alias - most likely, you'll do just fine + ## # with strings or cstrings as error + ## + ## type R = Result[int, string] + ## + ## # Once you have a type, use `ok` and `err`: + ## + ## func works(): R = + ## # ok says it went... ok! + ## R.ok 42 + ## func fails(): R = + ## # or type it like this, to not repeat the type! + ## result.err "bad luck" + ## + ## if (let w = works(); w.isOk): + ## echo w[], " or use value: ", w.value + ## + ## # In case you think your callers want to differentiate between errors: + ## type + ## Error = enum + ## a, b, c + ## type RE[T] = Result[T, Error] + ## + ## # In the expriments corner, you'll find the following syntax for passing + ## # errors up the stack: + ## func f(): R = + ## let x = ?works() - ?fails() + ## assert false, "will never reach" + ## + ## # If you provide this exception converter, this exception will be raised + ## # on dereference + ## func toException(v: Error): ref CatchableError = (ref CatchableError)(msg: $v) + ## try: + ## RE[int].err(a)[] + ## except CatchableError: + ## echo "in here!" + ## + ## ``` + ## + ## See the tests for more practical examples, specially when working with + ## back and forth with the exception world! + ## + ## # Potential benefits: + ## + ## * Handling errors becomes explicit and mandatory at the call site - + ## goodbye "out of sight, out of mind" + ## * Errors are a visible part of the API - when they change, so must the + ## calling code and compiler will point this out - nice! + ## * Errors are a visible part of the API - your fellow programmer is + ## reminded that things actually can go wrong + ## * Jives well with Nim `discard` + ## * Jives well with the new Defect exception hierarchy, where defects + ## are raised for unrecoverable errors and the rest of the API uses + ## results + ## * Error and value return have similar performance characteristics + ## * Caller can choose to turn them into exceptions at low cost - flexible + ## for libraries! + ## * Mostly relies on simple Nim features - though this library is no + ## exception in that compiler bugs were discovered writing it :) + ## + ## # Potential costs: + ## + ## * Handling errors becomes explicit and mandatory - if you'd rather ignore + ## them or just pass them to some catch-all, this is noise + ## * When composing operations, value must be lifted before processing, + ## adding potential verbosity / noise (fancy macro, anyone?) + ## * There's no call stack captured by default (see also `catch` and + ## `capture`) + ## * The extra branching may be more expensive for the non-error path + ## (though this can be minimized with PGO) + ## + ## The API visibility issue of exceptions can also be solved with + ## `{.raises.}` annotations - as of now, the compiler doesn't remind + ## you to do so, even though it knows what the right annotation should be. + ## `{.raises.}` does not participate in generic typing, making it just as + ## verbose but less flexible in some ways, if you want to type it out. + ## + ## Many system languages make a distinction between errors you want to + ## handle and those that are simply bugs or unrealistic to deal with.. + ## handling the latter will often involve aborting or crashing the funcess - + ## reliable systems like Erlang will try to relaunch it. + ## + ## On the flip side we have dynamic languages like python where there's + ## nothing exceptional about exceptions (hello StopIterator). Python is + ## rarely used to build reliable systems - its strengths lie elsewhere. + ## + ## # Exception bridge mode + ## + ## When the error of a `Result` is an `Exception`, or a `toException` helper + ## is present for your error type, the "Exception bridge mode" is + ## enabled and instead of raising `ResultError`, `tryGet` will raise the + ## given `Exception` on access. `[]` and `get` will continue to raise a + ## `Defect`. + ## + ## This is an experimental feature that may be removed. + ## + ## # Other languages + ## + ## Result-style error handling seems pretty popular lately, specially with + ## statically typed languages: + ## Haskell: https://hackage.haskell.org/package/base-4.11.1.0/docs/Data-Either.html + ## Rust: https://doc.rust-lang.org/std/result/enum.Result.html + ## Modern C++: https://github.com/viboes/std-make/tree/master/doc/proposal/expected + ## More C++: https://github.com/ned14/outcome + ## + ## Swift is interesting in that it uses a non-exception implementation but + ## calls errors exceptions and has lots of syntactic sugar to make them feel + ## that way by implicitly passing them up the call chain - with a mandatory + ## annotation that function may throw: + ## https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html + ## + ## # Considerations for the error type + ## + ## * Use a `string` or a `cstring` if you want to provide a diagnostic for + ## the caller without an expectation that they will differentiate between + ## different errors. Callers should never parse the given string! + ## * Use an `enum` to provide in-depth errors where the caller is expected + ## to have different logic for different errors + ## * Use a complex type to include error-specific meta-data - or make the + ## meta-data collection a visible part of your API in another way - this + ## way it remains discoverable by the caller! + ## + ## A natural "error API" progression is starting with `Option[T]`, then + ## `Result[T, cstring]`, `Result[T, enum]` and `Result[T, object]` in + ## escalating order of complexity. + ## + ## # Other implemenations in nim + ## + ## There are other implementations in nim that you might prefer: + ## * Either from nimfp: https://github.com/vegansk/nimfp/blob/master/src/fp/either.nim + ## * result_type: https://github.com/kapralos/result_type/ + ## + ## # Implementation notes + ## + ## This implementation is mostly based on the one in rust. Compared to it, + ## there are a few differences - if know of creative ways to improve things, + ## I'm all ears. + ## + ## * Rust has the enum variants which lend themselves to nice construction + ## where the full Result type isn't needed: `Err("some error")` doesn't + ## need to know value type - maybe some creative converter or something + ## can deal with this? + ## * Nim templates allow us to fail fast without extra effort, meaning the + ## other side of `and`/`or` isn't evaluated unless necessary - nice! + ## * Rust uses From traits to deal with result translation as the result + ## travels up the call stack - needs more tinkering - some implicit + ## conversions would be nice here + ## * Pattern matching in rust allows convenient extraction of value or error + ## in one go. + ## + ## # Performance considerations + ## + ## When returning a Result instead of a simple value, there are a few things + ## to take into consideration - in general, we are returning more + ## information directly to the caller which has an associated cost. + ## + ## Result is a value type, thus its performance characteristics + ## generally follow the performance of copying the value or error that + ## it stores. `Result` would benefit greatly from "move" support in the + ## language. + ## + ## In many cases, these performance costs are negligeable, but nonetheless + ## they are important to be aware of, to structure your code in an efficient + ## manner: + ## + ## * Memory overhead + ## Result is stored in memory as a union with a `bool` discriminator - + ## alignment makes it somewhat tricky to give an exact size, but in + ## general, `Result[int, int]` will take up `2*sizeof(int)` bytes: + ## 1 `int` for the discriminator and padding, 1 `int` for either the value + ## or the error. The additional size means that returning may take up more + ## registers or spill onto the stack. + ## * Loss of RVO + ## Nim does return-value-optimization by rewriting `proc f(): X` into + ## `proc f(result: var X)` - in an expression like `let x = f()`, this + ## allows it to avoid a copy from the "temporary" return value to `x` - + ## when using Result, this copy currently happens always because you need + ## to fetch the value from the Result in a second step: `let x = f().value` + ## * Extra copies + ## To avoid spurious evaluation of expressions in templates, we use a + ## temporary variable sometimes - this means an unnecessary copy for some + ## types. + ## * Bad codegen + ## When doing RVO, Nim generates poor and slow code: it uses a construct + ## called `genericReset` that will zero-initialize a value using dynamic + ## RTTI - a process that the C compiler subsequently is unable to + ## optimize. This applies to all types, but is exacerbated with Result + ## because of its bigger footprint - this should be fixed in compiler. + ## * Double zero-initialization bug + ## Nim has an initialization bug that causes additional poor performance: + ## `var x = f()` will be expanded into `var x; zeroInit(x); f(x)` where + ## `f(x)` will call the slow `genericReset` and zero-init `x` again, + ## unnecessarily. + ## + ## Comparing `Result` performance to exceptions in Nim is difficult - it + ## will depend on the error type used, the frequency at which exceptions + ## happen, the amount of error handling code in the application and the + ## compiler and backend used. + ## + ## * the default C backend in nim uses `setjmp` for exception handling - + ## the relative performance of the happy path will depend on the structure + ## of the code: how many exception handlers there are, how much unwinding + ## happens. `setjmp` works by taking a snapshot of the full CPU state and + ## saving it to memory when enterting a try block (or an implict try + ## block, such as is introduced with `defer` and similar constructs). + ## * an efficient exception handling mechanism (like the C++ backend or + ## `nlvm`) will usually have a lower cost on the happy path because the + ## value can be returned more efficiently. However, there is still a code + ## and data size increase depending on the specific situation, as well as + ## loss of optimization opportunities to consider. + ## * raising an exception is usually (a lot) slower than returning an error + ## through a Result - at raise time, capturing a call stack and allocating + ## memory for the Exception is expensive, so the performance difference + ## comes down to the complexity of the error type used. + ## * checking for errors with Result is local branching operation that also + ## happens on the happy path - this may be a cost. + ## + ## An accurate summary might be that Exceptions are at its most efficient + ## when errors are not handled and don't happen. + ## + ## # Relevant nim bugs + ## + ## https://github.com/nim-lang/Nim/issues/13799 - type issues + ## https://github.com/nim-lang/Nim/issues/8745 - genericReset slow + ## https://github.com/nim-lang/Nim/issues/13879 - double-zero-init slow + ## https://github.com/nim-lang/Nim/issues/14318 - generic error raises pragma + + case o: bool + of false: + e: E + of true: + v: T + + Opt*[T] = Result[T, void] + +func raiseResultError[T, E](self: Result[T, E]) {.noreturn, noinline.} = + # noinline because raising should take as little space as possible at call + # site + mixin toException + + when E is ref Exception: + if self.e.isNil: # for example Result.default()! + raise (ref ResultError[void])(msg: "Trying to access value with err (nil)") + raise self.e + elif compiles(toException(self.e)): + raise toException(self.e) + elif compiles($self.e): + raise (ref ResultError[E])( + error: self.e, msg: "Trying to access value with err: " & $self.e) + else: + raise (res ResultError[E])(msg: "Trying to access value with err", error: self.e) + +func raiseResultDefect(m: string, v: auto) {.noreturn, noinline.} = + mixin `$` + when compiles($v): raise (ref ResultDefect)(msg: m & ": " & $v) + else: raise (ref ResultDefect)(msg: m) + +func raiseResultDefect(m: string) {.noreturn, noinline.} = + raise (ref ResultDefect)(msg: m) + +template assertOk(self: Result) = + if not self.o: + when self.E isnot void: + raiseResultDefect("Trying to acces value with err Result", self.e) + else: + raiseResultDefect("Trying to acces value with err Result") + +template ok*[T, E](R: type Result[T, E], x: auto): R = + ## Initialize a result with a success and value + ## Example: `Result[int, string].ok(42)` + R(o: true, v: x) + +template ok*[T, E](self: var Result[T, E], x: auto) = + ## Set the result to success and update value + ## Example: `result.ok(42)` + self = ok(type self, x) + +template err*[T, E](R: type Result[T, E], x: auto): R = + ## Initialize the result to an error + ## Example: `Result[int, string].err("uh-oh")` + R(o: false, e: x) + +template err*[T](R: type Result[T, void]): R = + R(o: false) + +template err*[T, E](self: var Result[T, E], x: auto) = + ## Set the result as an error + ## Example: `result.err("uh-oh")` + self = err(type self, x) + +template err*[T](self: var Result[T, void]) = + ## Set the result as an error + ## Example: `result.err()` + self = err(type self) + +template ok*(v: auto): auto = ok(typeof(result), v) +template err*(v: auto): auto = err(typeof(result), v) + +template isOk*(self: Result): bool = self.o +template isErr*(self: Result): bool = not self.o + +template isSome*(o: Opt): bool = + ## Alias for `isOk` + isOk o + +template isNone*(o: Opt): bool = + ## Alias of `isErr` + isErr o + +func map*[T, E, A]( + self: Result[T, E], f: proc(x: T): A): Result[A, E] {.inline.} = + ## Transform value using f, or return error + ## + ## ``` + ## let r = Result[int, cstring).ok(42) + ## assert r.map(proc (v: int): int = $v).get() == "42" + ## ``` + if self.o: result.ok(f(self.v)) + else: result.err(self.e) + +func flatMap*[T, E, A]( + self: Result[T, E], f: proc(x: T): Result[A, E]): Result[A, E] {.inline.} = + if self.o: f(self.v) + else: Result[A, E].err(self.e) + +func mapErr*[T: not void, E, A]( + self: Result[T, E], f: proc(x: E): A): Result[T, A] {.inline.} = + ## Transform error using f, or return value + if self.o: result.ok(self.v) + else: result.err(f(self.e)) + +func mapConvert*[T0, E0]( + self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} = + ## Convert result value to A using an conversion + # Would be nice if it was automatic... + if self.o: result.ok(T1(self.v)) + else: result.err(self.e) + +func mapCast*[T0, E0]( + self: Result[T0, E0], T1: type): Result[T1, E0] {.inline.} = + ## Convert result value to A using a cast + ## Would be nice with nicer syntax... + if self.o: result.ok(cast[T1](self.v)) + else: result.err(self.e) + +template `and`*[T0, E, T1](self: Result[T0, E], other: Result[T1, E]): Result[T1, E] = + ## Evaluate `other` iff self.isOk, else return error + ## fail-fast - will not evaluate other if a is an error + let s = self + if s.o: + other + else: + when type(self) is type(other): + s + else: + type R = type(other) + err(R, s.e) + +template `or`*[T, E0, E1](self: Result[T, E0], other: Result[T, E1]): Result[T, E1] = + ## Evaluate `other` iff `not self.isOk`, else return `self` + ## fail-fast - will not evaluate `other` if `self` is ok + ## + ## ``` + ## func f(): Result[int, SomeEnum] = + ## f2() or err(EnumValue) # Collapse errors from other module / function + ## ``` + let s = self + if s.o: + when type(self) is type(other): + s + else: + type R = type(other) + ok(R, s.v) + else: + other + +template catch*(body: typed): Result[type(body), ref CatchableError] = + ## Catch exceptions for body and store them in the Result + ## + ## ``` + ## let r = catch: someFuncThatMayRaise() + ## ``` + type R = Result[type(body), ref CatchableError] + + try: + R.ok(body) + except CatchableError as e: + R.err(e) + +template capture*[E: Exception](T: type, someExceptionExpr: ref E): Result[T, ref E] = + ## Evaluate someExceptionExpr and put the exception into a result, making sure + ## to capture a call stack at the capture site: + ## + ## ``` + ## let e: Result[void, ValueError] = void.capture((ref ValueError)(msg: "test")) + ## echo e.error().getStackTrace() + ## ``` + type R = Result[T, ref E] + + var ret: R + try: + # TODO is this needed? I think so, in order to grab a call stack, but + # haven't actually tested... + if true: + # I'm sure there's a nicer way - this just works :) + raise someExceptionExpr + except E as caught: + ret = R.err(caught) + ret + +func `==`*[T0, E0, T1, E1](lhs: Result[T0, E0], rhs: Result[T1, E1]): bool {.inline.} = + if lhs.o != rhs.o: + false + elif lhs.o: # and rhs.o implied + lhs.v == rhs.v + else: + lhs.e == rhs.e + +func get*[T: not void, E](self: Result[T, E]): T {.inline.} = + ## Fetch value of result if set, or raise Defect + ## Exception bridge mode: raise given Exception instead + ## See also: Option.get + assertOk(self) + self.v + +func tryGet*[T: not void, E](self: Result[T, E]): T {.inline.} = + ## Fetch value of result if set, or raise + ## When E is an Exception, raise that exception - otherwise, raise a ResultError[E] + mixin raiseResultError + if not self.o: self.raiseResultError() + self.v + +func get*[T, E](self: Result[T, E], otherwise: T): T {.inline.} = + ## Fetch value of result if set, or return the value `otherwise` + ## See `valueOr` for a template version that avoids evaluating `otherwise` + ## unless necessary + if self.o: self.v + else: otherwise + +func get*[T, E](self: var Result[T, E]): var T {.inline.} = + ## Fetch value of result if set, or raise Defect + ## Exception bridge mode: raise given Exception instead + ## See also: Option.get + assertOk(self) + self.v + +template `[]`*[T: not void, E](self: Result[T, E]): T = + ## Fetch value of result if set, or raise Defect + ## Exception bridge mode: raise given Exception instead + mixin get + self.get() + +template `[]`*[T, E](self: var Result[T, E]): var T = + ## Fetch value of result if set, or raise Defect + ## Exception bridge mode: raise given Exception instead + mixin get + self.get() + +template unsafeGet*[T, E](self: Result[T, E]): T = + ## Fetch value of result if set, undefined behavior if unset + ## See also: Option.unsafeGet + assert self.o + + self.v + +func expect*[T: not void, E](self: Result[T, E], m: string): T = + ## Return value of Result, or raise a `Defect` with the given message - use + ## this helper to extract the value when an error is not expected, for example + ## because the program logic dictates that the operation should never fail + ## + ## ```nim + ## let r = Result[int, int].ok(42) + ## # Put here a helpful comment why you think this won't fail + ## echo r.expect("r was just set to ok(42)") + ## ``` + if not self.o: + raiseResultDefect(m, self.e) + self.v + +func expect*[T: not void, E](self: var Result[T, E], m: string): var T = + if not self.o: + raiseResultDefect(m, self.e) + self.v + +func `$`*(self: Result): string = + ## Returns string representation of `self` + if self.o: "Ok(" & $self.v & ")" + else: "Err(" & $self.e & ")" + +func error*[T, E](self: Result[T, E]): E = + ## Fetch error of result if set, or raise Defect + if self.o: + when T is not void: + raiseResultDefect("Trying to access error when value is set", self.v) + else: + raise (ref ResultDefect)(msg: "Trying to access error when value is set") + self.e + +template value*[T, E](self: Result[T, E]): T = + mixin get + self.get() + +template value*[T, E](self: var Result[T, E]): T = + mixin get + self.get() + +template valueOr*[T, E](self: Result[T, E], def: T): T = + ## Fetch value of result if set, or supplied default + ## default will not be evaluated iff value is set + if self.o: self.v + else: def + +# void support + +template ok*[E](R: type Result[void, E]): auto = + ## Initialize a result with a success and value + ## Example: `Result[int, string].ok(42)` + R(o: true) + +template ok*[E](self: var Result[void, E]) = + ## Set the result to success and update value + ## Example: `result.ok(42)` + mixin ok + self = (type self).ok() + +template ok*(): auto = + mixin ok + ok(typeof(result)) + +template err*(): auto = + mixin err + err(typeof(result)) + +# TODO: +# Supporting `map` and `get` operations on a `void` result is quite +# an unusual API. We should provide some motivating examples. + +func map*[E, A]( + self: Result[void, E], f: proc(): A): Result[A, E] {.inline.} = + ## Transform value using f, or return error + if self.o: result.ok(f()) + else: result.err(self.e) + +func flatMap*[E, A]( + self: Result[void, E], f: proc(): Result[A, E]): Result[A, E] {.inline.} = + if self.o: f(self.v) + else: Result[A, E].err(self.e) + +func mapErr*[E, A]( + self: Result[void, E], f: proc(x: E): A): Result[void, A] {.inline.} = + ## Transform error using f, or return value + if self.o: result.ok() + else: result.err(f(self.e)) + +func map*[T, E]( + self: Result[T, E], f: proc(x: T)): Result[void, E] {.inline.} = + ## Transform value using f, or return error + if self.o: f(self.v); result.ok() + else: result.err(self.e) + +func get*[E](self: Result[void, E]) {.inline.} = + ## Fetch value of result if set, or raise + ## See also: Option.get + mixin assertOk + assertOk(self) + +func tryGet*[E](self: Result[void, E]) {.inline.} = + ## Fetch value of result if set, or raise a CatchableError + mixin raiseResultError + if not self.o: + self.raiseResultError() + +template `[]`*[E](self: Result[void, E]) = + ## Fetch value of result if set, or raise + mixin get + self.get() + +template unsafeGet*[E](self: Result[void, E]) = + ## Fetch value of result if set, undefined behavior if unset + ## See also: Option.unsafeGet + assert self.o + +func expect*[E](self: Result[void, E], msg: string) = + if not self.o: + raise (ref ResultDefect)(msg: msg) + +func `$`*[E](self: Result[void, E]): string = + ## Returns string representation of `self` + if self.o: "Ok()" + else: "Err(" & $self.e & ")" + +template value*[E](self: Result[void, E]) = + mixin get + self.get() + +template value*[E](self: var Result[void, E]) = + mixin get + self.get() + +template `?`*[T, E](self: Result[T, E]): T = + ## Early return - if self is an error, we will return from the current + ## function, else we'll move on.. + ## + ## ``` + ## let v = ? funcWithResult() + ## echo v # prints value, not Result! + ## ``` + ## Experimental + # TODO the v copy is here to prevent multiple evaluations of self - could + # probably avoid it with some fancy macro magic.. + let v = (self) + if not v.o: + when typeof(result) is typeof(v): + return v + else: + return err(typeof(result), v.e) + + v.v diff --git a/tests/test_apng.nim b/tests/test_apng.nim index 1bbc475..7997d3c 100644 --- a/tests/test_apng.nim +++ b/tests/test_apng.nim @@ -50,7 +50,7 @@ proc convert(dir: string) = proc generateAPNG() = const numFrames = 7 - var frames: array[numFrames, PNGResult] + var frames: array[numFrames, PNGResult[string]] for i in 0.. Date: Fri, 5 Jun 2020 15:12:21 +0700 Subject: [PATCH 4/5] encodePNG new API --- nimPNG.nim | 176 +++++++++++++++++++++++++++++++------------ tests/test_codec.nim | 6 +- 2 files changed, 132 insertions(+), 50 deletions(-) diff --git a/nimPNG.nim b/nimPNG.nim index 1b44293..ce6f498 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -208,12 +208,12 @@ type minute*: int #range[0..59] second*: int #range[0..60] #to allow for leap seconds - PNG* = ref object + PNG*[T] = ref object # during encoding, settings is PNGEncoder # during decoding, settings is PNGDecoder settings*: PNGSettings chunks*: seq[PNGChunk] - pixels*: string + pixels*: T # w & h used during encoding process width*, height*: int # during encoding, apngChunks contains only fcTL chunks @@ -221,7 +221,7 @@ type apngChunks*: seq[APNGFrameChunk] firstFrameIsDefaultImage*: bool isAPNG*: bool - apngPixels*: seq[string] + apngPixels*: seq[T] APNGFrame*[T] = ref object ctl*: APNGFramecontrol @@ -872,8 +872,8 @@ proc makePNGDecoder*(): PNGDecoder = s.ignoreAdler32 = false result = s -proc parsePNG(s: Stream, settings: PNGDecoder): PNG = - var png: PNG +proc parsePNG[T](s: Stream, settings: PNGDecoder): PNG[T] = + var png: PNG[T] new(png) png.chunks = @[] png.apngChunks = @[] @@ -1662,7 +1662,7 @@ proc convertImpl*[T](output: var openArray[T], input: openArray[T], modeOut, mod cvt(p, input, px, modeIn) pxl(p, output, px, modeOut, tree) -proc convert*[T](png: PNG, colorType: PNGColorType, bitDepth: int): PNGResult[T] = +proc convert*[T](png: PNG[T], colorType: PNGColorType, bitDepth: int): PNGResult[T] = # TODO: check if this works according to the statement in the documentation: "The converter can convert # from greyscale input color type, to 8-bit greyscale or greyscale with alpha" # if(colorType notin {LCT_RGB, LCT_RGBA}) and (bitDepth != 8): @@ -1690,7 +1690,7 @@ proc convert*[T](png: PNG, colorType: PNGColorType, bitDepth: int): PNGResult[T] png.pixels.toOpenArray(0, png.pixels.len-1), modeOut, modeIn, numPixels) -proc convert*[T](png: PNG, colorType: PNGColorType, bitDepth: int, ctl: APNGFrameControl, data: string): APNGFrame[T] = +proc convert*[T](png: PNG[T], colorType: PNGColorType, bitDepth: int, ctl: APNGFrameControl, data: string): APNGFrame[T] = let modeIn = png.getColorMode() let modeOut = newColorMode(colorType, bitDepth) let size = getRawSize(ctl.width, ctl.height, modeOut) @@ -1719,7 +1719,7 @@ proc toString(chunk: APNGFrameData): string = type APNG[T] = ref object - png: PNG + png: PNG[T] result: PNGResult[T] proc processingAPNG[T](apng: APNG[T], colorType: PNGColorType, bitDepth: int) = @@ -1791,7 +1791,7 @@ proc decodePNG*(T: type, s: Stream, colorType: PNGColorType, bitDepth: int, sett if not bitDepthAllowed(colorType, bitDepth): raise PNGFatal("colorType and bitDepth combination not allowed") - var png = s.parsePNG(settings) + var png = parsePNG[T](s, settings) png.postProcessScanLines() if PNGDecoder(png.settings).colorConvert: @@ -1807,8 +1807,8 @@ proc decodePNG*(T: type, s: Stream, colorType: PNGColorType, bitDepth: int, sett var apng = APNG[T](png: png, result: result) apng.processingAPNG(colorType, bitDepth) -proc decodePNG*(T: type, s: Stream, settings = PNGDecoder(nil)): PNG = - var png = s.parsePNG(settings) +proc decodePNG*(T: type, s: Stream, settings = PNGDecoder(nil)): PNG[T] = + var png = parsePNG[T](s, settings) png.postProcessScanLines() if png.isAPNG: @@ -1865,10 +1865,10 @@ proc decodePNG24*(T: type, input: T, settings = PNGDecoder(nil)): PNGRes[T] = result.err(getCurrentExceptionMsg()) # these are legacy API -template decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): untyped = +proc decodePNG*(s: Stream, colorType: PNGColorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult[string] = decodePNG(string, s, colorType, bitDepth, settings) -template decodePNG*(s: Stream, settings = PNGDecoder(nil)): untyped = +proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG[string] = decodePNG(string, s, settings) when not defined(js): @@ -2211,6 +2211,8 @@ method writeChunk(chunk: PNGHist, png: PNG): bool = method writeChunk(chunk: PNGData, png: PNG): bool = var nz = nzDeflateInit(chunk.idat) chunk.data = zlib_compress(nz) + debugEcho "IDAT.IDAT: ", chunk.idat.len + debugEcho "IDAT.DATA: ", chunk.data.len result = true method writeChunk(chunk: PNGZtxt, png: PNG): bool = @@ -2725,7 +2727,7 @@ proc addChunkfdAT(png: PNG, sequenceNumber, frameDataPos: int) = chunk.frameDataPos = frameDataPos png.chunks.add chunk -proc frameConvert[T](png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) = +proc frameConvert[T](png: PNG[T], modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) = template input: untyped = png.apngPixels[frameNo] if modeIn != modeOut: @@ -2743,7 +2745,7 @@ proc frameConvert[T](png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int else: preProcessScanLines(png, input.toOpenArray(0, input.len-1), frameNo, w, h, modeOut, state) -proc encoderCore[T](png: PNG) = +proc encoderCore[T](png: PNG[T]) = let state = PNGEncoder(png.settings) var modeIn = newColorMode(state.modeIn) var modeOut = newColorMode(state.modeOut) @@ -2848,8 +2850,8 @@ proc encoderCore[T](png: PNG) = png.addChunkIEND() -proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = - var png: PNG +proc encodePNG*[T](input: T, w, h: int, settings = PNGEncoder(nil)): PNG[T] = + var png: PNG[T] new(png) png.chunks = @[] @@ -2859,10 +2861,10 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = png.width = w png.height = h shallowCopy(png.pixels, input) - encoderCore[string](png) + encoderCore[T](png) result = png -proc encodePNG*(input: string, colorType: PNGColorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG = +proc encodePNG*[T](input: T, colorType: PNGColorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG[T] = if not bitDepthAllowed(colorType, bitDepth): raise PNGFatal("colorType and bitDepth combination not allowed") @@ -2874,13 +2876,13 @@ proc encodePNG*(input: string, colorType: PNGColorType, bitDepth, w, h: int, set state.modeIn.bitDepth = bitDepth result = encodePNG(input, w, h, state) -proc encodePNG32*(input: string, w, h: int): PNG = +proc encodePNG32*[T](input: T, w, h: int): PNG[T] = result = encodePNG(input, LCT_RGBA, 8, w, h) -proc encodePNG24*(input: string, w, h: int): PNG = +proc encodePNG24*[T](input: T, w, h: int): PNG[T] = result = encodePNG(input, LCT_RGB, 8, w, h) -proc writeChunks*(png: PNG, s: Stream) = +proc writeChunks*[T](png: PNG[T], s: Stream) = s.write PNGSignature for chunk in png.chunks: @@ -2894,25 +2896,28 @@ proc writeChunks*(png: PNG, s: Stream) = s.write chunk.data s.writeInt32BE cast[int](chunk.crc) +type + PNGStatus* = Result[void, string] + PNGBytes*[T] = Result[T, string] + when not defined(js): - proc savePNG*(fileName, input: string, colorType: PNGColorType, bitDepth, w, h: int): bool = + proc savePNGImpl*[T](fileName: string, input: T, colorType: PNGColorType, bitDepth, w, h: int): PNGStatus = try: var png = encodePNG(input, colorType, bitDepth, w, h) var s = newFileStream(fileName, fmWrite) png.writeChunks s s.close() - result = true + result.ok() except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() - result = false + result.err(getCurrentExceptionMsg()) - proc savePNG32*(fileName, input: string, w, h: int): bool = - result = savePNG(fileName, input, LCT_RGBA, 8, w, h) + proc savePNG32Impl*[T](fileName: string, input: T, w, h: int): PNGStatus = + savePNGImpl(fileName, input, LCT_RGBA, 8, w, h) - proc savePNG24*(fileName, input: string, w, h: int): bool = - result = savePNG(fileName, input, LCT_RGB, 8, w, h) + proc savePNG24Impl*[T](fileName: string, input: T, w, h: int): PNGStatus = + savePNGImpl(fileName, input, LCT_RGB, 8, w, h) -proc prepareAPNG*(colorType: PNGColorType, bitDepth, numPlays: int, settings = PNGEncoder(nil)): PNG = +proc prepareAPNG*(T: type, colorType: PNGColorType, bitDepth, numPlays: int, settings = PNGEncoder(nil)): PNG[T] = var state: PNGEncoder if settings == nil: state = makePNGEncoder() else: state = settings @@ -2921,7 +2926,7 @@ proc prepareAPNG*(colorType: PNGColorType, bitDepth, numPlays: int, settings = P state.modeIn.colorType = colorType state.modeIn.bitDepth = bitDepth - var png: PNG + var png: PNG[T] new(png) png.chunks = @[] png.settings = state @@ -2935,13 +2940,13 @@ proc prepareAPNG*(colorType: PNGColorType, bitDepth, numPlays: int, settings = P result = png -proc prepareAPNG24*(numPlays = 0): PNG = - result = prepareAPNG(LCT_RGB, 8, numPlays) +proc prepareAPNG24*(T: type, numPlays = 0): PNG[T] = + prepareAPNG(T, LCT_RGB, 8, numPlays) -proc prepareAPNG32*(numPlays = 0): PNG = - result = prepareAPNG(LCT_RGBA, 8, numPlays) +proc prepareAPNG32*(T: type, numPlays = 0): PNG[T] = + prepareAPNG(T, LCT_RGBA, 8, numPlays) -proc addDefaultImage*(png: PNG, input: string, width, height: int, ctl = APNGFrameControl(nil)): bool = +proc addDefaultImage*[T](png: PNG[T], input: T, width, height: int, ctl = APNGFrameControl(nil)): bool = result = true png.firstFrameIsDefaultImage = ctl != nil if ctl != nil: @@ -2959,7 +2964,7 @@ proc addDefaultImage*(png: PNG, input: string, width, height: int, ctl = APNGFra png.width = width png.height = height -proc addFrame*(png: PNG, frame: string, ctl: APNGFrameControl): bool = +proc addFrame*[T](png: PNG[T], frame: T, ctl: APNGFrameControl): bool = result = true # addDefaultImage must be called first @@ -2976,27 +2981,104 @@ proc addFrame*(png: PNG, frame: string, ctl: APNGFrameControl): bool = png.apngPixels.add frame png.apngChunks.add ctl -proc encodeAPNG*(png: PNG): string = +proc encodeAPNGImpl*[T](png: PNG[T]): PNGBytes[T] = try: - encoderCore[string](png) + encoderCore[T](png) var s = newStringStream() png.writeChunks s - result = s.data + when T is string: + result.ok(s.data) + else: + result.ok(cast[seq[byte]](s.data)) except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() - result = "" + result.err(getCurrentExceptionMsg()) when not defined(js): - proc saveAPNG*(png: PNG, fileName: string): bool = + proc saveAPNGImpl*[T](png: PNG[T], fileName: string): PNGStatus = try: - encoderCore[string](png) + encoderCore[T](png) var s = newFileStream(fileName, fmWrite) png.writeChunks s s.close() - result = true + result.ok() except PNGError, IOError, NZError: - debugEcho getCurrentExceptionMsg() + result.err(getCurrentExceptionMsg()) + +when not defined(js): + proc savePNGLegacy*(fileName, input: string, colorType: PNGColorType, bitDepth, w, h: int): bool = + let res = savePNGImpl(fileName, input, colorType, bitDepth, w, h) + if res.isOk: result = true + else: result = false + debugEcho res.error() + + proc savePNG32Legacy*(fileName, input: string, w, h: int): bool = + let res = savePNG32Impl(fileName, input, w, h) + if res.isOk: result = true + else: + result = false + debugEcho res.error() + + proc savePNG24Legacy*(fileName, input: string, w, h: int): bool = + let res = savePNG24Impl(fileName, input, w, h) + if res.isOk: result = true + else: + result = false + debugEcho res.error() + + template savePNG*[T](fileName: string, input: T, colorType: PNGColorType, bitDepth, w, h: int): untyped = + when T is string: + savePNGLegacy(fileName, input, colorType, bitDepth, w , h) + else: + savePNGImpl(fileName, input, colorType, bitDepth, w , h) + + template savePNG32*[T](fileName: string, input: T, w, h: int): untyped = + when T is string: + savePNG32Legacy(fileName, input, w, h) + else: + savePNG32Impl(fileName, input, w, h) + + template savePNG24*[T](fileName: string, input: T, w, h: int): untyped = + when T is string: + savePNG24Legacy(fileName, input, w, h) + else: + savePNG24Impl(fileName, input, w, h) + +proc prepareAPNG*(colorType: PNGColorType, bitDepth, numPlays: int, settings = PNGEncoder(nil)): PNG[string] = + prepareAPNG(string, colorType, bitDepth, numPlays, settings) + +proc prepareAPNG24*(numPlays = 0): PNG[string] = + prepareAPNG24(string, numPlays) + +proc prepareAPNG32*(numPlays = 0): PNG[string] = + prepareAPNG32(string, numPlays) + +proc encodeAPNGLegacy*[T](png: PNG[T]): string = + let res = encodeAPNGImpl(png) + if res.isOk: + result = res.get() + else: + debugEcho res.error() + +template encodeAPNG*[T](png: PNG[T]): untyped = + when T is string: + encodeAPNGLegacy(png) + else: + encodeAPNGImpl(png) + +when not defined(js): + proc saveAPNGLegacy*[T](png: PNG[T], fileName: string): bool = + let res = saveAPNGImpl(png, fileName) + if res.isOk: result = true + else: + result = false + debugEcho res.error() + + template saveAPNG*[T](png: PNG[T], fileName: string): untyped = + when T is string: + savePNGLegacy(png, fileName) + else: + savePNGImpl(png, fileName) proc getFilterTypesInterlaced(png: PNG): seq[seq[PNGFilter]] = var header = PNGHeader(png.getChunk(IHDR)) diff --git a/tests/test_codec.nim b/tests/test_codec.nim index 024edc4..2ed4e97 100644 --- a/tests/test_codec.nim +++ b/tests/test_codec.nim @@ -560,7 +560,7 @@ proc testColorKeyConvert() = var png = s.decodePNG() var info = png.getInfo() - var image2 = convert[string](png, LCT_RGBA, 8) + var image2 = convert(png, LCT_RGBA, 8) assertEquals(32 , info.width) assertEquals(32 , info.height) @@ -759,7 +759,7 @@ proc doRGBAToPaletteTest(palette: openArray[int], expectedType = LCT_PALETTE) = s.setPosition 0 var png2 = s.decodePNG() var info = png2.getInfo() - var image2 = convert[string](png2, LCT_RGBA, 8) + var image2 = convert(png2, LCT_RGBA, 8) assertEquals(image2.data, image) @@ -908,7 +908,7 @@ proc testAutoColorModel(colors: string, inbitDepth: int, colorType: PNGcolorType s.setPosition 0 var raw = s.decodePNG() var info = raw.getInfo() - var decoded = convert[string](raw, LCT_RGBA, inbitdepth) + var decoded = convert(raw, LCT_RGBA, inbitdepth) assertEquals(num , info.width) assertEquals(1 , info.height) From fcbc03c7245fa006382fa55404327ce6c2db4943 Mon Sep 17 00:00:00 2001 From: jangko Date: Fri, 5 Jun 2020 15:40:24 +0700 Subject: [PATCH 5/5] get rid of method --- nimPNG.nim | 187 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 120 insertions(+), 67 deletions(-) diff --git a/nimPNG.nim b/nimPNG.nim index ce6f498..5a598f8 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -414,10 +414,7 @@ proc bitDepthAllowed(colorType: PNGColorType, bitDepth: int): bool = of LCT_PALETTE: result = bitDepth in {1, 2, 4, 8} else: result = bitDepth in {8, 16} -method validateChunk(chunk: PNGChunk, png: PNG): bool {.base, gcsafe.} = true -method parseChunk(chunk: PNGChunk, png: PNG): bool {.base, gcsafe.} = true - -method validateChunk(header: PNGHeader, png: PNG): bool = +proc validateChunk(header: PNGHeader, png: PNG): bool = if header.width < 1 or header.width > 0x7FFFFFFF: raise PNGFatal("image width not allowed: " & $header.width) if header.height < 1 or header.height > 0x7FFFFFFF: @@ -434,7 +431,7 @@ method validateChunk(header: PNGHeader, png: PNG): bool = raise PNGFatal("unsupported interlace method") result = true -method parseChunk(chunk: PNGHeader, png: PNG): bool = +proc parseChunk(chunk: PNGHeader, png: PNG): bool = if chunk.length != 13: return false chunk.width = chunk.readInt32() chunk.height = chunk.readInt32() @@ -445,7 +442,7 @@ method parseChunk(chunk: PNGHeader, png: PNG): bool = chunk.interlaceMethod = PNGInterlace(chunk.readByte()) result = true -method parseChunk(chunk: PNGPalette, png: PNG): bool = +proc parseChunk(chunk: PNGPalette, png: PNG): bool = let paletteSize = chunk.length div 3 if paletteSize > 256: raise PNGFatal("palette size to big") newSeq(chunk.palette, paletteSize) @@ -485,7 +482,7 @@ proc getRawSize(w, h: int, color: PNGColorMode): int = #proc getRawSizeLct(w, h: int, colorType: PNGColorType, bitDepth: int): int = # result = (w * h * LCTBPP(colorType, bitDepth) + 7) div 8 -method validateChunk(chunk: PNGData, png: PNG): bool = +proc validateChunk(chunk: PNGData, png: PNG): bool = var header = PNGHeader(png.getChunk(IHDR)) var predict = 0 @@ -507,13 +504,13 @@ method validateChunk(chunk: PNGData, png: PNG): bool = if chunk.idat.len != predict: raise PNGFatal("Decompress size doesn't match predict") result = true -method parseChunk(chunk: PNGData, png: PNG): bool = +proc parseChunk(chunk: PNGData, png: PNG): bool = var nz = nzInflateInit(chunk.data) nz.ignoreAdler32 = PNGDecoder(png.settings).ignoreAdler32 chunk.idat = zlib_decompress(nz) result = true -method parseChunk(chunk: PNGTrans, png: PNG): bool = +proc parseChunk(chunk: PNGTrans, png: PNG): bool = var header = PNGHeader(png.getChunk(IHDR)) if header == nil: return false @@ -542,7 +539,7 @@ method parseChunk(chunk: PNGTrans, png: PNG): bool = result = true -method parseChunk(chunk: PNGBackground, png: PNG): bool = +proc parseChunk(chunk: PNGBackground, png: PNG): bool = var header = PNGHeader(png.getChunk(IHDR)) if header.colorType == LCT_PALETTE: # error: this chunk must be 1 byte for indexed color image @@ -571,7 +568,7 @@ proc initChunk(chunk: PNGChunk, chunkType: PNGChunkType, data: string, crc: uint chunk.data = data chunk.pos = 0 -method validateChunk(chunk: PNGTime, png: PNG): bool = +proc validateChunk(chunk: PNGTime, png: PNG): bool = if chunk.year < 0 or chunk.year > 65535: raise PNGFatal("invalid year range[0..65535]") if chunk.month < 1 or chunk.month > 12: raise PNGFatal("invalid month range[1..12]") if chunk.day < 1 or chunk.day > 31: raise PNGFatal("invalid day range[1..32]") @@ -581,7 +578,7 @@ method validateChunk(chunk: PNGTime, png: PNG): bool = if chunk.second < 0 or chunk.second > 60: raise PNGFatal("invalid second range[0..60]") result = true -method parseChunk(chunk: PNGTime, png: PNG): bool = +proc parseChunk(chunk: PNGTime, png: PNG): bool = if chunk.length != 7: raise PNGFatal("tIME must be 7 bytes") chunk.year = chunk.readInt16() chunk.month = chunk.readByte() @@ -591,19 +588,19 @@ method parseChunk(chunk: PNGTime, png: PNG): bool = chunk.second = chunk.readByte() result = true -method parseChunk(chunk: PNGPhys, png: PNG): bool = +proc parseChunk(chunk: PNGPhys, png: PNG): bool = if chunk.length != 9: raise PNGFatal("pHYs must be 9 bytes") chunk.physX = chunk.readInt32() chunk.physY = chunk.readInt32() chunk.unit = chunk.readByte() result = true -method validateChunk(chunk: PNGText, png: PNG): bool = +proc validateChunk(chunk: PNGText, png: PNG): bool = if(chunk.keyword.len < 1) or (chunk.keyword.len > 79): raise PNGFatal("keyword too short or too long") result = true -method parseChunk(chunk: PNGText, png: PNG): bool = +proc parseChunk(chunk: PNGText, png: PNG): bool = var len = 0 while(len < chunk.length) and (chunk.data[len] != chr(0)): inc len if(len < 1) or (len > 79): raise PNGFatal("keyword too short or too long") @@ -613,19 +610,19 @@ method parseChunk(chunk: PNGText, png: PNG): bool = chunk.text = chunk.data.substr(textBegin) result = true -method validateChunk(chunk: PNGZtxt, png: PNG): bool = +proc validateChunk(chunk: PNGZtxt, png: PNG): bool = if(chunk.keyword.len < 1) or (chunk.keyword.len > 79): raise PNGFatal("keyword too short or too long") result = true -method parseChunk(chunk: PNGZtxt, png: PNG): bool = +proc parseChunk(chunk: PNGZtxt, png: PNG): bool = var len = 0 while(len < chunk.length) and (chunk.data[len] != chr(0)): inc len if(len < 1) or (len > 79): raise PNGFatal("keyword too short or too long") chunk.keyword = chunk.data.substr(0, len) - var compMethod = ord(chunk.data[len + 1]) # skip keyword null terminator - if compMethod != 0: raise PNGFatal("unsupported comp method") + var compproc = ord(chunk.data[len + 1]) # skip keyword null terminator + if compproc != 0: raise PNGFatal("unsupported comp proc") var nz = nzInflateInit(chunk.data.substr(len + 2)) nz.ignoreAdler32 = PNGDecoder(png.settings).ignoreAdler32 @@ -633,12 +630,12 @@ method parseChunk(chunk: PNGZtxt, png: PNG): bool = result = true -method validateChunk(chunk: PNGItxt, png: PNG): bool = +proc validateChunk(chunk: PNGItxt, png: PNG): bool = if(chunk.keyword.len < 1) or (chunk.keyword.len > 79): raise PNGFatal("keyword too short or too long") result = true -method parseChunk(chunk: PNGItxt, png: PNG): bool = +proc parseChunk(chunk: PNGItxt, png: PNG): bool = if chunk.length < 5: raise PNGFatal("iTXt len too short") var len = 0 @@ -649,8 +646,8 @@ method parseChunk(chunk: PNGItxt, png: PNG): bool = chunk.keyword = chunk.data.substr(0, len) var compressed = ord(chunk.data[len + 1]) == 1 # skip keyword null terminator - var compMethod = ord(chunk.data[len + 2]) - if compMethod != 0: raise PNGFatal("unsupported comp method") + var compproc = ord(chunk.data[len + 2]) + if compproc != 0: raise PNGFatal("unsupported comp proc") len = 0 var i = len + 3 @@ -677,12 +674,12 @@ method parseChunk(chunk: PNGItxt, png: PNG): bool = chunk.text = chunk.data.substr(textBegin) result = true -method parseChunk(chunk: PNGGamma, png: PNG): bool = +proc parseChunk(chunk: PNGGamma, png: PNG): bool = if chunk.length != 4: raise PNGFatal("invalid gAMA length") chunk.gamma = chunk.readInt32() result = true -method parseChunk(chunk: PNGChroma, png: PNG): bool = +proc parseChunk(chunk: PNGChroma, png: PNG): bool = if chunk.length != 32: raise PNGFatal("invalid Chroma length") chunk.whitePointX = chunk.readInt32() chunk.whitePointY = chunk.readInt32() @@ -694,31 +691,31 @@ method parseChunk(chunk: PNGChroma, png: PNG): bool = chunk.blueY = chunk.readInt32() result = true -method parseChunk(chunk: PNGStandarRGB, png: PNG): bool = +proc parseChunk(chunk: PNGStandarRGB, png: PNG): bool = if chunk.length != 1: raise PNGFatal("invalid sRGB length") chunk.renderingIntent = chunk.readByte() result = true -method validateChunk(chunk: PNGICCProfile, png: PNG): bool = +proc validateChunk(chunk: PNGICCProfile, png: PNG): bool = if(chunk.profileName.len < 1) or (chunk.profileName.len > 79): raise PNGFatal("keyword too short or too long") result = true -method parseChunk(chunk: PNGICCProfile, png: PNG): bool = +proc parseChunk(chunk: PNGICCProfile, png: PNG): bool = var len = 0 while(len < chunk.length) and (chunk.data[len] != chr(0)): inc len if(len < 1) or (len > 79): raise PNGFatal("keyword too short or too long") chunk.profileName = chunk.data.substr(0, len) - var compMethod = ord(chunk.data[len + 1]) # skip keyword null terminator - if compMethod != 0: raise PNGFatal("unsupported comp method") + var compproc = ord(chunk.data[len + 1]) # skip keyword null terminator + if compproc != 0: raise PNGFatal("unsupported comp proc") var nz = nzInflateInit(chunk.data.substr(len + 2)) nz.ignoreAdler32 = PNGDecoder(png.settings).ignoreAdler32 chunk.profile = zlib_decompress(nz) result = true -method parseChunk(chunk: PNGSPalette, png: PNG): bool = +proc parseChunk(chunk: PNGSPalette, png: PNG): bool = var len = 0 while(len < chunk.length) and (chunk.data[len] != chr(0)): inc len if(len < 1) or (len > 79): raise PNGFatal("keyword too short or too long") @@ -751,7 +748,7 @@ method parseChunk(chunk: PNGSPalette, png: PNG): bool = result = true -method parseChunk(chunk: PNGHist, png: PNG): bool = +proc parseChunk(chunk: PNGHist, png: PNG): bool = if not png.hasChunk(PLTE): raise PNGFatal("Histogram need PLTE") var plte = PNGPalette(png.getChunk(PLTE)) if plte.palette.len != (chunk.length div 2): raise PNGFatal("invalid histogram length") @@ -760,7 +757,7 @@ method parseChunk(chunk: PNGHist, png: PNG): bool = chunk.histogram[i] = chunk.readInt16() result = true -method parseChunk(chunk: PNGSbit, png: PNG): bool = +proc parseChunk(chunk: PNGSbit, png: PNG): bool = let header = PNGHEader(png.getChunk(IHDR)) var expectedLen = 0 @@ -778,12 +775,12 @@ method parseChunk(chunk: PNGSbit, png: PNG): bool = result = true -method parseChunk(chunk: APNGAnimationControl, png: PNG): bool = +proc parseChunk(chunk: APNGAnimationControl, png: PNG): bool = chunk.numFrames = chunk.readInt32() chunk.numPlays = chunk.readInt32() result = true -method parseChunk(chunk: APNGFrameControl, png: PNG): bool = +proc parseChunk(chunk: APNGFrameControl, png: PNG): bool = chunk.sequenceNumber = chunk.readInt32() chunk.width = chunk.readInt32() chunk.height = chunk.readInt32() @@ -795,7 +792,7 @@ method parseChunk(chunk: APNGFrameControl, png: PNG): bool = chunk.blendOp = chunk.readByte().APNG_BLEND_OP result = true -method validateChunk(chunk: APNGFrameControl, png: PNG): bool = +proc validateChunk(chunk: APNGFrameControl, png: PNG): bool = let header = PNGHEader(png.getChunk(IHDR)) result = true result = result and (chunk.xOffset >= 0) @@ -805,7 +802,7 @@ method validateChunk(chunk: APNGFrameControl, png: PNG): bool = 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 = +proc parseChunk(chunk: APNGFrameData, png: PNG): bool = chunk.sequenceNumber = chunk.readInt32() chunk.frameDataPos = chunk.pos result = true @@ -872,6 +869,42 @@ proc makePNGDecoder*(): PNGDecoder = s.ignoreAdler32 = false result = s +proc parseChunk(chunk: PNGChunk, png: PNG): bool = + case chunk.chunkType + of IHDR: result = parseChunk(PNGHeader(chunk), png) + of PLTE: result = parseChunk(PNGPalette(chunk), png) + of IDAT: result = parseChunk(PNGData(chunk), png) + of tRNS: result = parseChunk(PNGTrans(chunk), png) + of bKGD: result = parseChunk(PNGBackground(chunk), png) + of tIME: result = parseChunk(PNGTime(chunk), png) + of pHYs: result = parseChunk(PNGPhys(chunk), png) + of tEXt: result = parseChunk(PNGTExt(chunk), png) + of zTXt: result = parseChunk(PNGZtxt(chunk), png) + of iTXt: result = parseChunk(PNGItxt(chunk), png) + of gAMA: result = parseChunk(PNGGamma(chunk), png) + of cHRM: result = parseChunk(PNGChroma(chunk), png) + of iCCP: result = parseChunk(PNGICCProfile(chunk), png) + of sRGB: result = parseChunk(PNGStandarRGB(chunk), png) + of sPLT: result = parseChunk(PNGSPalette(chunk), png) + of hIST: result = parseChunk(PNGHist(chunk), png) + of sBIT: result = parseChunk(PNGSbit(chunk), png) + of acTL: result = parseChunk(APNGAnimationControl(chunk), png) + of fcTL: result = parseChunk(APNGFrameControl(chunk), png) + of fdAT: result = parseChunk(APNGFrameData(chunk), png) + else: result = true + +proc validateChunk(chunk: PNGChunk, png: PNG): bool = + case chunk.chunkType + of IHDR: result = validateChunk(PNGHeader(chunk), png) + of IDAT: result = validateChunk(PNGData(chunk), png) + of tIME: result = validateChunk(PNGTime(chunk), png) + of tEXt: result = validateChunk(PNGTExt(chunk), png) + of zTXt: result = validateChunk(PNGZtxt(chunk), png) + of iTXt: result = validateChunk(PNGItxt(chunk), png) + of iCCP: result = validateChunk(PNGICCProfile(chunk), png) + of fcTL: result = validateChunk(APNGFrameControl(chunk), png) + else: result = true + proc parsePNG[T](s: Stream, settings: PNGDecoder): PNG[T] = var png: PNG[T] new(png) @@ -936,7 +969,7 @@ proc postProcessScanLines[T](png: PNG; header: PNGHeader, w, h: int; input, outp # we can immediatly filter into the out buffer, no other steps needed unfilter(output, input, w, h, bpp) - else: # interlace_method is 1 (Adam7) + else: # interlace_proc is 1 (Adam7) var pass: PNGPass adam7PassValues(pass, w, h, bpp) @@ -2070,9 +2103,7 @@ proc writeInt32BE(s: Stream, value: int) = bigEndian32(addr(tmp), addr(val)) s.write(tmp) -method writeChunk(chunk: PNGChunk, png: PNG): bool {.base, gcsafe.} = true - -method writeChunk(chunk: PNGHeader, png: PNG): bool = +proc writeChunk(chunk: PNGHeader, png: PNG): bool = #estimate 13 bytes chunk.writeInt32(chunk.width) chunk.writeInt32(chunk.height) @@ -2083,7 +2114,7 @@ method writeChunk(chunk: PNGHeader, png: PNG): bool = chunk.writeByte(int(chunk.interlaceMethod)) result = true -method writeChunk(chunk: PNGPalette, png: PNG): bool = +proc writeChunk(chunk: PNGPalette, png: PNG): bool = #estimate 3 * palette.len for px in chunk.palette: chunk.writeByte(int(px.r)) @@ -2091,7 +2122,7 @@ method writeChunk(chunk: PNGPalette, png: PNG): bool = chunk.writeByte(int(px.b)) result = true -method writeChunk(chunk: PNGTrans, png: PNG): bool = +proc writeChunk(chunk: PNGTrans, png: PNG): bool = var header = PNGHeader(png.getChunk(IHDR)) if header.colorType == LCT_PALETTE: @@ -2116,7 +2147,7 @@ method writeChunk(chunk: PNGTrans, png: PNG): bool = raise PNGFatal("tRNS chunk not allowed for other color models") result = true -method writeChunk(chunk: PNGBackground, png: PNG): bool = +proc writeChunk(chunk: PNGBackground, png: PNG): bool = var header = PNGHeader(png.getChunk(IHDR)) if header.colorType == LCT_PALETTE: #estimate 1 bytes @@ -2131,7 +2162,7 @@ method writeChunk(chunk: PNGBackground, png: PNG): bool = chunk.writeInt16(chunk.bkgdB) result = true -method writeChunk(chunk: PNGTime, png: PNG): bool = +proc writeChunk(chunk: PNGTime, png: PNG): bool = #estimate 7 bytes chunk.writeInt16(chunk.year) chunk.writeByte(chunk.month) @@ -2141,26 +2172,26 @@ method writeChunk(chunk: PNGTime, png: PNG): bool = chunk.writeByte(chunk.second) result = true -method writeChunk(chunk: PNGPhys, png: PNG): bool = +proc writeChunk(chunk: PNGPhys, png: PNG): bool = #estimate 9 bytes chunk.writeInt32(chunk.physX) chunk.writeInt32(chunk.physY) chunk.writeByte(chunk.unit) result = true -method writeChunk(chunk: PNGText, png: PNG): bool = +proc writeChunk(chunk: PNGText, png: PNG): bool = #estimate chunk.keyword.len + chunk.text.len + 1 chunk.writeString chunk.keyword chunk.writeByte 0 #null separator chunk.writeString chunk.text result = true -method writeChunk(chunk: PNGGamma, png: PNG): bool = +proc writeChunk(chunk: PNGGamma, png: PNG): bool = #estimate 4 bytes chunk.writeInt32(chunk.gamma) result = true -method writeChunk(chunk: PNGChroma, png: PNG): bool = +proc writeChunk(chunk: PNGChroma, png: PNG): bool = #estimate 8 * 4 bytes chunk.writeInt32(chunk.whitePointX) chunk.writeInt32(chunk.whitePointY) @@ -2172,12 +2203,12 @@ method writeChunk(chunk: PNGChroma, png: PNG): bool = chunk.writeInt32(chunk.blueY) result = true -method writeChunk(chunk: PNGStandarRGB, png: PNG): bool = +proc writeChunk(chunk: PNGStandarRGB, png: PNG): bool = #estimate 1 byte chunk.writeByte(chunk.renderingIntent) result = true -method writeChunk(chunk: PNGSPalette, png: PNG): bool = +proc writeChunk(chunk: PNGSPalette, png: PNG): bool = #estimate chunk.paletteName.len + 2 #if sampleDepth == 8: estimate += chunk.palette.len * 6 #else: estimate += chunk.palette.len * 10 @@ -2202,29 +2233,27 @@ method writeChunk(chunk: PNGSPalette, png: PNG): bool = chunk.writeInt16(p.frequency) result = true -method writeChunk(chunk: PNGHist, png: PNG): bool = +proc writeChunk(chunk: PNGHist, png: PNG): bool = #estimate chunk.histogram.len * 2 for c in chunk.histogram: chunk.writeInt16 c result = true -method writeChunk(chunk: PNGData, png: PNG): bool = +proc writeChunk(chunk: PNGData, png: PNG): bool = var nz = nzDeflateInit(chunk.idat) chunk.data = zlib_compress(nz) - debugEcho "IDAT.IDAT: ", chunk.idat.len - debugEcho "IDAT.DATA: ", chunk.data.len result = true -method writeChunk(chunk: PNGZtxt, png: PNG): bool = +proc writeChunk(chunk: PNGZtxt, png: PNG): bool = #estimate chunk.keyword.len + 2 chunk.writeString chunk.keyword chunk.writeByte 0 #null separator - chunk.writeByte 0 #compression method(0: deflate) + chunk.writeByte 0 #compression proc(0: deflate) var nz = nzDeflateInit(chunk.text) chunk.writeString zlib_compress(nz) result = true -method writeChunk(chunk: PNGItxt, png: PNG): bool = +proc writeChunk(chunk: PNGItxt, png: PNG): bool = #estimate chunk.keyword.len + 2 # + chunk.languageTag.len + chunk.translatedKeyword.len let state = PNGEncoder(png.settings) @@ -2246,7 +2275,7 @@ method writeChunk(chunk: PNGItxt, png: PNG): bool = chunk.writeString chunk.keyword chunk.writeByte 0 #null separator chunk.writeByte compressed #compression flag(0: uncompressed, 1: compressed) - chunk.writeByte 0 #compression method(0: deflate) + chunk.writeByte 0 #compression proc(0: deflate) chunk.writeString chunk.languageTag chunk.writeByte 0 #null separator chunk.writeString chunk.translatedKeyword @@ -2254,22 +2283,22 @@ method writeChunk(chunk: PNGItxt, png: PNG): bool = chunk.writeString text result = true -method writeChunk(chunk: PNGICCProfile, png: PNG): bool = +proc writeChunk(chunk: PNGICCProfile, png: PNG): bool = #estimate chunk.profileName.len + 2 chunk.writeString chunk.profileName chunk.writeByte 0 #null separator - chunk.writeByte 0 #compression method(0: deflate) + chunk.writeByte 0 #compression proc(0: deflate) var nz = nzDeflateInit(chunk.profile) chunk.writeString zlib_compress(nz) result = true -method writeChunk(chunk: APNGAnimationControl, png: PNG): bool = +proc writeChunk(chunk: APNGAnimationControl, png: PNG): bool = # estimate 8 bytes chunk.writeInt32(chunk.numFrames) chunk.writeInt32(chunk.numPlays) result = true -method writeChunk(chunk: APNGFrameControl, png: PNG): bool = +proc writeChunk(chunk: APNGFrameControl, png: PNG): bool = # estimate 5*4 + 2*2 + 2 = 26 bytes chunk.writeInt32(chunk.sequenceNumber) chunk.writeInt32(chunk.width) @@ -2282,12 +2311,36 @@ method writeChunk(chunk: APNGFrameControl, png: PNG): bool = chunk.writeByte(ord(chunk.blendOp)) result = true -method writeChunk(chunk: APNGFrameData, png: PNG): bool = +proc writeChunk(chunk: APNGFrameData, png: PNG): bool = chunk.writeInt32(chunk.sequenceNumber) var nz = nzDeflateInit(png.apngPixels[chunk.frameDataPos]) chunk.writeString zlib_compress(nz) result = true +proc writeChunk(chunk: PNGChunk, png: PNG): bool = + case chunk.chunkType + of IHDR: result = writeChunk(PNGHeader(chunk), png) + of PLTE: result = writeChunk(PNGPalette(chunk), png) + of IDAT: result = writeChunk(PNGData(chunk), png) + of tRNS: result = writeChunk(PNGTrans(chunk), png) + of bKGD: result = writeChunk(PNGBackground(chunk), png) + of tIME: result = writeChunk(PNGTime(chunk), png) + of pHYs: result = writeChunk(PNGPhys(chunk), png) + of tEXt: result = writeChunk(PNGTExt(chunk), png) + of zTXt: result = writeChunk(PNGZtxt(chunk), png) + of iTXt: result = writeChunk(PNGItxt(chunk), png) + of gAMA: result = writeChunk(PNGGamma(chunk), png) + of cHRM: result = writeChunk(PNGChroma(chunk), png) + of iCCP: result = writeChunk(PNGICCProfile(chunk), png) + of sRGB: result = writeChunk(PNGStandarRGB(chunk), png) + of sPLT: result = writeChunk(PNGSPalette(chunk), png) + of hIST: result = writeChunk(PNGHist(chunk), png) + of sBIT: result = writeChunk(PNGSbit(chunk), png) + of acTL: result = writeChunk(APNGAnimationControl(chunk), png) + of fcTL: result = writeChunk(APNGFrameControl(chunk), png) + of fdAT: result = writeChunk(APNGFrameData(chunk), png) + else: result = true + proc isGreyscaleType(mode: PNGColorMode): bool = result = mode.colorType in {LCT_GREY, LCT_GREY_ALPHA} @@ -2502,7 +2555,7 @@ proc autoChooseColor(png: PNG, modeOut, modeIn: PNGColorMode) = modeOut.keyDefined = true proc filter[T](output: var openArray[T], input: openArray[T], w, h: int, modeOut: PNGColorMode, state: PNGEncoder) = - # For PNG filter method 0 + # For PNG filter proc 0 # out must be a buffer with as size: h + (w * h * bpp + 7) / 8, because there are # the scanlines with 1 extra byte per scanline @@ -2886,8 +2939,8 @@ proc writeChunks*[T](png: PNG[T], s: Stream) = s.write PNGSignature for chunk in png.chunks: - if not chunk.validateChunk(png): raise PNGFatal("combine chunk validation error") - if not chunk.writeChunk(png): raise PNGFatal("combine chunk write error") + if not chunk.validateChunk(png): raise PNGFatal("combine chunk validation error " & $chunk.chunkType) + if not chunk.writeChunk(png): raise PNGFatal("combine chunk write error " & $chunk.chunkType) chunk.length = chunk.data.len chunk.crc = crc32(crc32(0, $chunk.chunkType), chunk.data)