Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* eslint-env webworker */
// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system
import { Color } from "resource://gre/modules/Color.sys.mjs";
import { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs";
/**
* Worker for calculating the average relative luminance of an image
* off the main thread.
*/
export class WallpaperThemeWorker {
/**
* @function calculateTheme
* @param {{ blob: Blob }} options - Options object containing the image blob.
* @param {Blob} options.blob - The image file blob to analyze.
* @returns {Promise<"dark"|"light">} A promise that resolves to "dark" if the
* average luminance is below the contrast threshold, otherwise "light".
*/
async calculateTheme(blob) {
let totalLuminance = 0;
let count = 0;
// Create an offscreen image bitmap
const bitmap = await globalThis.createImageBitmap(blob);
const scale = Math.min(1, 256 / Math.max(bitmap.width, bitmap.height));
const width = Math.round(bitmap.width * scale);
const height = Math.round(bitmap.height * scale);
// Draw to an off-screen canvas
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0, width, height);
// get pixel data
const { data } = ctx.getImageData(0, 0, width, height);
// The +=1 in these loops means that it will look at every pixel
for (let row = 0; row < height; row += 1) {
for (let column = 0; column < width; column += 1) {
const index = (row * width + column) * 4;
const alpha = data[index + 3];
// Skip transparent pixels
if (alpha > 0) {
const red = data[index];
const green = data[index + 1];
const blue = data[index + 2];
const luminance = new Color(red, green, blue).relativeLuminance;
totalLuminance += luminance;
count++;
}
}
}
const averageLuminance = totalLuminance / count;
// Threshold taken from Color.sys.mjs module
const CONTRAST_BRIGHTTEXT_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
return averageLuminance <= CONTRAST_BRIGHTTEXT_THRESHOLD ? "dark" : "light";
}
}
// Promise worker boiler plate
const wallpaperWorker = new WallpaperThemeWorker();
const worker = new PromiseWorker.AbstractWorker();
worker.dispatch = function (method, args = []) {
return wallpaperWorker[method](...args);
};
worker.postMessage = function (message, ...transfers) {
self.postMessage(message, ...transfers);
};
worker.close = function () {
self.close();
};
self.addEventListener("message", msg => worker.handleMessage(msg));
self.addEventListener("unhandledrejection", function (error) {
throw error.reason;
});