summaryrefslogtreecommitdiffstats
path: root/image/test/unit
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--image/test/unit/async_load_tests.js302
-rw-r--r--image/test/unit/bug413512.icobin0 -> 17759 bytes
-rw-r--r--image/test/unit/bug815359.icobin0 -> 4286 bytes
-rw-r--r--image/test/unit/image1.pngbin0 -> 8415 bytes
-rw-r--r--image/test/unit/image1.webpbin0 -> 3206 bytes
-rw-r--r--image/test/unit/image1png16x16.jpgbin0 -> 1050 bytes
-rw-r--r--image/test/unit/image1png64x64.jpgbin0 -> 4507 bytes
-rw-r--r--image/test/unit/image1quality50.webpbin0 -> 1944 bytes
-rw-r--r--image/test/unit/image2.jpgbin0 -> 3494 bytes
-rw-r--r--image/test/unit/image2jpg16x16-win.pngbin0 -> 948 bytes
-rw-r--r--image/test/unit/image2jpg16x16.pngbin0 -> 955 bytes
-rw-r--r--image/test/unit/image2jpg16x16cropped.jpgbin0 -> 879 bytes
-rw-r--r--image/test/unit/image2jpg16x16cropped2.jpgbin0 -> 878 bytes
-rw-r--r--image/test/unit/image2jpg16x32cropped3.jpgbin0 -> 1127 bytes
-rw-r--r--image/test/unit/image2jpg16x32scaled.jpgbin0 -> 1219 bytes
-rw-r--r--image/test/unit/image2jpg32x16cropped4.jpgbin0 -> 1135 bytes
-rw-r--r--image/test/unit/image2jpg32x16scaled.jpgbin0 -> 1227 bytes
-rw-r--r--image/test/unit/image2jpg32x32-win.pngbin0 -> 3104 bytes
-rw-r--r--image/test/unit/image2jpg32x32.jpgbin0 -> 1634 bytes
-rw-r--r--image/test/unit/image2jpg32x32.pngbin0 -> 3026 bytes
-rw-r--r--image/test/unit/image3.icobin0 -> 1406 bytes
-rw-r--r--image/test/unit/image3ico16x16.pngbin0 -> 520 bytes
-rw-r--r--image/test/unit/image3ico32x32.pngbin0 -> 2280 bytes
-rw-r--r--image/test/unit/image4.gifbin0 -> 1809 bytes
-rw-r--r--image/test/unit/image4gif16x16bmp24bpp.icobin0 -> 894 bytes
-rw-r--r--image/test/unit/image4gif16x16bmp32bpp.icobin0 -> 1150 bytes
-rw-r--r--image/test/unit/image4gif32x32bmp24bpp.icobin0 -> 3262 bytes
-rw-r--r--image/test/unit/image4gif32x32bmp32bpp.icobin0 -> 4286 bytes
-rw-r--r--image/test/unit/image_load_helpers.js124
-rw-r--r--image/test/unit/test_async_notification.js15
-rw-r--r--image/test/unit/test_async_notification_404.js19
-rw-r--r--image/test/unit/test_async_notification_animated.js19
-rw-r--r--image/test/unit/test_encoder_apng.js582
-rw-r--r--image/test/unit/test_encoder_png.js263
-rw-r--r--image/test/unit/test_imgtools.js871
-rw-r--r--image/test/unit/test_moz_icon_uri.js157
-rw-r--r--image/test/unit/test_private_channel.js170
-rw-r--r--image/test/unit/xpcshell.toml50
38 files changed, 2572 insertions, 0 deletions
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
--- /dev/null
+++ b/image/test/unit/bug413512.ico
Binary files differ
diff --git a/image/test/unit/bug815359.ico b/image/test/unit/bug815359.ico
new file mode 100644
index 0000000000..a24b8fb6bb
--- /dev/null
+++ b/image/test/unit/bug815359.ico
Binary files differ
diff --git a/image/test/unit/image1.png b/image/test/unit/image1.png
new file mode 100644
index 0000000000..2fb37aeec4
--- /dev/null
+++ b/image/test/unit/image1.png
Binary files differ
diff --git a/image/test/unit/image1.webp b/image/test/unit/image1.webp
new file mode 100644
index 0000000000..b2a6f92aaa
--- /dev/null
+++ b/image/test/unit/image1.webp
Binary files differ
diff --git a/image/test/unit/image1png16x16.jpg b/image/test/unit/image1png16x16.jpg
new file mode 100644
index 0000000000..488b563c90
--- /dev/null
+++ b/image/test/unit/image1png16x16.jpg
Binary files differ
diff --git a/image/test/unit/image1png64x64.jpg b/image/test/unit/image1png64x64.jpg
new file mode 100644
index 0000000000..679dad2b95
--- /dev/null
+++ b/image/test/unit/image1png64x64.jpg
Binary files differ
diff --git a/image/test/unit/image1quality50.webp b/image/test/unit/image1quality50.webp
new file mode 100644
index 0000000000..f73d615657
--- /dev/null
+++ b/image/test/unit/image1quality50.webp
Binary files differ
diff --git a/image/test/unit/image2.jpg b/image/test/unit/image2.jpg
new file mode 100644
index 0000000000..b2131bf0c1
--- /dev/null
+++ b/image/test/unit/image2.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg16x16-win.png b/image/test/unit/image2jpg16x16-win.png
new file mode 100644
index 0000000000..a821626c07
--- /dev/null
+++ b/image/test/unit/image2jpg16x16-win.png
Binary files differ
diff --git a/image/test/unit/image2jpg16x16.png b/image/test/unit/image2jpg16x16.png
new file mode 100644
index 0000000000..b5b9a720a8
--- /dev/null
+++ b/image/test/unit/image2jpg16x16.png
Binary files differ
diff --git a/image/test/unit/image2jpg16x16cropped.jpg b/image/test/unit/image2jpg16x16cropped.jpg
new file mode 100644
index 0000000000..fca22cb30a
--- /dev/null
+++ b/image/test/unit/image2jpg16x16cropped.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg16x16cropped2.jpg b/image/test/unit/image2jpg16x16cropped2.jpg
new file mode 100644
index 0000000000..e51d3530d3
--- /dev/null
+++ b/image/test/unit/image2jpg16x16cropped2.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg16x32cropped3.jpg b/image/test/unit/image2jpg16x32cropped3.jpg
new file mode 100644
index 0000000000..13a3d26e54
--- /dev/null
+++ b/image/test/unit/image2jpg16x32cropped3.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg16x32scaled.jpg b/image/test/unit/image2jpg16x32scaled.jpg
new file mode 100644
index 0000000000..6abef0f99b
--- /dev/null
+++ b/image/test/unit/image2jpg16x32scaled.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg32x16cropped4.jpg b/image/test/unit/image2jpg32x16cropped4.jpg
new file mode 100644
index 0000000000..46f34918c8
--- /dev/null
+++ b/image/test/unit/image2jpg32x16cropped4.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg32x16scaled.jpg b/image/test/unit/image2jpg32x16scaled.jpg
new file mode 100644
index 0000000000..e302fbafd0
--- /dev/null
+++ b/image/test/unit/image2jpg32x16scaled.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg32x32-win.png b/image/test/unit/image2jpg32x32-win.png
new file mode 100644
index 0000000000..4d84df26a0
--- /dev/null
+++ b/image/test/unit/image2jpg32x32-win.png
Binary files differ
diff --git a/image/test/unit/image2jpg32x32.jpg b/image/test/unit/image2jpg32x32.jpg
new file mode 100644
index 0000000000..cf9a10a37f
--- /dev/null
+++ b/image/test/unit/image2jpg32x32.jpg
Binary files differ
diff --git a/image/test/unit/image2jpg32x32.png b/image/test/unit/image2jpg32x32.png
new file mode 100644
index 0000000000..42640cbb53
--- /dev/null
+++ b/image/test/unit/image2jpg32x32.png
Binary files differ
diff --git a/image/test/unit/image3.ico b/image/test/unit/image3.ico
new file mode 100644
index 0000000000..d44438903b
--- /dev/null
+++ b/image/test/unit/image3.ico
Binary files differ
diff --git a/image/test/unit/image3ico16x16.png b/image/test/unit/image3ico16x16.png
new file mode 100644
index 0000000000..fa61cc5046
--- /dev/null
+++ b/image/test/unit/image3ico16x16.png
Binary files differ
diff --git a/image/test/unit/image3ico32x32.png b/image/test/unit/image3ico32x32.png
new file mode 100644
index 0000000000..58a72e5c9d
--- /dev/null
+++ b/image/test/unit/image3ico32x32.png
Binary files differ
diff --git a/image/test/unit/image4.gif b/image/test/unit/image4.gif
new file mode 100644
index 0000000000..b1530bc81e
--- /dev/null
+++ b/image/test/unit/image4.gif
Binary files differ
diff --git a/image/test/unit/image4gif16x16bmp24bpp.ico b/image/test/unit/image4gif16x16bmp24bpp.ico
new file mode 100644
index 0000000000..890c81c272
--- /dev/null
+++ b/image/test/unit/image4gif16x16bmp24bpp.ico
Binary files differ
diff --git a/image/test/unit/image4gif16x16bmp32bpp.ico b/image/test/unit/image4gif16x16bmp32bpp.ico
new file mode 100644
index 0000000000..f8a9eb8adc
--- /dev/null
+++ b/image/test/unit/image4gif16x16bmp32bpp.ico
Binary files differ
diff --git a/image/test/unit/image4gif32x32bmp24bpp.ico b/image/test/unit/image4gif32x32bmp24bpp.ico
new file mode 100644
index 0000000000..28092818dc
--- /dev/null
+++ b/image/test/unit/image4gif32x32bmp24bpp.ico
Binary files differ
diff --git a/image/test/unit/image4gif32x32bmp32bpp.ico b/image/test/unit/image4gif32x32bmp32bpp.ico
new file mode 100644
index 0000000000..0e2d28c82a
--- /dev/null
+++ b/image/test/unit/image4gif32x32bmp32bpp.ico
Binary files 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"]