Menu

Les Orchard is typing…

Trying WordPress again

Generating a Firefox static theme from a web page

Folks would like to export their themes from Firefox Color as standalone theme add-ons. These can be submitted to AMO and allow for further advanced tinkering not supported by the theme editor in Color.

Up until today, I was thinking that we’d need to do this with server-side code like an AWS Lambda function. But, with more due diligence, I realized all the pieces exist to make this happen completely in the browser.


Themes in Firefox have gone through many iterations and changes. But, for present purposes, consider the recent addition of static themes. A static theme is a .zip archive, containing a manifest.json with the properties of the theme, accompanied by whatever images are used.

With that in mind, producing a static theme from the Color web app looks roughly like this:

// store is Firefox Color's Redux store
const state = store.getState();

// selectors.theme() extracts the current theme from the store
const themeData = selectors.theme(state);

// convertToBrowserTheme translates our Redux data into a Firefox theme
const theme = convertToBrowserTheme(themeData);

// Find JSZip at http://stuk.github.io/jszip/
const zip = new JSZip();

// This is a very minimal manifest for a theme
const manifest = {
  manifest_version: 2,
  version: "1.0",
  name: "My New Theme",
  theme
};

// Add a pretty-printed JSON string as manifest.json to the zip
zip.file("manifest.json", JSON.stringify(manifest, null, "  "));

// Generate the .zip as a base64 and reformat it as a data: URI
return zip
  .generateAsync({ type: "base64" })
  .then(data => "data:application/x-xpinstall;base64," + data);

Firefox Color uses its own internal representation of a theme to make things easier to manage in the React / Redux code that drives our UI. But, when the theme is applied to the browser by the Color add-on, this representation is converted to the Theme schema expected by dynamic theme API in Firefox.

Conveniently, this conversion code can be extracted & generalized to produce the theme property of manifest.json in a static theme. That’s what convertToBrowserTheme() does.

From there, Stuart Knightley’s JSZip module forms the keystone of the whole thing. To create a manifest.json file in an archive, I just have to generate the contents as a string with JSON.stringify() and feed that into zip.file().

Then, I can generate the .zip archive and encode it as a Base64 string. Again, this all works in a browser! With a small tweak, I can use that as a data: URL in a download link:

<a download="theme.xpi" href={exportedTheme}>theme.xpi</a>

Thanks to the HTML5 download attribute, clicking on this link in Firefox results in an Open / Save dialog to download the data:


Oh, but I mentioned images. That complicates things. In Firefox Color, there are two ways to get an image into your theme:

  1. Use one of the predefined background patterns – these are stored internally as file paths to images bundled with the Color add-on
  2. Import up to three images of your own – these are encoded as data: URLs from the imported image data

For producing a static theme, I need to convert each of these cases into both an image file in the archive and a file path in manifest.json. That leads me to two variations for handling image data:

function addImage(zip, data) {
  if (data.startsWith("data:")) {
    // Convert data: URL into binary file entry in zip
    const [meta, b64data] = data.split(",", 2);
    const [type] = meta.substr(5).split(/;/, 1);
    const filename = `images/${genId()}${extensions[type]}`;
    zip.file(filename, base64ToUint8array(b64data));
    return filename;
  }

  if (data.startsWith("images/")) {
    // Convert file path into pending image fetch.
    const filename = data;
    pendingImages.push(
      fetch(filename)
        .then(response => response.blob())
        .then(data => zip.file(filename, data))
    );
    return filename;
  }
}

For user-imported images, I have the data immediately available as data: URLs in the Redux store. After some hacky parsing of a data: URL, I can invent a unique filename and convert Base64 to binary before adding to the .zip archive. The genId() function helps with the former, and the base64ToUint8array() function helps with the latter. (I’m leaving out some details like these functions, but you can find them on GitHub.)

The built-in images, though, are only represented as file paths in Redux. Luckily, these paths correspond directly to images deployed with the web site. So, I can use fetch() to grab these via HTTP and add each to the archive as binary blobs. This is an asynchronous operation – so I assemble a pendingImages array collecting Promises that need resolution before we’re done.

Putting it all together slightly complicates my earlier code sample:

const zip = new JSZip();
const state = store.getState();
const themeData = selectors.theme(state);
const customBackgrounds = selectors.themeCustomImages(state);

// bgImages and customBackgrounds are sources of image data
const theme =
  convertToBrowserTheme(themeData, bgImages, customBackgrounds);

reset();

if (theme.images) {
  const { images } = theme;
  const { additional_backgrounds } = images;
  if (images.headerURL) {
    images.headerURL = addImage(zip, images.headerURL);
  }
  if (additional_backgrounds) {
    for (let idx = 0; idx < additional_backgrounds.length; idx++) {
      additional_backgrounds[idx] = addImage(
        zip,
        additional_backgrounds[idx]
      );
    }
  }
}

const manifest = {
  manifest_version: 2,
  version: "1.0",
  name,
  theme
};
zip.file("manifest.json", JSON.stringify(manifest, null, "  "));

return Promise.all(pendingImages)
  .then(() => zip.generateAsync({ type: "base64" }))
  .then(data => "data:application/x-xpinstall;base64," + data);
}

So here, I add the code necessary to walk through any images in the theme and convert them to paths in manifest.json and accompanying image files in the archive. And then, at the end of the function, I use Promise.all() to resolve downloading images as necessary before generating the data: URL for the .zip archive.


By the way, to keep this entry short(er), I’m skipping the details on how all the above is used within our React & Redux web app.

TL;DR: It’s a pretty nondescript async React & Redux flow.

The export code lives in a function named performThemeExport(), used as input to an action creator. A button click kicks off the process by dispatching the action. Since the action’s payload begins as an unresolved promise, the redux-promise middleware handles resolving the payload before updating the Redux store (and thus the UI) when the export is finished.


All-in-all, getting this stuff to work was a surprise. Finding JSZip and figuring out how to do the various Base64 / binary conversions were the pieces that really made it all fit together. While I’d seen those things before, they just hadn’t all slotted together in my head until just this afternoon.

Anyway, to wrap up: If you want to see all of this stuff in working form, you can follow along with the pull request I submitted to Firefox Color today. It’s still rough, UX-wise, and will see further tweaks before we consider it done.