michael@0: // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: 'use strict'; michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: const ColorAnalyzer = Components.classes["@mozilla.org/places/colorAnalyzer;1"] michael@0: .getService(Components.interfaces.mozIColorAnalyzer); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["ColorUtils"]; michael@0: michael@0: let ColorUtils = { michael@0: _initialized: false, michael@0: init: function() { michael@0: if (this._initialized) michael@0: return; michael@0: Services.obs.addObserver(this, "idle-daily", false); michael@0: Services.obs.addObserver(this, "quit-application", false); michael@0: this._initialized = true; michael@0: }, michael@0: uninit: function() { michael@0: Services.obs.removeObserver(this, "idle-daily"); michael@0: Services.obs.removeObserver(this, "quit-application"); michael@0: }, michael@0: michael@0: // default to keeping icon colorInfo for max 1 day michael@0: iconColorCacheMaxAge: 86400000, michael@0: michael@0: // in-memory store for favicon color data michael@0: _uriColorsMap: (function() { michael@0: let cache = new Map(); michael@0: // remove stale entries michael@0: cache.purge = function(maxAgeMs = 0) { michael@0: let cuttoff = Date.now() - (maxAgeMs || ColorUtils.iconColorCacheMaxAge); michael@0: for (let [key, value] of this) { michael@0: if (value.timestamp && value.timestamp >= cuttoff) { michael@0: continue; michael@0: } michael@0: this.delete(key); michael@0: } michael@0: } michael@0: return cache; michael@0: })(), michael@0: get iconColorCache() { michael@0: return ColorUtils._uriColorsMap; michael@0: }, michael@0: michael@0: observe: function (aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "idle-daily": michael@0: this.iconColorCache.purge(); michael@0: break; michael@0: case "quit-application": michael@0: this.uninit(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** Takes an icon and returns either an object with foreground and background color properties michael@0: * or a promise for the same. michael@0: * The foreground is the contrast color, the background is the primary/dominant one michael@0: */ michael@0: getForegroundAndBackgroundIconColors: function getForegroundAndBackgroundIconColors(aIconURI) { michael@0: let colorKey = aIconURI.spec; michael@0: let colorInfo = this._uriColorsMap.get(colorKey); michael@0: if (colorInfo) { michael@0: return colorInfo; michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: let wrappedIcon = aIconURI; michael@0: this._uriColorsMap.set(colorKey, deferred.promise); michael@0: michael@0: ColorAnalyzer.findRepresentativeColor(wrappedIcon, (success, color) => { michael@0: if (!success) { michael@0: this._uriColorsMap.delete(colorKey); michael@0: deferred.reject(); michael@0: } else { michael@0: colorInfo = { michael@0: foreground: this.bestTextColorForContrast(color), michael@0: background: this.convertDecimalToRgbColor(color), michael@0: timestamp: Date.now() michael@0: }; michael@0: deferred.resolve(colorInfo); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** returns the best color for text readability on top of aColor michael@0: * return color is in rgb(r,g,b) format, suitable to csss michael@0: * The color bightness algorithm is currently: http://www.w3.org/TR/AERT#color-contrast michael@0: */ michael@0: bestTextColorForContrast: function bestTextColorForContrast(aColor) { michael@0: let r = (aColor & 0xff0000) >> 16; michael@0: let g = (aColor & 0x00ff00) >> 8; michael@0: let b = (aColor & 0x0000ff); michael@0: michael@0: let w3cContrastValue = ((r*299)+(g*587)+(b*114))/1000; michael@0: w3cContrastValue = Math.round(w3cContrastValue); michael@0: let textColor = "rgb(255,255,255)"; michael@0: michael@0: if (w3cContrastValue > 125) { michael@0: // bright/light, use black text michael@0: textColor = "rgb(0,0,0)"; michael@0: } michael@0: return textColor; michael@0: }, michael@0: michael@0: toCSSRgbColor: function toCSSRgbColor(r, g, b, a) { michael@0: var values = [Math.round(r), Math.round(g), Math.round(b)]; michael@0: if (undefined !== a && a < 1) { michael@0: values.push(a); michael@0: return 'rgba('+values.join(',')+')'; michael@0: } michael@0: return 'rgb('+values.join(',')+')'; michael@0: }, michael@0: michael@0: /** michael@0: * converts a decimal(base10) number into CSS rgb color value string michael@0: */ michael@0: convertDecimalToRgbColor: function convertDecimalToRgbColor(aColor) { michael@0: let [r,g,b,a] = this.unpackDecimalColorWord(aColor); michael@0: return this.toCSSRgbColor(r,g,b,a); michael@0: }, michael@0: michael@0: /** michael@0: * unpack a decimal(base10) word for r,g,b,a values michael@0: */ michael@0: unpackDecimalColorWord: function unpackDecimalColorWord(aColor) { michael@0: let a = (aColor & 0xff000000) >> 24; michael@0: let r = (aColor & 0x00ff0000) >> 16; michael@0: let g = (aColor & 0x0000ff00) >> 8; michael@0: let b = (aColor & 0x000000ff); michael@0: // NB: falsy alpha treated as undefined, fully opaque michael@0: return a ? [r,g,b,a/255] : [r,g,b]; michael@0: }, michael@0: michael@0: /** michael@0: * create a decimal(base10) word for r,g,b values michael@0: */ michael@0: createDecimalColorWord: function createDecimalColorWord(r, g, b, a) { michael@0: let rgb = 0; michael@0: rgb |= b; michael@0: rgb |= (g << 8); michael@0: rgb |= (r << 16); michael@0: // pack alpha value if one is given michael@0: if (undefined !== a && a < 1) michael@0: rgb |= (Math.round(a*255) << 24); michael@0: return rgb; michael@0: }, michael@0: michael@0: /** michael@0: * Add 2 rgb(a) colors to get a flat color michael@0: */ michael@0: addRgbColors: function addRgbColors(color1, color2) { michael@0: let [r1, g1, b1] = this.unpackDecimalColorWord(color1); michael@0: let [r2, g2, b2, alpha] = this.unpackDecimalColorWord(color2); michael@0: michael@0: let color = {}; michael@0: // early return if 2nd color is opaque michael@0: if (!alpha || alpha >= 1) michael@0: return color2; michael@0: michael@0: return this.createDecimalColorWord( michael@0: Math.min(255, alpha * r2 + (1 - alpha) * r1), michael@0: Math.min(255, alpha * g2 + (1 - alpha) * g1), michael@0: Math.min(255, alpha * b2 + (1 - alpha) * b1) michael@0: ); michael@0: } michael@0: };