summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/accessibility/audit/contrast.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/accessibility/audit/contrast.js')
-rw-r--r--devtools/server/actors/accessibility/audit/contrast.js306
1 files changed, 306 insertions, 0 deletions
diff --git a/devtools/server/actors/accessibility/audit/contrast.js b/devtools/server/actors/accessibility/audit/contrast.js
new file mode 100644
index 0000000000..68e7b497f8
--- /dev/null
+++ b/devtools/server/actors/accessibility/audit/contrast.js
@@ -0,0 +1,306 @@
+/* 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
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyRequireGetter(
+ this,
+ "CssLogic",
+ "resource://devtools/server/actors/inspector/css-logic.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getCurrentZoom",
+ "resource://devtools/shared/layout/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "addPseudoClassLock",
+ "resource://devtools/server/actors/highlighters/utils/markup.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "removePseudoClassLock",
+ "resource://devtools/server/actors/highlighters/utils/markup.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getContrastRatioAgainstBackground",
+ "resource://devtools/shared/accessibility.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getTextProperties",
+ "resource://devtools/shared/accessibility.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "DevToolsWorker",
+ "resource://devtools/shared/worker/worker.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "InspectorActorUtils",
+ "resource://devtools/server/actors/inspector/utils.js"
+);
+
+const WORKER_URL = "resource://devtools/server/actors/accessibility/worker.js";
+const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
+const {
+ LARGE_TEXT: { BOLD_LARGE_TEXT_MIN_PIXELS, LARGE_TEXT_MIN_PIXELS },
+} = require("resource://devtools/shared/accessibility.js");
+
+loader.lazyGetter(this, "worker", () => new DevToolsWorker(WORKER_URL));
+
+/**
+ * Get canvas rendering context for the current target window bound by the bounds of the
+ * accessible objects.
+ * @param {Object} win
+ * Current target window.
+ * @param {Object} bounds
+ * Bounds for the accessible object.
+ * @param {Object} zoom
+ * Current zoom level for the window.
+ * @param {Object} scale
+ * Scale value to scale down the drawn image.
+ * @param {null|DOMNode} node
+ * If not null, a node that corresponds to the accessible object to be used to
+ * make its text color transparent.
+ * @return {CanvasRenderingContext2D}
+ * Canvas rendering context for the current window.
+ */
+function getImageCtx(win, bounds, zoom, scale, node) {
+ const doc = win.document;
+ const canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+
+ const { left, top, width, height } = bounds;
+ canvas.width = width * zoom * scale;
+ canvas.height = height * zoom * scale;
+ const ctx = canvas.getContext("2d", { alpha: false });
+ ctx.imageSmoothingEnabled = false;
+ ctx.scale(scale, scale);
+
+ // If node is passed, make its color related text properties invisible.
+ if (node) {
+ addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
+ }
+
+ ctx.drawWindow(
+ win,
+ left * zoom,
+ top * zoom,
+ width * zoom,
+ height * zoom,
+ "#fff",
+ ctx.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+
+ // Restore all inline styling.
+ if (node) {
+ removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
+ }
+
+ return ctx;
+}
+
+/**
+ * Calculate the transformed RGBA when a color matrix is set in docShell by
+ * multiplying the color matrix with the RGBA vector.
+ *
+ * @param {Array} rgba
+ * Original RGBA array which we want to transform.
+ * @param {Array} colorMatrix
+ * Flattened 4x5 color matrix that is set in docShell.
+ * A 4x5 matrix of the form:
+ * 1 2 3 4 5
+ * 6 7 8 9 10
+ * 11 12 13 14 15
+ * 16 17 18 19 20
+ * will be set in docShell as:
+ * [1, 6, 11, 16, 2, 7, 12, 17, 3, 8, 13, 18, 4, 9, 14, 19, 5, 10, 15, 20]
+ * @return {Array}
+ * Transformed RGBA after the color matrix is multiplied with the original RGBA.
+ */
+function getTransformedRGBA(rgba, colorMatrix) {
+ const transformedRGBA = [0, 0, 0, 0];
+
+ // Only use the first four columns of the color matrix corresponding to R, G, B and A
+ // color channels respectively. The fifth column is a fixed offset that does not need
+ // to be considered for the matrix multiplication. We end up multiplying a 4x4 color
+ // matrix with a 4x1 RGBA vector.
+ for (let i = 0; i < 16; i++) {
+ const row = i % 4;
+ const col = Math.floor(i / 4);
+ transformedRGBA[row] += colorMatrix[i] * rgba[col];
+ }
+
+ return transformedRGBA;
+}
+
+/**
+ * Find RGBA or a range of RGBAs for the background pixels under the text.
+ *
+ * @param {DOMNode} node
+ * Node for which we want to get the background color data.
+ * @param {Object} options
+ * - bounds {Object}
+ * Bounds for the accessible object.
+ * - win {Object}
+ * Target window.
+ * - size {Number}
+ * Font size of the selected text node
+ * - isBoldText {Boolean}
+ * True if selected text node is bold
+ * @return {Object}
+ * Object with one or more of the following RGBA fields: value, min, max
+ */
+function getBackgroundFor(node, { win, bounds, size, isBoldText }) {
+ const zoom = 1 / getCurrentZoom(win);
+ // When calculating colour contrast, we traverse image data for text nodes that are
+ // drawn both with and without transparent text. Image data arrays are typically really
+ // big. In cases when the font size is fairly large or when the page is zoomed in image
+ // data is especially large (retrieving it and/or traversing it takes significant amount
+ // of time). Here we optimize the size of the image data by scaling down the drawn nodes
+ // to a size where their text size equals either BOLD_LARGE_TEXT_MIN_PIXELS or
+ // LARGE_TEXT_MIN_PIXELS (lower threshold for large text size) depending on the font
+ // weight.
+ //
+ // IMPORTANT: this optimization, in some cases where background colour is non-uniform
+ // (gradient or image), can result in small (not noticeable) blending of the background
+ // colours. In turn this might affect the reported values of the contrast ratio. The
+ // delta is fairly small (<0.1) to noticeably skew the results.
+ //
+ // NOTE: this optimization does not help in cases where contrast is being calculated for
+ // nodes with a lot of text.
+ let scale =
+ ((isBoldText ? BOLD_LARGE_TEXT_MIN_PIXELS : LARGE_TEXT_MIN_PIXELS) / size) *
+ zoom;
+ // We do not need to scale the images if the font is smaller than large or if the page
+ // is zoomed out (scaling in this case would've been scaling up).
+ scale = scale > 1 ? 1 : scale;
+
+ const textContext = getImageCtx(win, bounds, zoom, scale);
+ const backgroundContext = getImageCtx(win, bounds, zoom, scale, node);
+
+ const { data: dataText } = textContext.getImageData(
+ 0,
+ 0,
+ bounds.width * scale,
+ bounds.height * scale
+ );
+ const { data: dataBackground } = backgroundContext.getImageData(
+ 0,
+ 0,
+ bounds.width * scale,
+ bounds.height * scale
+ );
+
+ return worker.performTask(
+ "getBgRGBA",
+ {
+ dataTextBuf: dataText.buffer,
+ dataBackgroundBuf: dataBackground.buffer,
+ },
+ [dataText.buffer, dataBackground.buffer]
+ );
+}
+
+/**
+ * Calculates the contrast ratio of the referenced DOM node.
+ *
+ * @param {DOMNode} node
+ * The node for which we want to calculate the contrast ratio.
+ * @param {Object} options
+ * - bounds {Object}
+ * Bounds for the accessible object.
+ * - win {Object}
+ * Target window.
+ * - appliedColorMatrix {Array|null}
+ * Simulation color matrix applied to
+ * to the viewport, if it exists.
+ * @return {Object}
+ * An object that may contain one or more of the following fields: error,
+ * isLargeText, value, min, max values for contrast.
+ */
+async function getContrastRatioFor(node, options = {}) {
+ const computedStyle = CssLogic.getComputedStyle(node);
+ const props = computedStyle ? getTextProperties(computedStyle) : null;
+
+ if (!props) {
+ return {
+ error: true,
+ };
+ }
+
+ const { isLargeText, isBoldText, size, opacity } = props;
+ const { appliedColorMatrix } = options;
+ const color = appliedColorMatrix
+ ? getTransformedRGBA(props.color, appliedColorMatrix)
+ : props.color;
+ let rgba = await getBackgroundFor(node, {
+ ...options,
+ isBoldText,
+ size,
+ });
+
+ if (!rgba) {
+ // Fallback (original) contrast calculation algorithm. It tries to get the
+ // closest background colour for the node and use it to calculate contrast.
+ const backgroundColor = InspectorActorUtils.getClosestBackgroundColor(node);
+ const backgroundImage = InspectorActorUtils.getClosestBackgroundImage(node);
+
+ if (backgroundImage !== "none") {
+ // Both approaches failed, at this point we don't have a better one yet.
+ return {
+ error: true,
+ };
+ }
+
+ let { r, g, b, a } = InspectorUtils.colorToRGBA(backgroundColor);
+ // If the element has opacity in addition to background alpha value, take it
+ // into account. TODO: this does not handle opacity set on ancestor
+ // elements (see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1544721).
+ if (opacity < 1) {
+ a = opacity * a;
+ }
+
+ return getContrastRatioAgainstBackground(
+ {
+ value: appliedColorMatrix
+ ? getTransformedRGBA([r, g, b, a], appliedColorMatrix)
+ : [r, g, b, a],
+ },
+ {
+ color,
+ isLargeText,
+ }
+ );
+ }
+
+ if (appliedColorMatrix) {
+ rgba = rgba.value
+ ? {
+ value: getTransformedRGBA(rgba.value, appliedColorMatrix),
+ }
+ : {
+ min: getTransformedRGBA(rgba.min, appliedColorMatrix),
+ max: getTransformedRGBA(rgba.max, appliedColorMatrix),
+ };
+ }
+
+ return getContrastRatioAgainstBackground(rgba, {
+ color,
+ isLargeText,
+ });
+}
+
+exports.getContrastRatioFor = getContrastRatioFor;
+exports.getBackgroundFor = getBackgroundFor;