--!parent
-- ************************************************************************
-- APNG Encoder - version based on strings (not byteArray), for Director 10 or older
--
-- @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._palette_colors = palette_colors
me._tranparency = transparency_index
-- write PNG signature
me._data = numtochar(137)&"PNG"
me._writeBytes(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 below
-- @param {integer} [blend_op=0] - how frame is blended, see comments below
-- @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
-- quantize to reduced 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 = ""
put me._int32ToStr(frame_image.width) after IHDR
put me._int32ToStr(frame_image.height) after IHDR
put numtochar(8)&numtochar(me._color_type)&numtochar(0)&numtochar(0)&numtochar(0) after IHDR
me._writeChunk("IHDR", IHDR)
-- add acTL (animation control chunk) => updated later
put me._int32ToStr(8)&"acTL"&me._int32ToStr(0)&me._int32ToStr(me._num_plays)&me._int32ToStr(0) after _data
end if
-- add fcTL (frame control chunk)
fcTL = ""
put me._int32ToStr(me._sequence_number) after fcTL
me._sequence_number = me._sequence_number + 1
put me._int32ToStr(frame_image.width) after fcTL
put me._int32ToStr(frame_image.height) after fcTL
put me._int32ToStr(x) after fcTL
put me._int32ToStr(y) after fcTL
-- 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.
put me._int16ToStr(ms) after fcTL -- delay_num
put me._int16ToStr(1000) after fcTL -- delay_den
put numtochar(dispose_op) after fcTL
put numtochar(blend_op) after fcTL
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
put me._int32ToStr(me._sequence_number) before chunk
me._sequence_number = me._sequence_number + 1
me._writeChunk("fdAT", chunk)
else
-- add PLTE chunk (custom palette)
if me._color_type=3 then
PLTE = ""
repeat with col in me._palette_colors
put numtochar(col.red)&numtochar(col.green)&numtochar(col.blue) after PLTE
end repeat
repeat with i = me._palette_colors.count+1 to 256
put numtochar(0)&numtochar(0)&numtochar(0) after PLTE
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 = ""
put numtochar(0) & numtochar(me._tranparency) after tRNS
me._writeChunk("tRNS", tRNS)
2: -- RGB
tRNS = ""
put numtochar(0) & numtochar(me._tranparency.red) after tRNS
put numtochar(0) & numtochar(me._tranparency.green) after tRNS
put numtochar(0) & numtochar(me._tranparency.blue) after tRNS
me._writeChunk("tRNS", tRNS)
3: -- Palette
tRNS = ""
if integerP(me._tranparency) then
repeat with i = 1 to 256
if i=me._tranparency then
put numtochar(0) after tRNS
else
put numtochar(255) after tRNS
end if
end repeat
else if listP(me._tranparency) then
repeat with t in me._tranparency
put numtochar(t) after tRNS
end repeat
repeat with i = me._tranparency.count+1 to 256
put numtochar(255) after tRNS
end repeat
end if
me._writeChunk("tRNS", tRNS)
end case
end if
me._writeChunk("IDAT", chunk)
end if
return TRUE
end
----------------------------------------
-- Returns APNG as binary string
-- Note: you can either call getData() or writeFile(), but not both
-- @return {string}
----------------------------------------
on getData (me)
-- add IEND chunk
put numtochar(0)&numtochar(0)&numtochar(0)&numtochar(0)&"IEND"&\
numtochar(174)&numtochar(66)&numtochar(96)&numtochar(130) after _data
actl_pos = 33
-- update num_frames in acTL chunk
put me._int32ToStr(me._num_frames) into char (actl_pos+9) to (actl_pos+12) of _data
-- update CRC of acTL chunk
crc_data = _data.char[actl_pos+5..actl_pos+16]
crc = me._ix.ix_xCrc32String(["data":crc_data])
put crc into char (actl_pos+17) to (actl_pos+20) of _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
put numtochar(0)&numtochar(0)&numtochar(0)&numtochar(0)&"IEND"&\
numtochar(174)&numtochar(66)&numtochar(96)&numtochar(130) after _data
actl_pos = 33
-- update num_frames in acTL chunk
put me._int32ToStr(me._num_frames) into char (actl_pos+9) to (actl_pos+12) of _data
-- update CRC of acTL chunk
crc_data = _data.char[actl_pos+5..actl_pos+16]
crc = me._ix.ix_xCrc32String(["data":crc_data])
put crc into char (actl_pos+17) to (actl_pos+20) of _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.writeString(me._data)
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(the moviePath & "bin\apngopt.exe", props)
return (exit_code=0)
end
----------------------------------------
-- @private
----------------------------------------
on _writeChunk (me, chunk_type, chunk_data)
put me._int32ToStr(chunk_data.length) after _data
put chunk_type before chunk_data
put chunk_data after _data
put me._ix.ix_xCrc32String(["data":chunk_data]) after _data
end
----------------------------------------
-- @private
----------------------------------------
on _writeBytes (me)
cnt = paramCount()
repeat with i = 2 to cnt
put numtochar(param(i)) after _data
end repeat
end
----------------------------------------
-- @private
----------------------------------------
on _int32ToStr (me, n)
return numtochar(bitAnd(n, 4278190080)/16777216)&\
numtochar(bitAnd(n, 16711680)/65536)&\
numtochar(bitAnd(n, 65280)/256)&\
numtochar(bitAnd(n, 255))
end
----------------------------------------
-- @private
----------------------------------------
on _int16ToStr (me, n)
return numtochar(bitAnd(n, 65280)/256)&\
numtochar(bitAnd(n, 255))
end