author | Maliha Islam <mislam@mozilla.com> |
Fri, 16 Aug 2019 00:55:26 +0000 | |
changeset 488396 | d9a83f0385d54763aabbadaea607a7451381beba |
parent 488395 | 71900a2d797bfc96bfdd2572d550df4bf349650b |
child 488397 | c009e486b4c2e82a516e95cfb635523ea215e275 |
push id | 36443 |
push user | ccoroiu@mozilla.com |
push date | Fri, 16 Aug 2019 09:48:15 +0000 |
treeherder | mozilla-central@5d4cbfe103bb [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | yzen, gl |
bugs | 1568053 |
milestone | 70.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/server/actors/accessibility/accessible.js +++ b/devtools/server/actors/accessibility/accessible.js @@ -31,16 +31,22 @@ loader.lazyRequireGetter( ); loader.lazyRequireGetter( this, "findCssSelector", "devtools/shared/inspector/css-logic", true ); loader.lazyRequireGetter(this, "events", "devtools/shared/event-emitter"); +loader.lazyRequireGetter( + this, + "getBounds", + "devtools/server/actors/highlighters/utils/accessibility", + true +); const RELATIONS_TO_IGNORE = new Set([ Ci.nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION, Ci.nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE, Ci.nsIAccessibleRelation.RELATION_CONTAINING_WINDOW, Ci.nsIAccessibleRelation.RELATION_PARENT_WINDOW_OF, Ci.nsIAccessibleRelation.RELATION_SUBWINDOW_OF, ]); @@ -452,17 +458,17 @@ const AccessibleActor = ActorClassWithSp const { DOMNode: rawNode } = this.rawAccessible; const win = rawNode.ownerGlobal; // Keep the reference to the walker actor in case the actor gets destroyed // during the colour contrast ratio calculation. const { walker } = this; walker.clearStyles(win); const contrastRatio = await getContrastRatioFor(rawNode.parentNode, { - bounds, + bounds: getBounds(win, bounds), win, }); walker.restoreStyles(win); return contrastRatio; },
--- a/devtools/server/actors/accessibility/audit/contrast.js +++ b/devtools/server/actors/accessibility/audit/contrast.js @@ -8,22 +8,16 @@ loader.lazyRequireGetter(this, "colorUti loader.lazyRequireGetter( this, "CssLogic", "devtools/server/actors/inspector/css-logic", true ); loader.lazyRequireGetter( this, - "getBounds", - "devtools/server/actors/highlighters/utils/accessibility", - true -); -loader.lazyRequireGetter( - this, "getCurrentZoom", "devtools/shared/layout/utils", true ); loader.lazyRequireGetter( this, "addPseudoClassLock", "devtools/server/actors/highlighters/utils/markup", @@ -32,17 +26,17 @@ loader.lazyRequireGetter( loader.lazyRequireGetter( this, "removePseudoClassLock", "devtools/server/actors/highlighters/utils/markup", true ); loader.lazyRequireGetter( this, - "getContrastRatioScore", + "getContrastRatioAgainstBackground", "devtools/shared/accessibility", true ); loader.lazyRequireGetter( this, "getTextProperties", "devtools/shared/accessibility", true @@ -114,43 +108,34 @@ function getImageCtx(win, bounds, zoom, if (node) { removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS); } return ctx; } /** - * Calculates the contrast ratio of the referenced DOM node. + * Find RGBA or a range of RGBAs for the background pixels under the text. * - * @param {DOMNode} node - * The node for which we want to calculate the contrast ratio. + * @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. - * + * - 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} - * An object that may contain one or more of the following fields: error, - * isLargeText, value, min, max values for contrast. + * Object with one or more of the following RGBA fields: value, min, max */ -async function getContrastRatioFor(node, options = {}) { - const computedStyle = CssLogic.getComputedStyle(node); - const props = computedStyle ? getTextProperties(computedStyle) : null; - - if (!props) { - return { - error: true, - }; - } - - const { color, isLargeText, isBoldText, size, opacity } = props; - const bounds = getBounds(options.win, options.bounds); - const zoom = 1 / getCurrentZoom(options.win); +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. @@ -164,40 +149,73 @@ async function getContrastRatioFor(node, // 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(options.win, bounds, zoom, scale); - const backgroundContext = getImageCtx(options.win, bounds, zoom, scale, node); + 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 ); - const rgba = await worker.performTask( + 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. + * @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 { color, isLargeText, isBoldText, size, opacity } = props; + + const 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") { @@ -210,54 +228,27 @@ async function getContrastRatioFor(node, let { r, g, b, a } = colorUtils.colorToRGBA(backgroundColor, true); // 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; } - const value = colorUtils.calculateContrastRatio([r, g, b, a], color); - return { - value, - color, - backgroundColor: [r, g, b, a], - isLargeText, - score: getContrastRatioScore(value, isLargeText), - }; - } - - if (rgba.value) { - const value = colorUtils.calculateContrastRatio(rgba.value, color); - return { - value, - color, - backgroundColor: rgba.value, - isLargeText, - score: getContrastRatioScore(value, isLargeText), - }; + return getContrastRatioAgainstBackground( + { + value: [r, g, b, a], + }, + { + color, + isLargeText, + } + ); } - let min = colorUtils.calculateContrastRatio(rgba.min, color); - let max = colorUtils.calculateContrastRatio(rgba.max, color); - - // Flip minimum and maximum contrast ratios if necessary. - if (min > max) { - [min, max] = [max, min]; - [rgba.min, rgba.max] = [rgba.max, rgba.min]; - } - - const score = getContrastRatioScore(min, isLargeText); - - return { - min, - max, + return getContrastRatioAgainstBackground(rgba, { color, - backgroundColorMin: rgba.min, - backgroundColorMax: rgba.max, isLargeText, - score, - scoreMin: score, - scoreMax: getContrastRatioScore(max, isLargeText), - }; + }); } exports.getContrastRatioFor = getContrastRatioFor; +exports.getBackgroundFor = getBackgroundFor;
--- a/devtools/server/actors/accessibility/walker.js +++ b/devtools/server/actors/accessibility/walker.js @@ -55,51 +55,41 @@ loader.lazyRequireGetter( loader.lazyRequireGetter( this, "isXUL", "devtools/server/actors/highlighters/utils/markup", true ); loader.lazyRequireGetter( this, - "loadSheet", - "devtools/shared/layout/utils", + "loadSheetForBackgroundCalculation", + "devtools/server/actors/utils/accessibility", true ); loader.lazyRequireGetter( this, "register", "devtools/server/actors/highlighters", true ); loader.lazyRequireGetter( this, - "removeSheet", - "devtools/shared/layout/utils", + "removeSheetForBackgroundCalculation", + "devtools/server/actors/utils/accessibility", true ); loader.lazyRequireGetter( this, "accessibility", "devtools/shared/constants", true ); const kStateHover = 0x00000004; // NS_EVENT_STATE_HOVER -const HIGHLIGHTER_STYLES_SHEET = `data:text/css;charset=utf-8, -* { - transition: none !important; -} - -:-moz-devtools-highlighted { - color: transparent !important; - text-shadow: none !important; -}`; - const { EVENT_TEXT_CHANGED, EVENT_TEXT_INSERTED, EVENT_TEXT_REMOVED, EVENT_ACCELERATOR_CHANGE, EVENT_ACTION_CHANGE, EVENT_DEFACTION_CHANGE, EVENT_DESCRIPTION_CHANGE, @@ -741,17 +731,17 @@ const AccessibleWalkerActor = ActorClass this._loadedSheets.set(win, requests + 1); return; } // Disable potential mouse driven transitions (This is important because accessibility // highlighter temporarily modifies text color related CSS properties. In case where // there are transitions that affect them, there might be unexpected side effects when // taking a snapshot for contrast measurement). - loadSheet(win, HIGHLIGHTER_STYLES_SHEET); + loadSheetForBackgroundCalculation(win); this._loadedSheets.set(win, 1); this.hideHighlighter(); }, /** * Restore CSS and overlays that could've interfered with the audit for an * accessible object by unloading accessibility highlighter style sheet used * for preventing transitions and applying transparency when calculating @@ -766,17 +756,17 @@ const AccessibleWalkerActor = ActorClass } if (requests > 1) { this._loadedSheets.set(win, requests - 1); return; } this.showHighlighter(); - removeSheet(win, HIGHLIGHTER_STYLES_SHEET); + removeSheetForBackgroundCalculation(win); this._loadedSheets.delete(win); }, hideHighlighter() { // TODO: Fix this workaround that temporarily removes higlighter bounds // overlay that can interfere with the contrast ratio calculation. if (this._highlighter) { const highlighter = this._highlighter.instance;
--- a/devtools/server/actors/inspector/node.js +++ b/devtools/server/actors/inspector/node.js @@ -682,16 +682,29 @@ const NodeActor = protocol.ActorClassWit * String with the background color of the form rgba(r, g, b, a). Defaults to * rgba(255, 255, 255, 1) if no background color is found. */ getClosestBackgroundColor: function() { return InspectorActorUtils.getClosestBackgroundColor(this.rawNode); }, /** + * Finds the background color range for the parent of a single text node + * (i.e. for multi-colored backgrounds with gradients, images) or a single + * background color for single-colored backgrounds. Defaults to the closest + * background color if an error is encountered. + * + * @return {Object} + * Object with one or more of the following properties: value, min, max + */ + getBackgroundColor: function() { + return InspectorActorUtils.getBackgroundColor(this); + }, + + /** * Returns an object with the width and height of the node's owner window. * * @return {Object} */ getOwnerGlobalDimensions: function() { const win = this.rawNode.ownerGlobal; return { innerWidth: win.innerWidth,
--- a/devtools/server/actors/inspector/utils.js +++ b/devtools/server/actors/inspector/utils.js @@ -34,16 +34,46 @@ loader.lazyRequireGetter( ); loader.lazyRequireGetter( this, "CssLogic", "devtools/server/actors/inspector/css-logic", true ); +loader.lazyRequireGetter( + this, + "getBackgroundFor", + "devtools/server/actors/accessibility/audit/contrast", + true +); +loader.lazyRequireGetter( + this, + "loadSheetForBackgroundCalculation", + "devtools/server/actors/utils/accessibility", + true +); +loader.lazyRequireGetter( + this, + "removeSheetForBackgroundCalculation", + "devtools/server/actors/utils/accessibility", + true +); +loader.lazyRequireGetter( + this, + "getAdjustedQuads", + "devtools/shared/layout/utils", + true +); +loader.lazyRequireGetter( + this, + "getTextProperties", + "devtools/shared/accessibility", + true +); const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const IMAGE_FETCHING_TIMEOUT = 500; /** * Returns the properly cased version of the node's tag name, which can be * used when displaying said name in the UI. @@ -401,19 +431,133 @@ function findGridParentContainerForNode( } } catch (e) { // Getting the parentNode can fail when the supplied node is in shadow DOM. } return null; } +/** + * Finds the background color range for the parent of a single text node + * (i.e. for multi-colored backgrounds with gradients, images) or a single + * background color for single-colored backgrounds. Defaults to the closest + * background color if an error is encountered. + * + * @param {Object} + * Node actor containing the following properties: + * {DOMNode} rawNode + * Node for which we want to calculate the color contrast. + * {WalkerActor} walker + * Walker actor used to check whether the node is the parent elm of a single text node. + * @return {Object} + * Object with one or more of the following properties: + * {Array|null} value + * RGBA array for single-colored background. Null for multi-colored backgrounds. + * {Array|null} min + * RGBA array for the min luminance color in a multi-colored background. + * Null for single-colored backgrounds. + * {Array|null} max + * RGBA array for the max luminance color in a multi-colored background. + * Null for single-colored backgrounds. + */ +async function getBackgroundColor({ rawNode: node, walker }) { + // Fall back to calculating contrast against closest bg if: + // - not element node + // - more than one child + // Avoid calculating bounds and creating doc walker by returning early. + if (node.nodeType != Node.ELEMENT_NODE || node.children.length > 0) { + return { + value: colorUtils.colorToRGBA( + getClosestBackgroundColor(node), + true, + true + ), + }; + } + + const bounds = getAdjustedQuads( + node.ownerGlobal, + node.firstChild, + "content" + )[0].bounds; + + // Fall back to calculating contrast against closest bg if there are no bounds for text node. + // Avoid creating doc walker by returning early. + if (!bounds) { + return { + value: colorUtils.colorToRGBA( + getClosestBackgroundColor(node), + true, + true + ), + }; + } + + const docWalker = walker.getDocumentWalker(node); + const firstChild = docWalker.firstChild(); + + // Fall back to calculating contrast against closest bg if: + // - more than one child + // - unique child is not a text node + if ( + !firstChild || + docWalker.nextSibling() || + firstChild.nodeType !== Node.TEXT_NODE + ) { + return { + value: colorUtils.colorToRGBA( + getClosestBackgroundColor(node), + true, + true + ), + }; + } + + // Try calculating complex backgrounds for node + const win = node.ownerGlobal; + loadSheetForBackgroundCalculation(win); + const computedStyle = CssLogic.getComputedStyle(node); + const props = computedStyle ? getTextProperties(computedStyle) : null; + + // Fall back to calculating contrast against closest bg if there are no text props. + if (!props) { + return { + value: colorUtils.colorToRGBA( + getClosestBackgroundColor(node), + true, + true + ), + }; + } + + const bgColor = await getBackgroundFor(node, { + bounds, + win, + convertBoundsRelativeToViewport: false, + size: props.size, + isBoldText: props.isBoldText, + }); + removeSheetForBackgroundCalculation(win); + + return ( + bgColor || { + value: colorUtils.colorToRGBA( + getClosestBackgroundColor(node), + true, + true + ), + } + ); +} + module.exports = { allAnonymousContentTreeWalkerFilter, findGridParentContainerForNode, + getBackgroundColor, getClosestBackgroundColor, getClosestBackgroundImage, getNodeDisplayName, getNodeGridFlexType, imageToImageData, isNodeDead, nodeDocument, scrollbarTreeWalkerFilter,
--- a/devtools/server/actors/utils/accessibility.js +++ b/devtools/server/actors/utils/accessibility.js @@ -1,16 +1,40 @@ /* 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, "Ci", "chrome", true); loader.lazyRequireGetter(this, "Services"); +loader.lazyRequireGetter( + this, + "loadSheet", + "devtools/shared/layout/utils", + true +); +loader.lazyRequireGetter( + this, + "removeSheet", + "devtools/shared/layout/utils", + true +); + +// Highlighter style used for preventing transitions and applying transparency +// when calculating colour contrast. +const HIGHLIGHTER_STYLES_SHEET = `data:text/css;charset=utf-8, +* { + transition: none !important; +} + +:-moz-devtools-highlighted { + color: transparent !important; + text-shadow: none !important; +}`; /** * Helper function that determines if nsIAccessible object is in defunct state. * * @param {nsIAccessible} accessible * object to be tested. * @return {Boolean} * True if accessible object is defunct, false otherwise. @@ -32,9 +56,33 @@ function isDefunct(accessible) { defunct = !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT); } catch (e) { defunct = true; } return defunct; } +/** + * Load highlighter style sheet used for preventing transitions and + * applying transparency when calculating colour contrast. + * + * @param {Window} win + * Window where highlighting happens. + */ +function loadSheetForBackgroundCalculation(win) { + loadSheet(win, HIGHLIGHTER_STYLES_SHEET); +} + +/** + * Unload highlighter style sheet used for preventing transitions + * and applying transparency when calculating colour contrast. + * + * @param {Window} win + * Window where highlighting was happenning. + */ +function removeSheetForBackgroundCalculation(win) { + removeSheet(win, HIGHLIGHTER_STYLES_SHEET); +} + exports.isDefunct = isDefunct; +exports.loadSheetForBackgroundCalculation = loadSheetForBackgroundCalculation; +exports.removeSheetForBackgroundCalculation = removeSheetForBackgroundCalculation;
--- a/devtools/shared/accessibility.js +++ b/devtools/shared/accessibility.js @@ -107,11 +107,83 @@ function getTextProperties(computedStyle color: [r, g, b, a], isLargeText, isBoldText, size, opacity, }; } +/** + * Calculates contrast ratio or range of contrast ratios of the referenced DOM node + * against the given background color data. If background is multi-colored, return a + * range, otherwise a single contrast ratio. + * + * @param {Object} backgroundColorData + * Object with one or more of the following properties: + * - value {Array} + * rgba array for single color background + * - min {Array} + * min luminance rgba array for multi color background + * - max {Array} + * max luminance rgba array for multi color background + * @param {Object} textData + * - color {Array} + * rgba array for text of referenced DOM node + * - isLargeText {Boolean} + * True if text of referenced DOM node is large + * @return {Object} + * An object that may contain one or more of the following fields: error, + * isLargeText, value, min, max values for contrast. + */ +function getContrastRatioAgainstBackground( + backgroundColorData, + { color, isLargeText } +) { + if (backgroundColorData.value) { + const value = colorUtils.calculateContrastRatio( + backgroundColorData.value, + color + ); + return { + value, + color, + backgroundColor: backgroundColorData.value, + isLargeText, + score: getContrastRatioScore(value, isLargeText), + }; + } + + let { + min: backgroundColorMin, + max: backgroundColorMax, + } = backgroundColorData; + let min = colorUtils.calculateContrastRatio(backgroundColorMin, color); + let max = colorUtils.calculateContrastRatio(backgroundColorMax, color); + + // Flip minimum and maximum contrast ratios if necessary. + if (min > max) { + [min, max] = [max, min]; + [backgroundColorMin, backgroundColorMax] = [ + backgroundColorMax, + backgroundColorMin, + ]; + } + + const score = getContrastRatioScore(min, isLargeText); + + return { + min, + max, + color, + backgroundColorMin, + backgroundColorMax, + isLargeText, + score, + scoreMin: score, + scoreMax: getContrastRatioScore(max, isLargeText), + }; +} + exports.getContrastRatioScore = getContrastRatioScore; exports.getTextProperties = getTextProperties; +exports.getContrastRatioAgainstBackground = getContrastRatioAgainstBackground; exports.LARGE_TEXT = LARGE_TEXT;
--- a/devtools/shared/css/color.js +++ b/devtools/shared/css/color.js @@ -1240,22 +1240,28 @@ function parseOldStyleRgb(lexer, hasAlph return rgba; } /** * Convert a string representing a color to an object holding the * color's components. Any valid CSS color form can be passed in. * - * @param {String} name the color - * @param {Boolean} useCssColor4ColorFunction use css-color-4 color function or not. - * @return {Object} an object of the form {r, g, b, a}; or null if the + * @param {String} name + * The color + * @param {Boolean} useCssColor4ColorFunction + * Use css-color-4 color function or not. + * @param {Boolean} toArray + * Return rgba array if true, otherwise object + * @return {Object|Array} + * An object of the form {r, g, b, a} if toArray is false, + * otherwise an array of the form [r, g, b, a]; or null if the * name was not a valid color */ -function colorToRGBA(name, useCssColor4ColorFunction = false) { +function colorToRGBA(name, useCssColor4ColorFunction = false, toArray = false) { name = name.trim().toLowerCase(); if (name in cssColors) { const result = cssColors[name]; return { r: result[0], g: result[1], b: result[2], a: result[3] }; } else if (name === "transparent") { return { r: 0, g: 0, b: 0, a: 0 }; } else if (name === "currentcolor") { @@ -1299,17 +1305,17 @@ function colorToRGBA(name, useCssColor4C if (!vals) { return null; } if (getToken(lexer) !== null) { return null; } - return { r: vals[0], g: vals[1], b: vals[2], a: vals[3] }; + return toArray ? vals : { r: vals[0], g: vals[1], b: vals[2], a: vals[3] }; } /** * Check whether a string names a valid CSS color. * * @param {String} name The string to check * @param {Boolean} useCssColor4ColorFunction use css-color-4 color function or not. * @return {Boolean} True if the string is a CSS color name.
--- a/devtools/shared/specs/node.js +++ b/devtools/shared/specs/node.js @@ -125,16 +125,20 @@ const nodeSpec = generateActorSpec({ response: RetVal("imageData"), }, getClosestBackgroundColor: { request: {}, response: { value: RetVal("string"), }, }, + getBackgroundColor: { + request: {}, + response: RetVal("json"), + }, getOwnerGlobalDimensions: { request: {}, response: RetVal("windowDimensions"), }, connectToRemoteFrame: { request: {}, // We are passing a target actor form here. // As we are manually fetching the form JSON via DebuggerServer.connectToFrame,