1. --!parent
  2.  
  3. -- ************************************************************************
  4. -- APNG Encoder - D11.5+ version (bytearray based)
  5. --
  6. -- @author Valentin Schmidt
  7. -- @version 0.6
  8. -- @requires xtra "ImgXtra", xtra "fileIO";
  9. --           Optional function optimize_apng() requires xtra "Shell" and apngopt.exe
  10. -- ************************************************************************
  11.  
  12. property _ix
  13. property _data
  14. property _color_type
  15. property _sequence_number
  16. property _num_plays
  17. property _num_frames
  18. property _palette_colors
  19. property _tranparency
  20.  
  21. -- constants for dispose_op parameter
  22. property DISPOSE_OP_NONE
  23. property DISPOSE_OP_BACKGROUND
  24. property DISPOSE_OP_PREVIOUS
  25.  
  26. -- constants for blend_op parameter
  27. property BLEND_OP_SOURCE
  28. property BLEND_OP_OVER
  29.  
  30. -- filter constants
  31. property FILTER_NONE
  32. property FILTER_SUB
  33. property FILTER_UP
  34.  
  35. ----------------------------------------
  36. -- @constructor
  37. ----------------------------------------
  38. on new (me)
  39.     me._ix = xtra("ImgXtra").new()
  40.  
  41.     -- no disposal is done on this frame before rendering the next; the contents
  42.     -- of the output buffer are left as is.
  43.     me.DISPOSE_OP_NONE = 0
  44.  
  45.     -- the frame's region of the output buffer is to be cleared to fully
  46.     -- transparent black before rendering the next frame.
  47.     me.DISPOSE_OP_BACKGROUND = 1
  48.  
  49.     -- the frame's region of the output buffer is to be reverted to the previous
  50.     -- contents before rendering the next frame.
  51.     me.DISPOSE_OP_PREVIOUS = 2
  52.  
  53.     -- all color components of the frame, including alpha, overwrite the current
  54.     -- contents of the frame's output buffer region
  55.     me.BLEND_OP_SOURCE = 0
  56.  
  57.     -- the frame should be composited onto the output buffer based on its alpha,
  58.     -- using a simple OVER operation as described in the "Alpha Channel Processing"
  59.     -- section of the PNG specification
  60.     me.BLEND_OP_OVER = 1
  61.  
  62.     -- the scanline is transmitted unmodified
  63.     me.FILTER_NONE = 0
  64.  
  65.     -- transmits the difference between each byte and the value of the
  66.     -- corresponding byte of the prior pixel
  67.     me.FILTER_SUB = 1
  68.  
  69.     -- transmits the difference between each byte and the value of the
  70.     -- corresponding byte of the pixel above
  71.     me.FILTER_UP = 2
  72.  
  73.     -- NOTE: other filters like "Average" and "Paeth" are currently not supported
  74.     -- by this library
  75.  
  76.     return me
  77. end
  78.  
  79. ----------------------------------------
  80. -- Starts a new APNG
  81. -- @param {integer} [num_plays=0] - how often to loop, 0 means endless looping (default)
  82. -- @param {list} [palette_colors] - if specified, all frame images will be mapped to
  83. --        the colors in this list
  84. -- @param {integer|list} [transparency_index] - index of color in palette_colors that
  85. --        will be transparent in the final APNG file; for 8-bit images transparency_index
  86. --        can also be a list of indexes
  87. ----------------------------------------
  88. on init (me, num_plays, palette_colors, transparency_index)
  89.     if voidP(num_plays) then num_plays = 0
  90.     me._num_plays = num_plays
  91.     me._num_frames = 0
  92.     me._sequence_number = 0
  93.  
  94.     me._data = bytearray()
  95.     me._data.endian = #bigEndian
  96.  
  97.     me._palette_colors = palette_colors
  98.     me._tranparency = transparency_index
  99.  
  100.     -- write PNG signature
  101.     me._writeBytes(137,80,78,71,13,10,26,10)
  102. end
  103.  
  104. ----------------------------------------
  105. -- Adds new frame to APNG
  106. -- @param {image} frame_image - the frame as Lingo image object
  107. -- @param {integer} ms - duration of the frame in milliseconds
  108. -- @param {integer} [x=0] - x-offset of the image
  109. -- @param {integer} [y=0] - y-offset of the image
  110. -- @param {integer} [dispose_op=0] - how frame is disposed, see comments above
  111. -- @param {integer} [blend_op=0] - how frame is blended, see comments above
  112. -- @param {integer} [filter=0] - PNG filter, see comments above
  113. -- @return {bool} success
  114. ----------------------------------------
  115. on addFrame (me, frame_image, ms, x, y, dispose_op, blend_op, filter)
  116.     if voidP(x) then x = 0
  117.     if voidP(y) then y = 0
  118.     if voidP(dispose_op) then dispose_op = 0
  119.     if voidP(blend_op) then blend_op = 0
  120.     if voidP(filter) then filter = 0
  121.  
  122.     me._num_frames = me._num_frames + 1
  123.  
  124.     if not voidP(me._palette_colors) then
  125.         -- map to color palette
  126.         props = [:]
  127.         props["image"] = frame_image
  128.         props["reserved_colors"] = me._palette_colors
  129.         frame_image = me._ix.ix_quantizeImage(props)
  130.     end if
  131.  
  132.     -- the "master" frame
  133.     if me._num_frames=1 then
  134.         x = 0
  135.         y = 0
  136.         case (frame_image.depth) of
  137.             24, 32:
  138.                 if frame_image.useAlpha then
  139.                     me._color_type = 6 -- RGB with alpha
  140.                 else
  141.                     me._color_type = 2 -- RGB
  142.                 end if
  143.             8:
  144.                 if frame_image.paletteRef=#grayscale then
  145.                     me._color_type = 0 -- GS
  146.                 else
  147.                     me._color_type = 3 -- Palette
  148.                 end if
  149.             otherwise:
  150.                 return FALSE -- 1, 4, 16-bit unsupported!
  151.         end case
  152.  
  153.         -- add IHDR chunk
  154.         IHDR = bytearray()
  155.         IHDR.endian = #bigEndian
  156.         IHDR.writeInt32(frame_image.width)
  157.         IHDR.writeInt32(frame_image.height)
  158.         IHDR.writeInt8(8) -- bit depth
  159.         IHDR.writeInt8(me._color_type)
  160.         IHDR.writeInt8(0) -- compression method
  161.         IHDR.writeInt8(0) -- filter method
  162.         IHDR.writeInt8(0) -- interlace method
  163.         me._writeChunk("IHDR", IHDR)
  164.  
  165.         -- add acTL (animation control chunk) => updated later
  166.         me._data.writeInt32(8)
  167.         me._data.writeRawString("acTL", 4)
  168.         me._data.writeInt32(0)
  169.         me._data.writeInt32(me._num_plays)
  170.         me._data.writeInt32(0)
  171.     end if
  172.  
  173.     -- add fcTL (frame control chunk)
  174.     fcTL = bytearray()
  175.     fcTL.endian = #bigEndian
  176.     fcTL.writeInt32(me._sequence_number)
  177.     me._sequence_number = me._sequence_number + 1
  178.     fcTL.writeInt32(frame_image.width)
  179.     fcTL.writeInt32(frame_image.height)
  180.     fcTL.writeInt32(x)
  181.     fcTL.writeInt32(y)
  182.  
  183.     -- The delay_num and delay_den parameters together specify a fraction
  184.     -- indicating the time to display the current frame, in seconds. If the denominator
  185.     -- is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies
  186.     -- 1/100ths of a second). If the the value of the numerator is 0 the decoder should
  187.     -- render the next frame as quickly as possible, though viewers may impose a
  188.     -- reasonable lower bound.
  189.     -- To keep things simple, we set delay_den to 1000, so delay_num is duration in ms.
  190.     fcTL.writeInt16(ms) -- delay_num
  191.     fcTL.writeInt16(1000) -- delay_den
  192.  
  193.     fcTL.writeInt8(dispose_op)
  194.     fcTL.writeInt8(blend_op)
  195.  
  196.     me._writeChunk("fcTL", fcTL)
  197.  
  198.     -- build IDAT or fdAT chunk
  199.     case (me._color_type) of
  200.         2: -- RGB
  201.             chunk = me._ix.ix_imageToRaw(["image":frame_image, "pattern":"RGB", "png_filter":filter])
  202.         6: -- RGBA
  203.             chunk = me._ix.ix_imageToRaw(["image":frame_image, "pattern":"RGBA", "png_filter":filter])
  204.         0, 3: -- grayscale/palette
  205.             chunk = me._ix.ix_imageToRaw(["image":frame_image, "png_filter":filter])
  206.     end case
  207.  
  208.     -- compress the IDAT/fdAT chunk
  209.     chunk = me._ix.ix_zlibCompress(["data":chunk])
  210.  
  211.     if me._sequence_number>1 then
  212.         me._data.writeInt32(chunk.length + 4)
  213.         pos = me._data.length
  214.         me._data.writeRawString("fdAT", 4)
  215.         me._data.writeInt32(me._sequence_number)
  216.         me._sequence_number = me._sequence_number + 1
  217.         me._data.writeByteArray(chunk)
  218.         me._data.writeByteArray(me._ix.ix_xCrc32String(["data":me._data, "offset":pos]))
  219.     else
  220.  
  221.         -- add PLTE chunk (custom palette)
  222.         if me._color_type=3 then
  223.             PLTE = bytearray(768)
  224.             repeat with col in me._palette_colors
  225.                 PLTE.writeInt8(col.red)
  226.                 PLTE.writeInt8(col.green)
  227.                 PLTE.writeInt8(col.blue)
  228.             end repeat
  229.             me._writeChunk("PLTE", PLTE)
  230.         end if
  231.  
  232.         -- add tRNS chunk (transparent color index)
  233.         if not voidP(me._tranparency) then
  234.             case (me._color_type) of
  235.                 0: -- GS
  236.                     tRNS = bytearray()
  237.                     tRNS.writeInt8(0)
  238.                     tRNS.writeInt8(me._tranparency)
  239.                     me._writeChunk("tRNS", tRNS)
  240.  
  241.                 2: -- RGB
  242.                     tRNS = bytearray()
  243.                     tRNS.writeInt8(0)
  244.                     tRNS.writeInt8(me._tranparency.red)
  245.                     tRNS.writeInt8(0)
  246.                     tRNS.writeInt8(me._tranparency.green)
  247.                     tRNS.writeInt8(0)
  248.                     tRNS.writeInt8(me._tranparency.blue)
  249.                     me._writeChunk("tRNS", tRNS)
  250.  
  251.                 3: -- Palette
  252.                     tRNS = bytearray(256, 255)
  253.                     if integerP(me._tranparency) then
  254.                         tRNS[me._tranparency] = 0
  255.                     else if listP(me._tranparency) then
  256.                         repeat with t in me._tranparency
  257.                             tRNS.writeInt8(t)
  258.                         end repeat
  259.                     end if
  260.                     me._writeChunk("tRNS", tRNS)
  261.             end case
  262.         end if
  263.  
  264.         me._writeChunk("IDAT", chunk)
  265.     end if
  266.  
  267.     return TRUE
  268. end
  269.  
  270. ----------------------------------------
  271. -- Returns APNG as byteArray
  272. -- Note: you can either call getData() or writeFile(), but not both
  273. -- @return {byteArray}
  274. ----------------------------------------
  275. on getData (me)
  276.  
  277.     -- add IEND chunk
  278.     me._writeBytes(0,0,0,0)
  279.     me._data.writeRawString("IEND", 4)
  280.     me._writeBytes(174,66,96,130)
  281.  
  282.     actl_pos = 33
  283.  
  284.     -- update num_frames in acTL chunk
  285.     me._data.position = actl_pos+9
  286.     me._data.writeInt32(me._num_frames)
  287.  
  288.     -- update CRC of acTL chunk
  289.     me._data.position = actl_pos+5
  290.     crc_data = _data.readByteArray(12)
  291.     me._data.position = actl_pos+17
  292.     me._data.writeByteArray(me._ix.ix_xCrc32String(["data":crc_data]))
  293.  
  294.     return me._data
  295. end
  296.  
  297. ----------------------------------------
  298. -- Saves APNG as file
  299. -- Note: you can either call getData() or writeFile(), but not both
  300. -- @param {string} png_file
  301. -- @return {bool} success
  302. ----------------------------------------
  303. on writeFile (me, png_file)
  304.  
  305.     -- add IEND chunk
  306.     me._writeBytes(0,0,0,0)
  307.     me._data.writeRawString("IEND", 4)
  308.     me._writeBytes(174,66,96,130)
  309.  
  310.     actl_pos = 33
  311.  
  312.     -- update num_frames in acTL chunk
  313.     me._data.position = actl_pos+9
  314.     me._data.writeInt32(me._num_frames)
  315.  
  316.     -- update CRC of acTL chunk
  317.     me._data.position = actl_pos+5
  318.     crc_data = _data.readByteArray(12)
  319.     me._data.position = actl_pos+17
  320.     me._data.writeByteArray(me._ix.ix_xCrc32String(["data":crc_data]))
  321.  
  322.     -- save to file
  323.     fp = xtra("fileIO").new()
  324.     fp.openFile(png_file, 2)
  325.     err = fp.status()
  326.     if not err then fp.delete()
  327.     else if (err and not (err = -37)) then return FALSE
  328.     fp.createFile(png_file)
  329.     if fp.status() then return FALSE
  330.     fp.openFile(png_file, 2)
  331.     if fp.status() then return FALSE
  332.     fp.writeByteArray(me._data, 1, me._data.length)
  333.     fp.closeFile()
  334.     return TRUE
  335. end
  336.  
  337. ----------------------------------------
  338. -- Utility function: pass a "typical" image for the animation; returns a palette as list of colors
  339. -- @param {image} master_image
  340. -- @param {integer} [palette_size=256]
  341. -- @return {list|FALSE}
  342. ----------------------------------------
  343. on findPalette (me, master_image, palette_size)
  344.     props = [:]
  345.     props["image"] = master_image
  346.     if integerP(palette_size) then props["palette_size"] = palette_size
  347.     img = me._ix.ix_quantizeImage(props)
  348.     if ilk(img)<>#image then return FALSE
  349.     palette_colors = me._ix.ix_getPaletteColors(["palette":img.paletteRef])
  350.     img.paletteRef.erase()
  351.     return palette_colors
  352. end
  353.  
  354. ----------------------------------------
  355. -- Utility function: optimizes APNG file using apngopt.exe
  356. -- Usage: ok = optimize_apng(_movie.path&"foo.png",\
  357. --     _movie.path&"foo_opt.png")
  358. --
  359. -- @requires Shell xtra, apngopt.exe
  360. -- @param {string} aPngFileIn
  361. -- @param {string} aPngFileOut
  362. -- @return {bool} success
  363. ----------------------------------------
  364. on optimize_apng (me, aPngFileIn, aPngFileOut)
  365.     sx = xtra("Shell").new()
  366.     props = [:]
  367.     props["show_cmd"] = 0
  368.     props["wait"] = 1
  369.     props["parameters"] = QUOTE & aPngFileIn & QUOTE && QUOTE & aPngFileOut & QUOTE
  370.     exit_code = sx.shell_execex(_movie.path & "bin\apngopt.exe", props)
  371.     return (exit_code=0)
  372. end
  373.  
  374. ----------------------------------------
  375. -- @private
  376. ----------------------------------------
  377. on _writeChunk (me, chunk_type, chunk_data)
  378.     me._data.writeInt32(chunk_data.length)
  379.     pos = me._data.length
  380.     me._data.writeRawString(chunk_type, 4)
  381.     me._data.writeByteArray(chunk_data)
  382.     me._data.writeByteArray(me._ix.ix_xCrc32String(["data":me._data, "offset":pos]))
  383. end
  384.  
  385. ----------------------------------------
  386. -- @private
  387. ----------------------------------------
  388. on _writeBytes (me)
  389.     cnt = paramCount()
  390.     repeat with i = 2 to cnt
  391.         me._data.writeInt8(param(i))
  392.     end repeat
  393. end
  394.  
[raw code]