From 05cce9b58f7f996806542b290267ae952557fcae Mon Sep 17 00:00:00 2001 From: andri lim Date: Fri, 24 Nov 2017 12:40:33 +0700 Subject: [PATCH 1/3] apng minor adjusment --- nimPNG.nim | 152 +++++++++++++++++++++++++++++++---------------------- readme.md | 2 +- 2 files changed, 91 insertions(+), 63 deletions(-) diff --git a/nimPNG.nim b/nimPNG.nim index 4724706..0b85ca6 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -181,6 +181,8 @@ type blendOp*: APNG_BLEND_OP APNGFrameData = ref object of APNGFrameChunk + # during decoding frameDataPos points to chunk.data[pos] + # during encoding frameDataPos points to png.apngPixels[pos] and png.apngChunks[pos] frameDataPos: int PNGPass = object @@ -214,9 +216,13 @@ type second*: int #range[0..60] #to allow for leap seconds PNG* = ref object + # during encoding, settings is PNGEncoder + # during decoding, settings is PNGDecoder settings*: PNGSettings chunks*: seq[PNGChunk] pixels*: string + # during encoding, apngChunks contains only fcTL chunks + # during decoding, apngChunks contains both fcTL and fdAT chunks apngChunks*: seq[APNGFrameChunk] firstFrameIsDefaultImage*: bool isAPNG*: bool @@ -1912,90 +1918,108 @@ proc toString(chunk: APNGFrameData): string = result = newString(fdatLen) copyMem(result[0].addr, fdatAddr, fdatLen) +type + APNG = ref object + png: PNG + result: PNGResult + +proc processingAPNG(apng: APNG, 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) + numFrames = 0 + lastChunkType = PNGChunkType(0) + start = 0 + + if apng.png.firstFrameIsDefaultImage: + start = 1 + # IDAT already processed, so we add a dummy here + frameData.add string(nil) + + for x in apng.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") + + apng.png.apngPixels = newSeqOfCap[string](numFrames) + + if apng.result != nil: + apng.result.frames = newSeqOfCap[APNGFrame](numFrames) + + if apng.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") + + if apng.result != nil: + var frame = new(APNGFrame) + frame.ctl = ctl + frame.data = apng.result.data + apng.result.frames.add frame + + for i in start.. Date: Fri, 24 Nov 2017 13:06:49 +0700 Subject: [PATCH 2/3] apng encoder preparation --- nimPNG.nim | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/nimPNG.nim b/nimPNG.nim index 0b85ca6..abbeee7 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -2132,6 +2132,9 @@ type unknown*: seq[PNGUnknown] + # APNG number of plays, 0 = infinite + numPlays*: int + PNGColorProfile = ref object colored: bool #not greyscale key: bool #if true, image is not opaque. Only if true and alpha is false, color key is possible. @@ -2413,9 +2416,30 @@ method writeChunk(chunk: PNGICCProfile, png: PNG): bool = chunk.writeString zlib_compress(nz) result = true -#method writeChunk(chunk: PNGICCProfile, png: PNG): bool = -#method writeChunk(chunk: PNGICCProfile, png: PNG): bool = -#method writeChunk(chunk: PNGICCProfile, png: PNG): bool = +method 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 = + # estimate 5*4 + 2*2 + 2 = 26 bytes + chunk.writeInt32(chunk.sequenceNumber) + chunk.writeInt32(chunk.width) + chunk.writeInt32(chunk.height) + chunk.writeInt32(chunk.xOffset) + chunk.writeInt32(chunk.yOffset) + chunk.writeInt16(chunk.delayNum) + chunk.writeInt16(chunk.delayDen) + chunk.writeByte(ord(chunk.disposeOp)) + chunk.writeByte(ord(chunk.blendOp)) + result = true + +method 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 isGreyscaleType(mode: PNGColorMode): bool = result = mode.colorType in {LCT_GREY, LCT_GREY_ALPHA} @@ -3065,6 +3089,22 @@ proc addChunkIEND(png: PNG) = var chunk = make[PNGEnd](IEND, 0) png.chunks.add chunk +proc addChunkacTL(png: PNG, numFrames, numPlays: int) = + var chunk = make[APNGAnimationControl](acTL, 8) + chunk.numFrames = numFrames + chunk.numPlays = numPlays + png.chunks.add chunk + +proc addChunkfcTL(png: PNG, chunk: APNGFrameControl) = + chunk.chunkType = fcTL + if chunk.data.isNil: + chunk.data = newStringOfCap(26) + png.chunks.add chunk + +proc addChunkfdAT(png: PNG, sequenceNumber, frameDataPos: int) = + var chunk = make[APNGFrameData](fdAT, 0) + png.chunks.add chunk + proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = var png: PNG new(png) From 769361b868717515661d34ce3e7bdecc1362dc79 Mon Sep 17 00:00:00 2001 From: andri lim Date: Fri, 24 Nov 2017 17:58:44 +0700 Subject: [PATCH 3/3] close #11, APNG encoder ready --- apng/raw/frame0.png | Bin 0 -> 349 bytes apng/raw/frame1.png | Bin 0 -> 356 bytes apng/raw/frame2.png | Bin 0 -> 361 bytes apng/raw/frame3.png | Bin 0 -> 375 bytes apng/raw/frame4.png | Bin 0 -> 382 bytes apng/raw/frame5.png | Bin 0 -> 393 bytes apng/raw/frame6.png | Bin 0 -> 399 bytes nimPNG.nim | 205 +++++++++++++++++++++++++++++++++++++------- readme.md | 33 ++++++- tester/test.nim | 44 ++++++++++ 10 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 apng/raw/frame0.png create mode 100644 apng/raw/frame1.png create mode 100644 apng/raw/frame2.png create mode 100644 apng/raw/frame3.png create mode 100644 apng/raw/frame4.png create mode 100644 apng/raw/frame5.png create mode 100644 apng/raw/frame6.png diff --git a/apng/raw/frame0.png b/apng/raw/frame0.png new file mode 100644 index 0000000000000000000000000000000000000000..818406f8821196683aee67fc2ebb65351b1d367a GIT binary patch literal 349 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tA%xPo6H0Ar*0NFDztiFkm=x;EgTe~DWM4fFGhtA literal 0 HcmV?d00001 diff --git a/apng/raw/frame1.png b/apng/raw/frame1.png new file mode 100644 index 0000000000000000000000000000000000000000..4959ad38323f520509b4ebc69068667e00b100ed GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tA%xpPnv`Ar*0NFDw*1Y{0WkcZc@lWk(Zv3)&~ztagA2n?RCelF{r G5}E+ah+?V$ literal 0 HcmV?d00001 diff --git a/apng/raw/frame2.png b/apng/raw/frame2.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c81e280f0517fa149e9ce496af1eb5ab72e697 GIT binary patch literal 361 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tA%xf1WOmAr*0NFDw*1Y{0!lvI6;>1s;*b3=DjSL74G){tBRWMjlTW$B>G+w-*crnH(8d4l--Ayl`N+G@*f$ z%R1r!)`rkF$^KV!0TY9{7-qiP>&F`P8)MM`u9rCdB bhJ^KH9?q}FlV9Zk1B=1a)z4*}Q$iB}wLy0{ literal 0 HcmV?d00001 diff --git a/apng/raw/frame4.png b/apng/raw/frame4.png new file mode 100644 index 0000000000000000000000000000000000000000..bcdd7613bab729249b1fd6a942e49ad021bf34fe GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tBRWMqy7E$B>G+w-*#enF1MHF2-_QV07l&70}M1 zp%u{1AR$#C_W7y!Z<$AN#*6Ro%Dw%m{@U^CJu@$Vw7PfZ^(UG9S7mbP&x-%|nHNi( i=1XE7^6>389s8%bTwm{SSKb4L8H1;*pUXO@geCx}l4lSA literal 0 HcmV?d00001 diff --git a/apng/raw/frame5.png b/apng/raw/frame5.png new file mode 100644 index 0000000000000000000000000000000000000000..767bd84620943216ec67814044988645d7a7755d GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tBRWMmbLx$B>G+w-*$rIt4PgUgT%hXpnN|oe<&> z+|F@1iA$h?BV+;N>*RSgO^c7c$$fVn(N- literal 0 HcmV?d00001 diff --git a/apng/raw/frame6.png b/apng/raw/frame6.png new file mode 100644 index 0000000000000000000000000000000000000000..f75c9dc5f12754ef8a273ca39bd952d46da1f1f8 GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tBRWMioyN$B>G+w-*+cG6gcY9&BaHP|#}I(zvuG zFj@gFqU+#{)Uiwph`{VN!#?!xM@0of0`|%3n`MWPy z81HZYT@n2JuZ3~__2(7Fw>R!_T;nk0;db5A+|ALvU%jQv%z=T(;OXk;vd$@?2>=SZ BrB(m{ literal 0 HcmV?d00001 diff --git a/nimPNG.nim b/nimPNG.nim index abbeee7..b852987 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -221,6 +221,8 @@ type settings*: PNGSettings chunks*: seq[PNGChunk] pixels*: string + # w & h used during encoding process + width*, height*: int # during encoding, apngChunks contains only fcTL chunks # during decoding, apngChunks contains both fcTL and fdAT chunks apngChunks*: seq[APNGFrameChunk] @@ -2169,6 +2171,7 @@ proc makePNGEncoder*(): PNGEncoder = s.textList = @[] s.itextList = @[] s.unknown = @[] + s.numPlays = 0 result = s proc addText*(state: PNGEncoder, keyword, text: string) = @@ -2475,8 +2478,7 @@ proc differ(p: RGBA16): bool = if (p.a and 255) != ((p.a shr 8) and 255): return true result = false -proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProfile = - var prof = makeColorProfile() +proc calculateColorProfile(input: string, w, h: int, mode: PNGColorMode, prof: PNGColorProfile, tree: var Table[RGBA8, int]) = let numPixels = w * h bpp = getBPP(mode) @@ -2488,7 +2490,6 @@ proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProf numColorsDone = false sixteen = false maxNumColors = 257 - tree = initTable[RGBA8, int]() if bpp <= 8: case bpp @@ -2589,6 +2590,19 @@ proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProf prof.keyR += prof.keyR shl 8 prof.keyG += prof.keyG shl 8 prof.keyB += prof.keyB shl 8 + +proc getColorProfile(png: PNG, mode: PNGColorMode): PNGColorProfile = + var + prof = makeColorProfile() + tree = initTable[RGBA8, int]() + + calculateColorProfile(png.pixels, png.width, png.height, mode, prof, tree) + + if png.isAPNG: + for i in 1.. 256): raise PNGError("invalid palette size, it is only allowed to be 1-256") - let inputSize = getRawSize(w, h, modeIn) - if input.len < inputSize: + let inputSize = getRawSize(png.width, png.height, modeIn) + if png.pixels.len < inputSize: raise PNGError("not enough input to encode") if state.autoConvert: - autoChooseColor(modeOut, input, w, h, modeIn) + png.autoChooseColor(modeOut, modeIn) if state.interlaceMethod notin {IM_NONE, IM_INTERLACED}: raise PNGError("unexisting interlace mode") @@ -3140,18 +3167,12 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = if not bitDepthAllowed(modeOut.colorType, modeOut.bitDepth): raise PNGError("colorType and bitDepth combination not allowed") - if modeIn != modeOut: - let size = (w * h * getBPP(modeOut) + 7) div 8 - let numPixels = w * h + if not png.isAPNG: png.apngPixels = @[""] + shallowCopy(png.apngPixels[0], png.pixels) + png.frameConvert(modeIn, modeOut, png.width, png.height, 0, state) + shallowCopy(png.pixels, png.apngPixels[0]) - var converted = newString(size) - var output = initBuffer(converted) - convert(output, initBuffer(input), modeOut, modeIn, numPixels) - preProcessScanLines(png, initBuffer(converted), w, h, modeOut, state) - else: - preProcessScanLines(png, initBuffer(input), w, h, modeOut, state) - - png.addChunkIHDR(w, h, modeOut, state) + png.addChunkIHDR(png.width, png.height, modeOut, state) #unknown chunks between IHDR and PLTE if state.unknown.len > 0: png.chunks.add state.unknown[0] @@ -3175,9 +3196,30 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG = if state.unknown.len > 1: png.chunks.add state.unknown[1] + if png.isAPNG: + if png.apngPixels.len != png.apngChunks.len: + raise PNGError("APNG encoder frame error") + if png.apngPixels.len == 0: + raise PNGError("APNG encoder no frame") + png.addChunkacTL(png.apngPixels.len, state.numPlays) + if png.firstFrameIsDefaultImage: + png.addChunkfcTL(APNGFrameControl(png.apngChunks[0]), sequenceNumber) + inc sequenceNumber + #IDAT (multiple IDAT chunks must be consecutive) png.addChunkIDAT(state) + if png.isAPNG: + let len = png.apngChunks.len + for i in 1..= 0) + result = result and (ctl.yOffset >= 0) + result = result and (ctl.width > 0) + result = result and (ctl.height > 0) + result = result and (ctl.xOffset + ctl.width <= png.width) + result = result and (ctl.yOffset + ctl.height <= png.height) + + if result: + png.apngPixels.add frame + png.apngChunks.add ctl + +proc encodeAPNG*(png: PNG): string = + try: + png.encoderCore() + var s = newStringStream() + png.writeChunks s + result = s.data + except: + debugEcho getCurrentExceptionMsg() + result = nil + +when not defined(js): + proc saveAPNG*(png: PNG, fileName: string): bool = + #try: + png.encoderCore() + var s = newFileStream(fileName, fmWrite) + png.writeChunks s + s.close() + result = true + #except: + #debugEcho getCurrentExceptionMsg() + #result = false + proc getFilterTypesInterlaced(png: PNG): seq[string] = var header = PNGHeader(png.getChunk(IHDR)) var idat = PNGData(png.getChunk(IDAT)) diff --git a/readme.md b/readme.md index 2a8dd0e..74d139d 100644 --- a/readme.md +++ b/readme.md @@ -95,7 +95,7 @@ pixels are stored as raw bytes using Nim's string as container: ## Animated PNG (APNG) -Since version 0.2.0, nimPNG provides support for [Animated PNG](https://wiki.mozilla.org/APNG_Specification). +Since version 0.2.0, nimPNG provides support for [Animated PNG](https://en.wikipedia.org/wiki/APNG). Both decoder and encoder recognize/generate APNG chunks correctly: acTL, fcTL, fdAT. @@ -122,7 +122,36 @@ Animation frames can be accessible via `png.frames`. If it is not an APNG, `png. ### Encoding -Under construction +```Nim +var png = prepareAPNG24() +``` + +* First step is to call prepareAPNG, prepareAPNG24, or prepareAPNG32. You also can specify how many times the animation +will be played + +```Nim + png.addDefaultImage(framePixels, w, h, ctl) +``` + +* Second step is also mandatory, you should call addDefaultImage. ctl is optional, if you provide a ctl(Frame Control), +the default image will be part of the animation. If ctl is nil, default image will not be part of animation. + +```Nim + png.addFrame(frames[i].data, ctl) +``` + +* Third step is calling addFrame one or more times. Here ctl is mandatory. + +```Nim + png.saveAPNG("rainbow.png") + # or + var str = png.encodeAPNG() +``` + +* Final step is to call saveAPNG if you want save it to file or call encodeAPNG if you want to get the result in a string container + +You can read the details of frame control from [spec](https://wiki.mozilla.org/APNG_Specification). +You can also see an example in tester/test.nim -> generateAPNG [nimpng-travisci]: https://travis-ci.org/jangko/nimPNG [badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master diff --git a/tester/test.nim b/tester/test.nim index edc0ede..bafbc78 100644 --- a/tester/test.nim +++ b/tester/test.nim @@ -51,9 +51,53 @@ proc convert(dir: string) = if png == nil: continue png.toBMP(bmpName) +proc generateAPNG() = + const numFrames = 7 + var frames: array[numFrames, PNGResult] + + for i in 0..