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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Formula from W3C's WCAG 2.0 spec's relative luminance, section 1.4.1,
*
* @return {number} Relative luminance, represented as number between 0 and 1.
*/
// Copied from Color.sys.mjs
function relativeLuminance(r, g, b) {
let colorArr = [r, g, b].map(color => {
if (color <= 10) {
return color / 255 / 12.92;
}
return Math.pow((color / 255 + 0.055) / 1.055, 2.4);
});
return colorArr[0] * 0.2126 + colorArr[1] * 0.7152 + colorArr[2] * 0.0722;
}
/**
* @function calculateTheme
* @param {Window} win - Window to use for constructors
* @param {Blob} 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".
*/
export async function calculateTheme(win, blob) {
let totalLuminance = 0;
let count = 0;
// Create an offscreen image bitmap
const bitmap = await win.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 win.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 = relativeLuminance(red, green, blue);
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";
}