WallpaperTheme.worker.mjs (2963B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* eslint-env webworker */ 6 // eslint-disable-next-line mozilla/reject-import-system-module-from-non-system 7 import { Color } from "resource://gre/modules/Color.sys.mjs"; 8 import { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs"; 9 10 /** 11 * Worker for calculating the average relative luminance of an image 12 * off the main thread. 13 */ 14 export class WallpaperThemeWorker { 15 /** 16 * @function calculateTheme 17 * @param {{ blob: Blob }} options - Options object containing the image blob. 18 * @param {Blob} options.blob - The image file blob to analyze. 19 * @returns {Promise<"dark"|"light">} A promise that resolves to "dark" if the 20 * average luminance is below the contrast threshold, otherwise "light". 21 */ 22 async calculateTheme(blob) { 23 let totalLuminance = 0; 24 let count = 0; 25 // Create an offscreen image bitmap 26 const bitmap = await globalThis.createImageBitmap(blob); 27 const scale = Math.min(1, 256 / Math.max(bitmap.width, bitmap.height)); 28 const width = Math.round(bitmap.width * scale); 29 const height = Math.round(bitmap.height * scale); 30 31 // Draw to an off-screen canvas 32 const canvas = new OffscreenCanvas(width, height); 33 const ctx = canvas.getContext("2d"); 34 ctx.drawImage(bitmap, 0, 0, width, height); 35 36 // get pixel data 37 const { data } = ctx.getImageData(0, 0, width, height); 38 39 // The +=1 in these loops means that it will look at every pixel 40 for (let row = 0; row < height; row += 1) { 41 for (let column = 0; column < width; column += 1) { 42 const index = (row * width + column) * 4; 43 const alpha = data[index + 3]; 44 // Skip transparent pixels 45 if (alpha > 0) { 46 const red = data[index]; 47 const green = data[index + 1]; 48 const blue = data[index + 2]; 49 const luminance = new Color(red, green, blue).relativeLuminance; 50 totalLuminance += luminance; 51 count++; 52 } 53 } 54 } 55 const averageLuminance = totalLuminance / count; 56 57 // Threshold taken from Color.sys.mjs module 58 const CONTRAST_BRIGHTTEXT_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05; 59 return averageLuminance <= CONTRAST_BRIGHTTEXT_THRESHOLD ? "dark" : "light"; 60 } 61 } 62 63 // Promise worker boiler plate 64 const wallpaperWorker = new WallpaperThemeWorker(); 65 const worker = new PromiseWorker.AbstractWorker(); 66 worker.dispatch = function (method, args = []) { 67 return wallpaperWorker[method](...args); 68 }; 69 worker.postMessage = function (message, ...transfers) { 70 self.postMessage(message, ...transfers); 71 }; 72 worker.close = function () { 73 self.close(); 74 }; 75 self.addEventListener("message", msg => worker.handleMessage(msg)); 76 self.addEventListener("unhandledrejection", function (error) { 77 throw error.reason; 78 });