Bug 1568053 - Create node actor method for getting complex bg color data for text nodes and refactor existing contrast calculation methods, r=yzen,gl
authorMaliha Islam <mislam@mozilla.com>
Fri, 16 Aug 2019 00:55:26 +0000
changeset 488396 d9a83f0385d54763aabbadaea607a7451381beba
parent 488395 71900a2d797bfc96bfdd2572d550df4bf349650b
child 488397 c009e486b4c2e82a516e95cfb635523ea215e275
push id36443
push userccoroiu@mozilla.com
push dateFri, 16 Aug 2019 09:48:15 +0000
treeherdermozilla-central@5d4cbfe103bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, gl
bugs1568053
milestone70.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
Bug 1568053 - Create node actor method for getting complex bg color data for text nodes and refactor existing contrast calculation methods, r=yzen,gl Differential Revision: https://phabricator.services.mozilla.com/D40086
devtools/server/actors/accessibility/accessible.js
devtools/server/actors/accessibility/audit/contrast.js
devtools/server/actors/accessibility/walker.js
devtools/server/actors/inspector/node.js
devtools/server/actors/inspector/utils.js
devtools/server/actors/utils/accessibility.js
devtools/shared/accessibility.js
devtools/shared/css/color.js
devtools/shared/specs/node.js
--- 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,