michael@0: /* michael@0: * Test for APNG encoding in libpr0n michael@0: * michael@0: */ michael@0: michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: michael@0: // dispose=[none|background|previous] michael@0: // blend=[source|over] michael@0: michael@0: var apng1A = { michael@0: // A 3x3 image with 3 frames, alternating red, green, blue. RGB format. michael@0: width : 3, height : 3, skipFirstFrame : false, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGB, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, michael@0: transparency : null, michael@0: michael@0: pixels : [ michael@0: 255,0,0, 255,0,0, 255,0,0, michael@0: 255,0,0, 255,0,0, 255,0,0, michael@0: 255,0,0, 255,0,0, 255,0,0 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, michael@0: transparency : null, michael@0: michael@0: pixels : [ michael@0: 0,255,0, 0,255,0, 0,255,0, michael@0: 0,255,0, 0,255,0, 0,255,0, michael@0: 0,255,0, 0,255,0, 0,255,0 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGB, stride : 9, michael@0: transparency : null, michael@0: michael@0: pixels : [ michael@0: 0,0,255, 0,0,255, 0,0,255, michael@0: 0,0,255, 0,0,255, 0,0,255, michael@0: 0,0,255, 0,0,255, 0,0,255 michael@0: ] michael@0: } michael@0: michael@0: ], michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABBJREFUCJlj+M/AAEEMWFgAj44I+H2CySsAAAAaZmNUTAAAAAEAAAADAAAAAwAAAAAAAAAAAfQD6AAA7mXKzAAAABNmZEFUAAAAAgiZY2D4zwBFWFgAhpcI+I731VcAAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABNmZEFUAAAABAiZY2Bg+A9DmCwAfaAI+AGmQVoAAAAASUVORK5CYII=" michael@0: }; michael@0: michael@0: michael@0: var apng1B = { michael@0: // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. michael@0: width : 3, height : 3, skipFirstFrame : false, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255, michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255, michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255 michael@0: ] michael@0: } michael@0: michael@0: ], michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gAAO5lyswAAAASZmRBVAAAAAIImWNg+I8EcXIAVOAR77Vyl9QAAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABRmZEFUAAAABAiZY2Bg+P8fgXFxAEvpEe8rClxSAAAAAElFTkSuQmCC" michael@0: }; michael@0: michael@0: michael@0: var apng1C = { michael@0: // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. michael@0: // The first frame is skipped, so it will only flash green/blue (or static red in an APNG-unaware viewer) michael@0: width : 3, height : 3, skipFirstFrame : true, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255, michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255, michael@0: 0,255,0,255, 0,255,0,255, 0,255,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255 michael@0: ] michael@0: } michael@0: michael@0: ], michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAACAAAAAPONk3AAAAARSURBVAiZY/jPwPAfhhlwcgBd1xHvwqIidQAAABpmY1RMAAAAAAAAAAMAAAADAAAAAAAAAAAB9APoAAB1FiAYAAAAEmZkQVQAAAABCJljYPiPBHFyAFTgEe+kD/2tAAAAGmZjVEwAAAACAAAAAwAAAAMAAAAAAAAAAAH0A+gAAJiA8/EAAAAUZmRBVAAAAAMImWNgYPj/H4FxcQBL6RHvC5ggGQAAAABJRU5ErkJggg==" michael@0: }; michael@0: michael@0: michael@0: var apng2A = { michael@0: // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. michael@0: // blend = over mode michael@0: // (The green frame is a horizontal gradient, and the blue frame is a michael@0: // vertical gradient. They stack as they animate.) michael@0: width : 3, height : 3, skipFirstFrame : false, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "over", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75, michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75, michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "over", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,255,75, 0,0,255,75, 0,0,255,75, michael@0: 0,0,255,180, 0,0,255,180, 0,0,255,180, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255 michael@0: ] michael@0: } michael@0: michael@0: ], michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gAAZli+loAAAAcZmRBVAAAAAIImWNg+M/wn+E/wxaG/wzeDDg5ACeGDvKVa3pyAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gAAXT0KbMAAAAcZmRBVAAAAAQImWNgYPjvjcAM/7cgMMP//zAMAPqkDvLn1m3SAAAAAElFTkSuQmCC" michael@0: }; michael@0: michael@0: michael@0: var apng2B = { michael@0: // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. michael@0: // blend = over, dispose = background michael@0: // (The green frame is a horizontal gradient, and the blue frame is a michael@0: // vertical gradient. Each frame is displayed individually, blended to michael@0: // whatever the background is.) michael@0: width : 3, height : 3, skipFirstFrame : false, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "background", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255, michael@0: 255,0,0,255, 255,0,0,255, 255,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "background", blend : "over", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75, michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75, michael@0: 0,255,0,255, 0,255,0,180, 0,255,0,75 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "background", blend : "over", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,255,75, 0,0,255,75, 0,0,255,75, michael@0: 0,0,255,180, 0,0,255,180, 0,0,255,180, michael@0: 0,0,255,255, 0,0,255,255, 0,0,255,255 michael@0: ] michael@0: } michael@0: michael@0: ], michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AEAbA0RWQAAABFJREFUCJlj+M/A8B+GGXByAF3XEe/CoiJ1AAAAGmZjVEwAAAABAAAAAwAAAAMAAAAAAAAAAAH0A+gBAYB5yxsAAAAcZmRBVAAAAAIImWNg+M/wn+E/wxaG/wzeDDg5ACeGDvKVa3pyAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gBAW3vGPIAAAAcZmRBVAAAAAQImWNgYPjvjcAM/7cgMMP//zAMAPqkDvLn1m3SAAAAAElFTkSuQmCC" michael@0: }; michael@0: michael@0: michael@0: var apng3 = { michael@0: // A 3x3 image with 4 frames. First frame is white, then 1x1 frames draw a diagonal line michael@0: width : 3, height : 3, skipFirstFrame : false, michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, michael@0: transparency : null, michael@0: plays : 0, michael@0: michael@0: frames : [ michael@0: { // frame #1 michael@0: width : 3, height : 3, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: michael@0: 255,255,255,255, 255,255,255,255, 255,255,255,255, michael@0: 255,255,255,255, 255,255,255,255, 255,255,255,255, michael@0: 255,255,255,255, 255,255,255,255, 255,255,255,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #2 michael@0: width : 1, height : 1, michael@0: x_offset : 0, y_offset : 0, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #3 michael@0: width : 1, height : 1, michael@0: x_offset : 1, y_offset : 1, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,0,255 michael@0: ] michael@0: }, michael@0: michael@0: { // frame #4 michael@0: width : 1, height : 1, michael@0: x_offset : 2, y_offset : 2, michael@0: dispose : "none", blend : "source", delay : 500, michael@0: michael@0: format : Ci.imgIEncoder.INPUT_FORMAT_RGBA, stride : 12, michael@0: michael@0: pixels : [ michael@0: 0,0,0,255 michael@0: ] michael@0: } michael@0: ], michael@0: michael@0: expected : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAAEAAAAAHzNZtAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAAA5JREFUCJlj+I8EGHByALuHI91kueRqAAAAGmZjVEwAAAABAAAAAQAAAAEAAAAAAAAAAAH0A+gAADJXfawAAAARZmRBVAAAAAIImWNgYGD4DwABBAEAbr5mpgAAABpmY1RMAAAAAwAAAAEAAAABAAAAAQAAAAEB9APoAAC4OHoxAAAAEWZkQVQAAAAECJljYGBg+A8AAQQBAJZ8LRAAAAAaZmNUTAAAAAUAAAABAAAAAQAAAAIAAAACAfQD6AAA/fh01wAAABFmZEFUAAAABgiZY2BgYPgPAAEEAQB3Eum9AAAAAElFTkSuQmCC" michael@0: }; michael@0: michael@0: // Main test entry point. michael@0: function run_test() { michael@0: dump("Checking apng1A...\n"); michael@0: run_test_for(apng1A); michael@0: dump("Checking apng1B...\n"); michael@0: run_test_for(apng1B); michael@0: dump("Checking apng1C...\n"); michael@0: run_test_for(apng1C); michael@0: michael@0: dump("Checking apng2A...\n"); michael@0: run_test_for(apng2A); michael@0: dump("Checking apng2B...\n"); michael@0: run_test_for(apng2B); michael@0: michael@0: dump("Checking apng3...\n"); michael@0: run_test_for(apng3); michael@0: }; michael@0: michael@0: michael@0: function run_test_for(input) { michael@0: var encoder, dataURL; michael@0: michael@0: encoder = encodeImage(input); michael@0: dataURL = makeDataURL(encoder, "image/png"); michael@0: do_check_eq(dataURL, input.expected); michael@0: }; michael@0: michael@0: michael@0: function encodeImage(input) { michael@0: var encoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); michael@0: encoder.QueryInterface(Ci.imgIEncoder); michael@0: michael@0: var options = ""; michael@0: if (input.transparency) { options += "transparency=" + input.transparency; } michael@0: options += ";frames=" + input.frames.length; michael@0: options += ";skipfirstframe=" + (input.skipFirstFrame ? "yes" : "no"); michael@0: options += ";plays=" + input.plays; michael@0: encoder.startImageEncode(input.width, input.height, input.format, options); michael@0: michael@0: for (var i = 0; i < input.frames.length; i++) { michael@0: var frame = input.frames[i]; michael@0: michael@0: options = ""; michael@0: if (frame.transparency) { options += "transparency=" + input.transparency; } michael@0: options += ";delay=" + frame.delay; michael@0: options += ";dispose=" + frame.dispose; michael@0: options += ";blend=" + frame.blend; michael@0: if (frame.x_offset > 0) { options += ";xoffset=" + frame.x_offset; } michael@0: if (frame.y_offset > 0) { options += ";yoffset=" + frame.y_offset; } michael@0: michael@0: encoder.addImageFrame(frame.pixels, frame.pixels.length, michael@0: frame.width, frame.height, frame.stride, frame.format, options); michael@0: } michael@0: michael@0: encoder.endImageEncode(); michael@0: michael@0: return encoder; michael@0: } michael@0: michael@0: michael@0: function makeDataURL(encoder, mimetype) { michael@0: var rawStream = encoder.QueryInterface(Ci.nsIInputStream); michael@0: michael@0: var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); michael@0: stream.QueryInterface(Ci.nsIBinaryInputStream); michael@0: michael@0: stream.setInputStream(rawStream); michael@0: michael@0: var bytes = stream.readByteArray(stream.available()); // returns int[] michael@0: michael@0: var base64String = toBase64(bytes); michael@0: michael@0: return "data:" + mimetype + ";base64," + base64String; michael@0: } michael@0: michael@0: /* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ michael@0: michael@0: /* Convert data (an array of integers) to a Base64 string. */ michael@0: const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + michael@0: '0123456789+/'; michael@0: const base64Pad = '='; michael@0: function toBase64(data) { michael@0: var result = ''; michael@0: var length = data.length; michael@0: var i; michael@0: // Convert every three bytes to 4 ascii characters. michael@0: for (i = 0; i < (length - 2); i += 3) { michael@0: result += toBase64Table[data[i] >> 2]; michael@0: result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; michael@0: result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; michael@0: result += toBase64Table[data[i+2] & 0x3f]; michael@0: } michael@0: michael@0: // Convert the remaining 1 or 2 bytes, pad out to 4 characters. michael@0: if (length%3) { michael@0: i = length - (length%3); michael@0: result += toBase64Table[data[i] >> 2]; michael@0: if ((length%3) == 2) { michael@0: result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; michael@0: result += toBase64Table[(data[i+1] & 0x0f) << 2]; michael@0: result += base64Pad; michael@0: } else { michael@0: result += toBase64Table[(data[i] & 0x03) << 4]; michael@0: result += base64Pad + base64Pad; michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: }