blob: 8fd023450cefadb5565c867073e588208c685895 [file] [log] [blame]
// Copyright 2014 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/paint/svg_inline_text_box_painter.h"
#include <memory>
#include "base/optional.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/markers/text_match_marker.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h"
#include "third_party/blink/renderer/core/layout/layout_theme.h"
#include "third_party/blink/renderer/core/layout/line/inline_flow_box.h"
#include "third_party/blink/renderer/core/layout/svg/layout_svg_inline_text.h"
#include "third_party/blink/renderer/core/layout/svg/line/svg_inline_text_box.h"
#include "third_party/blink/renderer/core/layout/svg/svg_layout_support.h"
#include "third_party/blink/renderer/core/layout/svg/svg_resources_cache.h"
#include "third_party/blink/renderer/core/paint/inline_text_box_painter.h"
#include "third_party/blink/renderer/core/paint/paint_info.h"
#include "third_party/blink/renderer/core/paint/selection_painting_utils.h"
#include "third_party/blink/renderer/core/paint/svg_paint_context.h"
#include "third_party/blink/renderer/core/style/applied_text_decoration.h"
#include "third_party/blink/renderer/core/style/shadow_list.h"
#include "third_party/blink/renderer/platform/fonts/text_run_paint_info.h"
#include "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h"
#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
namespace blink {
static inline bool TextShouldBePainted(
const LayoutSVGInlineText& text_layout_object) {
// Font::pixelSize(), returns FontDescription::computedPixelSize(), which
// returns "int(x + 0.5)". If the absolute font size on screen is below
// x=0.5, don't render anything.
return text_layout_object.ScaledFont()
.GetFontDescription()
.ComputedPixelSize();
}
bool SVGInlineTextBoxPainter::ShouldPaintSelection(
const PaintInfo& paint_info) const {
// Don't paint selections when printing.
if (paint_info.IsPrinting())
return false;
// Don't paint selections when rendering a mask, clip-path (as a mask),
// pattern or feImage (element reference.)
if (paint_info.IsRenderingResourceSubtree())
return false;
return svg_inline_text_box_.IsSelected();
}
static bool HasShadow(const PaintInfo& paint_info, const ComputedStyle& style) {
// Text shadows are disabled when printing. http://crbug.com/258321
return style.TextShadow() && !paint_info.IsPrinting();
}
LayoutObject& SVGInlineTextBoxPainter::InlineLayoutObject() const {
return *LineLayoutAPIShim::LayoutObjectFrom(
svg_inline_text_box_.GetLineLayoutItem());
}
LayoutObject& SVGInlineTextBoxPainter::ParentInlineLayoutObject() const {
return *LineLayoutAPIShim::LayoutObjectFrom(
svg_inline_text_box_.Parent()->GetLineLayoutItem());
}
LayoutSVGInlineText& SVGInlineTextBoxPainter::InlineText() const {
return ToLayoutSVGInlineText(InlineLayoutObject());
}
void SVGInlineTextBoxPainter::Paint(const PaintInfo& paint_info,
const LayoutPoint& paint_offset) {
DCHECK(paint_info.phase == PaintPhase::kForeground ||
paint_info.phase == PaintPhase::kSelection);
DCHECK(svg_inline_text_box_.Truncation() == kCNoTruncation);
if (svg_inline_text_box_.GetLineLayoutItem().StyleRef().Visibility() !=
EVisibility::kVisible ||
!svg_inline_text_box_.Len())
return;
// We're explicitly not supporting composition & custom underlines and custom
// highlighters -- unlike InlineTextBox. If we ever need that for SVG, it's
// very easy to refactor and reuse the code.
bool have_selection = ShouldPaintSelection(paint_info);
if (!have_selection && paint_info.phase == PaintPhase::kSelection)
return;
LayoutSVGInlineText& text_layout_object = InlineText();
if (!TextShouldBePainted(text_layout_object))
return;
DisplayItem::Type display_item_type =
DisplayItem::PaintPhaseToDrawingType(paint_info.phase);
if (!DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, svg_inline_text_box_, display_item_type)) {
LayoutObject& parent_layout_object = ParentInlineLayoutObject();
const ComputedStyle& style = parent_layout_object.StyleRef();
DrawingRecorder recorder(paint_info.context, svg_inline_text_box_,
display_item_type);
InlineTextBoxPainter text_painter(svg_inline_text_box_);
const DocumentMarkerVector& markers_to_paint =
text_painter.ComputeMarkersToPaint();
text_painter.PaintDocumentMarkers(
markers_to_paint, paint_info, paint_offset, style,
text_layout_object.ScaledFont(), DocumentMarkerPaintPhase::kBackground);
if (!svg_inline_text_box_.TextFragments().IsEmpty())
PaintTextFragments(paint_info, parent_layout_object);
text_painter.PaintDocumentMarkers(
markers_to_paint, paint_info, paint_offset, style,
text_layout_object.ScaledFont(), DocumentMarkerPaintPhase::kForeground);
}
}
void SVGInlineTextBoxPainter::PaintTextFragments(
const PaintInfo& paint_info,
LayoutObject& parent_layout_object) {
const ComputedStyle& style = parent_layout_object.StyleRef();
const SVGComputedStyle& svg_style = style.SvgStyle();
bool has_fill = svg_style.HasFill();
bool has_visible_stroke = svg_style.HasVisibleStroke();
const ComputedStyle* selection_style = &style;
bool should_paint_selection = ShouldPaintSelection(paint_info);
if (should_paint_selection) {
selection_style =
parent_layout_object.GetCachedPseudoStyle(kPseudoIdSelection);
if (selection_style) {
const SVGComputedStyle& svg_selection_style = selection_style->SvgStyle();
if (!has_fill)
has_fill = svg_selection_style.HasFill();
if (!has_visible_stroke)
has_visible_stroke = svg_selection_style.HasVisibleStroke();
} else {
selection_style = &style;
}
}
if (paint_info.IsRenderingClipPathAsMaskImage()) {
has_fill = true;
has_visible_stroke = false;
}
for (const SVGTextFragment& fragment : svg_inline_text_box_.TextFragments()) {
GraphicsContextStateSaver state_saver(paint_info.context, false);
base::Optional<AffineTransform> shader_transform;
if (fragment.IsTransformed()) {
state_saver.Save();
const auto fragment_transform = fragment.BuildFragmentTransform();
paint_info.context.ConcatCTM(fragment_transform);
DCHECK(fragment_transform.IsInvertible());
shader_transform = fragment_transform.Inverse();
}
// Spec: All text decorations except line-through should be drawn before the
// text is filled and stroked; thus, the text is rendered on top of these
// decorations.
const Vector<AppliedTextDecoration>& decorations =
style.AppliedTextDecorations();
for (const AppliedTextDecoration& decoration : decorations) {
if (EnumHasFlags(decoration.Lines(), TextDecoration::kUnderline))
PaintDecoration(paint_info, TextDecoration::kUnderline, fragment);
if (EnumHasFlags(decoration.Lines(), TextDecoration::kOverline))
PaintDecoration(paint_info, TextDecoration::kOverline, fragment);
}
for (int i = 0; i < 3; i++) {
switch (svg_style.PaintOrderType(i)) {
case PT_FILL:
if (has_fill) {
PaintText(paint_info, style, *selection_style, fragment,
kApplyToFillMode, should_paint_selection,
base::OptionalOrNullptr(shader_transform));
}
break;
case PT_STROKE:
if (has_visible_stroke) {
PaintText(paint_info, style, *selection_style, fragment,
kApplyToStrokeMode, should_paint_selection,
base::OptionalOrNullptr(shader_transform));
}
break;
case PT_MARKERS:
// Markers don't apply to text
break;
default:
NOTREACHED();
break;
}
}
// Spec: Line-through should be drawn after the text is filled and stroked;
// thus, the line-through is rendered on top of the text.
for (const AppliedTextDecoration& decoration : decorations) {
if (EnumHasFlags(decoration.Lines(), TextDecoration::kLineThrough))
PaintDecoration(paint_info, TextDecoration::kLineThrough, fragment);
}
}
}
void SVGInlineTextBoxPainter::PaintSelectionBackground(
const PaintInfo& paint_info) {
if (svg_inline_text_box_.GetLineLayoutItem().StyleRef().Visibility() !=
EVisibility::kVisible)
return;
DCHECK(!paint_info.IsPrinting());
if (paint_info.phase == PaintPhase::kSelection ||
!ShouldPaintSelection(paint_info))
return;
auto layout_item = svg_inline_text_box_.GetLineLayoutItem();
Color background_color = SelectionPaintingUtils::SelectionBackgroundColor(
layout_item.GetDocument(), layout_item.StyleRef(), layout_item.GetNode());
if (!background_color.Alpha())
return;
LayoutSVGInlineText& text_layout_object = InlineText();
if (!TextShouldBePainted(text_layout_object))
return;
const ComputedStyle& style =
svg_inline_text_box_.Parent()->GetLineLayoutItem().StyleRef();
int start_position, end_position;
svg_inline_text_box_.SelectionStartEnd(start_position, end_position);
const Vector<SVGTextFragmentWithRange> fragment_info_list =
CollectFragmentsInRange(start_position, end_position);
for (const SVGTextFragmentWithRange& fragment_with_range :
fragment_info_list) {
const SVGTextFragment& fragment = fragment_with_range.fragment;
GraphicsContextStateSaver state_saver(paint_info.context);
if (fragment.IsTransformed())
paint_info.context.ConcatCTM(fragment.BuildFragmentTransform());
paint_info.context.SetFillColor(background_color);
paint_info.context.FillRect(
svg_inline_text_box_.SelectionRectForTextFragment(
fragment, fragment_with_range.start_position,
fragment_with_range.end_position, style),
background_color);
}
}
static inline LayoutObject* FindLayoutObjectDefininingTextDecoration(
InlineFlowBox* parent_box) {
// Lookup first layout object in parent hierarchy which has text-decoration
// set.
LayoutObject* layout_object = nullptr;
while (parent_box) {
layout_object =
LineLayoutAPIShim::LayoutObjectFrom(parent_box->GetLineLayoutItem());
if (layout_object->Style() &&
layout_object->StyleRef().GetTextDecoration() != TextDecoration::kNone)
break;
parent_box = parent_box->Parent();
}
DCHECK(layout_object);
return layout_object;
}
// Offset from the baseline for |decoration|. Positive offsets are above the
// baseline.
static inline float BaselineOffsetForDecoration(TextDecoration decoration,
const FontMetrics& font_metrics,
float thickness) {
// FIXME: For SVG Fonts we need to use the attributes defined in the
// <font-face> if specified.
// Compatible with Batik/Presto.
if (decoration == TextDecoration::kUnderline)
return -thickness * 1.5f;
if (decoration == TextDecoration::kOverline)
return font_metrics.FloatAscent() - thickness;
if (decoration == TextDecoration::kLineThrough)
return font_metrics.FloatAscent() * 3 / 8.0f;
NOTREACHED();
return 0.0f;
}
static inline float ThicknessForDecoration(TextDecoration, const Font& font) {
// FIXME: For SVG Fonts we need to use the attributes defined in the
// <font-face> if specified.
// Compatible with Batik/Presto
return font.GetFontDescription().ComputedSize() / 20.0f;
}
void SVGInlineTextBoxPainter::PaintDecoration(const PaintInfo& paint_info,
TextDecoration decoration,
const SVGTextFragment& fragment) {
if (svg_inline_text_box_.GetLineLayoutItem()
.StyleRef()
.TextDecorationsInEffect() == TextDecoration::kNone)
return;
if (fragment.width <= 0)
return;
// Find out which style defined the text-decoration, as its fill/stroke
// properties have to be used for drawing instead of ours.
LayoutObject* decoration_layout_object =
FindLayoutObjectDefininingTextDecoration(svg_inline_text_box_.Parent());
const ComputedStyle& decoration_style = decoration_layout_object->StyleRef();
if (decoration_style.Visibility() != EVisibility::kVisible)
return;
float scaling_factor = 1;
Font scaled_font;
LayoutSVGInlineText::ComputeNewScaledFontForStyle(
*decoration_layout_object, scaling_factor, scaled_font);
DCHECK(scaling_factor);
float thickness = ThicknessForDecoration(decoration, scaled_font);
if (thickness <= 0)
return;
const SimpleFontData* font_data = scaled_font.PrimaryFont();
DCHECK(font_data);
if (!font_data)
return;
float decoration_offset = BaselineOffsetForDecoration(
decoration, font_data->GetFontMetrics(), thickness);
FloatPoint decoration_origin(fragment.x,
fragment.y - decoration_offset / scaling_factor);
Path path;
path.AddRect(
FloatRect(decoration_origin,
FloatSize(fragment.width, thickness / scaling_factor)));
const SVGComputedStyle& svg_decoration_style = decoration_style.SvgStyle();
for (int i = 0; i < 3; i++) {
switch (svg_decoration_style.PaintOrderType(i)) {
case PT_FILL:
if (svg_decoration_style.HasFill()) {
PaintFlags fill_flags;
if (!SVGPaintContext::PaintForLayoutObject(
paint_info, decoration_style, *decoration_layout_object,
kApplyToFillMode, fill_flags))
break;
fill_flags.setAntiAlias(true);
paint_info.context.DrawPath(path.GetSkPath(), fill_flags);
}
break;
case PT_STROKE:
if (svg_decoration_style.HasVisibleStroke()) {
PaintFlags stroke_flags;
if (!SVGPaintContext::PaintForLayoutObject(
paint_info, decoration_style, *decoration_layout_object,
kApplyToStrokeMode, stroke_flags))
break;
stroke_flags.setAntiAlias(true);
float stroke_scale_factor =
svg_decoration_style.VectorEffect() == VE_NON_SCALING_STROKE
? 1 / scaling_factor
: 1;
StrokeData stroke_data;
SVGLayoutSupport::ApplyStrokeStyleToStrokeData(
stroke_data, decoration_style, *decoration_layout_object,
stroke_scale_factor);
if (stroke_scale_factor != 1)
stroke_data.SetThickness(stroke_data.Thickness() *
stroke_scale_factor);
stroke_data.SetupPaint(&stroke_flags);
paint_info.context.DrawPath(path.GetSkPath(), stroke_flags);
}
break;
case PT_MARKERS:
break;
default:
NOTREACHED();
}
}
}
bool SVGInlineTextBoxPainter::SetupTextPaint(
const PaintInfo& paint_info,
const ComputedStyle& style,
LayoutSVGResourceMode resource_mode,
PaintFlags& flags,
const AffineTransform* shader_transform) {
LayoutSVGInlineText& text_layout_object = InlineText();
float scaling_factor = text_layout_object.ScalingFactor();
DCHECK(scaling_factor);
base::Optional<AffineTransform> paint_server_transform;
if (scaling_factor != 1 || shader_transform) {
paint_server_transform.emplace();
// Adjust the paint-server coordinate space.
paint_server_transform->Scale(scaling_factor);
if (shader_transform)
paint_server_transform->Multiply(*shader_transform);
}
if (!SVGPaintContext::PaintForLayoutObject(
paint_info, style, ParentInlineLayoutObject(), resource_mode, flags,
base::OptionalOrNullptr(paint_server_transform)))
return false;
flags.setAntiAlias(true);
if (HasShadow(paint_info, style)) {
flags.setLooper(style.TextShadow()->CreateDrawLooper(
DrawLooperBuilder::kShadowRespectsAlpha,
style.VisitedDependentColor(GetCSSPropertyColor())));
}
if (resource_mode == kApplyToStrokeMode) {
// The stroke geometry needs be generated based on the scaled font.
float stroke_scale_factor =
style.SvgStyle().VectorEffect() != VE_NON_SCALING_STROKE
? scaling_factor
: 1;
StrokeData stroke_data;
SVGLayoutSupport::ApplyStrokeStyleToStrokeData(
stroke_data, style, ParentInlineLayoutObject(), stroke_scale_factor);
if (stroke_scale_factor != 1)
stroke_data.SetThickness(stroke_data.Thickness() * stroke_scale_factor);
stroke_data.SetupPaint(&flags);
}
return true;
}
void SVGInlineTextBoxPainter::PaintText(const PaintInfo& paint_info,
TextRun& text_run,
const SVGTextFragment& fragment,
int start_position,
int end_position,
const PaintFlags& flags) {
LayoutSVGInlineText& text_layout_object = InlineText();
const Font& scaled_font = text_layout_object.ScaledFont();
float scaling_factor = text_layout_object.ScalingFactor();
DCHECK(scaling_factor);
FloatPoint text_origin(fragment.x, fragment.y);
FloatSize text_size(fragment.width, fragment.height);
GraphicsContext& context = paint_info.context;
GraphicsContextStateSaver state_saver(context, false);
if (scaling_factor != 1) {
text_origin.Scale(scaling_factor, scaling_factor);
text_size.Scale(scaling_factor);
state_saver.Save();
context.Scale(1 / scaling_factor, 1 / scaling_factor);
}
TextRunPaintInfo text_run_paint_info(text_run);
text_run_paint_info.from = start_position;
text_run_paint_info.to = end_position;
const SimpleFontData* font_data = scaled_font.PrimaryFont();
DCHECK(font_data);
if (!font_data)
return;
float baseline = font_data->GetFontMetrics().FloatAscent();
text_run_paint_info.bounds =
FloatRect(text_origin.X(), text_origin.Y() - baseline, text_size.Width(),
text_size.Height());
context.DrawText(scaled_font, text_run_paint_info, text_origin, flags);
// TODO(npm): Check that there are non-whitespace characters. See
// crbug.com/788444.
context.GetPaintController().SetTextPainted();
}
void SVGInlineTextBoxPainter::PaintText(
const PaintInfo& paint_info,
const ComputedStyle& style,
const ComputedStyle& selection_style,
const SVGTextFragment& fragment,
LayoutSVGResourceMode resource_mode,
bool should_paint_selection,
const AffineTransform* shader_transform) {
int start_position = 0;
int end_position = 0;
if (should_paint_selection) {
svg_inline_text_box_.SelectionStartEnd(start_position, end_position);
should_paint_selection =
svg_inline_text_box_.MapStartEndPositionsIntoFragmentCoordinates(
fragment, start_position, end_position);
}
// Fast path if there is no selection, just draw the whole chunk part using
// the regular style.
TextRun text_run = svg_inline_text_box_.ConstructTextRun(style, fragment);
if (!should_paint_selection || start_position >= end_position) {
PaintFlags flags;
if (SetupTextPaint(paint_info, style, resource_mode, flags,
shader_transform))
PaintText(paint_info, text_run, fragment, 0, fragment.length, flags);
return;
}
// Eventually draw text using regular style until the start position of the
// selection.
bool paint_selected_text_only = paint_info.phase == PaintPhase::kSelection;
if (start_position > 0 && !paint_selected_text_only) {
PaintFlags flags;
if (SetupTextPaint(paint_info, style, resource_mode, flags,
shader_transform))
PaintText(paint_info, text_run, fragment, 0, start_position, flags);
}
// Draw text using selection style from the start to the end position of the
// selection.
{
SVGResourcesCache::TemporaryStyleScope scope(ParentInlineLayoutObject(),
style, selection_style);
PaintFlags flags;
if (SetupTextPaint(paint_info, selection_style, resource_mode, flags,
shader_transform)) {
PaintText(paint_info, text_run, fragment, start_position, end_position,
flags);
}
}
// Eventually draw text using regular style from the end position of the
// selection to the end of the current chunk part.
if (end_position < static_cast<int>(fragment.length) &&
!paint_selected_text_only) {
PaintFlags flags;
if (SetupTextPaint(paint_info, style, resource_mode, flags,
shader_transform)) {
PaintText(paint_info, text_run, fragment, end_position, fragment.length,
flags);
}
}
}
Vector<SVGTextFragmentWithRange> SVGInlineTextBoxPainter::CollectTextMatches(
const DocumentMarker& marker) const {
const Vector<SVGTextFragmentWithRange> empty_text_match_list;
// SVG does not support grammar or spellcheck markers, so skip anything but
// TextMatch.
if (marker.GetType() != DocumentMarker::kTextMatch)
return empty_text_match_list;
if (!InlineLayoutObject()
.GetFrame()
->GetEditor()
.MarkedTextMatchesAreHighlighted())
return empty_text_match_list;
int marker_start_position =
std::max<int>(marker.StartOffset() - svg_inline_text_box_.Start(), 0);
int marker_end_position =
std::min<int>(marker.EndOffset() - svg_inline_text_box_.Start(),
svg_inline_text_box_.Len());
if (marker_start_position >= marker_end_position)
return empty_text_match_list;
return CollectFragmentsInRange(marker_start_position, marker_end_position);
}
Vector<SVGTextFragmentWithRange>
SVGInlineTextBoxPainter::CollectFragmentsInRange(int start_position,
int end_position) const {
Vector<SVGTextFragmentWithRange> fragment_info_list;
for (const SVGTextFragment& fragment : svg_inline_text_box_.TextFragments()) {
// TODO(ramya.v): If these can't be negative we should use unsigned.
int fragment_start_position = start_position;
int fragment_end_position = end_position;
if (!svg_inline_text_box_.MapStartEndPositionsIntoFragmentCoordinates(
fragment, fragment_start_position, fragment_end_position))
continue;
fragment_info_list.push_back(SVGTextFragmentWithRange(
fragment, fragment_start_position, fragment_end_position));
}
return fragment_info_list;
}
void SVGInlineTextBoxPainter::PaintTextMatchMarkerForeground(
const PaintInfo& paint_info,
const LayoutPoint& point,
const TextMatchMarker& marker,
const ComputedStyle& style,
const Font& font) {
const Vector<SVGTextFragmentWithRange> text_match_info_list =
CollectTextMatches(marker);
if (text_match_info_list.IsEmpty())
return;
Color text_color =
LayoutTheme::GetTheme().PlatformTextSearchColor(marker.IsActiveMatch());
PaintFlags fill_flags;
fill_flags.setColor(text_color.Rgb());
fill_flags.setAntiAlias(true);
PaintFlags stroke_flags;
bool should_paint_stroke = false;
if (SetupTextPaint(paint_info, style, kApplyToStrokeMode, stroke_flags,
nullptr)) {
should_paint_stroke = true;
stroke_flags.setLooper(nullptr);
stroke_flags.setColor(text_color.Rgb());
}
for (const SVGTextFragmentWithRange& text_match_info : text_match_info_list) {
const SVGTextFragment& fragment = text_match_info.fragment;
GraphicsContextStateSaver state_saver(paint_info.context);
if (fragment.IsTransformed())
paint_info.context.ConcatCTM(fragment.BuildFragmentTransform());
TextRun text_run = svg_inline_text_box_.ConstructTextRun(style, fragment);
PaintText(paint_info, text_run, fragment, text_match_info.start_position,
text_match_info.end_position, fill_flags);
if (should_paint_stroke) {
PaintText(paint_info, text_run, fragment, text_match_info.start_position,
text_match_info.end_position, stroke_flags);
}
}
}
void SVGInlineTextBoxPainter::PaintTextMatchMarkerBackground(
const PaintInfo& paint_info,
const LayoutPoint& point,
const TextMatchMarker& marker,
const ComputedStyle& style,
const Font& font) {
const Vector<SVGTextFragmentWithRange> text_match_info_list =
CollectTextMatches(marker);
if (text_match_info_list.IsEmpty())
return;
Color color = LayoutTheme::GetTheme().PlatformTextSearchHighlightColor(
marker.IsActiveMatch());
for (const SVGTextFragmentWithRange& text_match_info : text_match_info_list) {
const SVGTextFragment& fragment = text_match_info.fragment;
GraphicsContextStateSaver state_saver(paint_info.context, false);
if (fragment.IsTransformed()) {
state_saver.Save();
paint_info.context.ConcatCTM(fragment.BuildFragmentTransform());
}
FloatRect fragment_rect = svg_inline_text_box_.SelectionRectForTextFragment(
fragment, text_match_info.start_position, text_match_info.end_position,
style);
paint_info.context.SetFillColor(color);
paint_info.context.FillRect(fragment_rect);
}
}
} // namespace blink