diff --git a/nimPNG.nim b/nimPNG.nim index 8a00502..ab23a64 100644 --- a/nimPNG.nim +++ b/nimPNG.nim @@ -26,10 +26,12 @@ #------------------------------------- import streams, endians, tables, hashes, math -import nimPNG/[buffer, nimz] +import nimPNG/[buffer, nimz, filters] + +import strutils const - NIM_PNG_VERSION = "0.2.4" + NIM_PNG_VERSION = "0.2.6" type PNGChunkType = distinct int32 @@ -41,13 +43,6 @@ type LCT_GREY_ALPHA = 4, # greyscale with alpha: 8,16 bit LCT_RGBA = 6 # RGB with alpha: 8,16 bit - PNGFilter* = enum - FLT_NONE, - FLT_SUB, - FLT_UP, - FLT_AVERAGE, - FLT_PAETH - PNGSettings = ref object of RootObj PNGDecoder* = ref object of PNGSettings @@ -185,10 +180,6 @@ type # during encoding frameDataPos points to png.apngPixels[pos] and png.apngChunks[pos] frameDataPos: int - PNGPass = object - w, h: array[0..6, int] - filterStart, paddedStart, start: array[0..7, int] - PNGColorMode* = ref object colorType*: PNGcolorType bitDepth*: int @@ -302,12 +293,6 @@ const fcTL = makeChunkType("fcTL") fdAT = makeChunkType("fdAT") - # shared values used by multiple Adam7 related functions - ADAM7_IX = [ 0, 4, 0, 2, 0, 1, 0 ] # x start values - ADAM7_IY = [ 0, 0, 4, 0, 2, 0, 1 ] # y start values - ADAM7_DX = [ 8, 8, 4, 4, 2, 2, 1 ] # x delta values - ADAM7_DY = [ 8, 8, 8, 4, 4, 2, 2 ] # y delta values - proc PNGError(msg: string): ref Exception = new(result) result.msg = msg @@ -922,216 +907,7 @@ proc parsePNG(s: Stream, settings: PNGDecoder): PNG = if not idat.validateChunk(png): raise PNGError("bad IDAT") result = png -# Paeth predicter, used by PNG filter type 4 -proc paethPredictor(a, b, c: int): int = - let pa = abs(b - c) - let pb = abs(a - c) - let pc = abs(a + b - c - c) - - if(pc < pa) and (pc < pb): return c - elif pb < pa: return b - result = a - -proc readBitFromReversedStream(bitptr: var int, bitstream: DataBuf): int = - result = ((ord(bitstream[bitptr shr 3]) shr (7 - (bitptr and 0x7))) and 1) - inc bitptr - -proc readBitsFromReversedStream(bitptr: var int, bitstream: DataBuf, nbits: int): int = - result = 0 - var i = nbits - 1 - while i > -1: - result += readBitFromReversedStream(bitptr, bitstream) shl i - dec i - -proc `&=`(a: var char, b: char) = - a = chr(ord(a) and ord(b)) - -proc `|=`(a: var char, b: char) = - a = chr(ord(a) or ord(b)) - -proc setBitOfReversedStream0(bitptr: var int, bitstream: var DataBuf, bit: int) = - # the current bit in bitstream must be 0 for this to work - if bit != 0: - # earlier bit of huffman code is in a lesser significant bit of an earlier byte - bitstream[bitptr shr 3] |= cast[char](bit shl (7 - (bitptr and 0x7))) - inc bitptr - -proc setBitOfReversedStream(bitptr: var int, bitstream: var DataBuf, bit: int) = - #the current bit in bitstream may be 0 or 1 for this to work - if bit == 0: bitstream[bitptr shr 3] &= cast[char](not (1 shl (7 - (bitptr and 0x7)))) - else: bitstream[bitptr shr 3] |= cast[char](1 shl (7 - (bitptr and 0x7))) - inc bitptr - -# index: bitgroup index, bits: bitgroup size(1, 2 or 4), in: bitgroup value, out: octet array to add bits to -proc addColorBits(output: var DataBuf, index, bits, input: int) = - var m = 1 - if bits == 1: m = 7 - elif bits == 2: m = 3 - # p = the partial index in the byte, e.g. with 4 palettebits it is 0 for first half or 1 for second half - let p = index and m - - var val = input and ((1 shl bits) - 1) #filter out any other bits of the input value - val = val shl (bits * (m - p)) - let idx = index * bits div 8 - if p == 0: output[idx] = chr(val) - else: output[idx] = chr(ord(output[idx]) or val) - -proc unfilterScanLine(recon: var DataBuf, scanLine, precon: DataBuf, byteWidth, len: int, filterType: PNGFilter) = - # For PNG filter method 0 - # unfilter a PNG image scanLine by scanLine. when the pixels are smaller than 1 byte, - # the filter works byte per byte (byteWidth = 1) - # precon is the previous unfiltered scanLine, recon the result, scanLine the current one - # the incoming scanLines do NOT include the filtertype byte, that one is given in the parameter filterType instead - # recon and scanLine MAY be the same memory address! precon must be disjoint. - - case filterType - of FLT_NONE: - for i in 0..len-1: recon[i] = scanLine[i] - of FLT_SUB: - for i in 0..byteWidth-1: recon[i] = scanLine[i] - for i in byteWidth..len-1: recon[i] = chr((ord(scanLine[i]) + ord(recon[i - byteWidth])) mod 256) - of FLT_UP: - if not precon.isNil: - for i in 0..len-1: recon[i] = chr((ord(scanLine[i]) + ord(precon[i])) mod 256) - else: - for i in 0..len-1: recon[i] = scanLine[i] - of FLT_AVERAGE: - if not precon.isNil: - for i in 0..byteWidth-1: - recon[i] = chr((ord(scanLine[i]) + ord(precon[i]) div 2) mod 256) - for i in byteWidth..len-1: - recon[i] = chr((ord(scanLine[i]) + ((ord(recon[i - byteWidth]) + ord(precon[i])) div 2)) mod 256) - else: - for i in 0..byteWidth-1: recon[i] = scanLine[i] - for i in byteWidth..len-1: - recon[i] = chr((ord(scanLine[i]) + ord(recon[i - byteWidth]) div 2) mod 256) - of FLT_PAETH: - if not precon.isNil: - for i in 0..byteWidth-1: - recon[i] = chr((ord(scanLine[i]) + ord(precon[i])) mod 256) #paethPredictor(0, precon[i], 0) is always precon[i] - for i in byteWidth..len-1: - recon[i] = chr((ord(scanLine[i]) + paethPredictor(ord(recon[i - byteWidth]), ord(precon[i]), ord(precon[i - byteWidth]))) mod 256) - else: - for i in 0..byteWidth-1: recon[i] = scanLine[i] - for i in byteWidth..len-1: - # paethPredictor(recon[i - byteWidth], 0, 0) is always recon[i - byteWidth] - recon[i] = chr((ord(scanLine[i]) + ord(recon[i - byteWidth])) mod 256) - -proc unfilter(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - # For PNG filter method 0 - # this function unfilters a single image (e.g. without interlacing this is called once, with Adam7 seven times) - # output must have enough bytes allocated already, input must have the scanLines + 1 filtertype byte per scanLine - # w and h are image dimensions or dimensions of reduced image, bpp is bits per pixel - # input and output are allowed to be the same memory address (but aren't the same size since in has the extra filter bytes) - - var prevLine : DataBuf - - # byteWidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise - let byteWidth = (bpp + 7) div 8 - let lineBytes = (w * bpp + 7) div 8 - - for y in 0..h-1: - let outIndex = lineBytes * y - let inIndex = (1 + lineBytes) * y # the extra filterbyte added to each row - let filterType = PNGFilter(input[inindex]) - let scanLine = input.subbuffer(inIndex + 1) - var outp = output.subbuffer(outIndex) - unfilterScanLine(outp, scanLine, prevLine, byteWidth, lineBytes, filterType) - prevLine = output.subbuffer(outIndex) - -proc removePaddingBits(output: var DataBuf, input: DataBuf, olinebits, ilinebits, h: int) = - # After filtering there are still padding bits if scanLines have non multiple of 8 bit amounts. They need - # to be removed (except at last scanLine of (Adam7-reduced) image) before working with pure image buffers - # for the Adam7 code, the color convert code and the output to the user. - # in and out are allowed to be the same buffer, in may also be higher but still overlapping; in must - # have >= ilinebits*h bits, out must have >= olinebits*h bits, olinebits must be <= ilinebits - # also used to move bits after earlier such operations happened, e.g. in a sequence of reduced images from Adam7 - # only useful if (ilinebits - olinebits) is a value in the range 1..7 - - let diff = ilinebits - olinebits - var - ibp = 0 - obp = 0 # input and output bit pointers - for y in 0..h-1: - for x in 0..olinebits-1: - var bit = readBitFromReversedStream(ibp, input) - setBitOfReversedStream(obp, output, bit) - inc(ibp, diff) - -# Outputs various dimensions and positions in the image related to the Adam7 reduced images. -# passw: output containing the width of the 7 passes -# passh: output containing the height of the 7 passes -# filter_passstart: output containing the index of the start and end of each -# reduced image with filter bytes -# padded_passstart output containing the index of the start and end of each -# reduced image when without filter bytes but with padded scanLines -# passstart: output containing the index of the start and end of each reduced -# image without padding between scanLines, but still padding between the images -# w, h: width and height of non-interlaced image -# bpp: bits per pixel -# "padded" is only relevant if bpp is less than 8 and a scanLine or image does not -# end at a full byte -proc Adam7PassValues(pass: var PNGPass, w, h, bpp: int) = - #the passstart values have 8 values: - # the 8th one indicates the byte after the end of the 7th (= last) pass - - # calculate width and height in pixels of each pass - for i in 0..6: - pass.w[i] = (w + ADAM7_DX[i] - ADAM7_IX[i] - 1) div ADAM7_DX[i] - pass.h[i] = (h + ADAM7_DY[i] - ADAM7_IY[i] - 1) div ADAM7_DY[i] - if pass.w[i] == 0: pass.h[i] = 0 - if pass.h[i] == 0: pass.w[i] = 0 - - pass.filterStart[0] = 0 - pass.paddedStart[0] = 0 - pass.start[0] = 0 - for i in 0..6: - # if passw[i] is 0, it's 0 bytes, not 1 (no filtertype-byte) - pass.filterStart[i + 1] = pass.filterStart[i] - if (pass.w[i] != 0) and (pass.h[i] != 0): - pass.filterStart[i + 1] += pass.h[i] * (1 + (pass.w[i] * bpp + 7) div 8) - # bits padded if needed to fill full byte at end of each scanLine - pass.paddedStart[i + 1] = pass.paddedStart[i] + pass.h[i] * ((pass.w[i] * bpp + 7) div 8) - # only padded at end of reduced image - pass.start[i + 1] = pass.start[i] + (pass.h[i] * pass.w[i] * bpp + 7) div 8 - -# input: Adam7 interlaced image, with no padding bits between scanLines, but between -# reduced images so that each reduced image starts at a byte. -# output: the same pixels, but re-ordered so that they're now a non-interlaced image with size w*h -# bpp: bits per pixel -# output has the following size in bits: w * h * bpp. -# input is possibly bigger due to padding bits between reduced images. -# output must be big enough AND must be 0 everywhere if bpp < 8 in the current implementation -# (because that's likely a little bit faster) -# NOTE: comments about padding bits are only relevant if bpp < 8 - -proc Adam7Deinterlace(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - var pass: PNGPass - Adam7PassValues(pass, w, h, bpp) - - if bpp >= 8: - for i in 0..6: - var byteWidth = bpp div 8 - for y in 0..pass.h[i]-1: - for x in 0..pass.w[i]-1: - var inStart = pass.start[i] + (y * pass.w[i] + x) * byteWidth - var outStart = ((ADAM7_IY[i] + y * ADAM7_DY[i]) * w + ADAM7_IX[i] + x * ADAM7_DX[i]) * byteWidth - for b in 0..byteWidth-1: - output[outStart + b] = input[inStart + b] - else: # bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers - for i in 0..6: - var ilinebits = bpp * pass.w[i] - var olinebits = bpp * w - for y in 0..pass.h[i]-1: - for x in 0..pass.w[i]-1: - var ibp = (8 * pass.start[i]) + (y * ilinebits + x * bpp) - var obp = (ADAM7_IY[i] + y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + x * ADAM7_DX[i]) * bpp - for b in 0..bpp-1: - var bit = readBitFromReversedStream(ibp, input) - # note that this function assumes the out buffer is completely 0, use setBitOfReversedStream otherwise - setBitOfReversedStream0(obp, output, bit) - -proc postProcessScanLines(png: PNG; header: PNGHeader, w, h: int; input, output: var DataBuf) = +proc postProcessScanLines[T](png: PNG; header: PNGHeader, w, h: int; input, output: var openArray[T]) = # This function converts the filtered-padded-interlaced data # into pure 2D image buffer with the PNG's colorType. # Steps: @@ -1147,16 +923,19 @@ proc postProcessScanLines(png: PNG; header: PNGHeader, w, h: int; input, output: if(bpp < 8) and (bitsPerLine != bitsPerPaddedLine): unfilter(input, input, w, h, bpp) removePaddingBits(output, input, bitsPerLine, bitsPerPaddedLine, h) - # we can immediatly filter into the out buffer, no other steps needed - else: unfilter(output, input, w, h, bpp) + else: + # we can immediatly filter into the out buffer, no other steps needed + unfilter(output, input, w, h, bpp) + else: # interlace_method is 1 (Adam7) var pass: PNGPass - Adam7PassValues(pass, w, h, bpp) + adam7PassValues(pass, w, h, bpp) for i in 0..6: - var outp = input.subbuffer(pass.paddedStart[i]) - var inp = input.subbuffer(pass.filterStart[i]) - unfilter(outp, inp, pass.w[i], pass.h[i], bpp) + unfilter(input.toOpenArray(pass.paddedStart[i], input.len-1), + input.toOpenArray(pass.filterStart[i], input.len-1), + pass.w[i], pass.h[i], bpp + ) # TODO: possible efficiency improvement: # if in this reduced image the bits fit nicely in 1 scanLine, @@ -1164,11 +943,13 @@ proc postProcessScanLines(png: PNG; header: PNGHeader, w, h: int; input, output: if bpp < 8: # remove padding bits in scanLines; after this there still may be padding # bits between the different reduced images: each reduced image still starts nicely at a byte - outp = input.subbuffer(pass.start[i]) - inp = input.subbuffer(pass.paddedStart[i]) - removePaddingBits(outp, inp, pass.w[i] * bpp, ((pass.w[i] * bpp + 7) div 8) * 8, pass.h[i]) + removePaddingBits( + input.toOpenArray(pass.start[i], input.len-1), + input.toOpenArray(pass.paddedStart[i], input.len-1), + pass.w[i] * bpp, ((pass.w[i] * bpp + 7) div 8) * 8, pass.h[i] + ) - Adam7Deinterlace(output, input, w, h, bpp) + adam7Deinterlace(output, input, w, h, bpp) proc postProcessScanLines(png: PNG) = var header = PNGHeader(png.getChunk(IHDR)) @@ -1176,22 +957,24 @@ proc postProcessScanLines(png: PNG) = let h = header.height var idat = PNGData(png.getChunk(IDAT)) png.pixels = newString(idatRawSize(header.width, header.height, header)) - var input = initBuffer(idat.idat) - var output = initBuffer(png.pixels) - zeroMem(output) - png.postProcessScanLines(header, w, h, input, output) + png.postProcessScanLines(header, w, h, + idat.idat.toOpenArray(0, idat.idat.len-1), # input + png.pixels.toOpenArray(0, png.pixels.len-1) # output + ) -proc postProcessScanLines(png: PNG, ctl: APNGFrameControl, data: string) = +proc postProcessScanLines(png: PNG, ctl: APNGFrameControl, data: var string) = + # we use var string here to avoid realloc + # coz we use the input as output too var header = PNGHeader(png.getChunk(IHDR)) let w = ctl.width let h = ctl.height png.apngPixels.add newString(idatRawSize(ctl.width, ctl.height, header)) - var input = initBuffer(data) - var output = initBuffer(png.apngPixels[^1]) - zeroMem(output) - png.postProcessScanLines(header, w, h, input, output) + png.postProcessScanLines(header, w, h, + data.toOpenArray(0, data.len-1), # input + png.apngPixels[^1].toOpenArray(0, png.apngPixels[^1].len-1) + ) proc getColorMode(png: PNG): PNGColorMode = var header = PNGHeader(png.getChunk(IHDR)) @@ -1251,48 +1034,48 @@ proc getChunkNames*(png: PNG): string = if i < png.chunks.high: result.add ' ' inc i -proc RGBFromGrey8(output: var DataBuf, input: DataBuf, numPixels: int, mode: PNGColorMode) = - for i in 0..numPixels-1: +proc RGBFromGrey8[T](output: var openArray[T], input: openArray[T], numPixels: int, mode: PNGColorMode) = + for i in 0..= mode.paletteSize: @@ -1306,9 +1089,9 @@ proc RGBFromPalette8(output: var DataBuf, input: DataBuf, numPixels: int, mode: output[x+1] = mode.palette[index].g output[x+2] = mode.palette[index].b -proc RGBFromPalette124(output: var DataBuf, input: DataBuf, numPixels: int, mode: PNGColorMode) = +proc RGBFromPalette124[T](output: var openArray[T], input: openArray[T], numPixels: int, mode: PNGColorMode) = var obp = 0 - for i in 0..numPixels-1: + for i in 0..= mode.paletteSize: @@ -1322,40 +1105,40 @@ proc RGBFromPalette124(output: var DataBuf, input: DataBuf, numPixels: int, mode output[x+1] = mode.palette[index].g output[x+2] = mode.palette[index].b -proc RGBFromGreyAlpha8(output: var DataBuf, input: DataBuf, numPixels: int, mode: PNGColorMode) = - for i in 0..numPixels-1: +proc RGBFromGreyAlpha8[T](output: var openArray[T], input: openArray[T], numPixels: int, mode: PNGColorMode) = + for i in 0..= mode.paletteSize: @@ -1429,9 +1212,9 @@ proc RGBAFromPalette8(output: var DataBuf, input: DataBuf, numPixels: int, mode: output[x+2] = mode.palette[index].b output[x+3] = mode.palette[index].a -proc RGBAFromPalette124(output: var DataBuf, input: DataBuf, numPixels: int, mode: PNGColorMode) = +proc RGBAFromPalette124[T](output: var openArray[T], input: openArray[T], numPixels: int, mode: PNGColorMode) = var obp = 0 - for i in 0..numPixels-1: + for i in 0..= mode.paletteSize: @@ -1447,8 +1230,8 @@ proc RGBAFromPalette124(output: var DataBuf, input: DataBuf, numPixels: int, mod output[x+2] = mode.palette[index].b output[x+3] = mode.palette[index].a -proc RGBAFromGreyAlpha8(output: var DataBuf, input: DataBuf, numPixels: int, mode: PNGColorMode) = - for i in 0..numPixels-1: +proc RGBAFromGreyAlpha8[T](output: var openArray[T], input: openArray[T], numPixels: int, mode: PNGColorMode) = + for i in 0..= mode.paletteSize: # This is an error according to the PNG spec, @@ -1561,7 +1344,7 @@ proc RGBA8FromPalette8(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode p.b = mode.palette[index].b p.a = mode.palette[index].a -proc RGBA8FromPalette124(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA8FromPalette124[T](p: var RGBA8, input: openArray[T], px: int, mode: PNGColorMode) = var obp = px * mode.bitDepth let index = readBitsFromReversedStream(obp, input, mode.bitDepth) if index >= mode.paletteSize: @@ -1578,7 +1361,7 @@ proc RGBA8FromPalette124(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMo p.b = mode.palette[index].b p.a = mode.palette[index].a -proc RGBA8FromGreyAlpha8(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA8FromGreyAlpha8[T](p: var RGBA8, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 2 let val = input[i] p.r = val @@ -1586,7 +1369,7 @@ proc RGBA8FromGreyAlpha8(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMo p.b = val p.a = input[i+1] -proc RGBA8FromGreyAlpha16(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA8FromGreyAlpha16[T](p: var RGBA8, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 4 let val = input[i] p.r = val @@ -1594,21 +1377,21 @@ proc RGBA8FromGreyAlpha16(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorM p.b = val p.a = input[i+2] -proc RGBA8FromRGBA8(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA8FromRGBA8[T](p: var RGBA8, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 4 p.r = input[i] p.g = input[i+1] p.b = input[i+2] p.a = input[i+3] -proc RGBA8FromRGBA16(p: var RGBA8, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA8FromRGBA16[T](p: var RGBA8, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 8 p.r = input[i] p.g = input[i+2] p.b = input[i+4] p.a = input[i+6] -proc RGBA16FromGrey(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16FromGrey[T](p: var RGBA16, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 2 let val = 256'u16 * uint16(input[i]) + uint16(input[i + 1]) p.r = val @@ -1617,7 +1400,7 @@ proc RGBA16FromGrey(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) if mode.keyDefined and (val.int == mode.keyR): p.a = 0 else: p.a = 65535 -proc RGBA16FromRGB(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16FromRGB[T](p: var RGBA16, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 6 p.r = 256'u16 * uint16(input[i]) + uint16(input[i+1]) p.g = 256'u16 * uint16(input[i+2]) + uint16(input[i+3]) @@ -1626,7 +1409,7 @@ proc RGBA16FromRGB(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) = (int(p.g) == mode.keyG) and (int(p.b) == mode.keyB): p.a = 0 else: p.a = 65535 -proc RGBA16FromGreyAlpha(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16FromGreyAlpha[T](p: var RGBA16, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 4 let val = 256'u16 * uint16(input[i]) + uint16(input[i + 1]) p.r = val @@ -1634,33 +1417,33 @@ proc RGBA16FromGreyAlpha(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorM p.b = val p.a = 256'u16 * uint16(input[i + 2]) + uint16(input[i + 3]) -proc RGBA16FromRGBA(p: var RGBA16, input: DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16FromRGBA[T](p: var RGBA16, input: openArray[T], px: int, mode: PNGColorMode) = let i = px * 8 p.r = 256'u16 * uint16(input[i]) + uint16(input[i+1]) p.g = 256'u16 * uint16(input[i+2]) + uint16(input[i+3]) p.b = 256'u16 * uint16(input[i+4]) + uint16(input[i+5]) p.a = 256'u16 * uint16(input[i+6]) + uint16(input[i+7]) -proc RGBA8ToGrey8(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToGrey8[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = output[px] = p.r -proc RGBA8ToGrey16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToGrey16[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 2 output[i] = p.r output[i+1] = p.r -proc RGBA8ToGrey124(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToGrey124[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = # take the most significant bits of grey - let grey = (ord(p.r) shr (8 - mode.bitDepth)) and ((1 shl mode.bitDepth) - 1) + let grey = (int(p.r) shr (8 - mode.bitDepth)) and ((1 shl mode.bitDepth) - 1) addColorBits(output, px, mode.bitDepth, grey) -proc RGBA8ToRGB8(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToRGB8[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 3 output[i] = p.r output[i+1] = p.g output[i+2] = p.b -proc RGBA8ToRGB16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToRGB16[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 6 output[i] = p.r output[i+2] = p.g @@ -1669,32 +1452,32 @@ proc RGBA8ToRGB16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct output[i+3] = p.g output[i+5] = p.b -proc RGBA8ToPalette8(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = - output[px] = chr(ct[p]) +proc RGBA8ToPalette8[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = + output[px] = T(ct[p]) -proc RGBA8ToPalette124(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToPalette124[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = addColorBits(output, px, mode.bitDepth, ct[p]) -proc RGBA8ToGreyAlpha8(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToGreyAlpha8[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 2 output[i] = p.r output[i+1] = p.a -proc RGBA8ToGreyAlpha16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToGreyAlpha16[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 4 output[i] = p.r output[i+1] = p.r output[i+2] = p.a output[i+3] = p.a -proc RGBA8ToRGBA8(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToRGBA8[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 4 output[i] = p.r output[i+1] = p.g output[i+2] = p.b output[i+3] = p.a -proc RGBA8ToRGBA16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, ct: ColorTree8) = +proc RGBA8ToRGBA16[T](p: RGBA8, output: var openArray[T], px: int, mode: PNGColorMode, ct: ColorTree8) = let i = px * 8 output[i] = p.r output[i+2] = p.g @@ -1705,129 +1488,129 @@ proc RGBA8ToRGBA16(p: RGBA8, output: var DataBuf, px: int, mode: PNGColorMode, c output[i+5] = p.b output[i+7] = p.a -proc RGBA16ToGrey(p: RGBA16, output: var DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16ToGrey[T](p: RGBA16, output: var openArray[T], px: int, mode: PNGColorMode) = let i = px * 2 - output[i] = char((p.r shr 8) and 255) - output[i+1] = char(p.r and 255) + output[i] = T((p.r shr 8) and 255) + output[i+1] = T(p.r and 255) -proc RGBA16ToRGB(p: RGBA16, output: var DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16ToRGB[T](p: RGBA16, output: var openArray[T], px: int, mode: PNGColorMode) = let i = px * 6 - output[i] = char((p.r shr 8) and 255) - output[i+1] = char(p.r and 255) - output[i+2] = char((p.g shr 8) and 255) - output[i+3] = char(p.g and 255) - output[i+4] = char((p.b shr 8) and 255) - output[i+5] = char(p.b and 255) - -proc RGBA16ToGreyAlpha(p: RGBA16, output: var DataBuf, px: int, mode: PNGColorMode) = + output[i] = T((p.r shr 8) and 255) + output[i+1] = T(p.r and 255) + output[i+2] = T((p.g shr 8) and 255) + output[i+3] = T(p.g and 255) + output[i+4] = T((p.b shr 8) and 255) + output[i+5] = T(p.b and 255) + +proc RGBA16ToGreyAlpha[T](p: RGBA16, output: var openArray[T], px: int, mode: PNGColorMode) = let i = px * 4 - output[i] = char((p.r shr 8) and 255) - output[i+1] = char(p.r and 255) - output[i+2] = char((p.a shr 8) and 255) - output[i+3] = char(p.a and 255) + output[i] = T((p.r shr 8) and 255) + output[i+1] = T(p.r and 255) + output[i+2] = T((p.a shr 8) and 255) + output[i+3] = T(p.a and 255) -proc RGBA16ToRGBA(p: RGBA16, output: var DataBuf, px: int, mode: PNGColorMode) = +proc RGBA16ToRGBA[T](p: RGBA16, output: var openArray[T], px: int, mode: PNGColorMode) = let i = px * 8 - output[i] = char((p.r shr 8) and 255) - output[i+1] = char(p.r and 255) - output[i+2] = char((p.g shr 8) and 255) - output[i+3] = char(p.g and 255) - output[i+4] = char((p.b shr 8) and 255) - output[i+5] = char(p.b and 255) - output[i+6] = char((p.a shr 8) and 255) - output[i+7] = char(p.a and 255) - -proc getColorRGBA16(mode: PNGColorMode): convertRGBA16 = - if mode.colorType == LCT_GREY: return RGBA16FromGrey - elif mode.colorType == LCT_RGB: return RGBA16FromRGB - elif mode.colorType == LCT_GREY_ALPHA: return RGBA16FromGreyAlpha - elif mode.colorType == LCT_RGBA: return RGBA16FromRGBA + output[i] = T((p.r shr 8) and 255) + output[i+1] = T(p.r and 255) + output[i+2] = T((p.g shr 8) and 255) + output[i+3] = T(p.g and 255) + output[i+4] = T((p.b shr 8) and 255) + output[i+5] = T(p.b and 255) + output[i+6] = T((p.a shr 8) and 255) + output[i+7] = T(p.a and 255) + +proc getColorRGBA16[T](mode: PNGColorMode): convertRGBA16[T] = + if mode.colorType == LCT_GREY: return RGBA16FromGrey[T] + elif mode.colorType == LCT_RGB: return RGBA16FromRGB[T] + elif mode.colorType == LCT_GREY_ALPHA: return RGBA16FromGreyAlpha[T] + elif mode.colorType == LCT_RGBA: return RGBA16FromRGBA[T] else: raise PNGError("unsupported converter16") -proc getPixelRGBA16(mode: PNGColorMode): pixelRGBA16 = - if mode.colorType == LCT_GREY: return RGBA16ToGrey - elif mode.colorType == LCT_RGB: return RGBA16ToRGB - elif mode.colorType == LCT_GREY_ALPHA: return RGBA16ToGreyAlpha - elif mode.colorType == LCT_RGBA: return RGBA16ToRGBA +proc getPixelRGBA16[T](mode: PNGColorMode): pixelRGBA16[T] = + if mode.colorType == LCT_GREY: return RGBA16ToGrey[T] + elif mode.colorType == LCT_RGB: return RGBA16ToRGB[T] + elif mode.colorType == LCT_GREY_ALPHA: return RGBA16ToGreyAlpha[T] + elif mode.colorType == LCT_RGBA: return RGBA16ToRGBA[T] else: raise PNGError("unsupported pixel16 converter") -proc getColorRGBA8(mode: PNGColorMode): convertRGBA8 = +proc getColorRGBA8[T](mode: PNGColorMode): convertRGBA8[T] = if mode.colorType == LCT_GREY: - if mode.bitDepth == 8: return RGBA8FromGrey8 - elif mode.bitDepth == 16: return RGBA8FromGrey16 - else: return RGBA8FromGrey124 + if mode.bitDepth == 8: return RGBA8FromGrey8[T] + elif mode.bitDepth == 16: return RGBA8FromGrey16[T] + else: return RGBA8FromGrey124[T] elif mode.colorType == LCT_RGB: - if mode.bitDepth == 8: return RGBA8FromRGB8 - else: return RGBA8FromRGB16 + if mode.bitDepth == 8: return RGBA8FromRGB8[T] + else: return RGBA8FromRGB16[T] elif mode.colorType == LCT_PALETTE: - if mode.bitDepth == 8: return RGBA8FromPalette8 - else: return RGBA8FromPalette124 + if mode.bitDepth == 8: return RGBA8FromPalette8[T] + else: return RGBA8FromPalette124[T] elif mode.colorType == LCT_GREY_ALPHA: - if mode.bitDepth == 8: return RGBA8FromGreyAlpha8 - else: return RGBA8FromGreyAlpha16 + if mode.bitDepth == 8: return RGBA8FromGreyAlpha8[T] + else: return RGBA8FromGreyAlpha16[T] elif mode.colorType == LCT_RGBA: - if mode.bitDepth == 8: return RGBA8FromRGBA8 - else: return RGBA8FromRGBA16 + if mode.bitDepth == 8: return RGBA8FromRGBA8[T] + else: return RGBA8FromRGBA16[T] else: raise PNGError("unsupported converter8") -proc getPixelRGBA8(mode: PNGColorMode): pixelRGBA8 = +proc getPixelRGBA8[T](mode: PNGColorMode): pixelRGBA8[T] = if mode.colorType == LCT_GREY: - if mode.bitDepth == 8: return RGBA8ToGrey8 - elif mode.bitDepth == 16: return RGBA8ToGrey16 - else: return RGBA8ToGrey124 + if mode.bitDepth == 8: return RGBA8ToGrey8[T] + elif mode.bitDepth == 16: return RGBA8ToGrey16[T] + else: return RGBA8ToGrey124[T] elif mode.colorType == LCT_RGB: - if mode.bitDepth == 8: return RGBA8ToRGB8 - else: return RGBA8ToRGB16 + if mode.bitDepth == 8: return RGBA8ToRGB8[T] + else: return RGBA8ToRGB16[T] elif mode.colorType == LCT_PALETTE: - if mode.bitDepth == 8: return RGBA8ToPalette8 - else: return RGBA8ToPalette124 + if mode.bitDepth == 8: return RGBA8ToPalette8[T] + else: return RGBA8ToPalette124[T] elif mode.colorType == LCT_GREY_ALPHA: - if mode.bitDepth == 8: return RGBA8ToGreyAlpha8 - else: return RGBA8ToGreyAlpha16 + if mode.bitDepth == 8: return RGBA8ToGreyAlpha8[T] + else: return RGBA8ToGreyAlpha16[T] elif mode.colorType == LCT_RGBA: - if mode.bitDepth == 8: return RGBA8ToRGBA8 - else: return RGBA8ToRGBA16 + if mode.bitDepth == 8: return RGBA8ToRGBA8[T] + else: return RGBA8ToRGBA16[T] else: raise PNGError("unsupported pixel8 converter") -proc getConverterRGB(mode: PNGColorMode): convertRGBA = +proc getConverterRGB[T](mode: PNGColorMode): convertRGBA[T] = if mode.colorType == LCT_GREY: - if mode.bitDepth == 8: return RGBFromGrey8 - elif mode.bitDepth == 16: return RGBFromGrey16 - else: return RGBFromGrey124 + if mode.bitDepth == 8: return RGBFromGrey8[T] + elif mode.bitDepth == 16: return RGBFromGrey16[T] + else: return RGBFromGrey124[T] elif mode.colorType == LCT_RGB: - if mode.bitDepth == 8: return RGBFromRGB8 - else: return RGBFromRGB16 + if mode.bitDepth == 8: return RGBFromRGB8[T] + else: return RGBFromRGB16[T] elif mode.colorType == LCT_PALETTE: - if mode.bitDepth == 8: return RGBFromPalette8 - else: return RGBFromPalette124 + if mode.bitDepth == 8: return RGBFromPalette8[T] + else: return RGBFromPalette124[T] elif mode.colorType == LCT_GREY_ALPHA: - if mode.bitDepth == 8: return RGBFromGreyAlpha8 - else: return RGBFromGreyAlpha16 + if mode.bitDepth == 8: return RGBFromGreyAlpha8[T] + else: return RGBFromGreyAlpha16[T] elif mode.colorType == LCT_RGBA: - if mode.bitDepth == 8: return RGBFromRGBA8 - else: return RGBFromRGBA16 + if mode.bitDepth == 8: return RGBFromRGBA8[T] + else: return RGBFromRGBA16[T] else: raise PNGError("unsupported RGB converter") -proc getConverterRGBA(mode: PNGColorMode): convertRGBA = +proc getConverterRGBA[T](mode: PNGColorMode): convertRGBA[T] = if mode.colorType == LCT_GREY: - if mode.bitDepth == 8: return RGBAFromGrey8 - elif mode.bitDepth == 16: return RGBAFromGrey16 - else: return RGBAFromGrey124 + if mode.bitDepth == 8: return RGBAFromGrey8[T] + elif mode.bitDepth == 16: return RGBAFromGrey16[T] + else: return RGBAFromGrey124[T] elif mode.colorType == LCT_RGB: - if mode.bitDepth == 8: return RGBAFromRGB8 - else: return RGBAFromRGB16 + if mode.bitDepth == 8: return RGBAFromRGB8[T] + else: return RGBAFromRGB16[T] elif mode.colorType == LCT_PALETTE: - if mode.bitDepth == 8: return RGBAFromPalette8 - else: return RGBAFromPalette124 + if mode.bitDepth == 8: return RGBAFromPalette8[T] + else: return RGBAFromPalette124[T] elif mode.colorType == LCT_GREY_ALPHA: - if mode.bitDepth == 8: return RGBAFromGreyAlpha8 - else: return RGBAFromGreyAlpha16 + if mode.bitDepth == 8: return RGBAFromGreyAlpha8[T] + else: return RGBAFromGreyAlpha16[T] elif mode.colorType == LCT_RGBA: - if mode.bitDepth == 8: return RGBAFromRGBA8 - else: return RGBAFromRGBA16 + if mode.bitDepth == 8: return RGBAFromRGBA8[T] + else: return RGBAFromRGBA16[T] else: raise PNGError("unsupported RGBA converter") -proc convert*(output: var DataBuf, input: DataBuf, modeOut, modeIn: PNGColorMode, numPixels: int) = +proc convert*[T](output: var openArray[T], input: openArray[T], modeOut, modeIn: PNGColorMode, numPixels: int) = var tree: ColorTree8 if modeOut.colorType == LCT_PALETTE: var @@ -1849,68 +1632,68 @@ proc convert*(output: var DataBuf, input: DataBuf, modeOut, modeIn: PNGColorMode tree[palette[i]] = i if(modeIn.bitDepth == 16) and (modeOut.bitDepth == 16): - let cvt = getColorRGBA16(modeIn) - let pxl = getPixelRGBA16(modeOut) - for px in 0..numPixels-1: + let cvt = getColorRGBA16[T](modeIn) + let pxl = getPixelRGBA16[T](modeOut) + for px in 0..= modeOut.palettesize) and (modeIn.bitdepth == modeOut.bitdepth): - #If input should have same palette colors, keep original to preserve its order and prevent conversion + # If input should have same palette colors, keep original to preserve its order and prevent conversion modeIn.copyTo(modeOut) - else: #8-bit or 16-bit per channel + else: # 8-bit or 16-bit per channel modeOut.bitDepth = prof.bits if prof.alpha: if prof.colored: modeOut.colorType = LCT_RGBA @@ -2653,245 +2431,32 @@ proc autoChooseColor(png: PNG, modeOut, modeIn: PNGColorMode) = else: modeOut.colorType = LCT_GREY if prof.key and not prof.alpha: - #profile always uses 16-bit, mask converts it + # profile always uses 16-bit, mask converts it let mask = (1 shl modeOut.bitDepth) - 1 modeOut.keyR = prof.keyR and mask modeOut.keyG = prof.keyG and mask modeOut.keyB = prof.keyB and mask modeOut.keyDefined = true -proc addPaddingBits(output: var DataBuf, input: DataBuf, olinebits, ilinebits, h: int) = - #The opposite of the removePaddingBits function - #olinebits must be >= ilinebits - - let diff = olinebits - ilinebits - var - obp = 0 - ibp = 0 #bit pointers - - for y in 0..h-1: - for x in 0..ilinebits-1: - let bit = readBitFromReversedStream(ibp, input) - setBitOfReversedStream(obp, output, bit) - for x in 0..diff-1: setBitOfReversedStream(obp, output, 0) - -proc filterScanLine(output: var DataBuf, scanLine, prevLine: DataBuf, len, byteWidth: int, filterType: PNGFilter) = - case filterType - of FLT_NONE: - for i in 0..len-1: output[i] = scanLine[i] - of FLT_SUB: - for i in 0..byteWidth-1: output[i] = scanLine[i] - for i in byteWidth..len-1: - output[i] = chr((scanLine[i].uint - scanLine[i - byteWidth].uint) and 0xFF) - of FLT_UP: - if not prevLine.isNil: - for i in 0..len-1: - output[i] = chr((scanLine[i].uint - prevLine[i].uint) and 0xFF) - else: - for i in 0..len-1: output[i] = scanLine[i] - of FLT_AVERAGE: - if not prevLine.isNil: - for i in 0..byteWidth-1: - output[i] = chr((scanLine[i].uint - (prevLine[i].uint div 2)) and 0xFF) - for i in byteWidth..len-1: - output[i] = chr((scanLine[i].uint - ((scanLine[i - byteWidth].uint + prevLine[i].uint) div 2)) and 0xFF) - else: - for i in 0..byteWidth-1: output[i] = scanLine[i] - for i in byteWidth..len-1: - output[i] = chr((scanLine[i].uint - (scanLine[i - byteWidth].uint div 2)) and 0xFF) - of FLT_PAETH: - if not prevLine.isNil: - #paethPredictor(0, prevLine[i], 0) is always prevLine[i] - for i in 0..byteWidth-1: - output[i] = chr((scanLine[i].uint - prevLine[i].uint) and 0xFF) - for i in byteWidth..len-1: - output[i] = chr((scanLine[i].uint - paethPredictor(ord(scanLine[i - byteWidth]), ord(prevLine[i]), ord(prevLine[i - byteWidth])).uint) and 0xFF) - else: - for i in 0..byteWidth-1: output[i] = scanLine[i] - #paethPredictor(scanLine[i - byteWidth], 0, 0) is always scanLine[i - byteWidth] - for i in byteWidth..len-1: - output[i] = chr((scanLine[i].uint - scanLine[i - byteWidth].uint) and 0xFF) - -proc filterZero(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - #the width of a scanline in bytes, not including the filter type - let lineBytes = (w * bpp + 7) div 8 - #byteWidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise - let byteWidth = (bpp + 7) div 8 - var prevLine: DataBuf - - for y in 0..h-1: - let outindex = (1 + lineBytes) * y #the extra filterbyte added to each row - let inindex = lineBytes * y - output[outindex] = chr(int(FLT_NONE)) #filter type byte - var outp = output.subbuffer(outindex + 1) - let scanLine = input.subbuffer(inindex) - filterScanLine(outp, scanLine, prevLine, lineBytes, byteWidth, FLT_NONE) - prevLine = input.subbuffer(inindex) - -proc filterMinsum(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - let lineBytes = (w * bpp + 7) div 8 - let byteWidth = (bpp + 7) div 8 - - #adaptive filtering - var sum = [0, 0, 0, 0, 0] - var smallest = 0 - - #five filtering attempts, one for each filter type - var attempt: array[0..4, string] - var bestType = 0 - var prevLine: DataBuf - - for i in 0..attempt.high: - attempt[i] = newString(lineBytes) - - for y in 0..h-1: - #try the 5 filter types - for fType in 0..4: - var outp = initBuffer(attempt[fType]) - filterScanLine(outp, input.subbuffer(y * lineBytes), prevLine, lineBytes, byteWidth, PNGFilter(fType)) - #calculate the sum of the result - sum[fType] = 0 - if fType == 0: - for x in 0..lineBytes-1: - sum[fType] += ord(attempt[fType][x]) - else: - for x in 0..lineBytes-1: - #For differences, each byte should be treated as signed, values above 127 are negative - #(converted to signed char). Filtertype 0 isn't a difference though, so use unsigned there. - #This means filtertype 0 is almost never chosen, but that is justified. - let s = ord(attempt[fType][x]) - if s < 128: sum[fType] += s - else: sum[fType] += (255 - s) - - #check if this is smallest sum (or if type == 0 it's the first case so always store the values)*/ - if(fType == 0) or (sum[fType] < smallest): - bestType = fType - smallest = sum[fType] - - prevLine = input.subbuffer(y * lineBytes) - #now fill the out values - #the first byte of a scanline will be the filter type - output[y * (lineBytes + 1)] = chr(bestType) - for x in 0..lineBytes-1: - output[y * (lineBytes + 1) + 1 + x] = attempt[bestType][x] - -proc filterEntropy(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - let lineBytes = (w * bpp + 7) div 8 - let byteWidth = (bpp + 7) div 8 - var prevLine: DataBuf - - var sum: array[0..4, float] - var smallest = 0.0 - var bestType = 0 - var attempt: array[0..4, string] - var count: array[0..255, int] - - for i in 0..attempt.high: - attempt[i] = newString(lineBytes) - - for y in 0..h-1: - #try the 5 filter types - for fType in 0..4: - var outp = initBuffer(attempt[fType]) - filterScanLine(outp, input.subbuffer(y * lineBytes), prevLine, lineBytes, byteWidth, PNGFilter(fType)) - for x in 0..255: count[x] = 0 - for x in 0..lineBytes-1: - inc count[ord(attempt[fType][x])] - inc count[fType] #the filter type itself is part of the scanline - sum[fType] = 0 - for x in 0..255: - let p = float(count[x]) / float(lineBytes + 1) - if count[x] != 0: sum[fType] += log2(1 / p) * p - - #check if this is smallest sum (or if type == 0 it's the first case so always store the values) - if (fType == 0) or (sum[fType] < smallest): - bestType = fType - smallest = sum[fType] - - prevLine = input.subbuffer(y * lineBytes) - #now fill the out values*/ - #the first byte of a scanline will be the filter type - output[y * (lineBytes + 1)] = chr(bestType) - for x in 0..lineBytes-1: - output[y * (lineBytes + 1) + 1 + x] = attempt[bestType][x] - -proc filterPredefined(output: var DataBuf, input: DataBuf, w, h, bpp: int, state: PNGEncoder) = - let lineBytes = (w * bpp + 7) div 8 - let byteWidth = (bpp + 7) div 8 - var prevLine: DataBuf - - for y in 0..h-1: - let outindex = (1 + lineBytes) * y #the extra filterbyte added to each row - let inindex = lineBytes * y - let fType = ord(state.predefinedFilters[y]) - output[outindex] = chr(fType) #filter type byte - var outp = output.subbuffer(outindex + 1) - filterScanLine(outp, input.subbuffer(inindex), prevLine, lineBytes, byteWidth, PNGFilter(fType)) - prevLine = input.subbuffer(inindex) - -proc filterBruteForce(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - let lineBytes = (w * bpp + 7) div 8 - let byteWidth = (bpp + 7) div 8 - var prevLine: DataBuf - - #brute force filter chooser. - #deflate the scanline after every filter attempt to see which one deflates best. - #This is very slow and gives only slightly smaller, sometimes even larger, result*/ - - var size: array[0..4, int] - var attempt: array[0..4, string] #five filtering attempts, one for each filter type - var smallest = 0 - var bestType = 0 - - #use fixed tree on the attempts so that the tree is not adapted to the filtertype on purpose, - #to simulate the true case where the tree is the same for the whole image. Sometimes it gives - #better result with dynamic tree anyway. Using the fixed tree sometimes gives worse, but in rare - #cases better compression. It does make this a bit less slow, so it's worth doing this. - - for i in 0..attempt.high: - attempt[i] = newString(lineBytes) - - for y in 0..h-1: - #try the 5 filter types - for fType in 0..4: - #let testSize = attempt[fType].len - var outp = initBuffer(attempt[fType]) - filterScanline(outp, input.subbuffer(y * lineBytes), prevLine, lineBytes, byteWidth, PNGFilter(fType)) - size[fType] = 0 - - var nz = nzDeflateInit(attempt[fType]) - let data = zlib_compress(nz) - size[fType] = data.len - - #check if this is smallest size (or if type == 0 it's the first case so always store the values) - if(fType == 0) or (size[fType] < smallest): - bestType = fType - smallest = size[fType] - - prevLine = input.subbuffer(y * lineBytes) - output[y * (lineBytes + 1)] = chr(bestType) #the first byte of a scanline will be the filter type - for x in 0..lineBytes-1: - output[y * (lineBytes + 1) + 1 + x] = attempt[bestType][x] - -proc filter(output: var DataBuf, input: DataBuf, w, h: int, modeOut: PNGColorMode, state: PNGEncoder) = - #For PNG filter method 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 +proc filter[T](output: var openArray[T], input: openArray[T], w, h: int, modeOut: PNGColorMode, state: PNGEncoder) = + # For PNG filter method 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 let bpp = getBPP(modeOut) var strategy = state.filterStrategy - #There is a heuristic called the minimum sum of absolute differences heuristic, suggested by the PNG standard: + # There is a heuristic called the minimum sum of absolute differences heuristic, suggested by the PNG standard: # * If the image type is Palette, or the bit depth is smaller than 8, then do not filter the image (i.e. # use fixed filtering, with the filter None). # * (The other case) If the image type is Grayscale or RGB (with or without Alpha), and the bit depth is # not smaller than 8, then use adaptive filtering heuristic as follows: independently for each row, apply # all five filters and select the filter that produces the smallest sum of absolute values per row. - #This heuristic is used if filter strategy is LFS_MINSUM and filter_palette_zero is true. + # This heuristic is used if filter strategy is LFS_MINSUM and filter_palette_zero is true. - #If filter_palette_zero is true and filter_strategy is not LFS_MINSUM, the above heuristic is followed, - #but for "the other case", whatever strategy filter_strategy is set to instead of the minimum sum - #heuristic is used. + # If filter_palette_zero is true and filter_strategy is not LFS_MINSUM, the above heuristic is followed, + # but for "the other case", whatever strategy filter_strategy is set to instead of the minimum sum + # heuristic is used. if state.filterPaletteZero and (modeOut.colorType == LCT_PALETTE or modeOut.bitDepth < 8): strategy = LFS_ZERO @@ -2903,83 +2468,62 @@ proc filter(output: var DataBuf, input: DataBuf, w, h: int, modeOut: PNGColorMod of LFS_MINSUM: filterMinsum(output, input, w, h, bpp) of LFS_ENTROPY: filterEntropy(output, input, w, h, bpp) of LFS_BRUTE_FORCE: filterBruteForce(output, input, w, h, bpp) - of LFS_PREDEFINED: filterPredefined(output, input, w, h, bpp, state) - -#input: non-interlaced image with size w*h -#output: the same pixels, but re-ordered according to PNG's Adam7 interlacing, with -# no padding bits between scanlines, but between reduced images so that each -# reduced image starts at a byte. -#bpp: bits per pixel -#there are no padding bits, not between scanlines, not between reduced images -#in has the following size in bits: w * h * bpp. -#output is possibly bigger due to padding bits between reduced images -#NOTE: comments about padding bits are only relevant if bpp < 8 -proc Adam7Interlace(output: var DataBuf, input: DataBuf, w, h, bpp: int) = - var pass: PNGPass - Adam7PassValues(pass, w, h, bpp) - - if bpp >= 8: - for i in 0..6: - let byteWidth = bpp div 8 - for y in 0..pass.h[i]-1: - for x in 0..pass.w[i]-1: - let inStart = ((ADAM7_IY[i] + y * ADAM7_DY[i]) * w + ADAM7_IX[i] + x * ADAM7_DX[i]) * byteWidth - let outStart = pass.start[i] + (y * pass.w[i] + x) * byteWidth - for b in 0..byteWidth-1: - output[outStart + b] = input[inStart + b] - else: #bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers - for i in 0..6: - let ilinebits = bpp * pass.w[i] - let olinebits = bpp * w - var obp, ibp: int #bit pointers (for out and in buffer) - for y in 0..pass.h[i]-1: - for x in 0..pass.w[i]-1: - ibp = (ADAM7_IY[i] + y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + x * ADAM7_DX[i]) * bpp - obp = (8 * pass.start[i]) + (y * ilinebits + x * bpp) - for b in 0..bpp-1: - let bit = readBitFromReversedStream(ibp, input) - setBitOfReversedStream(obp, output, bit) - -proc preProcessScanLines(png: PNG, input: DataBuf, frameNo, w, h: int, modeOut: PNGColorMode, state: PNGEncoder) = - #This function converts the pure 2D image with the PNG's colorType, into filtered-padded-interlaced data. Steps: - # if no Adam7: 1) add padding bits (= posible extra bits per scanLine if bpp < 8) 2) filter - # if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter + of LFS_PREDEFINED: filterPredefined(output, input, w, h, bpp, state.predefinedFilters) + +proc preProcessScanLines[T](png: PNG, input: openArray[T], frameNo, w, h: int, modeOut: PNGColorMode, state: PNGEncoder) = + # This function converts the pure 2D image with the PNG's colorType, into filtered-padded-interlaced data. Steps: + # if no Adam7: 1) add padding bits (= posible extra bits per scanLine if bpp < 8) 2) filter + # if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter let bpp = getBPP(modeOut) if state.interlaceMethod == IM_NONE: - #image size plus an extra byte per scanLine + possible padding bits + # image size plus an extra byte per scanLine + possible padding bits let scanLen = (w * bpp + 7) div 8 let outSize = h + (h * scanLen) - png.apngPixels[frameNo] = newString(outSize) - var output = initBuffer(png.apngPixels[frameNo]) - #non multiple of 8 bits per scanLine, padding bits needed per scanLine + var output = newString(outSize) + + # non multiple of 8 bits per scanLine, padding bits needed per scanLine if(bpp < 8) and ((w * bpp) != (scanLen * 8)): - var padded = initBuffer(newString(h * scanLen)) - addPaddingBits(padded, input, scanLen * 8, w * bpp, h) + var padded = newString(h * scanLen) + addPaddingBits(padded.toOpenArray(0, padded.len-1), input, scanLen * 8, w * bpp, h) - filter(output, padded, w, h, modeOut, state) + filter(output.toOpenArray(0, output.len-1), + padded.toOpenArray(0, padded.len-1), + w, h, modeOut, state) else: - #we can immediatly filter into the out buffer, no other steps needed - filter(output, input, w, h, modeOut, state) + # we can immediatly filter into the out buffer, no other steps needed + filter(output.toOpenArray(0, output.len-1), + input, w, h, modeOut, state) + + shallowCopy(png.apngPixels[frameNo], output) else: #interlaceMethod is 1 (Adam7) var pass: PNGPass - Adam7PassValues(pass, w, h, bpp) + adam7PassValues(pass, w, h, bpp) let outSize = pass.filterStart[7] - png.apngPixels[frameNo] = newString(outSize) - var adam7 = initBuffer(newString(pass.start[7])) - var output = initBuffer(png.apngPixels[frameNo]) - Adam7Interlace(adam7, input, w, h, bpp) + var output = newString(outSize) + var adam7 = newString(pass.start[7]) + + adam7Interlace(adam7.toOpenArray(0, adam7.len-1), + input, w, h, bpp) for i in 0..6: if bpp < 8: - var padding = initBuffer(newString(pass.paddedStart[i + 1] - pass.paddedStart[i])) - addPaddingBits(padding, adam7.subbuffer(pass.start[i]), ((pass.w[i] * bpp + 7) div 8) * 8, pass.w[i] * bpp, pass.h[i]) - var outp = output.subbuffer(pass.filterStart[i]) - filter(outp, padding, pass.w[i], pass.h[i], modeOut, state) + var padding = newString(pass.paddedStart[i + 1] - pass.paddedStart[i]) + + addPaddingBits(padding.toOpenArray(0, padding.len-1), + adam7.toOpenArray(pass.start[i], adam7.len-1), + ((pass.w[i] * bpp + 7) div 8) * 8, pass.w[i] * bpp, pass.h[i]) + + filter(output.toOpenArray(pass.filterStart[i], output.len-1), + padding.toOpenArray(0, padding.len-1), + pass.w[i], pass.h[i], modeOut, state) else: - var outp = output.subbuffer(pass.filterStart[i]) - filter(outp, adam7.subbuffer(pass.paddedStart[i]), pass.w[i], pass.h[i], modeOut, state) + filter(output.toOpenArray(pass.filterStart[i], output.len-1), + adam7.toOpenArray(pass.paddedStart[i], adam7.len-1), + pass.w[i], pass.h[i], modeOut, state) + + shallowCopy(png.apngPixels[frameNo], output) #palette must have 4 * palettesize bytes allocated, and given in format RGBARGBARGBARGBA... #returns 0 if the palette is opaque, @@ -3121,18 +2665,22 @@ proc addChunkfdAT(png: PNG, sequenceNumber, frameDataPos: int) = png.chunks.add chunk proc frameConvert(png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) = + template input: untyped = png.apngPixels[frameNo] + if modeIn != modeOut: let size = (w * h * getBPP(modeOut) + 7) div 8 let numPixels = w * h var converted = newString(size) - var output = initBuffer(converted) # although in preProcessScanLines png.pixels is reinitialized, it is ok # because initBuffer(png.pixels) share the ownership - convert(output, initBuffer(png.apngPixels[frameNo]), modeOut, modeIn, numPixels) - preProcessScanLines(png, initBuffer(converted), frameNo, w, h, modeOut, state) + convert(converted.toOpenArray(0, converted.len-1), + input.toOpenArray(0, input.len-1), + modeOut, modeIn, numPixels) + + preProcessScanLines(png, converted.toOpenArray(0, converted.len-1), frameNo, w, h, modeOut, state) else: - preProcessScanLines(png, initBuffer(png.apngPixels[frameNo]), frameNo, w, h, modeOut, state) + preProcessScanLines(png, input.toOpenArray(0, input.len-1), frameNo, w, h, modeOut, state) proc encoderCore(png: PNG) = let state = PNGEncoder(png.settings) @@ -3150,6 +2698,10 @@ proc encoderCore(png: PNG) = (modeOut.paletteSize == 0 or modeOut.paletteSize > 256): raise PNGError("invalid palette size, it is only allowed to be 1-256") + if state.filterStrategy == LFS_PREDEFINED: + if state.predefinedFilters.len < png.width: + raise PNGError("predefinedFilters contains not enough filterType compared to image height") + let inputSize = getRawSize(png.width, png.height, modeIn) if png.pixels.len < inputSize: raise PNGError("not enough input to encode") diff --git a/nimPNG.nimble b/nimPNG.nimble index 609f0d6..3beeb0e 100644 --- a/nimPNG.nimble +++ b/nimPNG.nimble @@ -9,14 +9,6 @@ skipDirs = @["tests", "docs"] requires "nim >= 0.19.0" task tests, "Run tests": - exec "nim c -r tests/test_apng.nim" - exec "nim c -r tests/test_codec.nim" - exec "nim c -r tests/test_suite.nim" - exec "nim c -r tests/test_nimz.nim" - - exec "nim c -r -d:release tests/test_apng.nim" - exec "nim c -r -d:release tests/test_codec.nim" - exec "nim c -r -d:release tests/test_suite.nim" - exec "nim c -r -d:release tests/test_nimz.nim" - - exec "nim c -r --gc:arc -d:release tests/test_nimz.nim" + exec "nim -v" + exec "nim c -r -d:release tests/all_tests" + exec "nim c -r --gc:arc -d:release tests/all_tests" diff --git a/nimPNG/filters.nim b/nimPNG/filters.nim new file mode 100644 index 0000000..cf3afc8 --- /dev/null +++ b/nimPNG/filters.nim @@ -0,0 +1,585 @@ +import math, ../nimPNG/nimz + +type + PNGFilter* = enum + FLT_NONE, + FLT_SUB, + FLT_UP, + FLT_AVERAGE, + FLT_PAETH + + PNGPass* = object + w*, h*: array[0..6, int] + filterStart*, paddedStart*, start*: array[0..7, int] + +const + # shared values used by multiple Adam7 related functions + ADAM7_IX* = [ 0, 4, 0, 2, 0, 1, 0 ] # x start values + ADAM7_IY* = [ 0, 0, 4, 0, 2, 0, 1 ] # y start values + ADAM7_DX* = [ 8, 8, 4, 4, 2, 2, 1 ] # x delta values + ADAM7_DY* = [ 8, 8, 8, 4, 4, 2, 2 ] # y delta values + +# Paeth predicter, used by PNG filter type 4 +proc paethPredictor(a, b, c: int): uint = + let pa = abs(b - c) + let pb = abs(a - c) + let pc = abs(a + b - c - c) + + if(pc < pa) and (pc < pb): return c.uint + elif pb < pa: return b.uint + result = a.uint + +proc filterScanline*[T](output: var openArray[T], input: openArray[T], byteWidth, len: int, filterType: PNGFilter) = + template currPix(i): untyped = input[i].uint + template prevPix(i): untyped = input[i - byteWidth].uint + + case filterType + of FLT_NONE: + for i in 0.. 0: + output[0] = T(FLT_NONE) # filterType T + filterScanline(output.toOpenArray(1, output.len-1), # skip filterType + input, byteWidth, lineTs, FLT_NONE) + + # next line start from 1 + var prevIndex = 0 + for y in 1.. 0: + output[0] = T(predefinedFilters[0]) # filterType T + filterScanline(output.toOpenArray(1, output.len-1), # skip filterType + input, byteWidth, lineTs, predefinedFilters[0]) + + # next line start from 1 + var prevIndex = 0 + for y in 1.. 0: + unfilterScanLine(output, + input.toOpenArray(1, input.len-1), # skip the filterType + byteWidth, lineTs, + PNGFilter(input[0])) + + # next line start from 1 + var prevIndex = 0 + for y in 1.. -1: + result += readBitFromReversedStream(bitptr, bitstream) shl i + dec i + +proc `&=`[T](a: var T, b: T) = + a = T(int(a) and int(b)) + +proc `|=`[T](a: var T, b: T) = + a = T(int(a) or int(b)) + +proc setBitOfReversedStream0*[T](bitptr: var int, bitstream: var openArray[T], bit: int) = + # the current bit in bitstream must be 0 for this to work + if bit != 0: + # earlier bit of huffman code is in a lesser significant bit of an earlier T + bitstream[bitptr shr 3] |= cast[T](bit shl (7 - (bitptr and 0x7))) + inc bitptr + +proc setBitOfReversedStream*[T](bitptr: var int, bitstream: var openArray[T], bit: int) = + # the current bit in bitstream may be 0 or 1 for this to work + if bit == 0: bitstream[bitptr shr 3] &= cast[T](not (1 shl (7 - (bitptr and 0x7)))) + else: bitstream[bitptr shr 3] |= cast[T](1 shl (7 - (bitptr and 0x7))) + inc bitptr + +proc removePaddingBits*[T](output: var openArray[T], input: openArray[T], olinebits, ilinebits, h: int) = + # After filtering there are still padding bits if scanLines have non multiple of 8 bit amounts. They need + # to be removed (except at last scanLine of (Adam7-reduced) image) before working with pure image buffers + # for the Adam7 code, the color convert code and the output to the user. + # in and out are allowed to be the same buffer, in may also be higher but still overlapping; in must + # have >= ilinebits*h bits, out must have >= olinebits*h bits, olinebits must be <= ilinebits + # also used to move bits after earlier such operations happened, e.g. in a sequence of reduced images from Adam7 + # only useful if (ilinebits - olinebits) is a value in the range 1..7 + + let diff = ilinebits - olinebits + var + ibp = 0 + obp = 0 # input and output bit pointers + for y in 0..h-1: + for x in 0..olinebits-1: + var bit = readBitFromReversedStream(ibp, input) + setBitOfReversedStream(obp, output, bit) + inc(ibp, diff) + +# Outputs various dimensions and positions in the image related to the Adam7 reduced images. +# passw: output containing the width of the 7 passes +# passh: output containing the height of the 7 passes +# filter_passstart: output containing the index of the start and end of each +# reduced image with filter Ts +# padded_passstart output containing the index of the start and end of each +# reduced image when without filter Ts but with padded scanLines +# passstart: output containing the index of the start and end of each reduced +# image without padding between scanLines, but still padding between the images +# w, h: width and height of non-interlaced image +# bpp: bits per pixel +# "padded" is only relevant if bpp is less than 8 and a scanLine or image does not +# end at a full T +proc adam7PassValues*(pass: var PNGPass, w, h, bpp: int) = + # the passstart values have 8 values: + # the 8th one indicates the T after the end of the 7th (= last) pass + + # calculate width and height in pixels of each pass + for i in 0..6: + pass.w[i] = (w + ADAM7_DX[i] - ADAM7_IX[i] - 1) div ADAM7_DX[i] + pass.h[i] = (h + ADAM7_DY[i] - ADAM7_IY[i] - 1) div ADAM7_DY[i] + if pass.w[i] == 0: pass.h[i] = 0 + if pass.h[i] == 0: pass.w[i] = 0 + + pass.filterStart[0] = 0 + pass.paddedStart[0] = 0 + pass.start[0] = 0 + for i in 0..6: + # if passw[i] is 0, it's 0 Ts, not 1 (no filtertype-T) + pass.filterStart[i + 1] = pass.filterStart[i] + if (pass.w[i] != 0) and (pass.h[i] != 0): + pass.filterStart[i + 1] += pass.h[i] * (1 + (pass.w[i] * bpp + 7) div 8) + # bits padded if needed to fill full T at end of each scanLine + pass.paddedStart[i + 1] = pass.paddedStart[i] + pass.h[i] * ((pass.w[i] * bpp + 7) div 8) + # only padded at end of reduced image + pass.start[i + 1] = pass.start[i] + (pass.h[i] * pass.w[i] * bpp + 7) div 8 + +# input: Adam7 interlaced image, with no padding bits between scanLines, but between +# reduced images so that each reduced image starts at a T. +# output: the same pixels, but re-ordered so that they're now a non-interlaced image with size w*h +# bpp: bits per pixel +# output has the following size in bits: w * h * bpp. +# input is possibly bigger due to padding bits between reduced images. +# output must be big enough AND must be 0 everywhere if bpp < 8 in the current implementation +# (because that's likely a little bit faster) +# NOTE: comments about padding bits are only relevant if bpp < 8 +proc adam7Deinterlace*[T](output: var openArray[T], input: openArray[T], w, h, bpp: int) = + var pass: PNGPass + adam7PassValues(pass, w, h, bpp) + + if bpp >= 8: + for i in 0..6: + let byteWidth = bpp div 8 + for y in 0..= 8: + for i in 0..6: + let byteWidth = bpp div 8 + for y in 0..= ilinebits + + let diff = olinebits - ilinebits + var + obp = 0 + ibp = 0 #bit pointers + + for y in 0..h-1: + for x in 0..ilinebits-1: + let bit = readBitFromReversedStream(ibp, input) + setBitOfReversedStream(obp, output, bit) + for x in 0..diff-1: setBitOfReversedStream(obp, output, 0) diff --git a/nimPNG/nimz.nim b/nimPNG/nimz.nim index ed79d50..dacacf6 100644 --- a/nimPNG/nimz.nim +++ b/nimPNG/nimz.nim @@ -1,4 +1,4 @@ -import streams, sequtils, algorithm, strutils +import sequtils const FIRST_LENGTH_CODE_INDEX = 257 @@ -1224,6 +1224,12 @@ proc nzDeflateInit*(input: string): nzStream = template nzCompressInit*(input: string): nzStream = nzDeflateInit(input) +template nzCompressInit*(input: seq[byte]): nzStream = + nzDeflateInit(cast[string](input)) + +template nzCompressInit*(input: seq[char]): nzStream = + nzDeflateInit(cast[string](input)) + proc nzInflateInit*(input: string): nzStream = var nz = nzInit() nz.data = newStringOfCap(1024 * 1024 * 5) # Allocate 5MB in advance diff --git a/nimPNG/png_types.nim b/nimPNG/png_types.nim new file mode 100644 index 0000000..9a687df --- /dev/null +++ b/nimPNG/png_types.nim @@ -0,0 +1,14 @@ +type + PNGFilterStrategy* = enum + #every filter at zero + LFS_ZERO, + #Use filter that gives minimum sum, as described in the official PNG filter heuristic. + LFS_MINSUM, + #Use the filter type that gives smallest Shannon entropy for this scanLine. Depending + #on the image, this is better or worse than minsum. + LFS_ENTROPY, + #Brute-force-search PNG filters by compressing each filter for each scanLine. + #Experimental, very slow, and only rarely gives better compression than MINSUM. + LFS_BRUTE_FORCE, + #use predefined_filters buffer: you specify the filter type for each scanLine + LFS_PREDEFINED \ No newline at end of file diff --git a/tests/all_tests.nim b/tests/all_tests.nim new file mode 100644 index 0000000..114ddb0 --- /dev/null +++ b/tests/all_tests.nim @@ -0,0 +1,6 @@ +import + test_apng, + test_codec, + test_suite, + test_nimz, + test_filters diff --git a/tests/randutils.nim b/tests/randutils.nim new file mode 100644 index 0000000..a003afd --- /dev/null +++ b/tests/randutils.nim @@ -0,0 +1,61 @@ +import random, sets + +type + RandGen*[T] = object + minVal, maxVal: T + + Bytes* = seq[byte] + +proc rng*[T](minVal, maxVal: T): RandGen[T] = + doAssert(minVal <= maxVal) + result.minVal = minVal + result.maxVal = maxVal + +proc rng*[T](minMax: T): RandGen[T] = + rng(minMax, minMax) + +proc getVal*[T](x: RandGen[T]): T = + if x.minVal == x.maxVal: return x.minVal + rand(x.minVal..x.maxVal) + +proc randString*(len: int): string = + result = newString(len) + for i in 0.. 512: #assertTrue(s.data.len < image.data.len, "compressed size") + #debugEcho "PNG LEN: ", s.getPosition() + #debugEcho "PNG DATA: ", s.data.toHex + s.setPosition 0 var decoded = s.decodePNG(image.colorType, image.bitDepth) + #debugEcho "DECODED LEN: ", decoded.data.len + #debugEcho "DECODED DATA: ", decoded.data.toHex + assertEquals(image.width, decoded.width) assertEquals(image.height, decoded.height) @@ -485,12 +491,12 @@ proc testPredefinedFilters() = var state = makePNGEncoder() state.filterStrategy = LFS_PREDEFINED state.filterPaletteZero = false - + # everything to filter type 'FLT_AVERAGE' state.predefinedFilters = newSeq[PNGFilter](h) for i in 0..