Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 'use strict';
6 Components.utils.import("resource://gre/modules/Services.jsm");
7 Components.utils.import("resource://gre/modules/Promise.jsm");
9 const ColorAnalyzer = Components.classes["@mozilla.org/places/colorAnalyzer;1"]
10 .getService(Components.interfaces.mozIColorAnalyzer);
12 this.EXPORTED_SYMBOLS = ["ColorUtils"];
14 let ColorUtils = {
15 _initialized: false,
16 init: function() {
17 if (this._initialized)
18 return;
19 Services.obs.addObserver(this, "idle-daily", false);
20 Services.obs.addObserver(this, "quit-application", false);
21 this._initialized = true;
22 },
23 uninit: function() {
24 Services.obs.removeObserver(this, "idle-daily");
25 Services.obs.removeObserver(this, "quit-application");
26 },
28 // default to keeping icon colorInfo for max 1 day
29 iconColorCacheMaxAge: 86400000,
31 // in-memory store for favicon color data
32 _uriColorsMap: (function() {
33 let cache = new Map();
34 // remove stale entries
35 cache.purge = function(maxAgeMs = 0) {
36 let cuttoff = Date.now() - (maxAgeMs || ColorUtils.iconColorCacheMaxAge);
37 for (let [key, value] of this) {
38 if (value.timestamp && value.timestamp >= cuttoff) {
39 continue;
40 }
41 this.delete(key);
42 }
43 }
44 return cache;
45 })(),
46 get iconColorCache() {
47 return ColorUtils._uriColorsMap;
48 },
50 observe: function (aSubject, aTopic, aData) {
51 switch (aTopic) {
52 case "idle-daily":
53 this.iconColorCache.purge();
54 break;
55 case "quit-application":
56 this.uninit();
57 break;
58 }
59 },
61 /** Takes an icon and returns either an object with foreground and background color properties
62 * or a promise for the same.
63 * The foreground is the contrast color, the background is the primary/dominant one
64 */
65 getForegroundAndBackgroundIconColors: function getForegroundAndBackgroundIconColors(aIconURI) {
66 let colorKey = aIconURI.spec;
67 let colorInfo = this._uriColorsMap.get(colorKey);
68 if (colorInfo) {
69 return colorInfo;
70 }
72 let deferred = Promise.defer();
73 let wrappedIcon = aIconURI;
74 this._uriColorsMap.set(colorKey, deferred.promise);
76 ColorAnalyzer.findRepresentativeColor(wrappedIcon, (success, color) => {
77 if (!success) {
78 this._uriColorsMap.delete(colorKey);
79 deferred.reject();
80 } else {
81 colorInfo = {
82 foreground: this.bestTextColorForContrast(color),
83 background: this.convertDecimalToRgbColor(color),
84 timestamp: Date.now()
85 };
86 deferred.resolve(colorInfo);
87 }
88 });
89 return deferred.promise;
90 },
92 /** returns the best color for text readability on top of aColor
93 * return color is in rgb(r,g,b) format, suitable to csss
94 * The color bightness algorithm is currently: http://www.w3.org/TR/AERT#color-contrast
95 */
96 bestTextColorForContrast: function bestTextColorForContrast(aColor) {
97 let r = (aColor & 0xff0000) >> 16;
98 let g = (aColor & 0x00ff00) >> 8;
99 let b = (aColor & 0x0000ff);
101 let w3cContrastValue = ((r*299)+(g*587)+(b*114))/1000;
102 w3cContrastValue = Math.round(w3cContrastValue);
103 let textColor = "rgb(255,255,255)";
105 if (w3cContrastValue > 125) {
106 // bright/light, use black text
107 textColor = "rgb(0,0,0)";
108 }
109 return textColor;
110 },
112 toCSSRgbColor: function toCSSRgbColor(r, g, b, a) {
113 var values = [Math.round(r), Math.round(g), Math.round(b)];
114 if (undefined !== a && a < 1) {
115 values.push(a);
116 return 'rgba('+values.join(',')+')';
117 }
118 return 'rgb('+values.join(',')+')';
119 },
121 /**
122 * converts a decimal(base10) number into CSS rgb color value string
123 */
124 convertDecimalToRgbColor: function convertDecimalToRgbColor(aColor) {
125 let [r,g,b,a] = this.unpackDecimalColorWord(aColor);
126 return this.toCSSRgbColor(r,g,b,a);
127 },
129 /**
130 * unpack a decimal(base10) word for r,g,b,a values
131 */
132 unpackDecimalColorWord: function unpackDecimalColorWord(aColor) {
133 let a = (aColor & 0xff000000) >> 24;
134 let r = (aColor & 0x00ff0000) >> 16;
135 let g = (aColor & 0x0000ff00) >> 8;
136 let b = (aColor & 0x000000ff);
137 // NB: falsy alpha treated as undefined, fully opaque
138 return a ? [r,g,b,a/255] : [r,g,b];
139 },
141 /**
142 * create a decimal(base10) word for r,g,b values
143 */
144 createDecimalColorWord: function createDecimalColorWord(r, g, b, a) {
145 let rgb = 0;
146 rgb |= b;
147 rgb |= (g << 8);
148 rgb |= (r << 16);
149 // pack alpha value if one is given
150 if (undefined !== a && a < 1)
151 rgb |= (Math.round(a*255) << 24);
152 return rgb;
153 },
155 /**
156 * Add 2 rgb(a) colors to get a flat color
157 */
158 addRgbColors: function addRgbColors(color1, color2) {
159 let [r1, g1, b1] = this.unpackDecimalColorWord(color1);
160 let [r2, g2, b2, alpha] = this.unpackDecimalColorWord(color2);
162 let color = {};
163 // early return if 2nd color is opaque
164 if (!alpha || alpha >= 1)
165 return color2;
167 return this.createDecimalColorWord(
168 Math.min(255, alpha * r2 + (1 - alpha) * r1),
169 Math.min(255, alpha * g2 + (1 - alpha) * g1),
170 Math.min(255, alpha * b2 + (1 - alpha) * b1)
171 );
172 }
173 };