tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

contrast.js (10153B)


      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 "use strict";
      6 
      7 loader.lazyRequireGetter(
      8  this,
      9  "CssLogic",
     10  "resource://devtools/server/actors/inspector/css-logic.js",
     11  true
     12 );
     13 loader.lazyRequireGetter(
     14  this,
     15  "getCurrentZoom",
     16  "resource://devtools/shared/layout/utils.js",
     17  true
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "addPseudoClassLock",
     22  "resource://devtools/server/actors/highlighters/utils/markup.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "removePseudoClassLock",
     28  "resource://devtools/server/actors/highlighters/utils/markup.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "getContrastRatioAgainstBackground",
     34  "resource://devtools/shared/accessibility.js",
     35  true
     36 );
     37 loader.lazyRequireGetter(
     38  this,
     39  "getTextProperties",
     40  "resource://devtools/shared/accessibility.js",
     41  true
     42 );
     43 loader.lazyRequireGetter(
     44  this,
     45  "InspectorActorUtils",
     46  "resource://devtools/server/actors/inspector/utils.js"
     47 );
     48 const lazy = {};
     49 ChromeUtils.defineESModuleGetters(
     50  lazy,
     51  {
     52    DevToolsWorker: "resource://devtools/shared/worker/worker.sys.mjs",
     53  },
     54  { global: "contextual" }
     55 );
     56 
     57 const WORKER_URL = "resource://devtools/server/actors/accessibility/worker.js";
     58 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
     59 const {
     60  LARGE_TEXT: { BOLD_LARGE_TEXT_MIN_PIXELS, LARGE_TEXT_MIN_PIXELS },
     61 } = require("resource://devtools/shared/accessibility.js");
     62 
     63 loader.lazyGetter(this, "worker", () => new lazy.DevToolsWorker(WORKER_URL));
     64 
     65 /**
     66 * Get canvas rendering context for the current target window bound by the bounds of the
     67 * accessible objects.
     68 *
     69 * @param  {object}  win
     70 *         Current target window.
     71 * @param  {object}  bounds
     72 *         Bounds for the accessible object.
     73 * @param  {object}  zoom
     74 *         Current zoom level for the window.
     75 * @param  {object}  scale
     76 *         Scale value to scale down the drawn image.
     77 * @param  {null|DOMNode} node
     78 *         If not null, a node that corresponds to the accessible object to be used to
     79 *         make its text color transparent.
     80 * @return {CanvasRenderingContext2D}
     81 *         Canvas rendering context for the current window.
     82 */
     83 function getImageCtx(win, bounds, zoom, scale, node) {
     84  const doc = win.document;
     85  const canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     86 
     87  const { left, top, width, height } = bounds;
     88  canvas.width = width * zoom * scale;
     89  canvas.height = height * zoom * scale;
     90  const ctx = canvas.getContext("2d", { alpha: false });
     91  ctx.imageSmoothingEnabled = false;
     92  ctx.scale(scale, scale);
     93 
     94  // If node is passed, make its color related text properties invisible.
     95  if (node) {
     96    addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
     97  }
     98 
     99  ctx.drawWindow(
    100    win,
    101    left * zoom,
    102    top * zoom,
    103    width * zoom,
    104    height * zoom,
    105    "#fff",
    106    ctx.DRAWWINDOW_USE_WIDGET_LAYERS
    107  );
    108 
    109  // Restore all inline styling.
    110  if (node) {
    111    removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
    112  }
    113 
    114  return ctx;
    115 }
    116 
    117 /**
    118 * Calculate the transformed RGBA when a color matrix is set in docShell by
    119 * multiplying the color matrix with the RGBA vector.
    120 *
    121 * @param  {Array}  rgba
    122 *         Original RGBA array which we want to transform.
    123 * @param  {Array}  colorMatrix
    124 *         Flattened 4x5 color matrix that is set in docShell.
    125 *         A 4x5 matrix of the form:
    126 *           1  2  3  4  5
    127 *           6  7  8  9  10
    128 *           11 12 13 14 15
    129 *           16 17 18 19 20
    130 *         will be set in docShell as:
    131 *           [1, 6, 11, 16, 2, 7, 12, 17, 3, 8, 13, 18, 4, 9, 14, 19, 5, 10, 15, 20]
    132 * @return {Array}
    133 *         Transformed RGBA after the color matrix is multiplied with the original RGBA.
    134 */
    135 function getTransformedRGBA(rgba, colorMatrix) {
    136  const transformedRGBA = [0, 0, 0, 0];
    137 
    138  // Only use the first four columns of the color matrix corresponding to R, G, B and A
    139  // color channels respectively. The fifth column is a fixed offset that does not need
    140  // to be considered for the matrix multiplication. We end up multiplying a 4x4 color
    141  // matrix with a 4x1 RGBA vector.
    142  for (let i = 0; i < 16; i++) {
    143    const row = i % 4;
    144    const col = Math.floor(i / 4);
    145    transformedRGBA[row] += colorMatrix[i] * rgba[col];
    146  }
    147 
    148  return transformedRGBA;
    149 }
    150 
    151 /**
    152 * Find RGBA or a range of RGBAs for the background pixels under the text.
    153 *
    154 * @param  {DOMNode}  node
    155 *         Node for which we want to get the background color data.
    156 * @param  {object}  options
    157 *         - bounds       {object}
    158 *                        Bounds for the accessible object.
    159 *         - win          {object}
    160 *                        Target window.
    161 *         - size         {Number}
    162 *                        Font size of the selected text node
    163 *         - isBoldText   {Boolean}
    164 *                        True if selected text node is bold
    165 * @return {object}
    166 *         Object with one or more of the following RGBA fields: value, min, max
    167 */
    168 function getBackgroundFor(node, { win, bounds, size, isBoldText }) {
    169  const zoom = 1 / getCurrentZoom(win);
    170  // When calculating colour contrast, we traverse image data for text nodes that are
    171  // drawn both with and without transparent text. Image data arrays are typically really
    172  // big. In cases when the font size is fairly large or when the page is zoomed in image
    173  // data is especially large (retrieving it and/or traversing it takes significant amount
    174  // of time). Here we optimize the size of the image data by scaling down the drawn nodes
    175  // to a size where their text size equals either BOLD_LARGE_TEXT_MIN_PIXELS or
    176  // LARGE_TEXT_MIN_PIXELS (lower threshold for large text size) depending on the font
    177  // weight.
    178  //
    179  // IMPORTANT: this optimization, in some cases where background colour is non-uniform
    180  // (gradient or image), can result in small (not noticeable) blending of the background
    181  // colours. In turn this might affect the reported values of the contrast ratio. The
    182  // delta is fairly small (<0.1) to noticeably skew the results.
    183  //
    184  // NOTE: this optimization does not help in cases where contrast is being calculated for
    185  // nodes with a lot of text.
    186  let scale =
    187    ((isBoldText ? BOLD_LARGE_TEXT_MIN_PIXELS : LARGE_TEXT_MIN_PIXELS) / size) *
    188    zoom;
    189  // We do not need to scale the images if the font is smaller than large or if the page
    190  // is zoomed out (scaling in this case would've been scaling up).
    191  scale = scale > 1 ? 1 : scale;
    192 
    193  const textContext = getImageCtx(win, bounds, zoom, scale);
    194  const backgroundContext = getImageCtx(win, bounds, zoom, scale, node);
    195 
    196  const { data: dataText } = textContext.getImageData(
    197    0,
    198    0,
    199    bounds.width * scale,
    200    bounds.height * scale
    201  );
    202  const { data: dataBackground } = backgroundContext.getImageData(
    203    0,
    204    0,
    205    bounds.width * scale,
    206    bounds.height * scale
    207  );
    208 
    209  return worker.performTask(
    210    "getBgRGBA",
    211    {
    212      dataTextBuf: dataText.buffer,
    213      dataBackgroundBuf: dataBackground.buffer,
    214    },
    215    [dataText.buffer, dataBackground.buffer]
    216  );
    217 }
    218 
    219 /**
    220 * Calculates the contrast ratio of the referenced DOM node.
    221 *
    222 * @param  {DOMNode} node
    223 *         The node for which we want to calculate the contrast ratio.
    224 * @param  {object}  options
    225 *         - bounds                           {object}
    226 *                                            Bounds for the accessible object.
    227 *         - win                              {object}
    228 *                                            Target window.
    229 *         - appliedColorMatrix               {Array|null}
    230 *                                            Simulation color matrix applied to
    231 *                                            to the viewport, if it exists.
    232 * @return {object}
    233 *         An object that may contain one or more of the following fields: error,
    234 *         isLargeText, value, min, max values for contrast.
    235 */
    236 async function getContrastRatioFor(node, options = {}) {
    237  const computedStyle = CssLogic.getComputedStyle(node);
    238  const props = computedStyle ? getTextProperties(computedStyle) : null;
    239 
    240  if (!props) {
    241    return {
    242      error: true,
    243    };
    244  }
    245 
    246  const { isLargeText, isBoldText, size, opacity } = props;
    247  const { appliedColorMatrix } = options;
    248  const color = appliedColorMatrix
    249    ? getTransformedRGBA(props.color, appliedColorMatrix)
    250    : props.color;
    251  let rgba = await getBackgroundFor(node, {
    252    ...options,
    253    isBoldText,
    254    size,
    255  });
    256 
    257  if (!rgba) {
    258    // Fallback (original) contrast calculation algorithm. It tries to get the
    259    // closest background colour for the node and use it to calculate contrast.
    260    const backgroundColor = InspectorActorUtils.getClosestBackgroundColor(node);
    261    const backgroundImage = InspectorActorUtils.getClosestBackgroundImage(node);
    262 
    263    if (backgroundImage !== "none") {
    264      // Both approaches failed, at this point we don't have a better one yet.
    265      return {
    266        error: true,
    267      };
    268    }
    269 
    270    let { r, g, b, a } = InspectorUtils.colorToRGBA(backgroundColor);
    271    // If the element has opacity in addition to background alpha value, take it
    272    // into account. TODO: this does not handle opacity set on ancestor
    273    // elements (see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1544721).
    274    if (opacity < 1) {
    275      a = opacity * a;
    276    }
    277 
    278    return getContrastRatioAgainstBackground(
    279      {
    280        value: appliedColorMatrix
    281          ? getTransformedRGBA([r, g, b, a], appliedColorMatrix)
    282          : [r, g, b, a],
    283      },
    284      {
    285        color,
    286        isLargeText,
    287      }
    288    );
    289  }
    290 
    291  if (appliedColorMatrix) {
    292    rgba = rgba.value
    293      ? {
    294          value: getTransformedRGBA(rgba.value, appliedColorMatrix),
    295        }
    296      : {
    297          min: getTransformedRGBA(rgba.min, appliedColorMatrix),
    298          max: getTransformedRGBA(rgba.max, appliedColorMatrix),
    299        };
    300  }
    301 
    302  return getContrastRatioAgainstBackground(rgba, {
    303    color,
    304    isLargeText,
    305  });
    306 }
    307 
    308 exports.getContrastRatioFor = getContrastRatioFor;
    309 exports.getBackgroundFor = getBackgroundFor;