commit e17c721cc09b4de01c015c1ad03752e200d274da
parent d66df91712a3aea05e21de55f331f33a8a348034
Author: Andrew Overholt <overholt@mozilla.com>
Date: Thu, 30 Oct 2025 20:47:08 +0000
Bug 1996315 - Add QR code generation worker machinery r=mconley
Differential Revision: https://phabricator.services.mozilla.com/D270008
Diffstat:
8 files changed, 482 insertions(+), 0 deletions(-)
diff --git a/browser/components/moz.build b/browser/components/moz.build
@@ -55,6 +55,7 @@ DIRS += [
"prompts",
"protections",
"protocolhandler",
+ "qrcode",
"reportbrokensite",
"resistfingerprinting",
"screenshots",
diff --git a/browser/components/qrcode/QRCodeGenerator.sys.mjs b/browser/components/qrcode/QRCodeGenerator.sys.mjs
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * QR Code Generator with Firefox logo overlay
+ * This module generates QR codes with the Firefox logo in the center
+ * Uses a worker thread for QR generation to avoid blocking the main thread
+ */
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "QRCodeGenerator",
+ maxLogLevel: Services.prefs.getBoolPref("browser.qrcode.log", false)
+ ? "Debug"
+ : "Warn",
+ });
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ QRCodeWorker: "resource:///modules/QRCodeWorker.sys.mjs",
+});
+
+export const QRCodeGenerator = {
+ /**
+ * Generate a QR code for the given URL with Firefox logo overlay
+ *
+ * @param {string} url - The URL to encode
+ * @param {Document} document - The document to use for creating elements
+ * @returns {Promise<string>} - Data URI of the QR code with logo
+ */
+ async generateQRCode(url, document) {
+ // Create a fresh worker for this generation
+ // Worker will be terminated after use to free resources
+ const worker = new lazy.QRCodeWorker();
+
+ try {
+ // Generate the base QR code with high error correction to allow for logo overlay
+ // Use worker thread to avoid blocking main thread
+ const qrData = await worker.generateQRCode(url, "H");
+
+ // Use a higher resolution for better quality (scale up 4x)
+ const scale = 4;
+ const canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = qrData.width * scale;
+ canvas.height = qrData.height * scale;
+ const ctx = canvas.getContext("2d");
+
+ // Disable image smoothing for crisp QR code rendering
+ ctx.imageSmoothingEnabled = false;
+
+ // Load and draw the base QR code at high resolution
+ const qrImage = await this._loadImage(document, qrData.src);
+ ctx.drawImage(qrImage, 0, 0, qrData.width * scale, qrData.height * scale);
+
+ // Calculate logo size and position (center of QR code)
+ // Use 18% of QR code size (reduced from 25%) to stay within error correction limits
+ const logoSize = Math.floor(qrData.width * 0.18) * scale;
+ const centerX = Math.floor((qrData.width * scale) / 2);
+ const centerY = Math.floor((qrData.height * scale) / 2);
+
+ // Draw circular white background for logo with minimal padding
+ const padding = 4 * scale;
+ const radius = (logoSize + padding * 2) / 2;
+ ctx.fillStyle = "white";
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
+ ctx.fill();
+
+ // Load and draw the Firefox logo at high resolution
+ try {
+ const logoImage = await this._loadFirefoxLogo(document);
+ // Re-enable smoothing for the logo to avoid pixelation
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = "high";
+
+ // Draw logo centered
+ const logoX = centerX - logoSize / 2;
+ const logoY = centerY - logoSize / 2;
+ ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
+ } catch (e) {
+ lazy.logConsole.warn("Could not load Firefox logo for QR code:", e);
+ }
+
+ // Convert canvas to data URI
+ return canvas.toDataURL("image/png");
+ } finally {
+ // Always terminate the worker to free resources
+ try {
+ await worker.terminate();
+ lazy.logConsole.debug("QRCode worker terminated successfully");
+ } catch (e) {
+ lazy.logConsole.warn("Failed to terminate QRCode worker:", e);
+ }
+ }
+ },
+
+ /**
+ * Load an image from a URL/data URI
+ *
+ * @param {Document} document - The document to use for creating the image
+ * @param {string} src - The image source
+ * @returns {Promise<HTMLImageElement>}
+ */
+ _loadImage(document, src) {
+ return new Promise((resolve, reject) => {
+ const img = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "img"
+ );
+ img.onload = () => resolve(img);
+ img.onerror = reject;
+ img.src = src;
+ });
+ },
+
+ /**
+ * Load the Firefox logo
+ *
+ * @param {Document} document - The document to use for creating the image
+ * @returns {Promise<HTMLImageElement>}
+ */
+ async _loadFirefoxLogo(document) {
+ // Use the Firefox branding logo
+ return this._loadImage(
+ document,
+ "chrome://branding/content/about-logo.svg"
+ );
+ },
+};
diff --git a/browser/components/qrcode/QRCodeWorker.sys.mjs b/browser/components/qrcode/QRCodeWorker.sys.mjs
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * QRCodeWorker - Worker-based QR code generation
+ *
+ * This module provides a worker-based implementation for QR code generation
+ * to avoid blocking the main thread during QR code processing.
+ */
+
+import { BasePromiseWorker } from "resource://gre/modules/PromiseWorker.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "QRCodeWorker",
+ maxLogLevel: Services.prefs.getBoolPref("browser.qrcode.log", false)
+ ? "Debug"
+ : "Warn",
+ });
+});
+
+/**
+ * Worker wrapper for QR code generation
+ */
+export class QRCodeWorker extends BasePromiseWorker {
+ constructor() {
+ super("chrome://browser/content/qrcode/QRCodeWorker.worker.mjs");
+
+ // Set up logging
+ this.log = (...args) => lazy.logConsole.debug(...args);
+ }
+
+ /**
+ * Simple ping test for worker communication
+ *
+ * @returns {Promise<string>} Returns "pong"
+ */
+ async ping() {
+ return this.post("ping", []);
+ }
+
+ /**
+ * Check if the QRCode library is available in the worker
+ *
+ * @returns {Promise<boolean>} True if library is available
+ */
+ async hasQRCodeLibrary() {
+ return this.post("hasQRCodeLibrary", []);
+ }
+
+ /**
+ * Generate a QR code for the given URL
+ *
+ * @param {string} url - The URL to encode in the QR code
+ * @param {string} errorCorrectionLevel - Error correction level (L, M, Q, H)
+ * @returns {Promise<object>} Object with width, height, and src data URI
+ */
+ async generateQRCode(url, errorCorrectionLevel = "H") {
+ return this.post("generateQRCode", [url, errorCorrectionLevel]);
+ }
+
+ /**
+ * Terminate the worker and clean up resources
+ */
+ async terminate() {
+ super.terminate();
+ }
+}
diff --git a/browser/components/qrcode/content/QRCodeWorker.worker.mjs b/browser/components/qrcode/content/QRCodeWorker.worker.mjs
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * QRCode Worker Implementation
+ *
+ * This worker handles QR code generation off the main thread.
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(
+ lazy,
+ {
+ PromiseWorker: "resource://gre/modules/workers/PromiseWorker.mjs",
+ },
+ { global: "current" }
+);
+
+// Import require for loading CommonJS modules in the worker
+importScripts("resource://gre/modules/workers/require.js");
+
+// Now load the QRCode library with the full resource URI
+// eslint-disable-next-line no-undef
+const QRCode = require("resource://devtools/shared/qrcode/index.js");
+
+/**
+ * The QR Code generator that runs in a worker thread
+ */
+class QRCodeWorkerImpl {
+ constructor() {
+ this.#connectToPromiseWorker();
+ }
+
+ /**
+ * Simple ping test for worker communication
+ *
+ * @returns {string} Returns "pong"
+ */
+ ping() {
+ return "pong";
+ }
+
+ /**
+ * Check if the QRCode library is available
+ *
+ * @returns {boolean} True if library is loaded
+ */
+ hasQRCodeLibrary() {
+ return typeof QRCode !== "undefined" && QRCode !== null;
+ }
+
+ /**
+ * Generate a QR code for the given URL
+ *
+ * @param {string} url - The URL to encode
+ * @param {string} errorCorrectionLevel - Error correction level (L, M, Q, H)
+ * @returns {object} Object with width, height, and src data URI
+ */
+ generateQRCode(url, errorCorrectionLevel = "H") {
+ if (!QRCode || !QRCode.encodeToDataURI) {
+ throw new Error("QRCode library not available in worker");
+ }
+
+ // Generate the QR code data URI
+ const qrData = QRCode.encodeToDataURI(url, errorCorrectionLevel);
+
+ return {
+ width: qrData.width,
+ height: qrData.height,
+ src: qrData.src,
+ };
+ }
+
+ /**
+ * Glue code to connect the `QRCodeWorkerImpl` to the PromiseWorker interface.
+ */
+ #connectToPromiseWorker() {
+ const worker = new lazy.PromiseWorker.AbstractWorker();
+
+ worker.dispatch = (method, args = []) => {
+ if (!this[method]) {
+ throw new Error("Method does not exist: " + method);
+ }
+ return this[method](...args);
+ };
+
+ worker.close = () => self.close();
+
+ worker.postMessage = (message, ...transfers) => {
+ self.postMessage(message, ...transfers);
+ };
+
+ self.addEventListener("message", msg => worker.handleMessage(msg));
+ self.addEventListener("unhandledrejection", function (error) {
+ throw error.reason;
+ });
+ }
+}
+
+// Create the worker instance
+new QRCodeWorkerImpl();
diff --git a/browser/components/qrcode/moz.build b/browser/components/qrcode/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ "QRCodeGenerator.sys.mjs",
+ "QRCodeWorker.sys.mjs",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
diff --git a/browser/components/qrcode/test/xpcshell/test_QRCodeGenerator_worker_integration.js b/browser/components/qrcode/test/xpcshell/test_QRCodeGenerator_worker_integration.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test QRCodeGenerator with worker integration
+ */
+
+const { QRCodeGenerator } = ChromeUtils.importESModule(
+ "resource:///modules/QRCodeGenerator.sys.mjs"
+);
+
+/**
+ * Helper function to create a minimal mock document with required methods
+ */
+function createMockDocument() {
+ return {
+ createElementNS: (ns, tagName) => {
+ if (tagName === "canvas") {
+ return {
+ width: 0,
+ height: 0,
+ getContext: () => ({
+ imageSmoothingEnabled: false,
+ imageSmoothingQuality: "high",
+ fillStyle: "white",
+ beginPath: () => {},
+ arc: () => {},
+ fill: () => {},
+ drawImage: () => {},
+ }),
+ toDataURL: () => "data:image/png;base64,mock",
+ };
+ } else if (tagName === "img") {
+ return {
+ onload: null,
+ onerror: null,
+ set src(value) {
+ // Simulate image load
+ Services.tm.dispatchToMainThread(() => {
+ if (this.onload) {
+ this.onload();
+ }
+ });
+ },
+ };
+ }
+ return {};
+ },
+ };
+}
+
+add_task(async function test_generator_uses_worker() {
+ info("Testing QRCodeGenerator generates QR code using worker");
+
+ const mockDocument = createMockDocument();
+
+ // Generate a QR code using the worker
+ const testUrl = "https://mozilla.org";
+ const dataUri = await QRCodeGenerator.generateQRCode(testUrl, mockDocument);
+
+ Assert.ok(dataUri, "Should get a data URI from generateQRCode");
+ Assert.ok(dataUri.startsWith("data:image/"), "Should be a data URI");
+
+ // Worker is automatically cleaned up after generation
+});
+
+add_task(async function test_generator_multiple_generations() {
+ info("Testing QRCodeGenerator can generate multiple QR codes");
+
+ // Generate first QR code
+ const dataUri1 = await QRCodeGenerator.generateQRCode(
+ "https://mozilla.org",
+ createMockDocument()
+ );
+ Assert.ok(dataUri1, "Should get first data URI");
+
+ // Generate second QR code
+ const dataUri2 = await QRCodeGenerator.generateQRCode(
+ "https://firefox.com",
+ createMockDocument()
+ );
+ Assert.ok(dataUri2, "Should get second data URI");
+
+ // Each call creates and cleans up its own worker
+});
diff --git a/browser/components/qrcode/test/xpcshell/test_QRCodeWorker.js b/browser/components/qrcode/test/xpcshell/test_QRCodeWorker.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test QRCodeWorker functionality
+ */
+
+const { QRCodeWorker } = ChromeUtils.importESModule(
+ "resource:///modules/QRCodeWorker.sys.mjs"
+);
+
+add_task(async function test_worker_instantiation() {
+ info("Testing QRCodeWorker can be instantiated");
+
+ const worker = new QRCodeWorker();
+ Assert.ok(worker, "QRCodeWorker instance should be created");
+
+ // Clean up
+ await worker.terminate();
+});
+
+add_task(async function test_worker_responds_to_ping() {
+ info("Testing QRCodeWorker responds to ping message");
+
+ const worker = new QRCodeWorker();
+
+ // Test ping functionality
+ const response = await worker.ping();
+ Assert.equal(response, "pong", "Worker should respond with 'pong' to ping");
+
+ // Clean up
+ await worker.terminate();
+});
+
+add_task(async function test_worker_can_load_qrcode_library() {
+ info("Testing QRCodeWorker can load QRCode library");
+
+ const worker = new QRCodeWorker();
+
+ // Test that the worker can check if the QRCode library is available
+ const hasLibrary = await worker.hasQRCodeLibrary();
+ Assert.ok(hasLibrary, "Worker should have access to QRCode library");
+
+ // Clean up
+ await worker.terminate();
+});
+
+add_task(async function test_worker_can_generate_simple_qrcode() {
+ info("Testing QRCodeWorker can generate a simple QR code");
+
+ const worker = new QRCodeWorker();
+
+ // Test generating a very simple QR code
+ const testUrl = "https://mozilla.org";
+ const result = await worker.generateQRCode(testUrl);
+
+ Assert.ok(result, "Should get a result from generateQRCode");
+ Assert.ok(result.width, "Result should have a width");
+ Assert.ok(result.height, "Result should have a height");
+ Assert.ok(result.src, "Result should have a src data URI");
+ Assert.ok(result.src.startsWith("data:image/"), "src should be a data URI");
+
+ // Clean up
+ await worker.terminate();
+});
diff --git a/browser/components/qrcode/test/xpcshell/xpcshell.toml b/browser/components/qrcode/test/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+firefox-appdir = "browser"
+
+["test_QRCodeGenerator_worker_integration.js"]
+
+["test_QRCodeWorker.js"]