summaryrefslogtreecommitdiffstats
path: root/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp')
-rw-r--r--Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp579
1 files changed, 579 insertions, 0 deletions
diff --git a/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp b/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp
new file mode 100644
index 0000000..7eefad6
--- /dev/null
+++ b/Source/WebCore/rendering/svg/SVGTextLayoutEngine.cpp
@@ -0,0 +1,579 @@
+/*
+ * Copyright (C) Research In Motion Limited 2010. 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 "config.h"
+
+#if ENABLE(SVG)
+#include "SVGTextLayoutEngine.h"
+
+#include "RenderSVGInlineText.h"
+#include "RenderSVGTextPath.h"
+#include "SVGElement.h"
+#include "SVGInlineTextBox.h"
+#include "SVGTextLayoutEngineBaseline.h"
+#include "SVGTextLayoutEngineSpacing.h"
+
+// Set to a value > 0 to dump the text fragments
+#define DUMP_TEXT_FRAGMENTS 0
+
+namespace WebCore {
+
+SVGTextLayoutEngine::SVGTextLayoutEngine()
+ : m_x(0)
+ , m_y(0)
+ , m_dx(0)
+ , m_dy(0)
+ , m_isVerticalText(false)
+ , m_inPathLayout(false)
+ , m_textPathLength(0)
+ , m_textPathCurrentOffset(0)
+ , m_textPathSpacing(0)
+ , m_textPathScaling(1)
+{
+}
+
+void SVGTextLayoutEngine::updateCharacerPositionIfNeeded(float& x, float& y)
+{
+ if (m_inPathLayout)
+ return;
+
+ // Replace characters x/y position, with the current text position plus any
+ // relative adjustments, if it doesn't specify an absolute position itself.
+ if (x == SVGTextLayoutAttributes::emptyValue())
+ x = m_x + m_dx;
+
+ if (y == SVGTextLayoutAttributes::emptyValue())
+ y = m_y + m_dy;
+
+ m_dx = 0;
+ m_dy = 0;
+}
+
+void SVGTextLayoutEngine::updateCurrentTextPosition(float x, float y, float glyphAdvance)
+{
+ // Update current text position after processing the character.
+ if (m_isVerticalText) {
+ m_x = x;
+ m_y = y + glyphAdvance;
+ } else {
+ m_x = x + glyphAdvance;
+ m_y = y;
+ }
+}
+
+void SVGTextLayoutEngine::updateRelativePositionAdjustmentsIfNeeded(Vector<float>& dxValues, Vector<float>& dyValues, unsigned positionListOffset)
+{
+ // Update relative positioning information.
+ if (dxValues.isEmpty() && dyValues.isEmpty())
+ return;
+
+ float dx = 0;
+ if (!dxValues.isEmpty()) {
+ float& dxCurrent = dxValues.at(positionListOffset);
+ if (dxCurrent != SVGTextLayoutAttributes::emptyValue())
+ dx = dxCurrent;
+ }
+
+ float dy = 0;
+ if (!dyValues.isEmpty()) {
+ float& dyCurrent = dyValues.at(positionListOffset);
+ if (dyCurrent != SVGTextLayoutAttributes::emptyValue())
+ dy = dyCurrent;
+ }
+
+ if (m_inPathLayout) {
+ if (m_isVerticalText) {
+ m_dx += dx;
+ m_dy = dy;
+ } else {
+ m_dx = dx;
+ m_dy += dy;
+ }
+
+ return;
+ }
+
+ m_dx = dx;
+ m_dy = dy;
+}
+
+void SVGTextLayoutEngine::recordTextFragment(SVGInlineTextBox* textBox, RenderSVGInlineText* text, unsigned positionListOffset, const SVGTextMetrics& lastCharacterMetrics)
+{
+ ASSERT(!m_currentTextFragment.length);
+
+ // Figure out length of fragment.
+ m_currentTextFragment.length = positionListOffset - m_currentTextFragment.positionListOffset;
+
+ // Figure out fragment metrics.
+ if (m_currentTextFragment.length == 1) {
+ // Fast path, can rely on already computed per-character metrics.
+ m_currentTextFragment.width = lastCharacterMetrics.width();
+ m_currentTextFragment.height = lastCharacterMetrics.height();
+ } else {
+ // Need to measure the whole range (range metrics != sum of character metrics)
+ SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(text, m_currentTextFragment.positionListOffset, m_currentTextFragment.length);
+ m_currentTextFragment.width = metrics.width();
+ m_currentTextFragment.height = metrics.height();
+ }
+
+ textBox->textFragments().append(m_currentTextFragment);
+ m_currentTextFragment = SVGTextFragment();
+}
+
+bool SVGTextLayoutEngine::parentDefinesTextLength(RenderObject* parent) const
+{
+ RenderObject* currentParent = parent;
+ while (currentParent) {
+ SVGTextContentElement* textContentElement = SVGTextContentElement::elementFromRenderer(currentParent);
+ if (textContentElement) {
+ SVGTextContentElement::SVGLengthAdjustType lengthAdjust = static_cast<SVGTextContentElement::SVGLengthAdjustType>(textContentElement->lengthAdjust());
+ if (lengthAdjust == SVGTextContentElement::LENGTHADJUST_SPACING && textContentElement->textLength().value(textContentElement) > 0)
+ return true;
+ }
+
+ if (currentParent->isSVGText())
+ return false;
+
+ currentParent = currentParent->parent();
+ }
+
+ ASSERT_NOT_REACHED();
+ return false;
+}
+
+void SVGTextLayoutEngine::beginTextPathLayout(RenderObject* object, SVGTextLayoutEngine& lineLayout)
+{
+ ASSERT(object);
+
+ m_inPathLayout = true;
+ RenderSVGTextPath* textPath = toRenderSVGTextPath(object);
+
+ m_textPath = textPath->layoutPath();
+ m_textPathStartOffset = textPath->startOffset();
+ m_textPathLength = m_textPath.length();
+ if (m_textPathStartOffset > 0 && m_textPathStartOffset <= 1)
+ m_textPathStartOffset *= m_textPathLength;
+
+ float totalLength = 0;
+ unsigned totalCharacters = 0;
+
+ lineLayout.m_chunkLayoutBuilder.buildTextChunks(lineLayout.m_lineLayoutBoxes);
+ const Vector<SVGTextChunk>& textChunks = lineLayout.m_chunkLayoutBuilder.textChunks();
+
+ unsigned size = textChunks.size();
+ for (unsigned i = 0; i < size; ++i) {
+ const SVGTextChunk& chunk = textChunks.at(i);
+
+ float length = 0;
+ unsigned characters = 0;
+ chunk.calculateLength(length, characters);
+
+ // Handle text-anchor as additional start offset for text paths.
+ m_textPathStartOffset += chunk.calculateTextAnchorShift(length);
+
+ totalLength += length;
+ totalCharacters += characters;
+ }
+
+ m_textPathCurrentOffset = m_textPathStartOffset;
+
+ // Eventually handle textLength adjustments.
+ SVGTextContentElement::SVGLengthAdjustType lengthAdjust = SVGTextContentElement::LENGTHADJUST_UNKNOWN;
+ float desiredTextLength = 0;
+
+ if (SVGTextContentElement* textContentElement = SVGTextContentElement::elementFromRenderer(textPath)) {
+ lengthAdjust = static_cast<SVGTextContentElement::SVGLengthAdjustType>(textContentElement->lengthAdjust());
+ desiredTextLength = textContentElement->textLength().value(textContentElement);
+ }
+
+ if (!desiredTextLength)
+ return;
+
+ if (lengthAdjust == SVGTextContentElement::LENGTHADJUST_SPACING)
+ m_textPathSpacing = (desiredTextLength - totalLength) / totalCharacters;
+ else
+ m_textPathScaling = desiredTextLength / totalLength;
+}
+
+void SVGTextLayoutEngine::endTextPathLayout()
+{
+ m_inPathLayout = false;
+ m_textPath = Path();
+ m_textPathLength = 0;
+ m_textPathStartOffset = 0;
+ m_textPathCurrentOffset = 0;
+ m_textPathSpacing = 0;
+ m_textPathScaling = 1;
+}
+
+void SVGTextLayoutEngine::layoutInlineTextBox(SVGInlineTextBox* textBox)
+{
+ ASSERT(textBox);
+
+ RenderSVGInlineText* text = toRenderSVGInlineText(textBox->textRenderer());
+ ASSERT(text);
+ ASSERT(text->parent());
+ ASSERT(text->parent()->node());
+ ASSERT(text->parent()->node()->isSVGElement());
+
+ const RenderStyle* style = text->style();
+ ASSERT(style);
+
+ textBox->clearTextFragments();
+ m_isVerticalText = style->svgStyle()->isVerticalWritingMode();
+ layoutTextOnLineOrPath(textBox, text, style);
+
+ if (m_inPathLayout) {
+ m_pathLayoutBoxes.append(textBox);
+ return;
+ }
+
+ m_lineLayoutBoxes.append(textBox);
+}
+
+void SVGTextLayoutEngine::finishLayout()
+{
+ // 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.
+ m_chunkLayoutBuilder.layoutTextChunks(m_lineLayoutBoxes);
+
+ // Finalize transform matrices, after the chunk layout corrections have been applied, and all fragment x/y positions are finalized.
+ if (!m_lineLayoutBoxes.isEmpty()) {
+#if DUMP_TEXT_FRAGMENTS > 0
+ fprintf(stderr, "Line layout: ");
+#endif
+
+ finalizeTransformMatrices(m_lineLayoutBoxes);
+ }
+
+ if (!m_pathLayoutBoxes.isEmpty()) {
+#if DUMP_TEXT_FRAGMENTS > 0
+ fprintf(stderr, "Path layout: ");
+#endif
+ finalizeTransformMatrices(m_pathLayoutBoxes);
+ }
+}
+
+void SVGTextLayoutEngine::finalizeTransformMatrices(Vector<SVGInlineTextBox*>& boxes)
+{
+ unsigned boxCount = boxes.size();
+
+#if DUMP_TEXT_FRAGMENTS > 0
+ fprintf(stderr, "Dumping all text fragments in text sub tree, %i boxes\n", boxCount);
+
+ for (unsigned boxPosition = 0; boxPosition < boxCount; ++boxPosition) {
+ SVGInlineTextBox* textBox = boxes.at(boxPosition);
+ Vector<SVGTextFragment>& fragments = textBox->textFragments();
+ fprintf(stderr, "-> Box %i: Dumping text fragments for SVGInlineTextBox, textBox=%p, textRenderer=%p\n", boxPosition, textBox, textBox->textRenderer());
+ fprintf(stderr, " textBox properties, start=%i, len=%i\n", textBox->start(), textBox->len());
+ fprintf(stderr, " textRenderer properties, textLength=%i\n", textBox->textRenderer()->textLength());
+
+ const UChar* characters = textBox->textRenderer()->characters();
+
+ unsigned fragmentCount = fragments.size();
+ for (unsigned i = 0; i < fragmentCount; ++i) {
+ SVGTextFragment& fragment = fragments.at(i);
+ String fragmentString(characters + fragment.positionListOffset, fragment.length);
+ fprintf(stderr, " -> Fragment %i, x=%lf, y=%lf, width=%lf, height=%lf, positionListOffset=%i, length=%i, characters='%s'\n"
+ , i, fragment.x, fragment.y, fragment.width, fragment.height, fragment.positionListOffset, fragment.length, fragmentString.utf8().data());
+ }
+ }
+#endif
+
+
+ if (!boxCount)
+ return;
+
+ AffineTransform textBoxTransformation;
+ for (unsigned boxPosition = 0; boxPosition < boxCount; ++boxPosition) {
+ SVGInlineTextBox* textBox = boxes.at(boxPosition);
+ Vector<SVGTextFragment>& fragments = textBox->textFragments();
+
+ unsigned fragmentCount = fragments.size();
+ for (unsigned i = 0; i < fragmentCount; ++i) {
+ SVGTextFragment& fragment = fragments.at(i);
+ AffineTransform& transform = fragment.transform;
+ if (!transform.isIdentity()) {
+ transform.translateRight(fragment.x, fragment.y);
+ transform.translate(-fragment.x, -fragment.y);
+ }
+
+ m_chunkLayoutBuilder.transformationForTextBox(textBox, textBoxTransformation);
+ if (textBoxTransformation.isIdentity())
+ continue;
+
+ if (transform.isIdentity())
+ transform = textBoxTransformation;
+ else
+ transform.multiply(textBoxTransformation);
+ }
+ }
+
+ boxes.clear();
+}
+
+void SVGTextLayoutEngine::layoutTextOnLineOrPath(SVGInlineTextBox* textBox, RenderSVGInlineText* text, const RenderStyle* style)
+{
+ SVGElement* lengthContext = static_cast<SVGElement*>(text->parent()->node());
+
+ RenderObject* textParent = text->parent();
+ bool definesTextLength = textParent ? parentDefinesTextLength(textParent) : false;
+
+ const SVGRenderStyle* svgStyle = style->svgStyle();
+ ASSERT(svgStyle);
+
+ SVGTextLayoutAttributes& attributes = text->layoutAttributes();
+ Vector<float>& xValues = attributes.xValues();
+ Vector<float>& yValues = attributes.yValues();
+ Vector<float>& dxValues = attributes.dxValues();
+ Vector<float>& dyValues = attributes.dyValues();
+ Vector<float>& rotateValues = attributes.rotateValues();
+ Vector<SVGTextMetrics>& textMetricsValues = attributes.textMetricsValues();
+
+ unsigned boxStart = textBox->start();
+ unsigned boxLength = textBox->len();
+ unsigned textMetricsSize = textMetricsValues.size();
+ ASSERT(textMetricsSize <= xValues.size());
+ ASSERT(textMetricsSize <= yValues.size());
+ ASSERT(xValues.size() == yValues.size());
+
+ if (boxLength > textMetricsSize)
+ textMetricsSize = boxLength;
+
+ unsigned positionListOffset = 0;
+ unsigned metricsListOffset = 0;
+ const UChar* characters = text->characters();
+
+ const Font& font = style->font();
+ SVGTextLayoutEngineSpacing spacingLayout(font);
+ SVGTextLayoutEngineBaseline baselineLayout(font);
+
+ bool didStartTextFragment = false;
+ bool applySpacingToNextCharacter = false;
+
+ float lastAngle = 0;
+ float baselineShift = baselineLayout.calculateBaselineShift(svgStyle, lengthContext);
+ baselineShift -= baselineLayout.calculateAlignmentBaselineShift(m_isVerticalText, text);
+
+ // Main layout algorithm.
+ unsigned positionListSize = xValues.size();
+ for (; metricsListOffset < textMetricsSize && positionListOffset < positionListSize; ++metricsListOffset) {
+ SVGTextMetrics& metrics = textMetricsValues.at(metricsListOffset);
+ // Advance to text box start location.
+ if (positionListOffset < boxStart) {
+ positionListOffset += metrics.length();
+ continue;
+ }
+
+ // Stop if we've finished processing this text box.
+ if (positionListOffset >= boxStart + boxLength)
+ break;
+
+ float x = xValues.at(positionListOffset);
+ float y = yValues.at(positionListOffset);
+
+ // When we've advanced to the box start offset, determine using the original x/y values,
+ // wheter this character starts a new text chunk, before doing any further processing.
+ if (positionListOffset == boxStart)
+ textBox->setStartsNewTextChunk(text->characterStartsNewTextChunk(boxStart));
+
+ if (metrics == SVGTextMetrics::emptyMetrics()) {
+ positionListOffset += metrics.length();
+ continue;
+ }
+
+ const UChar* currentCharacter = characters + positionListOffset;
+ float angle = 0;
+ if (!rotateValues.isEmpty()) {
+ float newAngle = rotateValues.at(positionListOffset);
+ if (newAngle != SVGTextLayoutAttributes::emptyValue())
+ angle = newAngle;
+ }
+
+ // Calculate glyph orientation angle.
+ float orientationAngle = baselineLayout.calculateGlyphOrientationAngle(m_isVerticalText, svgStyle, *currentCharacter);
+
+ // Calculate glyph advance & x/y orientation shifts.
+ float xOrientationShift = 0;
+ float yOrientationShift = 0;
+ float glyphAdvance = baselineLayout.calculateGlyphAdvanceAndOrientation(m_isVerticalText, metrics, orientationAngle, xOrientationShift, yOrientationShift);
+
+ // Assign current text position to x/y values, if needed.
+ updateCharacerPositionIfNeeded(x, y);
+
+ // Apply dx/dy value adjustments to current text position, if needed.
+ updateRelativePositionAdjustmentsIfNeeded(dxValues, dyValues, positionListOffset);
+
+ // Calculate SVG Fonts kerning, if needed.
+ float kerning = spacingLayout.calculateSVGKerning(m_isVerticalText, metrics.glyph());
+
+ // Calculate CSS 'kerning', 'letter-spacing' and 'word-spacing' for next character, if needed.
+ float spacing = spacingLayout.calculateCSSKerningAndSpacing(svgStyle, lengthContext, currentCharacter);
+
+ float textPathOffset = 0;
+ if (m_inPathLayout) {
+ float scaledGlyphAdvance = glyphAdvance * m_textPathScaling;
+ if (m_isVerticalText) {
+ // If there's an absolute y position available, it marks the beginning of a new position along the path.
+ if (y != SVGTextLayoutAttributes::emptyValue())
+ m_textPathCurrentOffset = y + m_textPathStartOffset;
+
+ m_textPathCurrentOffset += m_dy - kerning;
+ m_dy = 0;
+
+ // Apply dx/dy correction and setup translations that move to the glyph midpoint.
+ xOrientationShift += m_dx + baselineShift;
+ yOrientationShift -= scaledGlyphAdvance / 2;
+ } else {
+ // If there's an absolute x position available, it marks the beginning of a new position along the path.
+ if (x != SVGTextLayoutAttributes::emptyValue())
+ m_textPathCurrentOffset = x + m_textPathStartOffset;
+
+ m_textPathCurrentOffset += m_dx - kerning;
+ m_dx = 0;
+
+ // Apply dx/dy correction and setup translations that move to the glyph midpoint.
+ xOrientationShift -= scaledGlyphAdvance / 2;
+ yOrientationShift += m_dy - baselineShift;
+ }
+
+ // Calculate current offset along path.
+ textPathOffset = m_textPathCurrentOffset + scaledGlyphAdvance / 2;
+
+ // Move to next character.
+ m_textPathCurrentOffset += scaledGlyphAdvance + m_textPathSpacing + spacing * m_textPathScaling;
+
+ // Skip character, if we're before the path.
+ if (textPathOffset < 0) {
+ positionListOffset += metrics.length();
+ continue;
+ }
+
+ // Stop processing, if the next character lies behind the path.
+ if (textPathOffset > m_textPathLength)
+ break;
+
+ bool ok = false;
+ FloatPoint point = m_textPath.pointAtLength(textPathOffset, ok);
+ ASSERT(ok);
+
+ x = point.x();
+ y = point.y();
+ angle = m_textPath.normalAngleAtLength(textPathOffset, ok);
+ ASSERT(ok);
+
+ // For vertical text on path, the actual angle has to be rotated 90 degrees anti-clockwise, not the orientation angle!
+ if (m_isVerticalText)
+ angle -= 90;
+ } else {
+ // Apply all previously calculated shift values.
+ if (m_isVerticalText) {
+ x += baselineShift;
+ y -= kerning;
+ } else {
+ x -= kerning;
+ y -= baselineShift;
+ }
+
+ x += m_dx;
+ y += m_dy;
+ }
+
+ // Determine wheter we have to start a new fragment.
+ bool shouldStartNewFragment = false;
+
+ if (m_dx || m_dy)
+ shouldStartNewFragment = true;
+
+ if (!shouldStartNewFragment && (m_isVerticalText || m_inPathLayout))
+ shouldStartNewFragment = true;
+
+ if (!shouldStartNewFragment && (angle || angle != lastAngle || orientationAngle))
+ shouldStartNewFragment = true;
+
+ if (!shouldStartNewFragment && (kerning || applySpacingToNextCharacter || definesTextLength))
+ shouldStartNewFragment = true;
+
+ // If we already started a fragment, close it now.
+ if (didStartTextFragment && shouldStartNewFragment) {
+ applySpacingToNextCharacter = false;
+ recordTextFragment(textBox, text, positionListOffset, textMetricsValues.at(metricsListOffset - 1));
+ }
+
+ // Eventually start a new fragment, if not yet done.
+ if (!didStartTextFragment || shouldStartNewFragment) {
+ ASSERT(!m_currentTextFragment.positionListOffset);
+ ASSERT(!m_currentTextFragment.length);
+
+ didStartTextFragment = true;
+ m_currentTextFragment.positionListOffset = positionListOffset;
+ m_currentTextFragment.x = x;
+ m_currentTextFragment.y = y;
+
+ // Build fragment transformation.
+ if (angle)
+ m_currentTextFragment.transform.rotate(angle);
+
+ if (xOrientationShift || yOrientationShift)
+ m_currentTextFragment.transform.translate(xOrientationShift, yOrientationShift);
+
+ if (orientationAngle)
+ m_currentTextFragment.transform.rotate(orientationAngle);
+
+ if (m_inPathLayout && m_textPathScaling != 1) {
+ if (m_isVerticalText)
+ m_currentTextFragment.transform.scaleNonUniform(1, m_textPathScaling);
+ else
+ m_currentTextFragment.transform.scaleNonUniform(m_textPathScaling, 1);
+ }
+ }
+
+ // Update current text position, after processing of the current character finished.
+ if (m_inPathLayout)
+ updateCurrentTextPosition(x, y, glyphAdvance);
+ else {
+ // Apply CSS 'kerning', 'letter-spacing' and 'word-spacing' to next character, if needed.
+ if (spacing)
+ applySpacingToNextCharacter = true;
+
+ float xNew = x - m_dx;
+ float yNew = y - m_dy;
+
+ if (m_isVerticalText)
+ xNew -= baselineShift;
+ else
+ yNew += baselineShift;
+
+ updateCurrentTextPosition(xNew, yNew, glyphAdvance + spacing);
+ }
+
+ positionListOffset += metrics.length();
+ lastAngle = angle;
+ }
+
+ if (!didStartTextFragment)
+ return;
+
+ // Close last open fragment, if needed.
+ recordTextFragment(textBox, text, positionListOffset, textMetricsValues.at(metricsListOffset - 1));
+}
+
+}
+
+#endif // ENABLE(SVG)