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
/**
* 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";
}
// The picker draws a tile at 86 pixels, so this stays sharp on a screen that
// packs in more pixels. Small enough that a whole library is a megabyte or two.
const THUMBNAIL_MAX_SIDE = 320;
const THUMBNAIL_TYPE = "image/jpeg";
const THUMBNAIL_QUALITY = 0.8;
/**
* Scales an image down for the wallpaper picker. Thumbnail scaling runs in
* content; the parent stores the result it receives.
*
* @param {Window} win - Window to use for constructors
* @param {Blob} blob - The full size image.
* @returns {Promise<Blob>} The scaled copy, as a JPEG.
*/
export const createThumbnail = async (win, blob) => {
const bitmap = await win.createImageBitmap(blob);
try {
const scale = Math.min(
1,
THUMBNAIL_MAX_SIDE / Math.max(bitmap.width, bitmap.height)
);
const width = Math.max(1, Math.round(bitmap.width * scale));
const height = Math.max(1, Math.round(bitmap.height * scale));
const canvas = new win.OffscreenCanvas(width, height);
canvas.getContext("2d").drawImage(bitmap, 0, 0, width, height);
return await canvas.convertToBlob({
type: THUMBNAIL_TYPE,
quality: THUMBNAIL_QUALITY,
});
} finally {
// Frees the decoded image rather than waiting for collection, which
// matters when someone adds a very large photo.
bitmap.close();
}
};