From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- image/test/unit/async_load_tests.js | 302 +++++++ image/test/unit/bug413512.ico | Bin 0 -> 17759 bytes image/test/unit/bug815359.ico | Bin 0 -> 4286 bytes image/test/unit/image1.png | Bin 0 -> 8415 bytes image/test/unit/image1.webp | Bin 0 -> 3206 bytes image/test/unit/image1png16x16.jpg | Bin 0 -> 1050 bytes image/test/unit/image1png64x64.jpg | Bin 0 -> 4507 bytes image/test/unit/image1quality50.webp | Bin 0 -> 1944 bytes image/test/unit/image2.jpg | Bin 0 -> 3494 bytes image/test/unit/image2jpg16x16-win.png | Bin 0 -> 948 bytes image/test/unit/image2jpg16x16.png | Bin 0 -> 955 bytes image/test/unit/image2jpg16x16cropped.jpg | Bin 0 -> 879 bytes image/test/unit/image2jpg16x16cropped2.jpg | Bin 0 -> 878 bytes image/test/unit/image2jpg16x32cropped3.jpg | Bin 0 -> 1127 bytes image/test/unit/image2jpg16x32scaled.jpg | Bin 0 -> 1219 bytes image/test/unit/image2jpg32x16cropped4.jpg | Bin 0 -> 1135 bytes image/test/unit/image2jpg32x16scaled.jpg | Bin 0 -> 1227 bytes image/test/unit/image2jpg32x32-win.png | Bin 0 -> 3104 bytes image/test/unit/image2jpg32x32.jpg | Bin 0 -> 1634 bytes image/test/unit/image2jpg32x32.png | Bin 0 -> 3026 bytes image/test/unit/image3.ico | Bin 0 -> 1406 bytes image/test/unit/image3ico16x16.png | Bin 0 -> 520 bytes image/test/unit/image3ico32x32.png | Bin 0 -> 2280 bytes image/test/unit/image4.gif | Bin 0 -> 1809 bytes image/test/unit/image4gif16x16bmp24bpp.ico | Bin 0 -> 894 bytes image/test/unit/image4gif16x16bmp32bpp.ico | Bin 0 -> 1150 bytes image/test/unit/image4gif32x32bmp24bpp.ico | Bin 0 -> 3262 bytes image/test/unit/image4gif32x32bmp32bpp.ico | Bin 0 -> 4286 bytes image/test/unit/image_load_helpers.js | 124 +++ image/test/unit/test_async_notification.js | 15 + image/test/unit/test_async_notification_404.js | 19 + .../test/unit/test_async_notification_animated.js | 19 + image/test/unit/test_encoder_apng.js | 582 ++++++++++++++ image/test/unit/test_encoder_png.js | 263 +++++++ image/test/unit/test_imgtools.js | 871 +++++++++++++++++++++ image/test/unit/test_moz_icon_uri.js | 157 ++++ image/test/unit/test_private_channel.js | 170 ++++ image/test/unit/xpcshell.toml | 50 ++ 38 files changed, 2572 insertions(+) create mode 100644 image/test/unit/async_load_tests.js create mode 100644 image/test/unit/bug413512.ico create mode 100644 image/test/unit/bug815359.ico create mode 100644 image/test/unit/image1.png create mode 100644 image/test/unit/image1.webp create mode 100644 image/test/unit/image1png16x16.jpg create mode 100644 image/test/unit/image1png64x64.jpg create mode 100644 image/test/unit/image1quality50.webp create mode 100644 image/test/unit/image2.jpg create mode 100644 image/test/unit/image2jpg16x16-win.png create mode 100644 image/test/unit/image2jpg16x16.png create mode 100644 image/test/unit/image2jpg16x16cropped.jpg create mode 100644 image/test/unit/image2jpg16x16cropped2.jpg create mode 100644 image/test/unit/image2jpg16x32cropped3.jpg create mode 100644 image/test/unit/image2jpg16x32scaled.jpg create mode 100644 image/test/unit/image2jpg32x16cropped4.jpg create mode 100644 image/test/unit/image2jpg32x16scaled.jpg create mode 100644 image/test/unit/image2jpg32x32-win.png create mode 100644 image/test/unit/image2jpg32x32.jpg create mode 100644 image/test/unit/image2jpg32x32.png create mode 100644 image/test/unit/image3.ico create mode 100644 image/test/unit/image3ico16x16.png create mode 100644 image/test/unit/image3ico32x32.png create mode 100644 image/test/unit/image4.gif create mode 100644 image/test/unit/image4gif16x16bmp24bpp.ico create mode 100644 image/test/unit/image4gif16x16bmp32bpp.ico create mode 100644 image/test/unit/image4gif32x32bmp24bpp.ico create mode 100644 image/test/unit/image4gif32x32bmp32bpp.ico create mode 100644 image/test/unit/image_load_helpers.js create mode 100644 image/test/unit/test_async_notification.js create mode 100644 image/test/unit/test_async_notification_404.js create mode 100644 image/test/unit/test_async_notification_animated.js create mode 100644 image/test/unit/test_encoder_apng.js create mode 100644 image/test/unit/test_encoder_png.js create mode 100644 image/test/unit/test_imgtools.js create mode 100644 image/test/unit/test_moz_icon_uri.js create mode 100644 image/test/unit/test_private_channel.js create mode 100644 image/test/unit/xpcshell.toml (limited to 'image/test/unit') diff --git a/image/test/unit/async_load_tests.js b/image/test/unit/async_load_tests.js new file mode 100644 index 0000000000..f19e146314 --- /dev/null +++ b/image/test/unit/async_load_tests.js @@ -0,0 +1,302 @@ +/* + * Test to ensure that image loading/decoding notifications are always + * delivered async, and in the order we expect. + * + * Must be included from a file that has a uri of the image to test defined in + * var uri. + */ +/* import-globals-from image_load_helpers.js */ + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +var server = new HttpServer(); +server.registerDirectory("/", do_get_file("")); +server.registerContentType("sjs", "sjs"); +server.start(-1); + +load("image_load_helpers.js"); + +var requests = []; +/* global uri */ + +// Return a closure that holds on to the listener from the original +// imgIRequest, and compares its results to the cloned one. +function getCloneStopCallback(original_listener) { + return function cloneStop(listener) { + Assert.equal(original_listener.state, listener.state); + + // Sanity check to make sure we didn't accidentally use the same listener + // twice. + Assert.notEqual(original_listener, listener); + do_test_finished(); + }; +} + +// Make sure that cloned requests get all the same callbacks as the original, +// but they aren't synchronous right now. +function checkClone(other_listener, aRequest) { + do_test_pending(); + + // For as long as clone notification is synchronous, we can't test the clone state reliably. + var listener = new ImageListener( + null, + function (foo, bar) { + do_test_finished(); + } /* getCloneStopCallback(other_listener)*/ + ); + listener.synchronous = false; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var clone = aRequest.clone(outer); + requests.push({ request: clone, locked: false }); +} + +// Ensure that all the callbacks were called on aRequest. +function checkSizeAndLoad(listener, aRequest) { + Assert.notEqual(listener.state & SIZE_AVAILABLE, 0); + Assert.notEqual(listener.state & LOAD_COMPLETE, 0); + + do_test_finished(); +} + +function secondLoadDone(oldlistener, aRequest) { + do_test_pending(); + + try { + var staticrequest = aRequest.getStaticRequest(); + + // For as long as clone notification is synchronous, we can't test the + // clone state reliably. + var listener = new ImageListener(null, checkSizeAndLoad); + listener.synchronous = false; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var staticrequestclone = staticrequest.clone(outer); + requests.push({ request: staticrequestclone, locked: false }); + } catch (e) { + // We can't create a static request. Most likely the request we started + // with didn't load successfully. + do_test_finished(); + } + + run_loadImageWithChannel_tests(); + + do_test_finished(); +} + +// Load the request a second time. This should come from the image cache, and +// therefore would be at most risk of being served synchronously. +function checkSecondLoad() { + do_test_pending(); + + var listener = new ImageListener(checkClone, secondLoadDone); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push({ + request: gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ), + locked: false, + }); + listener.synchronous = false; +} + +function firstLoadDone(oldlistener, aRequest) { + checkSecondLoad(uri); + + do_test_finished(); +} + +// Return a closure that allows us to check the stream listener's status when the +// image finishes loading. +function getChannelLoadImageStopCallback(streamlistener, next) { + return function channelLoadStop(imglistener, aRequest) { + next(); + + do_test_finished(); + }; +} + +// Load the request a second time. This should come from the image cache, and +// therefore would be at most risk of being served synchronously. +function checkSecondChannelLoad() { + do_test_pending(); + var channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var channellistener = new ChannelListener(); + channel.asyncOpen(channellistener); + + var listener = new ImageListener( + null, + getChannelLoadImageStopCallback(channellistener, all_done_callback) + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var outlistener = {}; + requests.push({ + request: gCurrentLoader.loadImageWithChannelXPCOM( + channel, + outer, + null, + outlistener + ), + locked: false, + }); + channellistener.outputListener = outlistener.value; + + listener.synchronous = false; +} + +function run_loadImageWithChannel_tests() { + // To ensure we're testing what we expect to, create a new loader and cache. + gCurrentLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader + ); + + do_test_pending(); + var channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + var channellistener = new ChannelListener(); + channel.asyncOpen(channellistener); + + var listener = new ImageListener( + null, + getChannelLoadImageStopCallback(channellistener, checkSecondChannelLoad) + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var outlistener = {}; + requests.push({ + request: gCurrentLoader.loadImageWithChannelXPCOM( + channel, + outer, + null, + outlistener + ), + locked: false, + }); + channellistener.outputListener = outlistener.value; + + listener.synchronous = false; +} + +function all_done_callback() { + server.stop(function () { + do_test_finished(); + }); +} + +function startImageCallback(otherCb) { + return function (listener, request) { + // Make sure we can load the same image immediately out of the cache. + do_test_pending(); + var listener2 = new ImageListener(null, function (foo, bar) { + do_test_finished(); + }); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener2); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push({ + request: gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ), + locked: false, + }); + listener2.synchronous = false; + + // Now that we've started another load, chain to the callback. + otherCb(listener, request); + }; +} + +var gCurrentLoader; + +function cleanup() { + for (let { request, locked } of requests) { + if (locked) { + try { + request.unlockImage(); + } catch (e) {} + } + request.cancelAndForgetObserver(0); + } +} + +function run_test() { + registerCleanupFunction(cleanup); + + gCurrentLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader + ); + + do_test_pending(); + var listener = new ImageListener( + startImageCallback(checkClone), + firstLoadDone + ); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + var req = gCurrentLoader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + null, + outer, + null, + 0, + null + ); + + // Ensure that we don't cause any mayhem when we lock an image. + req.lockImage(); + + requests.push({ request: req, locked: true }); + + listener.synchronous = false; +} diff --git a/image/test/unit/bug413512.ico b/image/test/unit/bug413512.ico new file mode 100644 index 0000000000..b2db0429f6 Binary files /dev/null and b/image/test/unit/bug413512.ico differ diff --git a/image/test/unit/bug815359.ico b/image/test/unit/bug815359.ico new file mode 100644 index 0000000000..a24b8fb6bb Binary files /dev/null and b/image/test/unit/bug815359.ico differ diff --git a/image/test/unit/image1.png b/image/test/unit/image1.png new file mode 100644 index 0000000000..2fb37aeec4 Binary files /dev/null and b/image/test/unit/image1.png differ diff --git a/image/test/unit/image1.webp b/image/test/unit/image1.webp new file mode 100644 index 0000000000..b2a6f92aaa Binary files /dev/null and b/image/test/unit/image1.webp differ diff --git a/image/test/unit/image1png16x16.jpg b/image/test/unit/image1png16x16.jpg new file mode 100644 index 0000000000..488b563c90 Binary files /dev/null and b/image/test/unit/image1png16x16.jpg differ diff --git a/image/test/unit/image1png64x64.jpg b/image/test/unit/image1png64x64.jpg new file mode 100644 index 0000000000..679dad2b95 Binary files /dev/null and b/image/test/unit/image1png64x64.jpg differ diff --git a/image/test/unit/image1quality50.webp b/image/test/unit/image1quality50.webp new file mode 100644 index 0000000000..f73d615657 Binary files /dev/null and b/image/test/unit/image1quality50.webp differ diff --git a/image/test/unit/image2.jpg b/image/test/unit/image2.jpg new file mode 100644 index 0000000000..b2131bf0c1 Binary files /dev/null and b/image/test/unit/image2.jpg differ diff --git a/image/test/unit/image2jpg16x16-win.png b/image/test/unit/image2jpg16x16-win.png new file mode 100644 index 0000000000..a821626c07 Binary files /dev/null and b/image/test/unit/image2jpg16x16-win.png differ diff --git a/image/test/unit/image2jpg16x16.png b/image/test/unit/image2jpg16x16.png new file mode 100644 index 0000000000..b5b9a720a8 Binary files /dev/null and b/image/test/unit/image2jpg16x16.png differ diff --git a/image/test/unit/image2jpg16x16cropped.jpg b/image/test/unit/image2jpg16x16cropped.jpg new file mode 100644 index 0000000000..fca22cb30a Binary files /dev/null and b/image/test/unit/image2jpg16x16cropped.jpg differ diff --git a/image/test/unit/image2jpg16x16cropped2.jpg b/image/test/unit/image2jpg16x16cropped2.jpg new file mode 100644 index 0000000000..e51d3530d3 Binary files /dev/null and b/image/test/unit/image2jpg16x16cropped2.jpg differ diff --git a/image/test/unit/image2jpg16x32cropped3.jpg b/image/test/unit/image2jpg16x32cropped3.jpg new file mode 100644 index 0000000000..13a3d26e54 Binary files /dev/null and b/image/test/unit/image2jpg16x32cropped3.jpg differ diff --git a/image/test/unit/image2jpg16x32scaled.jpg b/image/test/unit/image2jpg16x32scaled.jpg new file mode 100644 index 0000000000..6abef0f99b Binary files /dev/null and b/image/test/unit/image2jpg16x32scaled.jpg differ diff --git a/image/test/unit/image2jpg32x16cropped4.jpg b/image/test/unit/image2jpg32x16cropped4.jpg new file mode 100644 index 0000000000..46f34918c8 Binary files /dev/null and b/image/test/unit/image2jpg32x16cropped4.jpg differ diff --git a/image/test/unit/image2jpg32x16scaled.jpg b/image/test/unit/image2jpg32x16scaled.jpg new file mode 100644 index 0000000000..e302fbafd0 Binary files /dev/null and b/image/test/unit/image2jpg32x16scaled.jpg differ diff --git a/image/test/unit/image2jpg32x32-win.png b/image/test/unit/image2jpg32x32-win.png new file mode 100644 index 0000000000..4d84df26a0 Binary files /dev/null and b/image/test/unit/image2jpg32x32-win.png differ diff --git a/image/test/unit/image2jpg32x32.jpg b/image/test/unit/image2jpg32x32.jpg new file mode 100644 index 0000000000..cf9a10a37f Binary files /dev/null and b/image/test/unit/image2jpg32x32.jpg differ diff --git a/image/test/unit/image2jpg32x32.png b/image/test/unit/image2jpg32x32.png new file mode 100644 index 0000000000..42640cbb53 Binary files /dev/null and b/image/test/unit/image2jpg32x32.png differ diff --git a/image/test/unit/image3.ico b/image/test/unit/image3.ico new file mode 100644 index 0000000000..d44438903b Binary files /dev/null and b/image/test/unit/image3.ico differ diff --git a/image/test/unit/image3ico16x16.png b/image/test/unit/image3ico16x16.png new file mode 100644 index 0000000000..fa61cc5046 Binary files /dev/null and b/image/test/unit/image3ico16x16.png differ diff --git a/image/test/unit/image3ico32x32.png b/image/test/unit/image3ico32x32.png new file mode 100644 index 0000000000..58a72e5c9d Binary files /dev/null and b/image/test/unit/image3ico32x32.png differ diff --git a/image/test/unit/image4.gif b/image/test/unit/image4.gif new file mode 100644 index 0000000000..b1530bc81e Binary files /dev/null and b/image/test/unit/image4.gif differ diff --git a/image/test/unit/image4gif16x16bmp24bpp.ico b/image/test/unit/image4gif16x16bmp24bpp.ico new file mode 100644 index 0000000000..890c81c272 Binary files /dev/null and b/image/test/unit/image4gif16x16bmp24bpp.ico differ diff --git a/image/test/unit/image4gif16x16bmp32bpp.ico b/image/test/unit/image4gif16x16bmp32bpp.ico new file mode 100644 index 0000000000..f8a9eb8adc Binary files /dev/null and b/image/test/unit/image4gif16x16bmp32bpp.ico differ diff --git a/image/test/unit/image4gif32x32bmp24bpp.ico b/image/test/unit/image4gif32x32bmp24bpp.ico new file mode 100644 index 0000000000..28092818dc Binary files /dev/null and b/image/test/unit/image4gif32x32bmp24bpp.ico differ diff --git a/image/test/unit/image4gif32x32bmp32bpp.ico b/image/test/unit/image4gif32x32bmp32bpp.ico new file mode 100644 index 0000000000..0e2d28c82a Binary files /dev/null and b/image/test/unit/image4gif32x32bmp32bpp.ico differ diff --git a/image/test/unit/image_load_helpers.js b/image/test/unit/image_load_helpers.js new file mode 100644 index 0000000000..6d1e605bf5 --- /dev/null +++ b/image/test/unit/image_load_helpers.js @@ -0,0 +1,124 @@ +/* + * Helper structures to track callbacks from image and channel loads. + */ + +// START_REQUEST and STOP_REQUEST are used by ChannelListener, and +// stored in ChannelListener.requestStatus. +const START_REQUEST = 0x01; +const STOP_REQUEST = 0x02; +const DATA_AVAILABLE = 0x04; + +// One bit per callback that imageListener below implements. Stored in +// ImageListener.state. +const SIZE_AVAILABLE = 0x01; +const FRAME_UPDATE = 0x02; +const FRAME_COMPLETE = 0x04; +const LOAD_COMPLETE = 0x08; +const DECODE_COMPLETE = 0x10; + +// Safebrowsing requires that the profile dir is set. +do_get_profile(); + +// An implementation of imgIScriptedNotificationObserver with the ability to +// call specified functions on onStartRequest and onStopRequest. +function ImageListener(start_callback, stop_callback) { + this.sizeAvailable = function onSizeAvailable(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= SIZE_AVAILABLE; + + if (this.start_callback) { + this.start_callback(this, aRequest); + } + }; + this.frameComplete = function onFrameComplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= FRAME_COMPLETE; + }; + this.decodeComplete = function onDecodeComplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= DECODE_COMPLETE; + }; + this.loadComplete = function onLoadcomplete(aRequest) { + Assert.ok(!this.synchronous); + + this.state |= LOAD_COMPLETE; + + if (this.stop_callback) { + this.stop_callback(this, aRequest); + } + }; + this.frameUpdate = function onFrameUpdate(aRequest) {}; + this.isAnimated = function onIsAnimated() {}; + + // Initialize the synchronous flag to true to start. This must be set to + // false before exiting to the event loop! + this.synchronous = true; + + // A function to call when onStartRequest is called. + this.start_callback = start_callback; + + // A function to call when onStopRequest is called. + this.stop_callback = stop_callback; + + // The image load/decode state. + // A bitfield that tracks which callbacks have been called. Takes the bits + // defined above. + this.state = 0; +} + +function NS_FAILED(val) { + return !!(val & 0x80000000); +} + +function ChannelListener() { + this.onStartRequest = function onStartRequest(aRequest) { + if (this.outputListener) { + this.outputListener.onStartRequest(aRequest); + } + + this.requestStatus |= START_REQUEST; + }; + + this.onDataAvailable = function onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ) { + if (this.outputListener) { + this.outputListener.onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ); + } + + this.requestStatus |= DATA_AVAILABLE; + }; + + this.onStopRequest = function onStopRequest(aRequest, aStatusCode) { + if (this.outputListener) { + this.outputListener.onStopRequest(aRequest, aStatusCode); + } + + // If we failed (or were canceled - failure is implied if canceled), + // there's no use tracking our state, since it's meaningless. + if (NS_FAILED(aStatusCode)) { + this.requestStatus = 0; + } else { + this.requestStatus |= STOP_REQUEST; + } + }; + + // A listener to pass the notifications we get to. + this.outputListener = null; + + // The request's status. A bitfield that holds one or both of START_REQUEST + // and STOP_REQUEST, according to which callbacks have been called on the + // associated request. + this.requestStatus = 0; +} diff --git a/image/test/unit/test_async_notification.js b/image/test/unit/test_async_notification.js new file mode 100644 index 0000000000..3f5f47c271 --- /dev/null +++ b/image/test/unit/test_async_notification.js @@ -0,0 +1,15 @@ +/* + * Test for asynchronous image load/decode notifications in the case that the image load works. + */ + +// A simple 3x3 png; rows go red, green, blue. Stolen from the PNG encoder test. + +var pngspec = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="; +var ioService = Services.io; + +// This is used in async_load_tests.js +/* exported uri */ +var uri = ioService.newURI(pngspec); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_async_notification_404.js b/image/test/unit/test_async_notification_404.js new file mode 100644 index 0000000000..c4f836750c --- /dev/null +++ b/image/test/unit/test_async_notification_404.js @@ -0,0 +1,19 @@ +/* + * Test to ensure that load/decode notifications are delivered completely and + * asynchronously when dealing with a file that's a 404. + */ +/* import-globals-from async_load_tests.js */ + +var ioService = Services.io; + +// This is used in async_load_tests.js +// eslint-disable-next-line no-unused-vars +ChromeUtils.defineLazyGetter(this, "uri", function () { + return ioService.newURI( + "http://localhost:" + + server.identity.primaryPort + + "/async-notification-never-here.jpg" + ); +}); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_async_notification_animated.js b/image/test/unit/test_async_notification_animated.js new file mode 100644 index 0000000000..f201f90f60 --- /dev/null +++ b/image/test/unit/test_async_notification_animated.js @@ -0,0 +1,19 @@ +/* + * Test for asynchronous image load/decode notifications in the case that the + * image load works, but for an animated image. + * + * If this fails because a request wasn't cancelled, it's possible that + * imgContainer::ExtractFrame didn't set the new image's status correctly. + */ + +// transparent-animation.gif from the gif reftests. + +var spec = + "data:image/gif;base64,R0lGODlhZABkAIABAP8AAP///yH5BAkBAAEALAAAAABLAGQAAAK8jI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC8fyTNf2jef6zvf+DwwKh8Si8YhMKpchgPMJjUqnVOipis1ir9qul+sNV8HistVkTj/JajG7/UXDy+95tm4fy/NdPF/q93dWIqgVWAhwWKgoyPjnyAeZJ2lHOWcJh9mmqcaZ5mkGSreHOCXqRloadRrGGkeoapoa6+TaN0tra4gbq3vHq+q7BVwqrMeEnKy8zNzs/AwdLT1NXW19jZ1tUgAAIfkECQEAAQAsAAAAADQAZAAAArCMj6nL7Q+jnLTai7PevPsPhuJIluaJpurKtu4Lx/JM1/aN5/rO9/7vAAiHxKLxiCRCkswmc+mMSqHSapJqzSof2u4Q67WCw1MuOTs+N9Pqq7kdZcON8vk2aF+/88g6358HaCc4Rwhn2IaopnjGSOYYBukl2UWpZYm2x0enuXnX4NnXGQqAKTYaalqlWoZH+snwWsQah+pJ64Sr5ypbCvQLHCw8TFxsfIycrLzM3PxQAAAh+QQJAQABACwAAAAAGwBkAAACUIyPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gTE8kzX9o3n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvdrfYnH5LL5jE6r16sCADs="; +var ioService = Services.io; + +// This is used in async_load_tests.js +/* exported uri */ +var uri = ioService.newURI(spec); + +load("async_load_tests.js"); diff --git a/image/test/unit/test_encoder_apng.js b/image/test/unit/test_encoder_apng.js new file mode 100644 index 0000000000..f055a0335f --- /dev/null +++ b/image/test/unit/test_encoder_apng.js @@ -0,0 +1,582 @@ +/* + * Test for APNG encoding in ImageLib + * + */ + +// dispose=[none|background|previous] +// blend=[source|over] + +var apng1A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGB format. + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, + 0, 0, 255, 0, 0, 255, 0, 0, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, + 255, 0, 0, 255, 0, 0, 255, 0, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + transparency: null, + + pixels: [ + 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAAA9JREFUCFtj/M8ABYxYWAA5IQMBD9nE1QAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAADuZcrMAAAAFGZkQVQAAAACCFtjZPjPAAGMWFgANiQDAVBdoI8AAAAaZmNUTAAAAAMAAAADAAAAAwAAAAAAAAAAAfQD6AAAA/MZJQAAABVmZEFUAAAABAhbY2Rg+M8ABoxYWAAzJwMBWk5KPwAAAABJRU5ErkJggg==", +}; + +var apng1B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, + 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAADuZcrMAAAAFWZkQVQAAAACCFtjZPgPhFDAiJMDAInrBf4Q0nfOAAAAGmZjVEwAAAADAAAAAwAAAAMAAAAAAAAAAAH0A+gAAAPzGSUAAAAWZmRBVAAAAAQIW2NkYPj/nwEKGHFyAIbuBf50PCpiAAAAAElFTkSuQmCC", +}; + +var apng1C = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // The first frame is skipped, so it will only flash green/blue (or static red in an APNG-unaware viewer) + width: 3, + height: 3, + skipFirstFrame: true, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, + 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAACAAAAAPONk3AAAAASSURBVAhbY/zPwABEEMCIkwMAjOgF/rfl9xUAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABVmZEFUAAAAAQhbY2T4D4RQwIiTAwCJ6wX++lSqrAAAABpmY1RMAAAAAgAAAAMAAAADAAAAAAAAAAAB9APoAACYgPPxAAAAFmZkQVQAAAADCFtjZGD4/58BChhxcgCG7gX+PgKhKQAAAABJRU5ErkJggg==", +}; + +var apng2A = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over mode + // (The green frame is a horizontal gradient, and the blue frame is a + // vertical gradient. They stack as they animate.) + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, + 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 180, 0, 0, 255, + 180, 0, 0, 255, 180, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAAGZYvpaAAAAGWZkQVQAAAACCFtjZPgPhAwMW4F4OiNODgDI3wnis0vjTAAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAAF09CmzAAAAHGZkQVQAAAAECFtjZGD4780ABYxAzhZkzn8YBwBn4AT/ernr+wAAAABJRU5ErkJggg==", +}; + +var apng2B = { + // A 3x3 image with 3 frames, alternating red, green, blue. RGBA format. + // blend = over, dispose = background + // (The green frame is a horizontal gradient, and the blue frame is a + // vertical gradient. Each frame is displayed individually, blended to + // whatever the background is.) + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, + 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, + ], + }, + + { + // frame #2 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, + 0, 180, 0, 255, 0, 75, 0, 255, 0, 255, 0, 255, 0, 180, 0, 255, 0, 75, + ], + }, + + { + // frame #3 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "background", + blend: "over", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 75, 0, 0, 255, 180, 0, 0, 255, + 180, 0, 0, 255, 180, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAADAAAAAM7tusAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AEAbA0RWQAAABJJREFUCFtj/M/AAEQQwIiTAwCM6AX+t+X3FQAAABpmY1RMAAAAAQAAAAMAAAADAAAAAAAAAAAB9APoAQGAecsbAAAAGWZkQVQAAAACCFtjZPgPhAwMW4F4OiNODgDI3wnis0vjTAAAABpmY1RMAAAAAwAAAAMAAAADAAAAAAAAAAAB9APoAQFt7xjyAAAAHGZkQVQAAAAECFtjZGD4780ABYxAzhZkzn8YBwBn4AT/ernr+wAAAABJRU5ErkJggg==", +}; + +var apng3 = { + // A 3x3 image with 4 frames. First frame is white, then 1x1 frames draw a diagonal line + width: 3, + height: 3, + skipFirstFrame: false, + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + transparency: null, + plays: 0, + + frames: [ + { + // frame #1 + width: 3, + height: 3, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + ], + }, + + { + // frame #2 + width: 1, + height: 1, + x_offset: 0, + y_offset: 0, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + + { + // frame #3 + width: 1, + height: 1, + x_offset: 1, + y_offset: 1, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + + { + // frame #4 + width: 1, + height: 1, + x_offset: 2, + y_offset: 2, + dispose: "none", + blend: "source", + delay: 500, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [0, 0, 0, 255], + }, + ], + + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAACGFjVEwAAAAEAAAAAHzNZtAAAAAaZmNUTAAAAAAAAAADAAAAAwAAAAAAAAAAAfQD6AAAdRYgGAAAABFJREFUCFtj/A8EDFDAiJMDABlqC/jamhxvAAAAGmZjVEwAAAABAAAAAQAAAAEAAAAAAAAAAAH0A+gAADJXfawAAAARZmRBVAAAAAIIW2NgYGD4DwABBAEA0iEgKQAAABpmY1RMAAAAAwAAAAEAAAABAAAAAQAAAAEB9APoAAC4OHoxAAAAEWZkQVQAAAAECFtjYGBg+A8AAQQBACrja58AAAAaZmNUTAAAAAUAAAABAAAAAQAAAAIAAAACAfQD6AAA/fh01wAAABFmZEFUAAAABghbY2BgYPgPAAEEAQDLja8yAAAAAElFTkSuQmCC", +}; + +// Main test entry point. +function run_test() { + dump("Checking apng1A...\n"); + run_test_for(apng1A); + dump("Checking apng1B...\n"); + run_test_for(apng1B); + dump("Checking apng1C...\n"); + run_test_for(apng1C); + + dump("Checking apng2A...\n"); + run_test_for(apng2A); + dump("Checking apng2B...\n"); + run_test_for(apng2B); + + dump("Checking apng3...\n"); + run_test_for(apng3); +} + +function run_test_for(input) { + var encoder, dataURL; + + encoder = encodeImage(input); + dataURL = makeDataURL(encoder, "image/png"); + Assert.equal(dataURL, input.expected); +} + +function encodeImage(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + options += ";frames=" + input.frames.length; + options += ";skipfirstframe=" + (input.skipFirstFrame ? "yes" : "no"); + options += ";plays=" + input.plays; + encoder.startImageEncode(input.width, input.height, input.format, options); + + for (var i = 0; i < input.frames.length; i++) { + var frame = input.frames[i]; + + options = ""; + if (frame.transparency) { + options += "transparency=" + input.transparency; + } + options += ";delay=" + frame.delay; + options += ";dispose=" + frame.dispose; + options += ";blend=" + frame.blend; + if (frame.x_offset > 0) { + options += ";xoffset=" + frame.x_offset; + } + if (frame.y_offset > 0) { + options += ";yoffset=" + frame.y_offset; + } + + encoder.addImageFrame( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + } + + encoder.endImageEncode(); + + return encoder; +} + +function makeDataURL(encoder, mimetype) { + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); + + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + stream.QueryInterface(Ci.nsIBinaryInputStream); + + stream.setInputStream(rawStream); + + var bytes = stream.readByteArray(stream.available()); // returns int[] + + var base64String = toBase64(bytes); + + return "data:" + mimetype + ";base64," + base64String; +} + +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = + // eslint-disable-next-line no-useless-concat + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +const base64Pad = "="; +function toBase64(data) { + var result = ""; + var length = data.length; + var i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < length - 2; i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length % 3) { + i = length - (length % 3); + result += toBase64Table[data[i] >> 2]; + if (length % 3 == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[(data[i + 1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +} diff --git a/image/test/unit/test_encoder_png.js b/image/test/unit/test_encoder_png.js new file mode 100644 index 0000000000..73e91f573f --- /dev/null +++ b/image/test/unit/test_encoder_png.js @@ -0,0 +1,263 @@ +/* + * Test for PNG encoding in ImageLib + * + */ + +var png1A = { + // A 3x3 image, rows are red, green, blue. + // RGB format, transparency defaults. + + transparency: null, + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +var png1B = { + // A 3x3 image, rows are red, green, blue. + // RGB format, transparency=none. + + transparency: "none", + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGB, + stride: 9, + + pixels: [ + 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 255, 0, 0, 255, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +var png2A = { + // A 3x3 image, rows are: red, green, blue. Columns are: 0%, 33%, 66% transparent. + + transparency: null, + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 170, 255, 0, 0, 85, 0, 255, 0, 255, 0, 255, + 0, 170, 0, 255, 0, 85, 0, 0, 255, 255, 0, 0, 255, 170, 0, 0, 255, 85, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAGUlEQVQIW2P8z8AARAyrQZgRyETiMPyHcwDKCwoAGxxLEQAAAABJRU5ErkJggg==", +}; + +var png2B = { + // A 3x3 image, rows are: red, green, blue. Columns are: 0%, 33%, 66% transparent, + // but transparency will be ignored. + + transparency: "none", + + frames: [ + { + width: 3, + height: 3, + + format: Ci.imgIEncoder.INPUT_FORMAT_RGBA, + stride: 12, + + pixels: [ + 255, 0, 0, 255, 255, 0, 0, 170, 255, 0, 0, 85, 0, 255, 0, 255, 0, 255, + 0, 170, 0, 255, 0, 85, 0, 0, 255, 255, 0, 0, 255, 170, 0, 0, 255, 85, + ], + }, + ], + expected: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAFElEQVQIW2P8zwAFjAwwJiMDjAkANiQDAUpvlioAAAAASUVORK5CYII=", +}; + +// Main test entry point. +function run_test() { + dump("Checking png1A...\n"); + run_test_for(png1A); + dump("Checking png1B...\n"); + run_test_for(png1B); + dump("Checking png2A...\n"); + run_test_for(png2A); + dump("Checking png2B...\n"); + run_test_for(png2B); +} + +function run_test_for(input) { + var encoder, dataURL; + + encoder = encodeImage(input); + dataURL = makeDataURL(encoder, "image/png"); + Assert.equal(dataURL, input.expected); + + encoder = encodeImageAsync(input); + dataURL = makeDataURLFromAsync(encoder, "image/png", input.expected); +} + +function encodeImage(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + + var frame = input.frames[0]; + encoder.initFromData( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + return encoder; +} + +function _encodeImageAsyncFactory(frame, options, encoder) { + function finishEncode() { + encoder.addImageFrame( + frame.pixels, + frame.pixels.length, + frame.width, + frame.height, + frame.stride, + frame.format, + options + ); + encoder.endImageEncode(); + } + return finishEncode; +} + +function encodeImageAsync(input) { + var encoder = + Cc["@mozilla.org/image/encoder;2?type=image/png"].createInstance(); + encoder.QueryInterface(Ci.imgIEncoder); + + var options = ""; + if (input.transparency) { + options += "transparency=" + input.transparency; + } + + var frame = input.frames[0]; + encoder.startImageEncode(frame.width, frame.height, frame.format, options); + + do_timeout(50, _encodeImageAsyncFactory(frame, options, encoder)); + return encoder; +} + +function makeDataURL(encoder, mimetype) { + var rawStream = encoder.QueryInterface(Ci.nsIInputStream); + + var stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + stream.QueryInterface(Ci.nsIBinaryInputStream); + + stream.setInputStream(rawStream); + + var bytes = stream.readByteArray(stream.available()); // returns int[] + + var base64String = toBase64(bytes); + + return "data:" + mimetype + ";base64," + base64String; +} + +function makeDataURLFromAsync(encoder, mimetype, expected) { + do_test_pending(); + var rawStream = encoder.QueryInterface(Ci.nsIAsyncInputStream); + + var currentThread = + Cc["@mozilla.org/thread-manager;1"].getService().currentThread; + + var bytes = []; + + var binarystream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(); + binarystream.QueryInterface(Ci.nsIBinaryInputStream); + + var asyncReader = { + onInputStreamReady(stream) { + binarystream.setInputStream(stream); + var available = 0; + try { + available = stream.available(); + } catch (e) {} + + if (available > 0) { + bytes = bytes.concat(binarystream.readByteArray(available)); + stream.asyncWait(this, 0, 0, currentThread); + } else { + var base64String = toBase64(bytes); + var dataURL = "data:" + mimetype + ";base64," + base64String; + Assert.equal(dataURL, expected); + do_test_finished(); + } + }, + }; + rawStream.asyncWait(asyncReader, 0, 0, currentThread); +} + +/* toBase64 copied from extensions/xml-rpc/src/nsXmlRpcClient.js */ + +/* Convert data (an array of integers) to a Base64 string. */ +const toBase64Table = + // eslint-disable-next-line no-useless-concat + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +const base64Pad = "="; +function toBase64(data) { + var result = ""; + var length = data.length; + var i; + // Convert every three bytes to 4 ascii characters. + for (i = 0; i < length - 2; i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + if (length % 3) { + i = length - (length % 3); + result += toBase64Table[data[i] >> 2]; + if (length % 3 == 2) { + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[(data[i + 1] & 0x0f) << 2]; + result += base64Pad; + } else { + result += toBase64Table[(data[i] & 0x03) << 4]; + result += base64Pad + base64Pad; + } + } + + return result; +} diff --git a/image/test/unit/test_imgtools.js b/image/test/unit/test_imgtools.js new file mode 100644 index 0000000000..689a4ed56f --- /dev/null +++ b/image/test/unit/test_imgtools.js @@ -0,0 +1,871 @@ +/* + * Tests for imgITools + */ + +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +/* + * dumpToFile() + * + * For test development, dumps the specified array to a file. + * Call |dumpToFile(outData);| in a test to file to a file. + */ +// eslint-disable-next-line no-unused-vars +function dumpToFile(aData) { + var outputFile = do_get_cwd(); + outputFile.append("testdump.webp"); + + var outputStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + // WR_ONLY|CREATE|TRUNC + outputStream.init(outputFile, 0x02 | 0x08 | 0x20, 0o644, null); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(outputStream); + + bos.writeByteArray(aData); + + outputStream.close(); +} + +/* + * getFileInputStream() + * + * Returns an input stream for the specified file. + */ +function getFileInputStream(aFile) { + var inputStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + // init the stream as RD_ONLY, -1 == default permissions. + inputStream.init(aFile, 0x01, -1, null); + + // Blah. The image decoders use ReadSegments, which isn't implemented on + // file input streams. Use a buffered stream to make it work. + var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance( + Ci.nsIBufferedInputStream + ); + bis.init(inputStream, 1024); + + return bis; +} + +/* + * streamToArray() + * + * Consumes an input stream, and returns its bytes as an array. + */ +function streamToArray(aStream) { + var size = aStream.available(); + + // use a binary input stream to grab the bytes. + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bis.setInputStream(aStream); + + var bytes = bis.readByteArray(size); + if (size != bytes.length) { + throw new Error("Didn't read expected number of bytes"); + } + + return bytes; +} + +/* + * compareArrays + * + * Compares two arrays, and throws if there's a difference. + */ +function compareArrays(aArray1, aArray2) { + Assert.equal(aArray1.length, aArray2.length); + + for (var i = 0; i < aArray1.length; i++) { + if (aArray1[i] != aArray2[i]) { + throw new Error("arrays differ at index " + i); + } + } +} + +/* + * checkExpectedError + * + * Checks to see if a thrown error was expected or not, and if it + * matches the expected value. + */ +function checkExpectedError(aExpectedError, aActualError) { + if (aExpectedError) { + if (!aActualError) { + throw new Error("Didn't throw as expected (" + aExpectedError + ")"); + } + + if (!aExpectedError.test(aActualError)) { + throw new Error("Threw (" + aActualError + "), not (" + aExpectedError); + } + + // We got the expected error, so make a note in the test log. + dump("...that error was expected.\n\n"); + } else if (aActualError) { + throw new Error("Threw unexpected error: " + aActualError); + } +} + +function run_test() { + try { + /* ========== 0 ========== */ + var testnum = 0; + var testdesc = "imgITools setup"; + var err = null; + + var imgTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools); + + if (!imgTools) { + throw new Error("Couldn't get imgITools service"); + } + + // Ugh, this is an ugly hack. The pixel values we get in Windows are sometimes + // +/- 1 value compared to other platforms, so we need to compare against a + // different set of reference images. nsIXULRuntime.OS doesn't seem to be + // available in xpcshell, so we'll use this as a kludgy way to figure out if + // we're running on Windows. + var isWindows = mozinfo.os == "win"; + + /* ========== 1 ========== */ + testnum++; + testdesc = "test decoding a PNG"; + + // 64x64 png, 8415 bytes. + var imgName = "image1.png"; + var inMimeType = "image/png"; + var imgFile = do_get_file(imgName); + + var istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + var buffer = NetUtil.readInputStreamToString(istream, istream.available()); + var container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 64); + Assert.equal(container.height, 64); + + /* ========== 2 ========== */ + testnum++; + testdesc = "test encoding a scaled JPEG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 16, 16); + + var encodedBytes = streamToArray(istream); + // Get bytes for expected result + var refName = "image1png16x16.jpg"; + var refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1050); + var referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 3 ========== */ + testnum++; + testdesc = "test encoding an unscaled JPEG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/jpeg"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1png64x64.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 4507); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 4 ========== */ + testnum++; + testdesc = "test decoding a JPEG"; + + // 32x32 jpeg, 3494 bytes. + imgName = "image2.jpg"; + inMimeType = "image/jpeg"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 3494); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + /* ========== 5 ========== */ + testnum++; + testdesc = "test encoding a scaled PNG"; + + if (!isWindows) { + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/png", 16, 16); + + encodedBytes = streamToArray(istream); + // Get bytes for expected result + refName = isWindows ? "image2jpg16x16-win.png" : "image2jpg16x16.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 955); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + } + + /* ========== 6 ========== */ + testnum++; + testdesc = "test encoding an unscaled PNG"; + + if (!isWindows) { + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/png"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = isWindows ? "image2jpg32x32-win.png" : "image2jpg32x32.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3026); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + } + + /* ========== 7 ========== */ + testnum++; + testdesc = "test decoding a ICO"; + + // 16x16 ico, 1406 bytes. + imgName = "image3.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 1406); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 16); + Assert.equal(container.height, 16); + + /* ========== 8 ========== */ + testnum++; + testdesc = "test encoding a scaled PNG"; // note that we're scaling UP + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/png", 32, 32); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image3ico32x32.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 2280); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 9 ========== */ + testnum++; + testdesc = "test encoding an unscaled PNG"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage(container, "image/png"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image3ico16x16.png"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 520); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 10 ========== */ + testnum++; + testdesc = "test decoding a GIF"; + + // 32x32 gif, 1809 bytes. + imgName = "image4.gif"; + inMimeType = "image/gif"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 1809); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + /* ========== 11 ========== */ + testnum++; + testdesc = + "test encoding an unscaled ICO with format options " + + "(format=bmp;bpp=32)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage( + container, + "image/vnd.microsoft.icon", + "format=bmp;bpp=32" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif32x32bmp32bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 4286); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 12 ========== */ + testnum++; + testdesc = + // eslint-disable-next-line no-useless-concat + "test encoding a scaled ICO with format options " + "(format=bmp;bpp=32)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage( + container, + "image/vnd.microsoft.icon", + 16, + 16, + "format=bmp;bpp=32" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif16x16bmp32bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1150); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 13 ========== */ + testnum++; + testdesc = + "test encoding an unscaled ICO with format options " + + "(format=bmp;bpp=24)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeImage( + container, + "image/vnd.microsoft.icon", + "format=bmp;bpp=24" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif32x32bmp24bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3262); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 14 ========== */ + testnum++; + testdesc = + // eslint-disable-next-line no-useless-concat + "test encoding a scaled ICO with format options " + "(format=bmp;bpp=24)"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage( + container, + "image/vnd.microsoft.icon", + 16, + 16, + "format=bmp;bpp=24" + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image4gif16x16bmp24bpp.ico"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 894); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 15 ========== */ + testnum++; + testdesc = "test cropping a JPG"; + + // 32x32 jpeg, 3494 bytes. + imgName = "image2.jpg"; + inMimeType = "image/jpeg"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 3494); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // It's not easy to look at the pixel values from JS, so just + // check the container's size. + Assert.equal(container.width, 32); + Assert.equal(container.height, 32); + + // encode a cropped image + istream = imgTools.encodeCroppedImage( + container, + "image/jpeg", + 0, + 0, + 16, + 16 + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x16cropped.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 879); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 16 ========== */ + testnum++; + testdesc = "test cropping a JPG with an offset"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage( + container, + "image/jpeg", + 16, + 16, + 16, + 16 + ); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x16cropped2.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 878); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 17 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 16, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x32cropped3.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1127); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 18 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given width"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 0, 16); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x16cropped4.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1135); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 19 ========== */ + testnum++; + testdesc = "test cropping a JPG without a given width and height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 0, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x32.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1634); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 20 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given width"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 0, 16); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x16scaled.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1227); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 21 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 16, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg16x32scaled.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1219); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 22 ========== */ + testnum++; + testdesc = "test scaling a JPG without a given width and height"; + + // we'll reuse the container from the previous test + istream = imgTools.encodeScaledImage(container, "image/jpeg", 0, 0); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image2jpg32x32.jpg"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1634); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 23 ========== */ + testnum++; + testdesc = "test invalid arguments for cropping"; + + var numErrors = 0; + + try { + // width/height can't be negative + imgTools.encodeScaledImage(container, "image/jpeg", -1, -1); + } catch (e) { + numErrors++; + } + + try { + // offsets can't be negative + imgTools.encodeCroppedImage(container, "image/jpeg", -1, -1, 16, 16); + } catch (e) { + numErrors++; + } + + try { + // width/height can't be negative + imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, -1, -1); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 17, 17, 16, 16); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 0, 0, 33, 33); + } catch (e) { + numErrors++; + } + + try { + // out of bounds + imgTools.encodeCroppedImage(container, "image/jpeg", 1, 1, 0, 0); + } catch (e) { + numErrors++; + } + + Assert.equal(numErrors, 6); + + /* ========== 24 ========== */ + testnum++; + testdesc = "test encoding webp"; + + // Load picture that we want to convert + imgName = "image1.png"; + inMimeType = "image/png"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // Convert image to webp + istream = imgTools.encodeImage(container, "image/webp"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1.webp"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 3206); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== 25 ========== */ + testnum++; + testdesc = "test encoding webp with quality parameter"; + + // Load picture that we want to convert + imgName = "image1.png"; + inMimeType = "image/png"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 8415); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // Convert image to webp + istream = imgTools.encodeImage(container, "image/webp", "quality=50"); + encodedBytes = streamToArray(istream); + + // Get bytes for expected result + refName = "image1quality50.webp"; + refFile = do_get_file(refName); + istream = getFileInputStream(refFile); + Assert.equal(istream.available(), 1944); + referenceBytes = streamToArray(istream); + + // compare the encoder's output to the reference file. + compareArrays(encodedBytes, referenceBytes); + + /* ========== bug 363986 ========== */ + testnum = 363986; + testdesc = "test PNG and JPEG and WEBP encoders' Read/ReadSegments methods"; + + var testData = [ + { + preImage: "image3.ico", + preImageMimeType: "image/x-icon", + refImage: "image3ico16x16.png", + refImageMimeType: "image/png", + }, + { + preImage: "image1.png", + preImageMimeType: "image/png", + refImage: "image1png64x64.jpg", + refImageMimeType: "image/jpeg", + }, + { + preImage: "image1.png", + preImageMimeType: "image/png", + refImage: "image1.webp", + refImageMimeType: "image/webp", + }, + ]; + + for (var i = 0; i < testData.length; ++i) { + var dict = testData[i]; + + imgFile = do_get_file(dict.refImage); + istream = getFileInputStream(imgFile); + var refBytes = streamToArray(istream); + + imgFile = do_get_file(dict.preImage); + istream = getFileInputStream(imgFile); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + dict.preImageMimeType + ); + + istream = imgTools.encodeImage(container, dict.refImageMimeType); + + var sstream = Cc["@mozilla.org/storagestream;1"].createInstance( + Ci.nsIStorageStream + ); + sstream.init(4096, 4294967295, null); + var ostream = sstream.getOutputStream(0); + var bostream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + + // use a tiny buffer to make sure the image data doesn't fully fit in it + bostream.init(ostream, 8); + + bostream.writeFrom(istream, istream.available()); + bostream.flush(); + bostream.close(); + + var encBytes = streamToArray(sstream.newInputStream(0)); + + compareArrays(refBytes, encBytes); + } + + /* ========== bug 413512 ========== */ + testnum = 413512; + testdesc = "test decoding bad favicon (bug 413512)"; + + imgName = "bug413512.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 17759); + var errsrc = "none"; + + try { + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + // We expect to hit an error during encoding because the ICO header of the + // image is fine, but the actual resources are corrupt. Since + // decodeImageFromBuffer() only performs a metadata decode, it doesn't decode + // far enough to realize this, but we'll find out when we do a full decode + // during encodeImage(). + try { + istream = imgTools.encodeImage(container, "image/png"); + } catch (e) { + err = e; + errsrc = "encode"; + } + } catch (e) { + err = e; + errsrc = "decode"; + } + + Assert.equal(errsrc, "encode"); + checkExpectedError(/NS_ERROR_FAILURE/, err); + + /* ========== bug 815359 ========== */ + testnum = 815359; + testdesc = "test correct ico hotspots (bug 815359)"; + + imgName = "bug815359.ico"; + inMimeType = "image/x-icon"; + imgFile = do_get_file(imgName); + + istream = getFileInputStream(imgFile); + Assert.equal(istream.available(), 4286); + + buffer = NetUtil.readInputStreamToString(istream, istream.available()); + container = imgTools.decodeImageFromBuffer( + buffer, + buffer.length, + inMimeType + ); + + Assert.equal(container.hotspotX, 10); + Assert.equal(container.hotspotY, 9); + + /* ========== end ========== */ + } catch (e) { + throw new Error( + "FAILED in test #" + testnum + " -- " + testdesc + ": " + e + ); + } +} diff --git a/image/test/unit/test_moz_icon_uri.js b/image/test/unit/test_moz_icon_uri.js new file mode 100644 index 0000000000..0111d71d2a --- /dev/null +++ b/image/test/unit/test_moz_icon_uri.js @@ -0,0 +1,157 @@ +/* + * Test icon URI functionality + * + */ + +// There are 3 types of valid icon URIs: +// 1. moz-icon:[valid URL] +// 2. moz-icon://[file name] +// 3. moz-icon://stock/[icon identifier] +// Plus we also support moz-icon://[valid URL] for backwards compatibility. + +// Main test entry point. + +function run_test() { + let ioService = Services.io; + let currentSpec = ""; // the uri spec that we're currently testing + let exception = false; // whether or not an exception was thrown + let uri = null; // the current URI + let iconURI = null; // the current icon URI + + // Note that if the scheme is not correct the ioservice won't even create an icon URI + // so don't bother testing incorrect schemes here. + + // Make sure a valid file name icon URI can be created and that we can obtain + // all arguments, the spec, and the file extension. + currentSpec = "moz-icon://foo.html?contentType=bar&size=button&state=normal"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.iconSize, "button"); + Assert.equal(iconURI.iconState, "normal"); + Assert.equal(iconURI.contentType, "bar"); + Assert.equal(iconURI.fileExtension, ".html"); + + // Make sure a valid file name icon URI can be created with a numeric size, + // and make sure the numeric size is handled properly + currentSpec = "moz-icon://foo.html?size=3"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.iconSize, ""); + Assert.equal(iconURI.imageSize, 3); + + // Make sure a valid stock icon URI can be created and that we can obtain + // the stock icon's name. + currentSpec = "moz-icon://stock/foo"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + Assert.equal(iconURI.stockIcon, "foo"); + + // Make sure an invalid stock icon URI, missing icon identifier, throws. + currentSpec = "moz-icon://stock/?size=3"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.ok(exception); + exception = false; // reset exception value + + // Make sure a valid file URL icon URI can be created and that we can obtain + // the URL and QI it to an nsIFileURL. + currentSpec = "moz-icon:file://foo.txt"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + let fileURL = null; + try { + fileURL = iconURI.iconURL.QueryInterface(Ci.nsIFileURL); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + Assert.notEqual(fileURL, null); + + // Now test a file URI which has been created with an extra // + currentSpec = "moz-icon://file://foo.txt"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + iconURI = uri.QueryInterface(Ci.nsIMozIconURI); + fileURL = null; + try { + fileURL = iconURI.iconURL.QueryInterface(Ci.nsIFileURL); + } catch (e) { + exception = true; + } + Assert.equal(exception, false); + exception = false; // reset exception value + + Assert.notEqual(fileURL, null); + + // Now test a simple invalid icon URI. This should fail. + currentSpec = "moz-icon:foo"; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value + + // Now test an icon URI that has a URI for a path but that is not a URL. This should fail. + // This is png data for a little red dot that I got from wikipedia. + currentSpec = + "moz-icon:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg=="; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value + + // Now test a URI that should be a file name but is ridiculously long. This should fail. + currentSpec = + "moz-icon://data:application/vnd.ms-excel;base64,PHhtbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtOCI+PHNzOldvcmtib29rIHhtbG5zOnNzPSJ1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOm9mZmljZTpzcHJlYWRzaGVldCIgeG1sbnM6eD0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6ZXhjZWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+PG86RG9jdW1lbnRQcm9wZXJ0aWVzPjxvOlRpdGxlPkFycmF5IEdyaWQ8L286VGl0bGU+PC9vOkRvY3VtZW50UHJvcGVydGllcz48c3M6RXhjZWxXb3JrYm9vaz48c3M6V2luZG93SGVpZ2h0PjkwMDA8L3NzOldpbmRvd0hlaWdodD48c3M6V2luZG93V2lkdGg+MTc0ODA8L3NzOldpbmRvd1dpZHRoPjxzczpQcm90ZWN0U3RydWN0dXJlPkZhbHNlPC9zczpQcm90ZWN0U3RydWN0dXJlPjxzczpQcm90ZWN0V2luZG93cz5GYWxzZTwvc3M6UHJvdGVjdFdpbmRvd3M+PC9zczpFeGNlbFdvcmtib29rPjxzczpTdHlsZXM+PHNzOlN0eWxlIHNzOklEPSJEZWZhdWx0Ij48c3M6QWxpZ25tZW50IHNzOlZlcnRpY2FsPSJUb3AiIHNzOldyYXBUZXh0PSIxIiAvPjxzczpGb250IHNzOkZvbnROYW1lPSJhcmlhbCIgc3M6U2l6ZT0iMTAiIC8+PHNzOkJvcmRlcnM+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJUb3AiIC8+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJCb3R0b20iIC8+PHNzOkJvcmRlciBzczpDb2xvcj0iI2U0ZTRlNCIgc3M6V2VpZ2h0PSIxIiBzczpMaW5lU3R5bGU9IkNvbnRpbnVvdXMiIHNzOlBvc2l0aW9uPSJMZWZ0IiAvPjxzczpCb3JkZXIgc3M6Q29sb3I9IiNlNGU0ZTQiIHNzOldlaWdodD0iMSIgc3M6TGluZVN0eWxlPSJDb250aW51b3VzIiBzczpQb3NpdGlvbj0iUmlnaHQiIC8+PC9zczpCb3JkZXJzPjxzczpJbnRlcmlvciAvPjxzczpOdW1iZXJGb3JtYXQgLz48c3M6UHJvdGVjdGlvbiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOklEPSJ0aXRsZSI+PHNzOkJvcmRlcnMgLz48c3M6Rm9udCAvPjxzczpBbGlnbm1lbnQgc3M6V3JhcFRleHQ9IjEiIHNzOlZlcnRpY2FsPSJDZW50ZXIiIHNzOkhvcml6b250YWw9IkNlbnRlciIgLz48c3M6TnVtYmVyRm9ybWF0IHNzOkZvcm1hdD0iQCIgLz48L3NzOlN0eWxlPjxzczpTdHlsZSBzczpJRD0iaGVhZGVyY2VsbCI+PHNzOkZvbnQgc3M6Qm9sZD0iMSIgc3M6U2l6ZT0iMTAiIC8+PHNzOkFsaWdubWVudCBzczpXcmFwVGV4dD0iMSIgc3M6SG9yaXpvbnRhbD0iQ2VudGVyIiAvPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNBM0M5RjEiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6SUQ9ImV2ZW4iPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNDQ0ZGRkYiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJldmVuIiBzczpJRD0iZXZlbmRhdGUiPjxzczpOdW1iZXJGb3JtYXQgc3M6Rm9ybWF0PSJ5eXl5LW1tLWRkIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOlBhcmVudD0iZXZlbiIgc3M6SUQ9ImV2ZW5pbnQiPjxzczpOdW1iZXJGb3JtYXQgc3M6Rm9ybWF0PSIwIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOlBhcmVudD0iZXZlbiIgc3M6SUQ9ImV2ZW5mbG9hdCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAuMDAwIiAvPjwvc3M6U3R5bGU+PHNzOlN0eWxlIHNzOklEPSJvZGQiPjxzczpJbnRlcmlvciBzczpQYXR0ZXJuPSJTb2xpZCIgc3M6Q29sb3I9IiNDQ0NDRkYiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJvZGQiIHNzOklEPSJvZGRkYXRlIj48c3M6TnVtYmVyRm9ybWF0IHNzOkZvcm1hdD0ieXl5eS1tbS1kZCIgLz48L3NzOlN0eWxlPjxzczpTdHlsZSBzczpQYXJlbnQ9Im9kZCIgc3M6SUQ9Im9kZGludCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAiIC8+PC9zczpTdHlsZT48c3M6U3R5bGUgc3M6UGFyZW50PSJvZGQiIHNzOklEPSJvZGRmbG9hdCI+PHNzOk51bWJlckZvcm1hdCBzczpGb3JtYXQ9IjAuMDAwIiAvPjwvc3M6U3R5bGU+PC9zczpTdHlsZXM+PHNzOldvcmtzaGVldCBzczpOYW1lPSJBcnJheSBHcmlkIj48c3M6TmFtZXM+PHNzOk5hbWVkUmFuZ2Ugc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiBzczpSZWZlcnNUbz0iPSdBcnJheSBHcmlkJyFSMTpSMiIgLz48L3NzOk5hbWVzPjxzczpUYWJsZSB4OkZ1bGxSb3dzPSIxIiB4OkZ1bGxDb2x1bW5zPSIxIiBzczpFeHBhbmRlZENvbHVtbkNvdW50PSI1IiBzczpFeHBhbmRlZFJvd0NvdW50PSIzMSI+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSIyNzEiIC8+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSI3NSIgLz48c3M6Q29sdW1uIHNzOkF1dG9GaXRXaWR0aD0iMSIgc3M6V2lkdGg9Ijc1IiAvPjxzczpDb2x1bW4gc3M6QXV0b0ZpdFdpZHRoPSIxIiBzczpXaWR0aD0iNzUiIC8+PHNzOkNvbHVtbiBzczpBdXRvRml0V2lkdGg9IjEiIHNzOldpZHRoPSI4NSIgLz48c3M6Um93IHNzOkhlaWdodD0iMzgiPjxzczpDZWxsIHNzOlN0eWxlSUQ9InRpdGxlIiBzczpNZXJnZUFjcm9zcz0iNCI+PHNzOkRhdGEgeG1sbnM6aHRtbD0iaHR0cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MCIgc3M6VHlwZT0iU3RyaW5nIj48aHRtbDpCPiAoYykyMDA4IFNFQk4gVUE8L2h0bWw6Qj48L3NzOkRhdGE+PHNzOk5hbWVkQ2VsbCBzczpOYW1lPSJQcmludF9UaXRsZXMiIC8+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3cgc3M6QXV0b0ZpdEhlaWdodD0iMSI+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Db21wYW55PC9zczpEYXRhPjxzczpOYW1lZENlbGwgc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiAvPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJoZWFkZXJjZWxsIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlByaWNlPC9zczpEYXRhPjxzczpOYW1lZENlbGwgc3M6TmFtZT0iUHJpbnRfVGl0bGVzIiAvPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJoZWFkZXJjZWxsIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkNoYW5nZTwvc3M6RGF0YT48c3M6TmFtZWRDZWxsIHNzOk5hbWU9IlByaW50X1RpdGxlcyIgLz48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4lIENoYW5nZTwvc3M6RGF0YT48c3M6TmFtZWRDZWxsIHNzOk5hbWU9IlByaW50X1RpdGxlcyIgLz48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iaGVhZGVyY2VsbCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5MYXN0IFVwZGF0ZWQ8L3NzOkRhdGE+PHNzOk5hbWVkQ2VsbCBzczpOYW1lPSJQcmludF9UaXRsZXMiIC8+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zbSBDbzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj43MS43Mjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BVCZUIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zMS42MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPi0wLjQ4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTEuNTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BbGNvYSBJbmM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MjkuMDE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC40Mjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjQ3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+QWx0cmlhIEdyb3VwIEluYzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjgzLjgxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4yODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMzQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5BbWVyaWNhbiBFeHByZXNzIENvbXBhbnk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NTIuNTU8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+QW1lcmljYW4gSW50ZXJuYXRpb25hbCBHcm91cCwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjY0LjEzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4zMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNDk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Cb2VpbmcgQ28uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjc1LjQzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC43MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkNhdGVycGlsbGFyIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj42Ny4yNzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuOTI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjM5PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+Q2l0aWdyb3VwLCBJbmMuPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjQ5LjM3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wNDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkUuSS4gZHUgUG9udCBkZSBOZW1vdXJzIGFuZCBDb21wYW55PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDAuNDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjUxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MS4yODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkV4eG9uIE1vYmlsIENvcnA8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjguMTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC40Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC42NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkdlbmVyYWwgRWxlY3RyaWMgQ29tcGFueTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjM0LjE0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTAuMDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4tMC4yMzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkdlbmVyYWwgTW90b3JzIENvcnBvcmF0aW9uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjMwLjI3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDk8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+My43NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkhld2xldHQtUGFja2FyZCBDby48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNi41Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPi0wLjAzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+LTAuMDg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5Ib25leXdlbGwgSW50bCBJbmM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MzguNzc8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wNTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjEzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+SW50ZWwgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xOS44ODwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMzE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjU4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+SW50ZXJuYXRpb25hbCBCdXNpbmVzcyBNYWNoaW5lczwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj44MS40MTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQ0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5KUCBNb3JnYW4gJiBDaGFzZSAmIENvPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDUuNzM8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjA3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4xNTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPkpvaG5zb24gJiBKb2huc29uPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjY0LjcyPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMDY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wOTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPk1jRG9uYWxkJ3MgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNi43Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuODY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yLjQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5NZXJjayAmIENvLiwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj40MC45Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5NaWNyb3NvZnQgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yNS44NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuMTQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjU0PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+UGZpemVyIEluYzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4yNy45Njwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjQ8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MS40NTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlRoZSBDb2NhLUNvbGEgQ29tcGFueTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjQ1LjA3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4yNjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjAuNTg8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5UaGUgSG9tZSBEZXBvdCwgSW5jLjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4zNC42NDwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjM1PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMDI8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+V2VkIFNlcCAwMSAyMDEwIDAwOjAwOjAwIEdNVCsxMDAwIChFU1QpPC9zczpEYXRhPjwvc3M6Q2VsbD48L3NzOlJvdz48c3M6Um93PjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5UaGUgUHJvY3RlciAmIEdhbWJsZSBDb21wYW55PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjEuOTE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjAxPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4wMjwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPlVuaXRlZCBUZWNobm9sb2dpZXMgQ29ycG9yYXRpb248L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NjMuMjY8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC41NTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4wLjg4PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PHNzOlJvdz48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+VmVyaXpvbiBDb21tdW5pY2F0aW9uczwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjM1LjU3PC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJvZGQiPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC4zOTwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0ib2RkIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPjEuMTE8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9Im9kZCI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XZWQgU2VwIDAxIDIwMTAgMDA6MDA6MDAgR01UKzEwMDAgKEVTVCk8L3NzOkRhdGE+PC9zczpDZWxsPjwvc3M6Um93PjxzczpSb3c+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj5XYWwtTWFydCBTdG9yZXMsIEluYy48L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+NDUuNDU8L3NzOkRhdGE+PC9zczpDZWxsPjxzczpDZWxsIHNzOlN0eWxlSUQ9ImV2ZW4iPjxzczpEYXRhIHNzOlR5cGU9IlN0cmluZyI+MC43Mzwvc3M6RGF0YT48L3NzOkNlbGw+PHNzOkNlbGwgc3M6U3R5bGVJRD0iZXZlbiI+PHNzOkRhdGEgc3M6VHlwZT0iU3RyaW5nIj4xLjYzPC9zczpEYXRhPjwvc3M6Q2VsbD48c3M6Q2VsbCBzczpTdHlsZUlEPSJldmVuIj48c3M6RGF0YSBzczpUeXBlPSJTdHJpbmciPldlZCBTZXAgMDEgMjAxMCAwMDowMDowMCBHTVQrMTAwMCAoRVNUKTwvc3M6RGF0YT48L3NzOkNlbGw+PC9zczpSb3c+PC9zczpUYWJsZT48eDpXb3Jrc2hlZXRPcHRpb25zPjx4OlBhZ2VTZXR1cD48eDpMYXlvdXQgeDpDZW50ZXJIb3Jpem9udGFsPSIxIiB4Ok9yaWVudGF0aW9uPSJMYW5kc2NhcGUiIC8+PHg6Rm9vdGVyIHg6RGF0YT0iUGFnZSAmYW1wO1Agb2YgJmFtcDtOIiB4Ok1hcmdpbj0iMC41IiAvPjx4OlBhZ2VNYXJnaW5zIHg6VG9wPSIwLjUiIHg6UmlnaHQ9IjAuNSIgeDpMZWZ0PSIwLjUiIHg6Qm90dG9tPSIwLjgiIC8+PC94OlBhZ2VTZXR1cD48eDpGaXRUb1BhZ2UgLz48eDpQcmludD48eDpQcmludEVycm9ycz5CbGFuazwveDpQcmludEVycm9ycz48eDpGaXRXaWR0aD4xPC94OkZpdFdpZHRoPjx4OkZpdEhlaWdodD4zMjc2NzwveDpGaXRIZWlnaHQ+PHg6VmFsaWRQcmludGVySW5mbyAvPjx4OlZlcnRpY2FsUmVzb2x1dGlvbj42MDA8L3g6VmVydGljYWxSZXNvbHV0aW9uPjwveDpQcmludD48eDpTZWxlY3RlZCAvPjx4OkRvTm90RGlzcGxheUdyaWRsaW5lcyAvPjx4OlByb3RlY3RPYmplY3RzPkZhbHNlPC94OlByb3RlY3RPYmplY3RzPjx4OlByb3RlY3RTY2VuYXJpb3M+RmFsc2U8L3g6UHJvdGVjdFNjZW5hcmlvcz48L3g6V29ya3NoZWV0T3B0aW9ucz48L3NzOldvcmtzaGVldD48L3NzOldvcmtib29rPg=="; + try { + uri = ioService.newURI(currentSpec); + } catch (e) { + exception = true; + } + Assert.equal(exception, true); + exception = false; // reset exception value +} diff --git a/image/test/unit/test_private_channel.js b/image/test/unit/test_private_channel.js new file mode 100644 index 0000000000..b2d0e74e2c --- /dev/null +++ b/image/test/unit/test_private_channel.js @@ -0,0 +1,170 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +var server = new HttpServer(); +server.registerPathHandler("/image.png", imageHandler); +server.start(-1); + +/* import-globals-from image_load_helpers.js */ +load("image_load_helpers.js"); + +var gHits = 0; + +var gIoService = Services.io; +var gPublicLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader +); +var gPrivateLoader = Cc["@mozilla.org/image/loader;1"].createInstance( + Ci.imgILoader +); +gPrivateLoader.QueryInterface(Ci.imgICache).respectPrivacyNotifications(); + +var nonPrivateLoadContext = Cu.createLoadContext(); +var privateLoadContext = Cu.createPrivateLoadContext(); + +function imageHandler(metadata, response) { + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=" + ); + response.bodyOutputStream.write(body, body.length); +} + +var requests = []; +var listeners = []; + +var gImgPath = "http://localhost:" + server.identity.primaryPort + "/image.png"; + +function setup_chan(path, isPrivate, callback) { + var uri = NetUtil.newURI(gImgPath); + var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + var principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: isPrivate ? 1 : 0, + }); + var chan = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE, + }); + chan.notificationCallbacks = isPrivate + ? privateLoadContext + : nonPrivateLoadContext; + var channelListener = new ChannelListener(); + chan.asyncOpen(channelListener); + + var listener = new ImageListener(null, callback); + var outlistener = {}; + var loader = isPrivate ? gPrivateLoader : gPublicLoader; + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + listeners.push(outer); + requests.push( + loader.loadImageWithChannelXPCOM(chan, outer, null, outlistener) + ); + channelListener.outputListener = outlistener.value; + listener.synchronous = false; +} + +function loadImage(isPrivate, callback) { + var listener = new ImageListener(null, callback); + var outer = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .createScriptedObserver(listener); + var uri = gIoService.newURI(gImgPath); + var loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + loadGroup.notificationCallbacks = isPrivate + ? privateLoadContext + : nonPrivateLoadContext; + var loader = isPrivate ? gPrivateLoader : gPublicLoader; + var referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.NO_REFERRER_WHEN_DOWNGRADE, + true, + null + ); + requests.push( + loader.loadImageXPCOM( + uri, + null, + referrerInfo, + null, + loadGroup, + outer, + null, + 0, + null + ) + ); + listener.synchronous = false; +} + +function run_loadImage_tests() { + function observer() { + Services.obs.removeObserver(observer, "cacheservice:empty-cache"); + gHits = 0; + loadImage(false, function () { + loadImage(false, function () { + loadImage(true, function () { + loadImage(true, function () { + Assert.equal(gHits, 2); + server.stop(do_test_finished); + }); + }); + }); + }); + } + + for (let loader of [gPublicLoader, gPrivateLoader]) { + loader.QueryInterface(Ci.imgICache).clearCache(true); + loader.QueryInterface(Ci.imgICache).clearCache(false); + } + Services.obs.addObserver(observer, "cacheservice:empty-cache"); + let cs = Services.cache2; + cs.clear(); +} + +function cleanup() { + for (var i = 0; i < requests.length; ++i) { + requests[i].cancelAndForgetObserver(0); + } +} + +function run_test() { + registerCleanupFunction(cleanup); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + // We create a public channel that loads an image, then an identical + // one that should cause a cache read. We then create a private channel + // and load the same image, and do that a second time to ensure a cache + // read. In total, we should cause two separate http responses to occur, + // since the private channels shouldn't be able to use the public cache. + setup_chan("/image.png", false, function () { + setup_chan("/image.png", false, function () { + setup_chan("/image.png", true, function () { + setup_chan("/image.png", true, function () { + Assert.equal(gHits, 2); + run_loadImage_tests(); + }); + }); + }); + }); +} diff --git a/image/test/unit/xpcshell.toml b/image/test/unit/xpcshell.toml new file mode 100644 index 0000000000..5e1af40fdc --- /dev/null +++ b/image/test/unit/xpcshell.toml @@ -0,0 +1,50 @@ +[DEFAULT] +head = "" +support-files = [ + "async_load_tests.js", + "bug413512.ico", + "bug815359.ico", + "image1.png", + "image1.webp", + "image1quality50.webp", + "image1png16x16.jpg", + "image1png64x64.jpg", + "image2.jpg", + "image2jpg16x16-win.png", + "image2jpg16x16.png", + "image2jpg16x16cropped.jpg", + "image2jpg16x16cropped2.jpg", + "image2jpg16x32cropped3.jpg", + "image2jpg16x32scaled.jpg", + "image2jpg32x16cropped4.jpg", + "image2jpg32x16scaled.jpg", + "image2jpg32x32-win.png", + "image2jpg32x32.jpg", + "image2jpg32x32.png", + "image3.ico", + "image3ico16x16.png", + "image3ico32x32.png", + "image4.gif", + "image4gif16x16bmp24bpp.ico", + "image4gif16x16bmp32bpp.ico", + "image4gif32x32bmp24bpp.ico", + "image4gif32x32bmp32bpp.ico", + "image_load_helpers.js", +] + + +["test_async_notification.js"] + +["test_async_notification_404.js"] + +["test_async_notification_animated.js"] + +["test_encoder_apng.js"] + +["test_encoder_png.js"] + +["test_imgtools.js"] + +["test_moz_icon_uri.js"] + +["test_private_channel.js"] -- cgit v1.2.3