After Width: | Height: | Size: 349 B |
After Width: | Height: | Size: 356 B |
After Width: | Height: | Size: 361 B |
After Width: | Height: | Size: 375 B |
After Width: | Height: | Size: 382 B |
After Width: | Height: | Size: 393 B |
After Width: | Height: | Size: 399 B |
395
nimPNG.nim
|
@ -181,6 +181,8 @@ type
|
||||||
blendOp*: APNG_BLEND_OP
|
blendOp*: APNG_BLEND_OP
|
||||||
|
|
||||||
APNGFrameData = ref object of APNGFrameChunk
|
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
|
frameDataPos: int
|
||||||
|
|
||||||
PNGPass = object
|
PNGPass = object
|
||||||
|
@ -214,9 +216,15 @@ type
|
||||||
second*: int #range[0..60] #to allow for leap seconds
|
second*: int #range[0..60] #to allow for leap seconds
|
||||||
|
|
||||||
PNG* = ref object
|
PNG* = ref object
|
||||||
|
# during encoding, settings is PNGEncoder
|
||||||
|
# during decoding, settings is PNGDecoder
|
||||||
settings*: PNGSettings
|
settings*: PNGSettings
|
||||||
chunks*: seq[PNGChunk]
|
chunks*: seq[PNGChunk]
|
||||||
pixels*: string
|
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]
|
apngChunks*: seq[APNGFrameChunk]
|
||||||
firstFrameIsDefaultImage*: bool
|
firstFrameIsDefaultImage*: bool
|
||||||
isAPNG*: bool
|
isAPNG*: bool
|
||||||
|
@ -1912,90 +1920,108 @@ proc toString(chunk: APNGFrameData): string =
|
||||||
result = newString(fdatLen)
|
result = newString(fdatLen)
|
||||||
copyMem(result[0].addr, fdatAddr, 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..<numFrames:
|
||||||
|
let ctl = frameControl[i]
|
||||||
|
var nz = nzInflateInit(frameData[i])
|
||||||
|
nz.ignoreAdler32 = PNGDecoder(apng.png.settings).ignoreAdler32
|
||||||
|
let idat = zlib_decompress(nz)
|
||||||
|
apng.png.postProcessScanLines(ctl, idat)
|
||||||
|
|
||||||
|
if apng.result != nil:
|
||||||
|
if PNGDecoder(apng.png.settings).colorConvert:
|
||||||
|
apng.result.frames.add apng.png.convert(colorType, bitDepth, ctl, apng.png.apngPixels[^1])
|
||||||
|
else:
|
||||||
|
var frame = new(APNGFrame)
|
||||||
|
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*(s: Stream, colorType: PNGcolorType, bitDepth: int, settings = PNGDecoder(nil)): PNGResult =
|
||||||
if not bitDepthAllowed(colorType, bitDepth):
|
if not bitDepthAllowed(colorType, bitDepth):
|
||||||
raise PNGError("colorType and bitDepth combination not allowed")
|
raise PNGError("colorType and bitDepth combination not allowed")
|
||||||
|
|
||||||
var png = s.parsePNG(settings)
|
var png = s.parsePNG(settings)
|
||||||
let header = PNGHeader(png.getChunk(IHDR))
|
|
||||||
# shadowing the settings
|
|
||||||
let settings = PNGDecoder(png.settings)
|
|
||||||
png.postProcessScanLines()
|
png.postProcessScanLines()
|
||||||
|
|
||||||
if settings.colorConvert:
|
if PNGDecoder(png.settings).colorConvert:
|
||||||
result = png.convert(colorType, bitDepth)
|
result = png.convert(colorType, bitDepth)
|
||||||
else:
|
else:
|
||||||
new(result)
|
new(result)
|
||||||
|
let header = PNGHeader(png.getChunk(IHDR))
|
||||||
result.width = header.width
|
result.width = header.width
|
||||||
result.height = header.height
|
result.height = header.height
|
||||||
result.data = png.pixels
|
result.data = png.pixels
|
||||||
|
|
||||||
if png.isAPNG:
|
if png.isAPNG:
|
||||||
var
|
var apng = APNG(png: png, result: result)
|
||||||
actl = APNGAnimationControl(png.getChunk(acTL))
|
apng.processingAPNG(colorType, bitDepth)
|
||||||
frameControl = newSeqOfCap[APNGFrameControl](actl.numFrames)
|
|
||||||
frameData = newSeqOfCap[string](actl.numFrames)
|
|
||||||
numFrames = 0
|
|
||||||
lastChunkType = PNGChunkType(0)
|
|
||||||
start = 0
|
|
||||||
|
|
||||||
if png.firstFrameIsDefaultImage:
|
|
||||||
start = 1
|
|
||||||
# IDAT already processed, so we add a dummy here
|
|
||||||
frameData.add string(nil)
|
|
||||||
|
|
||||||
for x in png.apngChunks:
|
|
||||||
if x.chunkType == fcTL:
|
|
||||||
frameControl.add APNGFrameControl(x)
|
|
||||||
inc numFrames
|
|
||||||
lastChunkType = fcTL
|
|
||||||
else:
|
|
||||||
let y = APNGFrameData(x)
|
|
||||||
if lastChunkType == fdAT:
|
|
||||||
frameData[^1].add y.toString()
|
|
||||||
else:
|
|
||||||
frameData.add y.toString()
|
|
||||||
lastChunkType = fdAT
|
|
||||||
|
|
||||||
if actl.numFrames == 0 or actl.numFrames != numFrames or actl.numFrames != frameData.len:
|
|
||||||
raise PNGError("animation numFrames error")
|
|
||||||
|
|
||||||
png.apngPixels = newSeqOfCap[string](numFrames)
|
|
||||||
result.frames = newSeqOfCap[APNGFrame](numFrames)
|
|
||||||
|
|
||||||
if png.firstFrameIsDefaultImage:
|
|
||||||
let ctl = frameControl[0]
|
|
||||||
if ctl.width != header.width or ctl.height != header.height:
|
|
||||||
raise PNGError("animation control error: dimension")
|
|
||||||
if ctl.xOffset != 0 or ctl.xOffset != 0:
|
|
||||||
raise PNGError("animation control error: offset")
|
|
||||||
var frame = new(APNGFrame)
|
|
||||||
frame.ctl = ctl
|
|
||||||
frame.data = result.data
|
|
||||||
result.frames.add frame
|
|
||||||
|
|
||||||
for i in start..<numFrames:
|
|
||||||
let ctl = frameControl[i]
|
|
||||||
var nz = nzInflateInit(frameData[i])
|
|
||||||
nz.ignoreAdler32 = PNGDecoder(png.settings).ignoreAdler32
|
|
||||||
let idat = zlib_decompress(nz)
|
|
||||||
png.postProcessScanLines(ctl, idat)
|
|
||||||
|
|
||||||
if PNGDecoder(png.settings).colorConvert:
|
|
||||||
result.frames.add png.convert(colorType, bitDepth, ctl, png.apngPixels[^1])
|
|
||||||
else:
|
|
||||||
var frame = new(APNGFrame)
|
|
||||||
frame.ctl = ctl
|
|
||||||
frame.data = png.apngPixels[^1]
|
|
||||||
result.frames.add frame
|
|
||||||
|
|
||||||
proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG =
|
proc decodePNG*(s: Stream, settings = PNGDecoder(nil)): PNG =
|
||||||
var png = s.parsePNG(settings)
|
var png = s.parsePNG(settings)
|
||||||
png.postProcessScanLines()
|
png.postProcessScanLines()
|
||||||
|
|
||||||
|
if png.isAPNG:
|
||||||
|
var apng = APNG(png: png, result: nil)
|
||||||
|
apng.processingAPNG(PNGColorType(0), 0)
|
||||||
|
|
||||||
result = png
|
result = png
|
||||||
|
|
||||||
when not defined(js):
|
when not defined(js):
|
||||||
proc loadPNG*(fileName: string, colorType: PNGcolorType, bitDepth: int, settings: PNGDecoder): PNGResult =
|
proc loadPNG*(fileName: string, colorType: PNGColorType, bitDepth: int, settings: PNGDecoder): PNGResult =
|
||||||
try:
|
try:
|
||||||
var s = newFileStream(fileName, fmRead)
|
var s = newFileStream(fileName, fmRead)
|
||||||
if s == nil: return nil
|
if s == nil: return nil
|
||||||
|
@ -2108,6 +2134,9 @@ type
|
||||||
|
|
||||||
unknown*: seq[PNGUnknown]
|
unknown*: seq[PNGUnknown]
|
||||||
|
|
||||||
|
# APNG number of plays, 0 = infinite
|
||||||
|
numPlays*: int
|
||||||
|
|
||||||
PNGColorProfile = ref object
|
PNGColorProfile = ref object
|
||||||
colored: bool #not greyscale
|
colored: bool #not greyscale
|
||||||
key: bool #if true, image is not opaque. Only if true and alpha is false, color key is possible.
|
key: bool #if true, image is not opaque. Only if true and alpha is false, color key is possible.
|
||||||
|
@ -2142,6 +2171,7 @@ proc makePNGEncoder*(): PNGEncoder =
|
||||||
s.textList = @[]
|
s.textList = @[]
|
||||||
s.itextList = @[]
|
s.itextList = @[]
|
||||||
s.unknown = @[]
|
s.unknown = @[]
|
||||||
|
s.numPlays = 0
|
||||||
result = s
|
result = s
|
||||||
|
|
||||||
proc addText*(state: PNGEncoder, keyword, text: string) =
|
proc addText*(state: PNGEncoder, keyword, text: string) =
|
||||||
|
@ -2389,6 +2419,31 @@ method writeChunk(chunk: PNGICCProfile, png: PNG): bool =
|
||||||
chunk.writeString zlib_compress(nz)
|
chunk.writeString zlib_compress(nz)
|
||||||
result = true
|
result = true
|
||||||
|
|
||||||
|
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 =
|
proc isGreyscaleType(mode: PNGColorMode): bool =
|
||||||
result = mode.colorType in {LCT_GREY, LCT_GREY_ALPHA}
|
result = mode.colorType in {LCT_GREY, LCT_GREY_ALPHA}
|
||||||
|
|
||||||
|
@ -2423,8 +2478,7 @@ proc differ(p: RGBA16): bool =
|
||||||
if (p.a and 255) != ((p.a shr 8) and 255): return true
|
if (p.a and 255) != ((p.a shr 8) and 255): return true
|
||||||
result = false
|
result = false
|
||||||
|
|
||||||
proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProfile =
|
proc calculateColorProfile(input: string, w, h: int, mode: PNGColorMode, prof: PNGColorProfile, tree: var Table[RGBA8, int]) =
|
||||||
var prof = makeColorProfile()
|
|
||||||
let
|
let
|
||||||
numPixels = w * h
|
numPixels = w * h
|
||||||
bpp = getBPP(mode)
|
bpp = getBPP(mode)
|
||||||
|
@ -2436,7 +2490,6 @@ proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProf
|
||||||
numColorsDone = false
|
numColorsDone = false
|
||||||
sixteen = false
|
sixteen = false
|
||||||
maxNumColors = 257
|
maxNumColors = 257
|
||||||
tree = initTable[RGBA8, int]()
|
|
||||||
|
|
||||||
if bpp <= 8:
|
if bpp <= 8:
|
||||||
case bpp
|
case bpp
|
||||||
|
@ -2537,6 +2590,19 @@ proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProf
|
||||||
prof.keyR += prof.keyR shl 8
|
prof.keyR += prof.keyR shl 8
|
||||||
prof.keyG += prof.keyG shl 8
|
prof.keyG += prof.keyG shl 8
|
||||||
prof.keyB += prof.keyB 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..<png.apngChunks.len:
|
||||||
|
var ctl = APNGFrameControl(png.apngChunks[i])
|
||||||
|
calculateColorProfile(png.apngPixels[i], ctl.width, ctl.height, mode, prof, tree)
|
||||||
|
|
||||||
result = prof
|
result = prof
|
||||||
|
|
||||||
#Automatically chooses color type that gives smallest amount of bits in the
|
#Automatically chooses color type that gives smallest amount of bits in the
|
||||||
|
@ -2544,10 +2610,12 @@ proc getColorProfile(input: string, w, h: int, mode: PNGColorMode): PNGColorProf
|
||||||
#are less than 256 colors, ...
|
#are less than 256 colors, ...
|
||||||
#Updates values of mode with a potentially smaller color model. mode_out should
|
#Updates values of mode with a potentially smaller color model. mode_out should
|
||||||
#contain the user chosen color model, but will be overwritten with the new chosen one.
|
#contain the user chosen color model, but will be overwritten with the new chosen one.
|
||||||
proc autoChooseColor(modeOut: PNGColorMode, input: string, w, h: int, modeIn: PNGColorMode) =
|
proc autoChooseColor(png: PNG, modeOut, modeIn: PNGColorMode) =
|
||||||
var prof = getColorProfile(input, w, h, modeIn)
|
var prof = png.getColorProfile(modeIn)
|
||||||
modeOut.keyDefined = false
|
modeOut.keyDefined = false
|
||||||
|
|
||||||
|
let w = png.width
|
||||||
|
let h = png.height
|
||||||
if prof.key and ((w * h) <= 16):
|
if prof.key and ((w * h) <= 16):
|
||||||
prof.alpha = true #too few pixels to justify tRNS chunk overhead
|
prof.alpha = true #too few pixels to justify tRNS chunk overhead
|
||||||
if prof.bits < 8: prof.bits = 8 #PNG has no alphachannel modes with less than 8-bit per channel
|
if prof.bits < 8: prof.bits = 8 #PNG has no alphachannel modes with less than 8-bit per channel
|
||||||
|
@ -2876,7 +2944,7 @@ proc Adam7Interlace(output: var DataBuf, input: DataBuf, w, h, bpp: int) =
|
||||||
let bit = readBitFromReversedStream(ibp, input)
|
let bit = readBitFromReversedStream(ibp, input)
|
||||||
setBitOfReversedStream(obp, output, bit)
|
setBitOfReversedStream(obp, output, bit)
|
||||||
|
|
||||||
proc preProcessScanLines(png: PNG, input: DataBuf, w, h: int, modeOut: PNGColorMode, state: PNGEncoder) =
|
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:
|
#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 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
|
# if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter
|
||||||
|
@ -2886,8 +2954,8 @@ proc preProcessScanLines(png: PNG, input: DataBuf, w, h: int, modeOut: PNGColorM
|
||||||
#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 scanLen = (w * bpp + 7) div 8
|
||||||
let outSize = h + (h * scanLen)
|
let outSize = h + (h * scanLen)
|
||||||
png.pixels = newString(outSize)
|
png.apngPixels[frameNo] = newString(outSize)
|
||||||
var output = initBuffer(png.pixels)
|
var output = initBuffer(png.apngPixels[frameNo])
|
||||||
#non multiple of 8 bits per scanLine, padding bits needed per scanLine
|
#non multiple of 8 bits per scanLine, padding bits needed per scanLine
|
||||||
if(bpp < 8) and ((w * bpp) != (scanLen * 8)):
|
if(bpp < 8) and ((w * bpp) != (scanLen * 8)):
|
||||||
var padded = initBuffer(newString(h * scanLen))
|
var padded = initBuffer(newString(h * scanLen))
|
||||||
|
@ -2902,9 +2970,9 @@ proc preProcessScanLines(png: PNG, input: DataBuf, w, h: int, modeOut: PNGColorM
|
||||||
var pass: PNGPass
|
var pass: PNGPass
|
||||||
Adam7PassValues(pass, w, h, bpp)
|
Adam7PassValues(pass, w, h, bpp)
|
||||||
let outSize = pass.filterStart[7]
|
let outSize = pass.filterStart[7]
|
||||||
png.pixels = newString(outSize)
|
png.apngPixels[frameNo] = newString(outSize)
|
||||||
var adam7 = initBuffer(newString(pass.start[7]))
|
var adam7 = initBuffer(newString(pass.start[7]))
|
||||||
var output = initBuffer(png.pixels)
|
var output = initBuffer(png.apngPixels[frameNo])
|
||||||
|
|
||||||
Adam7Interlace(adam7, input, w, h, bpp)
|
Adam7Interlace(adam7, input, w, h, bpp)
|
||||||
for i in 0..6:
|
for i in 0..6:
|
||||||
|
@ -3037,17 +3105,44 @@ proc addChunkIEND(png: PNG) =
|
||||||
var chunk = make[PNGEnd](IEND, 0)
|
var chunk = make[PNGEnd](IEND, 0)
|
||||||
png.chunks.add chunk
|
png.chunks.add chunk
|
||||||
|
|
||||||
proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
proc addChunkacTL(png: PNG, numFrames, numPlays: int) =
|
||||||
var png: PNG
|
var chunk = make[APNGAnimationControl](acTL, 8)
|
||||||
new(png)
|
chunk.numFrames = numFrames
|
||||||
png.chunks = @[]
|
chunk.numPlays = numPlays
|
||||||
|
png.chunks.add chunk
|
||||||
|
|
||||||
if settings == nil: png.settings = makePNGEncoder()
|
proc addChunkfcTL(png: PNG, chunk: APNGFrameControl, sequenceNumber: int) =
|
||||||
else: png.settings = settings
|
chunk.chunkType = fcTL
|
||||||
|
if chunk.data.isNil:
|
||||||
|
chunk.data = newStringOfCap(26)
|
||||||
|
chunk.sequenceNumber = sequenceNumber
|
||||||
|
png.chunks.add chunk
|
||||||
|
|
||||||
|
proc addChunkfdAT(png: PNG, sequenceNumber, frameDataPos: int) =
|
||||||
|
var chunk = make[APNGFrameData](fdAT, 0)
|
||||||
|
chunk.sequenceNumber = sequenceNumber
|
||||||
|
chunk.frameDataPos = frameDataPos
|
||||||
|
png.chunks.add chunk
|
||||||
|
|
||||||
|
proc frameConvert(png: PNG, modeIn, modeOut: PNGColorMode, w, h, frameNo: int, state: PNGEncoder) =
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
preProcessScanLines(png, initBuffer(png.apngPixels[frameNo]), frameNo, w, h, modeOut, state)
|
||||||
|
|
||||||
|
proc encoderCore(png: PNG) =
|
||||||
let state = PNGEncoder(png.settings)
|
let state = PNGEncoder(png.settings)
|
||||||
var modeIn = newColorMode(state.modeIn)
|
var modeIn = newColorMode(state.modeIn)
|
||||||
var modeOut = newColorMode(state.modeOut)
|
var modeOut = newColorMode(state.modeOut)
|
||||||
|
var sequenceNumber = 0
|
||||||
|
|
||||||
if not bitDepthAllowed(modeIn.colorType, modeIn.bitDepth):
|
if not bitDepthAllowed(modeIn.colorType, modeIn.bitDepth):
|
||||||
raise PNGError("modeIn colorType and bitDepth combination not allowed")
|
raise PNGError("modeIn colorType and bitDepth combination not allowed")
|
||||||
|
@ -3059,12 +3154,12 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
(modeOut.paletteSize == 0 or modeOut.paletteSize > 256):
|
(modeOut.paletteSize == 0 or modeOut.paletteSize > 256):
|
||||||
raise PNGError("invalid palette size, it is only allowed to be 1-256")
|
raise PNGError("invalid palette size, it is only allowed to be 1-256")
|
||||||
|
|
||||||
let inputSize = getRawSize(w, h, modeIn)
|
let inputSize = getRawSize(png.width, png.height, modeIn)
|
||||||
if input.len < inputSize:
|
if png.pixels.len < inputSize:
|
||||||
raise PNGError("not enough input to encode")
|
raise PNGError("not enough input to encode")
|
||||||
|
|
||||||
if state.autoConvert:
|
if state.autoConvert:
|
||||||
autoChooseColor(modeOut, input, w, h, modeIn)
|
png.autoChooseColor(modeOut, modeIn)
|
||||||
|
|
||||||
if state.interlaceMethod notin {IM_NONE, IM_INTERLACED}:
|
if state.interlaceMethod notin {IM_NONE, IM_INTERLACED}:
|
||||||
raise PNGError("unexisting interlace mode")
|
raise PNGError("unexisting interlace mode")
|
||||||
|
@ -3072,18 +3167,12 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
if not bitDepthAllowed(modeOut.colorType, modeOut.bitDepth):
|
if not bitDepthAllowed(modeOut.colorType, modeOut.bitDepth):
|
||||||
raise PNGError("colorType and bitDepth combination not allowed")
|
raise PNGError("colorType and bitDepth combination not allowed")
|
||||||
|
|
||||||
if modeIn != modeOut:
|
if not png.isAPNG: png.apngPixels = @[""]
|
||||||
let size = (w * h * getBPP(modeOut) + 7) div 8
|
shallowCopy(png.apngPixels[0], png.pixels)
|
||||||
let numPixels = w * h
|
png.frameConvert(modeIn, modeOut, png.width, png.height, 0, state)
|
||||||
|
shallowCopy(png.pixels, png.apngPixels[0])
|
||||||
|
|
||||||
var converted = newString(size)
|
png.addChunkIHDR(png.width, png.height, modeOut, state)
|
||||||
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)
|
|
||||||
#unknown chunks between IHDR and PLTE
|
#unknown chunks between IHDR and PLTE
|
||||||
if state.unknown.len > 0:
|
if state.unknown.len > 0:
|
||||||
png.chunks.add state.unknown[0]
|
png.chunks.add state.unknown[0]
|
||||||
|
@ -3107,9 +3196,30 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
if state.unknown.len > 1:
|
if state.unknown.len > 1:
|
||||||
png.chunks.add state.unknown[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)
|
#IDAT (multiple IDAT chunks must be consecutive)
|
||||||
png.addChunkIDAT(state)
|
png.addChunkIDAT(state)
|
||||||
|
|
||||||
|
if png.isAPNG:
|
||||||
|
let len = png.apngChunks.len
|
||||||
|
for i in 1..<len:
|
||||||
|
var ctl = APNGFrameControl(png.apngChunks[i])
|
||||||
|
png.addChunkfcTL(ctl, sequenceNumber)
|
||||||
|
inc sequenceNumber
|
||||||
|
|
||||||
|
png.frameConvert(modeIn, modeOut, ctl.width, ctl.height, i, state)
|
||||||
|
png.addChunkfdAT(sequenceNumber, i)
|
||||||
|
inc sequenceNumber
|
||||||
|
|
||||||
if state.timeDefined: png.addChunktIME(state)
|
if state.timeDefined: png.addChunktIME(state)
|
||||||
|
|
||||||
for txt in state.textList:
|
for txt in state.textList:
|
||||||
|
@ -3128,11 +3238,24 @@ proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
png.chunks.add state.unknown[2]
|
png.chunks.add state.unknown[2]
|
||||||
|
|
||||||
png.addChunkIEND()
|
png.addChunkIEND()
|
||||||
|
|
||||||
|
proc encodePNG*(input: string, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
|
var png: PNG
|
||||||
|
new(png)
|
||||||
|
png.chunks = @[]
|
||||||
|
|
||||||
|
if settings == nil: png.settings = makePNGEncoder()
|
||||||
|
else: png.settings = settings
|
||||||
|
|
||||||
|
png.width = w
|
||||||
|
png.height = h
|
||||||
|
shallowCopy(png.pixels, input)
|
||||||
|
png.encoderCore()
|
||||||
result = png
|
result = png
|
||||||
|
|
||||||
proc encodePNG*(input: string, colorType: PNGcolorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG =
|
proc encodePNG*(input: string, colorType: PNGcolorType, bitDepth, w, h: int, settings = PNGEncoder(nil)): PNG =
|
||||||
if not bitDepthAllowed(colorType, bitDepth):
|
if not bitDepthAllowed(colorType, bitDepth):
|
||||||
raise PNGError("colorType and bitDepth combination not allowed")
|
raise PNGError("colorType and bitDepth combination not allowed")
|
||||||
|
|
||||||
var state: PNGEncoder
|
var state: PNGEncoder
|
||||||
if settings == nil: state = makePNGEncoder()
|
if settings == nil: state = makePNGEncoder()
|
||||||
|
@ -3180,6 +3303,92 @@ when not defined(js):
|
||||||
proc savePNG24*(fileName, input: string, w, h: int): bool =
|
proc savePNG24*(fileName, input: string, w, h: int): bool =
|
||||||
result = savePNG(fileName, input, LCT_RGB, 8, w, h)
|
result = savePNG(fileName, input, LCT_RGB, 8, w, h)
|
||||||
|
|
||||||
|
proc prepareAPNG*(colorType: PNGcolorType, bitDepth, numPlays: int, settings = PNGEncoder(nil)): PNG =
|
||||||
|
var state: PNGEncoder
|
||||||
|
if settings == nil: state = makePNGEncoder()
|
||||||
|
else: state = settings
|
||||||
|
|
||||||
|
state.numPlays = numPlays
|
||||||
|
state.modeIn.colorType = colorType
|
||||||
|
state.modeIn.bitDepth = bitDepth
|
||||||
|
|
||||||
|
var png: PNG
|
||||||
|
new(png)
|
||||||
|
png.chunks = @[]
|
||||||
|
png.settings = state
|
||||||
|
png.isAPNG = true
|
||||||
|
png.apngChunks = @[]
|
||||||
|
png.apngPixels = @[]
|
||||||
|
png.pixels = nil
|
||||||
|
png.firstFrameIsDefaultImage = false
|
||||||
|
png.width = 0
|
||||||
|
png.height = 0
|
||||||
|
|
||||||
|
result = png
|
||||||
|
|
||||||
|
proc prepareAPNG24*(numPlays = 0): PNG =
|
||||||
|
result = prepareAPNG(LCT_RGB, 8, numPlays)
|
||||||
|
|
||||||
|
proc prepareAPNG32*(numPlays = 0): PNG =
|
||||||
|
result = prepareAPNG(LCT_RGBA, 8, numPlays)
|
||||||
|
|
||||||
|
proc addDefaultImage*(png: PNG, input: string, width, height: int, ctl = APNGFrameControl(nil)): bool =
|
||||||
|
result = true
|
||||||
|
png.firstFrameIsDefaultImage = ctl != nil
|
||||||
|
if ctl != nil:
|
||||||
|
png.apngChunks.add ctl
|
||||||
|
png.apngPixels.add nil # add dummy
|
||||||
|
result = result and (ctl.xOffset == 0)
|
||||||
|
result = result and (ctl.yOffset == 0)
|
||||||
|
result = result and (ctl.width == width)
|
||||||
|
result = result and (ctl.height == height)
|
||||||
|
else:
|
||||||
|
png.apngChunks.add nil
|
||||||
|
png.apngPixels.add ""
|
||||||
|
|
||||||
|
shallowCopy(png.pixels, input)
|
||||||
|
png.width = width
|
||||||
|
png.height = height
|
||||||
|
|
||||||
|
proc addFrame*(png: PNG, frame: string, ctl: APNGFrameControl): bool =
|
||||||
|
result = true
|
||||||
|
|
||||||
|
# addDefaultImage must be called first
|
||||||
|
if png.apngPixels.len == 0 or png.apngChunks.len == 0: return false
|
||||||
|
if ctl.isNil: return false
|
||||||
|
result = result and (ctl.xOffset >= 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] =
|
proc getFilterTypesInterlaced(png: PNG): seq[string] =
|
||||||
var header = PNGHeader(png.getChunk(IHDR))
|
var header = PNGHeader(png.getChunk(IHDR))
|
||||||
var idat = PNGData(png.getChunk(IDAT))
|
var idat = PNGData(png.getChunk(IDAT))
|
||||||
|
|
35
readme.md
|
@ -81,7 +81,7 @@ to create PNG:
|
||||||
special notes:
|
special notes:
|
||||||
|
|
||||||
* Use **loadPNG** or **savePNG** if you need specific input/output format by supplying supported **colorType** and **bitDepth** information.
|
* Use **loadPNG** or **savePNG** if you need specific input/output format by supplying supported **colorType** and **bitDepth** information.
|
||||||
* Use **encodePNG** or **decodePNG** to do *in-memory* encoding/decoding by supplying desired colorType and bitDepth information
|
* Use **encodePNG** or **decodePNG** to do *in-memory* encoding/decoding by supplying desired **colorType** and **bitDepth** information
|
||||||
|
|
||||||
pixels are stored as raw bytes using Nim's string as container:
|
pixels are stored as raw bytes using Nim's string as container:
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ pixels are stored as raw bytes using Nim's string as container:
|
||||||
|
|
||||||
## Animated PNG (APNG)
|
## 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.
|
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
|
### 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
|
[nimpng-travisci]: https://travis-ci.org/jangko/nimPNG
|
||||||
[badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master
|
[badge-nimpng-travisci]: https://travis-ci.org/jangko/nimPNG.svg?branch=master
|
||||||
|
|
|
@ -51,9 +51,53 @@ proc convert(dir: string) =
|
||||||
if png == nil: continue
|
if png == nil: continue
|
||||||
png.toBMP(bmpName)
|
png.toBMP(bmpName)
|
||||||
|
|
||||||
|
proc generateAPNG() =
|
||||||
|
const numFrames = 7
|
||||||
|
var frames: array[numFrames, PNGResult]
|
||||||
|
|
||||||
|
for i in 0..<numFrames:
|
||||||
|
frames[i] = loadPNG24(".." & DirSep & "apng" & DirSep & "raw" & DirSep & "frame" & $i & ".png")
|
||||||
|
|
||||||
|
var png = prepareAPNG24()
|
||||||
|
|
||||||
|
var ctl = new(APNGFrameControl)
|
||||||
|
ctl.width = frames[0].width
|
||||||
|
ctl.height = frames[0].height
|
||||||
|
ctl.xOffset = 0
|
||||||
|
ctl.yOffset = 0
|
||||||
|
# half second delay = delayNum/delayDen
|
||||||
|
ctl.delayNum = 1
|
||||||
|
ctl.delayDen = 2
|
||||||
|
ctl.disposeOp = APNG_DISPOSE_OP_NONE
|
||||||
|
ctl.blendOp = APNG_BLEND_OP_SOURCE
|
||||||
|
|
||||||
|
if not png.addDefaultImage(frames[0].data, frames[0].width, frames[0].height, ctl):
|
||||||
|
echo "failed to add default image"
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
for i in 1..<numFrames:
|
||||||
|
var ctl = new(APNGFrameControl)
|
||||||
|
ctl.width = frames[i].width
|
||||||
|
ctl.height = frames[i].height
|
||||||
|
ctl.xOffset = 0
|
||||||
|
ctl.yOffset = 0
|
||||||
|
ctl.delayNum = 1
|
||||||
|
ctl.delayDen = 2
|
||||||
|
ctl.disposeOp = APNG_DISPOSE_OP_NONE
|
||||||
|
ctl.blendOp = APNG_BLEND_OP_SOURCE
|
||||||
|
|
||||||
|
if not png.addFrame(frames[i].data, ctl):
|
||||||
|
echo "failed to add frames"
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
if not png.saveAPNG("rainbow.png"):
|
||||||
|
echo "failed to save rainbow.png"
|
||||||
|
quit(1)
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
let data = loadPNG32("sample.png")
|
let data = loadPNG32("sample.png")
|
||||||
assert(not data.isNil)
|
assert(not data.isNil)
|
||||||
convert(".." & DirSep & "apng")
|
convert(".." & DirSep & "apng")
|
||||||
|
generateAPNG()
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|