--!parent
-- ************************************************************************
-- APNG Encoder - D11.5+ version (bytearray based)
--
-- @author Valentin Schmidt
-- @version 0.6
-- @requires xtra "ImgXtra", xtra "fileIO";
-- Optional function optimize_apng() requires xtra "Shell" and apngopt.exe
-- ************************************************************************
property _ix
property _data
property _color_type
property _sequence_number
property _num_plays
property _num_frames
property _palette_colors
property _tranparency
-- constants for dispose_op parameter
property DISPOSE_OP_NONE
property DISPOSE_OP_BACKGROUND
property DISPOSE_OP_PREVIOUS
-- constants for blend_op parameter
property BLEND_OP_SOURCE
property BLEND_OP_OVER
-- filter constants
property FILTER_NONE
property FILTER_SUB
property FILTER_UP
----------------------------------------
-- @constructor
----------------------------------------
on new (me)
me._ix = xtra("ImgXtra").new()
-- no disposal is done on this frame before rendering the next; the contents
-- of the output buffer are left as is.
me.DISPOSE_OP_NONE = 0
-- the frame's region of the output buffer is to be cleared to fully
-- transparent black before rendering the next frame.
me.DISPOSE_OP_BACKGROUND = 1
-- the frame's region of the output buffer is to be reverted to the previous
-- contents before rendering the next frame.
me.DISPOSE_OP_PREVIOUS = 2
-- all color components of the frame, including alpha, overwrite the current
-- contents of the frame's output buffer region
me.BLEND_OP_SOURCE = 0
-- the frame should be composited onto the output buffer based on its alpha,
-- using a simple OVER operation as described in the "Alpha Channel Processing"
-- section of the PNG specification
me.BLEND_OP_OVER = 1
-- the scanline is transmitted unmodified
me.FILTER_NONE = 0
-- transmits the difference between each byte and the value of the
-- corresponding byte of the prior pixel
me.FILTER_SUB = 1
-- transmits the difference between each byte and the value of the
-- corresponding byte of the pixel above
me.FILTER_UP = 2
-- NOTE: other filters like "Average" and "Paeth" are currently not supported
-- by this library
return me
end
----------------------------------------
-- Starts a new APNG
-- @param {integer} [num_plays=0] - how often to loop, 0 means endless looping (default)
-- @param {list} [palette_colors] - if specified, all frame images will be mapped to
-- the colors in this list
-- @param {integer|list} [transparency_index] - index of color in palette_colors that
-- will be transparent in the final APNG file; for 8-bit images transparency_index
-- can also be a list of indexes
----------------------------------------
on init (me, num_plays, palette_colors, transparency_index)
if voidP(num_plays) then num_plays = 0
me._num_plays = num_plays
me._num_frames = 0
me._sequence_number = 0
me._data = bytearray()
me._data.endian = #bigEndian
me._palette_colors = palette_colors
me._tranparency = transparency_index
-- write PNG signature
me._writeBytes(137,80,78,71,13,10,26,10)
end
----------------------------------------
-- Adds new frame to APNG
-- @param {image} frame_image - the frame as Lingo image object
-- @param {integer} ms - duration of the frame in milliseconds
-- @param {integer} [x=0] - x-offset of the image
-- @param {integer} [y=0] - y-offset of the image
-- @param {integer} [dispose_op=0] - how frame is disposed, see comments above
-- @param {integer} [blend_op=0] - how frame is blended, see comments above
-- @param {integer} [filter=0] - PNG filter, see comments above
-- @return {bool} success
----------------------------------------
on addFrame (me, frame_image, ms, x, y, dispose_op, blend_op, filter)
if voidP(x) then x = 0
if voidP(y) then y = 0
if voidP(dispose_op) then dispose_op = 0
if voidP(blend_op) then blend_op = 0
if voidP(filter) then filter = 0
me._num_frames = me._num_frames + 1
if not voidP(me._palette_colors) then
-- map to color palette
props = [:]
props["image"] = frame_image
props["reserved_colors"] = me._palette_colors
frame_image = me._ix.ix_quantizeImage(props)
end if
-- the "master" frame
if me._num_frames=1 then
x = 0
y = 0
case (frame_image.depth) of
24, 32:
if frame_image.useAlpha then
me._color_type = 6 -- RGB with alpha
else
me._color_type = 2 -- RGB
end if
8:
if frame_image.paletteRef=#grayscale then
me._color_type = 0 -- GS
else
me._color_type = 3 -- Palette
end if
otherwise:
return FALSE -- 1, 4, 16-bit unsupported!
end case
-- add IHDR chunk
IHDR = bytearray()
IHDR.endian = #bigEndian
IHDR.writeInt32(frame_image.width)
IHDR.writeInt32(frame_image.height)
IHDR.writeInt8(8) -- bit depth
IHDR.writeInt8(me._color_type)
IHDR.writeInt8(0) -- compression method
IHDR.writeInt8(0) -- filter method
IHDR.writeInt8(0) -- interlace method
me._writeChunk("IHDR", IHDR)
-- add acTL (animation control chunk) => updated later
me._data.writeInt32(8)
me._data.writeRawString("acTL", 4)
me._data.writeInt32(0)
me._data.writeInt32(me._num_plays)
me._data.writeInt32(0)
end if
-- add fcTL (frame control chunk)
fcTL = bytearray()
fcTL.endian = #bigEndian
fcTL.writeInt32(me._sequence_number)
me._sequence_number = me._sequence_number + 1
fcTL.writeInt32(frame_image.width)
fcTL.writeInt32(frame_image.height)
fcTL.writeInt32(x)
fcTL.writeInt32(y)
-- The delay_num and delay_den parameters together specify a fraction
-- indicating the time to display the current frame, in seconds. If the denominator
-- is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies
-- 1/100ths of a second). If the the value of the numerator is 0 the decoder should
-- render the next frame as quickly as possible, though viewers may impose a
-- reasonable lower bound.
-- To keep things simple, we set delay_den to 1000, so delay_num is duration in ms.
fcTL.writeInt16(ms) -- delay_num
fcTL.writeInt16(1000) -- delay_den
fcTL.writeInt8(dispose_op)
fcTL.writeInt8(blend_op)
me._writeChunk("fcTL", fcTL)
-- build IDAT or fdAT chunk
case (me._color_type) of
2: -- RGB
chunk = me._ix.ix_imageToRaw(["image":frame_image, "pattern":"RGB", "png_filter":filter])
6: -- RGBA
chunk = me._ix.ix_imageToRaw(["image":frame_image, "pattern":"RGBA", "png_filter":filter])
0, 3: -- grayscale/palette
chunk = me._ix.ix_imageToRaw(["image":frame_image, "png_filter":filter])
end case
-- compress the IDAT/fdAT chunk
chunk = me._ix.ix_zlibCompress(["data":chunk])
if me._sequence_number>1 then
me._data.writeInt32(chunk.length + 4)
pos = me._data.length
me._data.writeRawString("fdAT", 4)
me._data.writeInt32(me._sequence_number)
me._sequence_number = me._sequence_number + 1
me._data.writeByteArray(chunk)
me._data.writeByteArray(me._ix.ix_xCrc32String(["data":me._data, "offset":pos]))
else
-- add PLTE chunk (custom palette)
if me._color_type=3 then
PLTE = bytearray(768)
repeat with col in me._palette_colors
PLTE.writeInt8(col.red)
PLTE.writeInt8(col.green)
PLTE.writeInt8(col.blue)
end repeat
me._writeChunk("PLTE", PLTE)
end if
-- add tRNS chunk (transparent color index)
if not voidP(me._tranparency) then
case (me._color_type) of
0: -- GS
tRNS = bytearray()
tRNS.writeInt8(0)
tRNS.writeInt8(me._tranparency)
me._writeChunk("tRNS", tRNS)
2: -- RGB
tRNS = bytearray()
tRNS.writeInt8(0)
tRNS.writeInt8(me._tranparency.red)
tRNS.writeInt8(0)
tRNS.writeInt8(me._tranparency.green)
tRNS.writeInt8(0)
tRNS.writeInt8(me._tranparency.blue)
me._writeChunk("tRNS", tRNS)
3: -- Palette
tRNS = bytearray(256, 255)
if integerP(me._tranparency) then
tRNS[me._tranparency] = 0
else if listP(me._tranparency) then
repeat with t in me._tranparency
tRNS.writeInt8(t)
end repeat
end if
me._writeChunk("tRNS", tRNS)
end case
end if
me._writeChunk("IDAT", chunk)
end if
return TRUE
end
----------------------------------------
-- Returns APNG as byteArray
-- Note: you can either call getData() or writeFile(), but not both
-- @return {byteArray}
----------------------------------------
on getData (me)
-- add IEND chunk
me._writeBytes(0,0,0,0)
me._data.writeRawString("IEND", 4)
me._writeBytes(174,66,96,130)
actl_pos = 33
-- update num_frames in acTL chunk
me._data.position = actl_pos+9
me._data.writeInt32(me._num_frames)
-- update CRC of acTL chunk
me._data.position = actl_pos+5
crc_data = _data.readByteArray(12)
me._data.position = actl_pos+17
me._data.writeByteArray(me._ix.ix_xCrc32String(["data":crc_data]))
return me._data
end
----------------------------------------
-- Saves APNG as file
-- Note: you can either call getData() or writeFile(), but not both
-- @param {string} png_file
-- @return {bool} success
----------------------------------------
on writeFile (me, png_file)
-- add IEND chunk
me._writeBytes(0,0,0,0)
me._data.writeRawString("IEND", 4)
me._writeBytes(174,66,96,130)
actl_pos = 33
-- update num_frames in acTL chunk
me._data.position = actl_pos+9
me._data.writeInt32(me._num_frames)
-- update CRC of acTL chunk
me._data.position = actl_pos+5
crc_data = _data.readByteArray(12)
me._data.position = actl_pos+17
me._data.writeByteArray(me._ix.ix_xCrc32String(["data":crc_data]))
-- save to file
fp = xtra("fileIO").new()
fp.openFile(png_file, 2)
err = fp.status()
if not err then fp.delete()
else if (err and not (err = -37)) then return FALSE
fp.createFile(png_file)
if fp.status() then return FALSE
fp.openFile(png_file, 2)
if fp.status() then return FALSE
fp.writeByteArray(me._data, 1, me._data.length)
fp.closeFile()
return TRUE
end
----------------------------------------
-- Utility function: pass a "typical" image for the animation; returns a palette as list of colors
-- @param {image} master_image
-- @param {integer} [palette_size=256]
-- @return {list|FALSE}
----------------------------------------
on findPalette (me, master_image, palette_size)
props = [:]
props["image"] = master_image
if integerP(palette_size) then props["palette_size"] = palette_size
img = me._ix.ix_quantizeImage(props)
if ilk(img)<>#image then return FALSE
palette_colors = me._ix.ix_getPaletteColors(["palette":img.paletteRef])
img.paletteRef.erase()
return palette_colors
end
----------------------------------------
-- Utility function: optimizes APNG file using apngopt.exe
-- Usage: ok = optimize_apng(_movie.path&"foo.png",\
-- _movie.path&"foo_opt.png")
--
-- @requires Shell xtra, apngopt.exe
-- @param {string} aPngFileIn
-- @param {string} aPngFileOut
-- @return {bool} success
----------------------------------------
on optimize_apng (me, aPngFileIn, aPngFileOut)
sx = xtra("Shell").new()
props = [:]
props["show_cmd"] = 0
props["wait"] = 1
props["parameters"] = QUOTE & aPngFileIn & QUOTE && QUOTE & aPngFileOut & QUOTE
exit_code = sx.shell_execex(_movie.path & "bin\apngopt.exe", props)
return (exit_code=0)
end
----------------------------------------
-- @private
----------------------------------------
on _writeChunk (me, chunk_type, chunk_data)
me._data.writeInt32(chunk_data.length)
pos = me._data.length
me._data.writeRawString(chunk_type, 4)
me._data.writeByteArray(chunk_data)
me._data.writeByteArray(me._ix.ix_xCrc32String(["data":me._data, "offset":pos]))
end
----------------------------------------
-- @private
----------------------------------------
on _writeBytes (me)
cnt = paramCount()
repeat with i = 2 to cnt
me._data.writeInt8(param(i))
end repeat
end