blob: a1439b86bdba7139e3b7f7aef48d70d33519df65 [file] [log] [blame]
/*
* Copyright (C) Research In Motion Limited 2010-2012. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "core/layout/svg/SVGTextLayoutEngine.h"
#include "core/layout/api/LineLayoutAPIShim.h"
#include "core/layout/api/LineLayoutSVGTextPath.h"
#include "core/layout/svg/LayoutSVGInlineText.h"
#include "core/layout/svg/SVGTextChunkBuilder.h"
#include "core/layout/svg/SVGTextLayoutEngineBaseline.h"
#include "core/layout/svg/SVGTextLayoutEngineSpacing.h"
#include "core/layout/svg/line/SVGInlineFlowBox.h"
#include "core/layout/svg/line/SVGInlineTextBox.h"
#include "core/svg/SVGElement.h"
#include "core/svg/SVGLengthContext.h"
#include "core/svg/SVGTextContentElement.h"
#include "platform/wtf/AutoReset.h"
namespace blink {
SVGTextLayoutEngine::SVGTextLayoutEngine(
const Vector<LayoutSVGInlineText*>& descendant_text_nodes)
: descendant_text_nodes_(descendant_text_nodes),
current_logical_text_node_index_(0),
logical_character_offset_(0),
logical_metrics_list_offset_(0),
is_vertical_text_(false),
in_path_layout_(false),
text_length_spacing_in_effect_(false),
text_path_(nullptr),
text_path_current_offset_(0),
text_path_displacement_(0),
text_path_spacing_(0),
text_path_scaling_(1) {
DCHECK(!descendant_text_nodes_.IsEmpty());
}
SVGTextLayoutEngine::~SVGTextLayoutEngine() = default;
bool SVGTextLayoutEngine::SetCurrentTextPosition(const SVGCharacterData& data) {
bool has_x = data.HasX();
if (has_x)
text_position_.SetX(data.x);
bool has_y = data.HasY();
if (has_y)
text_position_.SetY(data.y);
// If there's an absolute x/y position available, it marks the beginning of
// a new position along the path.
if (in_path_layout_) {
// TODO(fs): If a new chunk (== absolute position) is defined while in
// path layout mode, alignment should be based on that chunk and not
// the path as a whole. (Re: the addition of m_textPathStartOffset
// below.)
if (is_vertical_text_) {
if (has_y)
text_path_current_offset_ = data.y + text_path_start_offset_;
} else {
if (has_x)
text_path_current_offset_ = data.x + text_path_start_offset_;
}
}
return has_x || has_y;
}
void SVGTextLayoutEngine::AdvanceCurrentTextPosition(float glyph_advance) {
// TODO(fs): m_textPathCurrentOffset should preferably also be updated
// here, but that requires a bit more untangling yet.
if (is_vertical_text_)
text_position_.SetY(text_position_.Y() + glyph_advance);
else
text_position_.SetX(text_position_.X() + glyph_advance);
}
bool SVGTextLayoutEngine::ApplyRelativePositionAdjustmentsIfNeeded(
const SVGCharacterData& data) {
FloatPoint delta;
bool has_dx = data.HasDx();
if (has_dx)
delta.SetX(data.dx);
bool has_dy = data.HasDy();
if (has_dy)
delta.SetY(data.dy);
// Apply dx/dy value adjustments to current text position, if needed.
text_position_.MoveBy(delta);
if (in_path_layout_) {
if (is_vertical_text_)
delta = delta.TransposedPoint();
text_path_current_offset_ += delta.X();
text_path_displacement_ += delta.Y();
}
return has_dx || has_dy;
}
void SVGTextLayoutEngine::ComputeCurrentFragmentMetrics(
SVGInlineTextBox* text_box) {
LineLayoutSVGInlineText text_line_layout =
LineLayoutSVGInlineText(text_box->GetLineLayoutItem());
TextRun run = text_box->ConstructTextRun(text_line_layout.StyleRef(),
current_text_fragment_);
float scaling_factor = text_line_layout.ScalingFactor();
DCHECK(scaling_factor);
const Font& scaled_font = text_line_layout.ScaledFont();
FloatRect glyph_overflow_bounds;
const SimpleFontData* font_data = scaled_font.PrimaryFont();
DCHECK(font_data);
if (!font_data)
return;
float width = scaled_font.Width(run, nullptr, &glyph_overflow_bounds);
current_text_fragment_.glyph_overflow.SetFromBounds(glyph_overflow_bounds,
scaled_font, width);
current_text_fragment_.glyph_overflow.top /= scaling_factor;
current_text_fragment_.glyph_overflow.left /= scaling_factor;
current_text_fragment_.glyph_overflow.right /= scaling_factor;
current_text_fragment_.glyph_overflow.bottom /= scaling_factor;
float height = font_data->GetFontMetrics().FloatHeight();
current_text_fragment_.height = height / scaling_factor;
current_text_fragment_.width = width / scaling_factor;
}
void SVGTextLayoutEngine::RecordTextFragment(SVGInlineTextBox* text_box) {
DCHECK(!current_text_fragment_.length);
// Figure out length of fragment.
current_text_fragment_.length = visual_metrics_iterator_.CharacterOffset() -
current_text_fragment_.character_offset;
// Figure out fragment metrics.
ComputeCurrentFragmentMetrics(text_box);
text_box->TextFragments().push_back(current_text_fragment_);
current_text_fragment_ = SVGTextFragment();
}
void SVGTextLayoutEngine::BeginTextPathLayout(SVGInlineFlowBox* flow_box) {
// Build text chunks for all <textPath> children, using the line layout
// algorithm. This is needeed as text-anchor is just an additional startOffset
// for text paths.
SVGTextLayoutEngine line_layout(descendant_text_nodes_);
line_layout.text_length_spacing_in_effect_ = text_length_spacing_in_effect_;
line_layout.LayoutCharactersInTextBoxes(flow_box);
in_path_layout_ = true;
LineLayoutSVGTextPath text_path =
LineLayoutSVGTextPath(flow_box->GetLineLayoutItem());
text_path_ = text_path.LayoutPath();
if (!text_path_)
return;
text_path_start_offset_ =
text_path.CalculateStartOffset(text_path_->length());
SVGTextPathChunkBuilder text_path_chunk_layout_builder;
text_path_chunk_layout_builder.ProcessTextChunks(
line_layout.line_layout_boxes_);
text_path_start_offset_ +=
text_path_chunk_layout_builder.TotalTextAnchorShift();
text_path_current_offset_ = text_path_start_offset_;
// Eventually handle textLength adjustments.
SVGLengthAdjustType length_adjust = kSVGLengthAdjustUnknown;
float desired_text_length = 0;
if (SVGTextContentElement* text_content_element =
SVGTextContentElement::ElementFromLineLayoutItem(text_path)) {
SVGLengthContext length_context(text_content_element);
length_adjust =
text_content_element->lengthAdjust()->CurrentValue()->EnumValue();
if (text_content_element->TextLengthIsSpecifiedByUser())
desired_text_length =
text_content_element->textLength()->CurrentValue()->Value(
length_context);
else
desired_text_length = 0;
}
if (!desired_text_length)
return;
float total_length = text_path_chunk_layout_builder.TotalLength();
if (length_adjust == kSVGLengthAdjustSpacing) {
text_path_spacing_ = 0;
if (text_path_chunk_layout_builder.TotalCharacters() > 1) {
text_path_spacing_ = desired_text_length - total_length;
text_path_spacing_ /=
text_path_chunk_layout_builder.TotalCharacters() - 1;
}
} else {
text_path_scaling_ = desired_text_length / total_length;
}
}
void SVGTextLayoutEngine::EndTextPathLayout() {
in_path_layout_ = false;
text_path_ = nullptr;
text_path_start_offset_ = 0;
text_path_current_offset_ = 0;
text_path_spacing_ = 0;
text_path_scaling_ = 1;
}
void SVGTextLayoutEngine::LayoutInlineTextBox(SVGInlineTextBox* text_box) {
DCHECK(text_box);
LineLayoutSVGInlineText text_line_layout =
LineLayoutSVGInlineText(text_box->GetLineLayoutItem());
DCHECK(text_line_layout.Parent());
DCHECK(text_line_layout.Parent().GetNode());
DCHECK(text_line_layout.Parent().GetNode()->IsSVGElement());
const ComputedStyle& style = text_line_layout.StyleRef();
text_box->ClearTextFragments();
is_vertical_text_ = !style.IsHorizontalWritingMode();
LayoutTextOnLineOrPath(text_box, text_line_layout, style);
if (in_path_layout_)
return;
line_layout_boxes_.push_back(text_box);
}
static bool DefinesTextLengthWithSpacing(const InlineFlowBox* start) {
SVGTextContentElement* text_content_element =
SVGTextContentElement::ElementFromLineLayoutItem(
start->GetLineLayoutItem());
return text_content_element &&
text_content_element->lengthAdjust()->CurrentValue()->EnumValue() ==
kSVGLengthAdjustSpacing &&
text_content_element->TextLengthIsSpecifiedByUser();
}
void SVGTextLayoutEngine::LayoutCharactersInTextBoxes(InlineFlowBox* start) {
bool text_length_spacing_in_effect =
text_length_spacing_in_effect_ || DefinesTextLengthWithSpacing(start);
AutoReset<bool> text_length_spacing_scope(&text_length_spacing_in_effect_,
text_length_spacing_in_effect);
for (InlineBox* child = start->FirstChild(); child;
child = child->NextOnLine()) {
if (child->IsSVGInlineTextBox()) {
DCHECK(child->GetLineLayoutItem().IsSVGInlineText());
LayoutInlineTextBox(ToSVGInlineTextBox(child));
} else {
// Skip generated content.
Node* node = child->GetLineLayoutItem().GetNode();
if (!node)
continue;
SVGInlineFlowBox* flow_box = ToSVGInlineFlowBox(child);
bool is_text_path = IsSVGTextPathElement(*node);
if (is_text_path)
BeginTextPathLayout(flow_box);
LayoutCharactersInTextBoxes(flow_box);
if (is_text_path)
EndTextPathLayout();
}
}
}
void SVGTextLayoutEngine::FinishLayout() {
visual_metrics_iterator_ = SVGInlineTextMetricsIterator();
// After all text fragments are stored in their correpsonding
// SVGInlineTextBoxes, we can layout individual text chunks.
// Chunk layouting is only performed for line layout boxes, not for path
// layout, where it has already been done.
SVGTextChunkBuilder chunk_layout_builder;
chunk_layout_builder.ProcessTextChunks(line_layout_boxes_);
line_layout_boxes_.clear();
}
const LayoutSVGInlineText* SVGTextLayoutEngine::NextLogicalTextNode() {
DCHECK_LT(current_logical_text_node_index_, descendant_text_nodes_.size());
++current_logical_text_node_index_;
if (current_logical_text_node_index_ == descendant_text_nodes_.size())
return nullptr;
logical_metrics_list_offset_ = 0;
logical_character_offset_ = 0;
return descendant_text_nodes_[current_logical_text_node_index_];
}
const LayoutSVGInlineText* SVGTextLayoutEngine::CurrentLogicalCharacterMetrics(
SVGTextMetrics& logical_metrics) {
// If we've consumed all text nodes, there can be no more metrics.
if (current_logical_text_node_index_ == descendant_text_nodes_.size())
return nullptr;
const LayoutSVGInlineText* logical_text_node =
descendant_text_nodes_[current_logical_text_node_index_];
const Vector<SVGTextMetrics>* metrics_list =
&logical_text_node->MetricsList();
unsigned metrics_list_size = metrics_list->size();
DCHECK_LE(logical_metrics_list_offset_, metrics_list_size);
// Find the next non-collapsed text metrics cell.
while (true) {
// If we run out of metrics, move to the next set of non-empty layout
// attributes.
if (logical_metrics_list_offset_ == metrics_list_size) {
logical_text_node = NextLogicalTextNode();
if (!logical_text_node)
return nullptr;
metrics_list = &logical_text_node->MetricsList();
metrics_list_size = metrics_list->size();
// Return to the while so that we check if the new metrics list is
// non-empty before using it.
continue;
}
DCHECK(metrics_list_size);
logical_metrics = metrics_list->at(logical_metrics_list_offset_);
// Stop if we found the next valid logical text metrics object.
if (!logical_metrics.IsEmpty())
break;
AdvanceToNextLogicalCharacter(logical_metrics);
}
return logical_text_node;
}
void SVGTextLayoutEngine::AdvanceToNextLogicalCharacter(
const SVGTextMetrics& logical_metrics) {
++logical_metrics_list_offset_;
logical_character_offset_ += logical_metrics.length();
}
void SVGTextLayoutEngine::LayoutTextOnLineOrPath(
SVGInlineTextBox* text_box,
LineLayoutSVGInlineText text_line_layout,
const ComputedStyle& style) {
if (in_path_layout_ && !text_path_)
return;
// Find the start of the current text box in the metrics list.
visual_metrics_iterator_.AdvanceToTextStart(text_line_layout,
text_box->Start());
const Font& font = style.GetFont();
SVGTextLayoutEngineSpacing spacing_layout(font, style.EffectiveZoom());
SVGTextLayoutEngineBaseline baseline_layout(font, style.EffectiveZoom());
bool did_start_text_fragment = false;
bool apply_spacing_to_next_character = false;
bool needs_fragment_per_glyph =
is_vertical_text_ || in_path_layout_ || text_length_spacing_in_effect_;
float last_angle = 0;
float baseline_shift_value = baseline_layout.CalculateBaselineShift(style);
baseline_shift_value -= baseline_layout.CalculateAlignmentBaselineShift(
is_vertical_text_, text_line_layout);
FloatPoint baseline_shift;
if (is_vertical_text_)
baseline_shift.SetX(baseline_shift_value);
else
baseline_shift.SetY(-baseline_shift_value);
// Main layout algorithm.
const unsigned box_end_offset = text_box->Start() + text_box->Len();
while (!visual_metrics_iterator_.IsAtEnd() &&
visual_metrics_iterator_.CharacterOffset() < box_end_offset) {
const SVGTextMetrics& visual_metrics = visual_metrics_iterator_.Metrics();
if (visual_metrics.IsEmpty()) {
visual_metrics_iterator_.Next();
continue;
}
SVGTextMetrics logical_metrics(SVGTextMetrics::kSkippedSpaceMetrics);
const LayoutSVGInlineText* logical_text_node =
CurrentLogicalCharacterMetrics(logical_metrics);
if (!logical_text_node)
break;
const SVGCharacterData data =
logical_text_node->CharacterDataMap().at(logical_character_offset_ + 1);
// TODO(fs): Use the return value to eliminate the additional
// hash-lookup below when determining if this text box should be tagged
// as starting a new text chunk.
SetCurrentTextPosition(data);
// When we've advanced to the box start offset, determine using the original
// x/y values, whether this character starts a new text chunk, before doing
// any further processing.
if (visual_metrics_iterator_.CharacterOffset() == text_box->Start())
text_box->SetStartsNewTextChunk(
logical_text_node->CharacterStartsNewTextChunk(
logical_character_offset_));
bool has_relative_position = ApplyRelativePositionAdjustmentsIfNeeded(data);
// Determine the orientation of the current glyph.
// Font::width() calculates the resolved FontOrientation for each character,
// but that value is not exposed today to avoid the API complexity.
UChar32 current_character = text_line_layout.CodepointAt(
visual_metrics_iterator_.CharacterOffset());
FontOrientation font_orientation = font.GetFontDescription().Orientation();
font_orientation = AdjustOrientationForCharacterInMixedVertical(
font_orientation, current_character);
// Calculate glyph advance. The shaping engine takes care of x/y orientation
// shifts for different fontOrientation values.
float glyph_advance = visual_metrics.Advance(font_orientation);
// Calculate CSS 'letter-spacing' and 'word-spacing' for the character, if
// needed.
float spacing = spacing_layout.CalculateCSSSpacing(current_character);
FloatPoint text_path_shift;
float angle = 0;
FloatPoint position;
if (in_path_layout_) {
float scaled_glyph_advance = glyph_advance * text_path_scaling_;
// Setup translations that move to the glyph midpoint.
text_path_shift.Set(-scaled_glyph_advance / 2, text_path_displacement_);
if (is_vertical_text_)
text_path_shift = text_path_shift.TransposedPoint();
text_path_shift += baseline_shift;
// Calculate current offset along path.
float text_path_offset =
text_path_current_offset_ + scaled_glyph_advance / 2;
// Move to next character.
text_path_current_offset_ += scaled_glyph_advance + text_path_spacing_ +
spacing * text_path_scaling_;
PathPositionMapper::PositionType position_type =
text_path_->PointAndNormalAtLength(text_path_offset, position, angle);
// Skip character, if we're before the path.
if (position_type == PathPositionMapper::kBeforePath) {
AdvanceToNextLogicalCharacter(logical_metrics);
visual_metrics_iterator_.Next();
continue;
}
// Stop processing if the next character lies behind the path.
if (position_type == PathPositionMapper::kAfterPath)
break;
text_position_ = position;
// For vertical text on path, the actual angle has to be rotated 90
// degrees anti-clockwise, not the orientation angle!
if (is_vertical_text_)
angle -= 90;
} else {
position = text_position_;
position += baseline_shift;
}
if (data.HasRotate())
angle += data.rotate;
// Determine whether we have to start a new fragment.
bool should_start_new_fragment =
needs_fragment_per_glyph || has_relative_position || angle ||
angle != last_angle || apply_spacing_to_next_character;
// If we already started a fragment, close it now.
if (did_start_text_fragment && should_start_new_fragment) {
apply_spacing_to_next_character = false;
RecordTextFragment(text_box);
}
// Eventually start a new fragment, if not yet done.
if (!did_start_text_fragment || should_start_new_fragment) {
DCHECK(!current_text_fragment_.character_offset);
DCHECK(!current_text_fragment_.length);
did_start_text_fragment = true;
current_text_fragment_.character_offset =
visual_metrics_iterator_.CharacterOffset();
current_text_fragment_.metrics_list_offset =
visual_metrics_iterator_.MetricsListOffset();
current_text_fragment_.x = position.X();
current_text_fragment_.y = position.Y();
// Build fragment transformation.
if (angle)
current_text_fragment_.transform.Rotate(angle);
if (text_path_shift.X() || text_path_shift.Y())
current_text_fragment_.transform.Translate(text_path_shift.X(),
text_path_shift.Y());
// For vertical text, always rotate by 90 degrees regardless of
// fontOrientation.
// The shaping engine takes care of the necessary orientation.
if (is_vertical_text_)
current_text_fragment_.transform.Rotate(90);
current_text_fragment_.is_vertical = is_vertical_text_;
current_text_fragment_.is_text_on_path =
in_path_layout_ && text_path_scaling_ != 1;
if (current_text_fragment_.is_text_on_path)
current_text_fragment_.length_adjust_scale = text_path_scaling_;
}
// Advance current text position after processing of the current character
// finished.
AdvanceCurrentTextPosition(glyph_advance + spacing);
// Apply CSS 'letter-spacing' and 'word-spacing' to the next character, if
// needed.
if (!in_path_layout_ && spacing)
apply_spacing_to_next_character = true;
AdvanceToNextLogicalCharacter(logical_metrics);
visual_metrics_iterator_.Next();
last_angle = angle;
}
if (!did_start_text_fragment)
return;
// Close last open fragment, if needed.
RecordTextFragment(text_box);
}
} // namespace blink