| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/inspector/inspector_contrast.h" |
| |
| #include "third_party/blink/renderer/core/css/css_color_value.h" |
| #include "third_party/blink/renderer/core/css/css_computed_style_declaration.h" |
| #include "third_party/blink/renderer/core/css/css_gradient_value.h" |
| #include "third_party/blink/renderer/core/css/properties/computed_style_utils.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/html_embed_element.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_dom_agent.h" |
| #include "third_party/blink/renderer/core/inspector/inspector_dom_snapshot_agent.h" |
| #include "third_party/blink/renderer/core/layout/geometry/physical_rect.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/style/style_generated_image.h" |
| #include "ui/gfx/color_utils.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool NodeIsElementWithLayoutObject(Node* node) { |
| if (auto* element = DynamicTo<Element>(node)) { |
| if (element->GetLayoutObject()) |
| return true; |
| } |
| return false; |
| } |
| |
| // Blends the colors from the given gradient with the existing colors. |
| void BlendWithColorsFromGradient(cssvalue::CSSGradientValue* gradient, |
| Vector<Color>& colors, |
| bool& found_non_transparent_color, |
| bool& found_opaque_color, |
| const LayoutObject& layout_object) { |
| const Document& document = layout_object.GetDocument(); |
| const ComputedStyle& style = layout_object.StyleRef(); |
| |
| Vector<Color> stop_colors = gradient->GetStopColors(document, style); |
| if (colors.IsEmpty()) { |
| colors.AppendRange(stop_colors.begin(), stop_colors.end()); |
| } else { |
| if (colors.size() > 1) { |
| // Gradient on gradient is too complicated, bail out. |
| colors.clear(); |
| return; |
| } |
| |
| Color existing_color = colors.front(); |
| colors.clear(); |
| for (auto stop_color : stop_colors) { |
| found_non_transparent_color = |
| found_non_transparent_color || (stop_color.Alpha() != 0); |
| colors.push_back(existing_color.Blend(stop_color)); |
| } |
| } |
| found_opaque_color = |
| found_opaque_color || gradient->KnownToBeOpaque(document, style); |
| } |
| |
| // Gets the colors from an image style, if one exists and it is a gradient. |
| void AddColorsFromImageStyle(const ComputedStyle& style, |
| const LayoutObject& layout_object, |
| Vector<Color>& colors, |
| bool& found_opaque_color, |
| bool& found_non_transparent_color) { |
| const FillLayer& background_layers = style.BackgroundLayers(); |
| if (!background_layers.AnyLayerHasImage()) |
| return; |
| |
| StyleImage* style_image = background_layers.GetImage(); |
| // hasImage() does not always indicate that this is non-null |
| if (!style_image) |
| return; |
| |
| if (!style_image->IsGeneratedImage()) { |
| // Make no assertions about the colors in non-generated images |
| colors.clear(); |
| found_opaque_color = false; |
| return; |
| } |
| |
| StyleGeneratedImage* gen_image = To<StyleGeneratedImage>(style_image); |
| CSSValue* image_css = gen_image->CssValue(); |
| if (auto* gradient = DynamicTo<cssvalue::CSSGradientValue>(image_css)) { |
| BlendWithColorsFromGradient(gradient, colors, found_non_transparent_color, |
| found_opaque_color, layout_object); |
| } |
| } |
| |
| PhysicalRect GetNodeRect(Node* node) { |
| PhysicalRect rect = node->BoundingBox(); |
| Document* document = &node->GetDocument(); |
| while (!document->IsInMainFrame()) { |
| HTMLFrameOwnerElement* owner_element = document->LocalOwner(); |
| if (!owner_element) |
| break; |
| rect.offset.left += owner_element->BoundingBox().offset.left; |
| rect.offset.top += owner_element->BoundingBox().offset.top; |
| document = &owner_element->GetDocument(); |
| } |
| return rect; |
| } |
| |
| } // namespace |
| |
| InspectorContrast::InspectorContrast(Document* document) { |
| if (!document->IsInMainFrame()) { |
| // If document is in a frame, use the top level document to collect nodes |
| // for all frames. |
| for (HTMLFrameOwnerElement* owner_element = document->LocalOwner(); |
| owner_element; |
| owner_element = owner_element->GetDocument().LocalOwner()) { |
| document = &owner_element->GetDocument(); |
| } |
| } |
| |
| document_ = document; |
| } |
| |
| void InspectorContrast::CollectNodesAndBuildRTreeIfNeeded() { |
| TRACE_EVENT0("devtools.contrast", |
| "InspectorContrast::CollectNodesAndBuildRTreeIfNeeded"); |
| |
| if (rtree_built_) |
| return; |
| |
| LocalFrame* frame = document_->GetFrame(); |
| if (!frame) |
| return; |
| LayoutView* layout_view = frame->ContentLayoutObject(); |
| if (!layout_view) |
| return; |
| |
| if (!layout_view->GetFrameView()->UpdateLifecycleToPrePaintClean( |
| DocumentUpdateReason::kInspector)) { |
| return; |
| } |
| |
| InspectorDOMAgent::CollectNodes( |
| document_, INT_MAX, true, |
| WTF::BindRepeating(&NodeIsElementWithLayoutObject), &elements_); |
| SortElementsByPaintOrder(elements_, document_); |
| rtree_.Build( |
| elements_, |
| [](const HeapVector<Member<Node>>& items, size_t index) { |
| return PixelSnappedIntRect(GetNodeRect(items[index])); |
| }, |
| [](const HeapVector<Member<Node>>& items, size_t index) { |
| return items[index]; |
| }); |
| |
| rtree_built_ = true; |
| } |
| |
| std::vector<ContrastInfo> InspectorContrast::GetElementsWithContrastIssues( |
| bool report_aaa, |
| size_t max_elements = 0) { |
| TRACE_EVENT0("devtools.contrast", |
| "InspectorContrast::GetElementsWithContrastIssues"); |
| CollectNodesAndBuildRTreeIfNeeded(); |
| std::vector<ContrastInfo> result; |
| for (Node* node : elements_) { |
| auto info = GetContrast(To<Element>(node)); |
| if (info.able_to_compute_contrast && |
| ((info.contrast_ratio < info.threshold_aa) || |
| (info.contrast_ratio < info.threshold_aaa && report_aaa))) { |
| result.push_back(std::move(info)); |
| if (max_elements && result.size() >= max_elements) |
| return result; |
| } |
| } |
| return result; |
| } |
| |
| static bool IsLargeFont(const TextInfo& text_info) { |
| String font_size_css = text_info.font_size; |
| String font_weight = text_info.font_weight; |
| // font_size_css always has 'px' appended at the end; |
| String font_size_str = font_size_css.Substring(0, font_size_css.length() - 2); |
| double font_size_px = font_size_str.ToDouble(); |
| double font_size_pt = font_size_px * 72 / 96; |
| bool is_bold = font_weight == "bold" || font_weight == "bolder" || |
| font_weight == "600" || font_weight == "700" || |
| font_weight == "800" || font_weight == "900"; |
| if (is_bold) { |
| return font_size_pt >= 14; |
| } |
| return font_size_pt >= 18; |
| } |
| |
| ContrastInfo InspectorContrast::GetContrast(Element* top_element) { |
| TRACE_EVENT0("devtools.contrast", "InspectorContrast::GetContrast"); |
| |
| ContrastInfo result; |
| |
| auto* text_node = DynamicTo<Text>(top_element->firstChild()); |
| if (!text_node || text_node->nextSibling()) |
| return result; |
| |
| const String& text = text_node->data().StripWhiteSpace(); |
| if (text.IsEmpty()) |
| return result; |
| |
| const LayoutObject* layout_object = top_element->GetLayoutObject(); |
| const CSSValue* text_color_value = ComputedStyleUtils::ComputedPropertyValue( |
| CSSProperty::Get(CSSPropertyID::kColor), layout_object->StyleRef()); |
| if (!text_color_value->IsColorValue()) |
| return result; |
| |
| float text_opacity = 1.0f; |
| Vector<Color> bgcolors = GetBackgroundColors(top_element, &text_opacity); |
| // TODO(crbug/1174511): Compute contrast only if the element has a single |
| // color background to be consistent with the current UI. In the future, we |
| // should return a range of contrast values. |
| if (bgcolors.size() != 1) |
| return result; |
| |
| Color text_color = |
| static_cast<const cssvalue::CSSColorValue*>(text_color_value)->Value(); |
| |
| text_color = text_color.CombineWithAlpha(text_opacity); |
| |
| float contrast_ratio = color_utils::GetContrastRatio( |
| SkColor(bgcolors.at(0).Blend(text_color)), SkColor(bgcolors.at(0))); |
| |
| auto text_info = GetTextInfo(top_element); |
| bool is_large_font = IsLargeFont(text_info); |
| |
| result.able_to_compute_contrast = true; |
| result.contrast_ratio = contrast_ratio; |
| result.threshold_aa = is_large_font ? 3.0 : 4.5; |
| result.threshold_aaa = is_large_font ? 4.5 : 7.0; |
| result.font_size = text_info.font_size; |
| result.font_weight = text_info.font_weight; |
| result.element = top_element; |
| |
| return result; |
| } |
| |
| TextInfo InspectorContrast::GetTextInfo(Element* element) { |
| TextInfo info; |
| auto* computed_style_info = |
| MakeGarbageCollected<CSSComputedStyleDeclaration>(element, true); |
| const CSSValue* font_size = |
| computed_style_info->GetPropertyCSSValue(CSSPropertyID::kFontSize); |
| if (font_size) |
| info.font_size = font_size->CssText(); |
| const CSSValue* font_weight = |
| computed_style_info->GetPropertyCSSValue(CSSPropertyID::kFontWeight); |
| if (font_weight) |
| info.font_weight = font_weight->CssText(); |
| return info; |
| } |
| |
| Vector<Color> InspectorContrast::GetBackgroundColors(Element* element, |
| float* text_opacity) { |
| Vector<Color> colors; |
| // TODO: only support the single text child node here. |
| // Follow up with a larger fix post-merge. |
| auto* text_node = DynamicTo<Text>(element->firstChild()); |
| if (!text_node || element->firstChild()->nextSibling()) { |
| return colors; |
| } |
| |
| PhysicalRect content_bounds = GetNodeRect(text_node); |
| LocalFrameView* view = text_node->GetDocument().View(); |
| if (!view) |
| return colors; |
| |
| // Start with the "default" page color (typically white). |
| colors.push_back(view->BaseBackgroundColor()); |
| |
| GetColorsFromRect(content_bounds, text_node->GetDocument(), element, colors, |
| text_opacity); |
| |
| return colors; |
| } |
| |
| // Get the elements which overlap the given rectangle. |
| std::vector<Member<Node>> InspectorContrast::ElementsFromRect( |
| const PhysicalRect& rect, |
| Document& document) { |
| CollectNodesAndBuildRTreeIfNeeded(); |
| std::vector<Member<Node>> overlapping_elements; |
| rtree_.Search(PixelSnappedIntRect(rect), &overlapping_elements); |
| return overlapping_elements; |
| } |
| |
| bool InspectorContrast::GetColorsFromRect(PhysicalRect rect, |
| Document& document, |
| Element* top_element, |
| Vector<Color>& colors, |
| float* text_opacity) { |
| std::vector<Member<Node>> elements_under_rect = |
| ElementsFromRect(rect, document); |
| |
| bool found_opaque_color = false; |
| bool found_top_element = false; |
| |
| *text_opacity = 1.0f; |
| |
| for (auto e = elements_under_rect.begin(); |
| !found_top_element && e != elements_under_rect.end(); ++e) { |
| const Element* element = To<Element>(e->Get()); |
| if (element == top_element) |
| found_top_element = true; |
| |
| const LayoutObject* layout_object = element->GetLayoutObject(); |
| |
| if (IsA<HTMLCanvasElement>(element) || IsA<HTMLEmbedElement>(element) || |
| IsA<HTMLImageElement>(element) || IsA<HTMLObjectElement>(element) || |
| IsA<HTMLPictureElement>(element) || element->IsSVGElement() || |
| IsA<HTMLVideoElement>(element)) { |
| colors.clear(); |
| found_opaque_color = false; |
| continue; |
| } |
| |
| const ComputedStyle* style = layout_object->Style(); |
| if (!style) |
| continue; |
| |
| // If background elements are hidden, ignore their background colors. |
| if (element != top_element && style->Visibility() == EVisibility::kHidden) |
| continue; |
| |
| Color background_color = |
| style->VisitedDependentColor(GetCSSPropertyBackgroundColor()); |
| |
| // Opacity applies to the entire element so mix it with the alpha channel. |
| if (style->HasOpacity()) { |
| background_color = background_color.CombineWithAlpha( |
| background_color.Alpha() / 255 * style->Opacity()); |
| // If the background element is the ancestor of the top element or is the |
| // top element, the opacity affects the text color of the top element. |
| if (element == top_element || |
| FlatTreeTraversal::IsDescendantOf(*top_element, *element)) { |
| *text_opacity *= style->Opacity(); |
| } |
| } |
| |
| bool found_non_transparent_color = false; |
| if (background_color.Alpha() != 0) { |
| found_non_transparent_color = true; |
| if (background_color.HasAlpha()) { |
| if (colors.IsEmpty()) { |
| colors.push_back(background_color); |
| } else { |
| for (auto& color : colors) |
| color = color.Blend(background_color); |
| } |
| } else { |
| colors.clear(); |
| colors.push_back(background_color); |
| found_opaque_color = true; |
| } |
| } |
| |
| AddColorsFromImageStyle(*style, *layout_object, colors, found_opaque_color, |
| found_non_transparent_color); |
| |
| bool contains = found_top_element || GetNodeRect(e->Get()).Contains(rect); |
| if (!contains && found_non_transparent_color) { |
| // Only return colors if some opaque element covers up this one. |
| colors.clear(); |
| found_opaque_color = false; |
| } |
| } |
| return found_opaque_color; |
| } |
| |
| // Sorts unsorted_elements in place, first painted go first. |
| void InspectorContrast::SortElementsByPaintOrder( |
| HeapVector<Member<Node>>& unsorted_elements, |
| Document* document) { |
| std::unique_ptr<InspectorDOMSnapshotAgent::PaintOrderMap> paint_layer_tree = |
| InspectorDOMSnapshotAgent::BuildPaintLayerTree(document); |
| |
| std::stable_sort( |
| unsorted_elements.begin(), unsorted_elements.end(), |
| [&paint_layer_tree = paint_layer_tree](Node* a, Node* b) { |
| const LayoutObject* a_layout = To<Element>(a)->GetLayoutObject(); |
| const LayoutObject* b_layout = To<Element>(b)->GetLayoutObject(); |
| int a_order = 0; |
| int b_order = 0; |
| |
| auto a_item = paint_layer_tree->find(a_layout->PaintingLayer()); |
| if (a_item != paint_layer_tree->end()) |
| a_order = a_item->value; |
| |
| auto b_item = paint_layer_tree->find(b_layout->PaintingLayer()); |
| if (b_item != paint_layer_tree->end()) |
| b_order = b_item->value; |
| |
| return a_order < b_order; |
| }); |
| } |
| |
| } // namespace blink |