commit 6c9c4294f1c5e8c1638e315c2dc92913615f6c3f
parent 59202fb317fdbb463d68416fc7a822c0e42995e3
Author: Tom Schuster <tschuster@mozilla.com>
Date: Fri, 12 Dec 2025 07:56:27 +0000
Bug 1999468 - Display SVG favicons using the moz-remote-image: protocol (disabled by default). r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D273423
Diffstat:
8 files changed, 114 insertions(+), 5 deletions(-)
diff --git a/browser/actors/LinkHandlerParent.sys.mjs b/browser/actors/LinkHandlerParent.sys.mjs
@@ -3,8 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
- TYPE_SVG,
TYPE_ICO,
+ SVG_DATA_URI_PREFIX,
TRUSTED_FAVICON_SCHEMES,
blobAsDataURL,
} from "moz-src:///browser/modules/FaviconUtils.sys.mjs";
@@ -224,7 +224,7 @@ export class LinkHandlerParent extends JSWindowActorParent {
if (
!images &&
!TRUSTED_FAVICON_SCHEMES.includes(iconURI.scheme) &&
- !iconURL.startsWith(`data:${TYPE_SVG};base64,`)
+ !iconURL.startsWith(SVG_DATA_URI_PREFIX)
) {
console.error(
`Not allowed to set favicon "${iconURL}" with that scheme!`
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -2667,6 +2667,9 @@ pref("browser.tabs.fadeOutUnloadedTabs", false);
// Whether tabs can be "split" or displayed side by side at once.
pref("browser.tabs.splitView.enabled", false);
+// Whether SVG favicons should be safely re-encoded using the moz-remote-image:// protocol.
+pref("browser.tabs.remoteSVGIconDecoding", false);
+
// If true, unprivileged extensions may use experimental APIs on
// nightly and developer edition.
pref("extensions.experiments.enabled", false);
diff --git a/browser/base/content/test/favicons/browser.toml b/browser/base/content/test/favicons/browser.toml
@@ -91,6 +91,12 @@ support-files = [
"file_favicon.png^headers^",
]
+["browser_favicon_svg.js"]
+support-files = [
+ "file_favicon_svg.html",
+ "file_favicon.svg"
+]
+
["browser_icon_discovery.js"]
["browser_invalid_href_fallback.js"]
diff --git a/browser/base/content/test/favicons/browser_favicon_svg.js b/browser/base/content/test/favicons/browser_favicon_svg.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ImageTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ImageTestUtils.sys.mjs"
+);
+
+const TEST_ROOT =
+ "http://mochi.test:8888/browser/browser/base/content/test/favicons/";
+
+const PAGE_URL = TEST_ROOT + "file_favicon_svg.html";
+const SVG_URL = TEST_ROOT + "file_favicon.svg";
+const SVG_DATA_URL = `data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iZ3JlZW4iIC8+Cjwvc3ZnPgo=`;
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.remoteSVGIconDecoding", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL, waitForLoad: false },
+ async browser => {
+ await waitForFavicon(browser, SVG_URL);
+ is(browser.mIconURL, SVG_DATA_URL, "Got the SVG data URL");
+
+ let tabIconImg = gBrowser
+ .getTabForBrowser(browser)
+ .querySelector(".tab-icon-image");
+
+ let expectedParams = new URLSearchParams({
+ url: SVG_DATA_URL,
+ width: 16,
+ height: 16,
+ });
+
+ is(
+ tabIconImg.src,
+ "moz-remote-image://?" + expectedParams,
+ "Image was loaded with the moz-remote-image: protocol"
+ );
+
+ if (!tabIconImg.complete) {
+ info("Awaiting tab-icon-image load");
+ await new Promise(resolve =>
+ tabIconImg.addEventListener("load", resolve, { once: true })
+ );
+ }
+
+ let screenshotDataURL = TestUtils.screenshotArea(tabIconImg, window);
+ await ImageTestUtils.assertEqualImage(
+ window,
+ screenshotDataURL,
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHElEQVQ4T2NkaGD4z0ABYBw1YNSAUQPAYBgYAACDTRgBSE6IpwAAAABJRU5ErkJggg==",
+ "Got green favicon"
+ );
+ }
+ );
+});
diff --git a/browser/base/content/test/favicons/file_favicon.svg b/browser/base/content/test/favicons/file_favicon.svg
@@ -0,0 +1,3 @@
+<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <rect width="100" height="100" fill="green" />
+</svg>
diff --git a/browser/base/content/test/favicons/file_favicon_svg.html b/browser/base/content/test/favicons/file_favicon_svg.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <link rel="icon" href="file_favicon.svg" type="image/svg+xml">
+</head>
+
+<body>
+</body>
+
+</html>
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -119,6 +119,7 @@
TaskbarTabs: "resource:///modules/taskbartabs/TaskbarTabs.sys.mjs",
UrlbarProviderOpenTabs:
"moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
+ SVG_DATA_URI_PREFIX: "moz-src:///browser/modules/FaviconUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(this, "tabLocalization", () => {
return new Localization(
@@ -184,6 +185,12 @@
"security.notification_enable_delay",
500
);
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_remoteSVGIconDecoding",
+ "browser.tabs.remoteSVGIconDecoding",
+ false
+ );
if (AppConstants.MOZ_CRASHREPORTER) {
ChromeUtils.defineESModuleGetters(this, {
@@ -1130,7 +1137,21 @@
aTab.removeAttribute("image");
}
if (aIconURL) {
- aTab.setAttribute("image", aIconURL);
+ let url = aIconURL;
+ if (
+ this._remoteSVGIconDecoding &&
+ url.startsWith(this.SVG_DATA_URI_PREFIX)
+ ) {
+ // 16px is hardcoded for .tab-icon-image in tabs.css
+ let size = Math.floor(16 * window.devicePixelRatio);
+ let params = new URLSearchParams({
+ url,
+ width: size,
+ height: size,
+ });
+ url = "moz-remote-image://?" + params;
+ }
+ aTab.setAttribute("image", url);
} else {
aTab.removeAttribute("image");
}
diff --git a/browser/modules/FaviconUtils.sys.mjs b/browser/modules/FaviconUtils.sys.mjs
@@ -2,8 +2,14 @@
* 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/. */
-export const TYPE_ICO = "image/x-icon";
-export const TYPE_SVG = "image/svg+xml";
+const TYPE_ICO = "image/x-icon";
+const TYPE_SVG = "image/svg+xml";
+
+export { TYPE_ICO, TYPE_SVG };
+
+// SVG images are send as raw data URLs from the content process to the parent.
+// The raw data: URL should NOT be used directly when displaying, but instead wrapped with a moz-remote-image: for safe re-encoding!
+export const SVG_DATA_URI_PREFIX = `data:${TYPE_SVG};base64,`;
// URL schemes that we don't want to load and convert to data URLs.
export const TRUSTED_FAVICON_SCHEMES = Object.freeze([