TextRun SVGTextMetrics::constructTextRun(LineLayoutSVGInlineText textLayoutItem, unsigned position, unsigned length, TextDirection textDirection) { const ComputedStyle& style = textLayoutItem.styleRef(); TextRun run(static_cast<const LChar*>(nullptr) // characters, will be set below if non-zero. , 0 // length, will be set below if non-zero. , 0 // xPos, only relevant with allowTabs=true , 0 // padding, only relevant for justified text, not relevant for SVG , TextRun::AllowTrailingExpansion , textDirection , isOverride(style.unicodeBidi()) /* directionalOverride */); if (length) { if (textLayoutItem.is8Bit()) run.setText(textLayoutItem.characters8() + position, length); else run.setText(textLayoutItem.characters16() + position, length); } // We handle letter & word spacing ourselves. run.disableSpacing(); // Propagate the maximum length of the characters buffer to the TextRun, even when we're only processing a substring. run.setCharactersLength(textLayoutItem.textLength() - position); ASSERT(run.charactersLength() >= run.length()); return run; }
bool SVGInlineTextBox::nodeAtPoint(HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, LayoutUnit, LayoutUnit) { // FIXME: integrate with InlineTextBox::nodeAtPoint better. ASSERT(!isLineBreak()); PointerEventsHitRules hitRules(PointerEventsHitRules::SVG_TEXT_HITTESTING, result.hitTestRequest(), lineLayoutItem().style()->pointerEvents()); bool isVisible = lineLayoutItem().style()->visibility() == VISIBLE; if (isVisible || !hitRules.requireVisible) { if (hitRules.canHitBoundingBox || (hitRules.canHitStroke && (lineLayoutItem().style()->svgStyle().hasStroke() || !hitRules.requireStroke)) || (hitRules.canHitFill && (lineLayoutItem().style()->svgStyle().hasFill() || !hitRules.requireFill))) { LayoutPoint boxOrigin(x(), y()); boxOrigin.moveBy(accumulatedOffset); LayoutRect rect(boxOrigin, size()); if (locationInContainer.intersects(rect)) { LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->lineLayoutItem()); ASSERT(lineLayoutItem.scalingFactor()); float baseline = lineLayoutItem.scaledFont().fontMetrics().floatAscent() / lineLayoutItem.scalingFactor(); FloatPoint floatLocation = FloatPoint(locationInContainer.point()); for (const SVGTextFragment& fragment : m_textFragments) { FloatQuad fragmentQuad = fragment.boundingQuad(baseline); if (fragmentQuad.containsPoint(floatLocation)) { lineLayoutItem.updateHitTestResult(result, locationInContainer.point() - toLayoutSize(accumulatedOffset)); if (!result.addNodeToListBasedTestResult(lineLayoutItem.node(), locationInContainer, rect)) return true; } } } } } return false; }
static inline FloatRect calculateFragmentBoundaries(LineLayoutSVGInlineText textLineLayout, const SVGTextFragment& fragment) { float scalingFactor = textLineLayout.scalingFactor(); ASSERT(scalingFactor); float baseline = textLineLayout.scaledFont().fontMetrics().floatAscent() / scalingFactor; return fragment.boundingBox(baseline); }
int SVGInlineTextBox::offsetForPositionInFragment( const SVGTextFragment& fragment, LayoutUnit position, bool includePartialGlyphs) const { LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->getLineLayoutItem()); float scalingFactor = lineLayoutItem.scalingFactor(); ASSERT(scalingFactor); const ComputedStyle& style = lineLayoutItem.styleRef(); TextRun textRun = constructTextRun(style, fragment); // Eventually handle lengthAdjust="spacingAndGlyphs". // FIXME: Handle vertical text. if (fragment.isTransformed()) { AffineTransform fragmentTransform = fragment.buildFragmentTransform(); textRun.setHorizontalGlyphStretch( clampTo<float>(fragmentTransform.xScale())); } return fragment.characterOffset - start() + lineLayoutItem.scaledFont().offsetForPosition( textRun, position * scalingFactor, includePartialGlyphs); }
FloatRect SVGInlineTextBox::selectionRectForTextFragment( const SVGTextFragment& fragment, int startPosition, int endPosition, const ComputedStyle& style) const { ASSERT(startPosition < endPosition); LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->getLineLayoutItem()); float scalingFactor = lineLayoutItem.scalingFactor(); ASSERT(scalingFactor); const Font& scaledFont = lineLayoutItem.scaledFont(); const SimpleFontData* fontData = scaledFont.primaryFont(); DCHECK(fontData); if (!fontData) return FloatRect(); const FontMetrics& scaledFontMetrics = fontData->getFontMetrics(); FloatPoint textOrigin(fragment.x, fragment.y); if (scalingFactor != 1) textOrigin.scale(scalingFactor, scalingFactor); textOrigin.move(0, -scaledFontMetrics.floatAscent()); FloatRect selectionRect = scaledFont.selectionRectForText( constructTextRun(style, fragment), textOrigin, fragment.height * scalingFactor, startPosition, endPosition); if (scalingFactor == 1) return selectionRect; selectionRect.scale(1 / scalingFactor); return selectionRect; }
// Execute a query in "logical" order starting at |queryRoot|. This means // walking the lines boxes for each layout object in layout tree (pre)order. static void logicalQuery(LayoutObject* queryRoot, QueryData* queryData, ProcessTextFragmentCallback fragmentCallback) { if (!queryRoot) return; // Walk the layout tree in pre-order, starting at the specified root, and // run the query for each text node. Vector<SVGInlineTextBox*> textBoxes; for (LayoutObject* layoutObject = queryRoot->slowFirstChild(); layoutObject; layoutObject = layoutObject->nextInPreOrder(queryRoot)) { if (!layoutObject->isSVGInlineText()) continue; LineLayoutSVGInlineText textLineLayout = LineLayoutSVGInlineText(toLayoutSVGInlineText(layoutObject)); ASSERT(textLineLayout.style()); // TODO(fs): Allow filtering the search earlier, since we should be // able to trivially reject (prune) at least some of the queries. collectTextBoxesInLogicalOrder(textLineLayout, textBoxes); for (const SVGInlineTextBox* textBox : textBoxes) { if (queryTextBox(queryData, textBox, fragmentCallback)) return; queryData->currentOffset += textBox->len(); } } }
static inline FloatRect calculateFragmentBoundaries(LineLayoutSVGInlineText textLineLayout, const SVGTextFragment& fragment) { float scalingFactor = textLineLayout.scalingFactor(); ASSERT(scalingFactor); float baseline = textLineLayout.scaledFont().fontMetrics().floatAscent() / scalingFactor; AffineTransform fragmentTransform; FloatRect fragmentRect(fragment.x, fragment.y - baseline, fragment.width, fragment.height); fragment.buildFragmentTransform(fragmentTransform); return fragmentTransform.mapRect(fragmentRect); }
SVGTextMetrics::SVGTextMetrics(LineLayoutSVGInlineText textLayoutItem, unsigned length, float width) { ASSERT(textLayoutItem); float scalingFactor = textLayoutItem.scalingFactor(); ASSERT(scalingFactor); m_width = width / scalingFactor; m_height = textLayoutItem.scaledFont().fontMetrics().floatHeight() / scalingFactor; m_length = length; }
LayoutRect SVGInlineTextBox::calculateBoundaries() const { LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->lineLayoutItem()); float scalingFactor = lineLayoutItem.scalingFactor(); ASSERT(scalingFactor); LayoutUnit baseline = lineLayoutItem.scaledFont().fontMetrics().floatAscent() / scalingFactor; LayoutRect textBoundingRect; for (const SVGTextFragment& fragment : m_textFragments) textBoundingRect.unite(LayoutRect(fragment.overflowBoundingBox(baseline))); return textBoundingRect; }
SVGTextMetrics::SVGTextMetrics(LineLayoutSVGInlineText textLayoutItem, const TextRun& run) { ASSERT(textLayoutItem); float scalingFactor = textLayoutItem.scalingFactor(); ASSERT(scalingFactor); const Font& scaledFont = textLayoutItem.scaledFont(); // Calculate width/height using the scaled font, divide this result by the scalingFactor afterwards. m_width = scaledFont.width(run) / scalingFactor; m_height = scaledFont.fontMetrics().floatHeight() / scalingFactor; ASSERT(run.length() >= 0); m_length = static_cast<unsigned>(run.length()); }
static void collectTextBoxesInLogicalOrder(LineLayoutSVGInlineText textLineLayout, Vector<SVGInlineTextBox*>& textBoxes) { textBoxes.shrink(0); for (InlineTextBox* textBox = textLineLayout.firstTextBox(); textBox; textBox = textBox->nextTextBox()) textBoxes.append(toSVGInlineTextBox(textBox)); std::sort(textBoxes.begin(), textBoxes.end(), InlineTextBox::compareByStart); }
LayoutRect SVGInlineTextBox::calculateBoundaries() const { LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->getLineLayoutItem()); const SimpleFontData* fontData = lineLayoutItem.scaledFont().primaryFont(); DCHECK(fontData); if (!fontData) return LayoutRect(); float scalingFactor = lineLayoutItem.scalingFactor(); ASSERT(scalingFactor); LayoutUnit baseline(fontData->getFontMetrics().floatAscent() / scalingFactor); LayoutRect textBoundingRect; for (const SVGTextFragment& fragment : m_textFragments) textBoundingRect.unite(LayoutRect(fragment.overflowBoundingBox(baseline))); return textBoundingRect; }
void SVGTextLayoutEngine::layoutInlineTextBox(SVGInlineTextBox* textBox) { ASSERT(textBox); LineLayoutSVGInlineText textLineLayout = LineLayoutSVGInlineText(textBox->lineLayoutItem()); ASSERT(textLineLayout.parent()); ASSERT(textLineLayout.parent().node()); ASSERT(textLineLayout.parent().node()->isSVGElement()); const ComputedStyle& style = textLineLayout.styleRef(); textBox->clearTextFragments(); m_isVerticalText = !style.isHorizontalWritingMode(); layoutTextOnLineOrPath(textBox, textLineLayout, style); if (m_inPathLayout) return; m_lineLayoutBoxes.append(textBox); }
void SVGTextMetricsCalculator::setupBidiRuns() { const ComputedStyle& style = m_text.styleRef(); m_textDirection = style.direction(); if (isOverride(style.unicodeBidi())) return; BidiStatus status(LTR, false); status.last = status.lastStrong = WTF::Unicode::OtherNeutral; m_bidiResolver.setStatus(status); m_bidiResolver.setPositionIgnoringNestedIsolates(TextRunIterator(&m_run, 0)); const bool hardLineBreak = false; const bool reorderRuns = false; m_bidiResolver.createBidiRunsForLine(TextRunIterator(&m_run, m_run.length()), NoVisualOverride, hardLineBreak, reorderRuns); BidiRunList<BidiCharacterRun>& bidiRuns = m_bidiResolver.runs(); m_bidiRun = bidiRuns.firstRun(); }
bool SVGInlineTextBox::nodeAtPoint(HitTestResult& result, const HitTestLocation& locationInContainer, const LayoutPoint& accumulatedOffset, LayoutUnit, LayoutUnit) { // FIXME: integrate with InlineTextBox::nodeAtPoint better. ASSERT(!isLineBreak()); PointerEventsHitRules hitRules(PointerEventsHitRules::SVG_TEXT_HITTESTING, result.hitTestRequest(), getLineLayoutItem().style()->pointerEvents()); bool isVisible = getLineLayoutItem().style()->visibility() == EVisibility::Visible; if (isVisible || !hitRules.requireVisible) { if (hitRules.canHitBoundingBox || (hitRules.canHitStroke && (getLineLayoutItem().style()->svgStyle().hasStroke() || !hitRules.requireStroke)) || (hitRules.canHitFill && (getLineLayoutItem().style()->svgStyle().hasFill() || !hitRules.requireFill))) { LayoutRect rect(topLeft(), LayoutSize(logicalWidth(), logicalHeight())); rect.moveBy(accumulatedOffset); if (locationInContainer.intersects(rect)) { LineLayoutSVGInlineText lineLayoutItem = LineLayoutSVGInlineText(this->getLineLayoutItem()); const SimpleFontData* fontData = lineLayoutItem.scaledFont().primaryFont(); DCHECK(fontData); if (!fontData) return false; DCHECK(lineLayoutItem.scalingFactor()); float baseline = fontData->getFontMetrics().floatAscent() / lineLayoutItem.scalingFactor(); FloatPoint floatLocation = FloatPoint(locationInContainer.point()); for (const SVGTextFragment& fragment : m_textFragments) { FloatQuad fragmentQuad = fragment.boundingQuad(baseline); if (fragmentQuad.containsPoint(floatLocation)) { lineLayoutItem.updateHitTestResult( result, locationInContainer.point() - toLayoutSize(accumulatedOffset)); if (result.addNodeToListBasedTestResult(lineLayoutItem.node(), locationInContainer, rect) == StopHitTesting) return true; } } } } } return false; }
void SVGTextLayoutEngine::layoutTextOnLineOrPath(SVGInlineTextBox* textBox, LineLayoutSVGInlineText textLineLayout, const ComputedStyle& style) { if (m_inPathLayout && !m_textPathCalculator) return; // Find the start of the current text box in the metrics list. m_visualMetricsIterator.advanceToTextStart(&textLineLayout, textBox->start()); const Font& font = style.font(); SVGTextLayoutEngineSpacing spacingLayout(font, style.effectiveZoom()); SVGTextLayoutEngineBaseline baselineLayout(font, style.effectiveZoom()); bool didStartTextFragment = false; bool applySpacingToNextCharacter = false; float lastAngle = 0; float baselineShift = baselineLayout.calculateBaselineShift(style); baselineShift -= baselineLayout.calculateAlignmentBaselineShift(m_isVerticalText, textLineLayout); // Main layout algorithm. const unsigned boxEndOffset = textBox->start() + textBox->len(); while (!m_visualMetricsIterator.isAtEnd() && m_visualMetricsIterator.characterOffset() < boxEndOffset) { const SVGTextMetrics& visualMetrics = m_visualMetricsIterator.metrics(); if (visualMetrics.isEmpty()) { m_visualMetricsIterator.next(); continue; } SVGTextLayoutAttributes* logicalAttributes = nullptr; if (!currentLogicalCharacterAttributes(logicalAttributes)) break; ASSERT(logicalAttributes); SVGTextMetrics logicalMetrics(SVGTextMetrics::SkippedSpaceMetrics); if (!currentLogicalCharacterMetrics(logicalAttributes, logicalMetrics)) break; SVGCharacterDataMap& characterDataMap = logicalAttributes->characterDataMap(); SVGCharacterData data; SVGCharacterDataMap::iterator it = characterDataMap.find(m_logicalCharacterOffset + 1); if (it != characterDataMap.end()) data = it->value; float x = data.x; float y = data.y; // 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 (m_visualMetricsIterator.characterOffset() == textBox->start()) textBox->setStartsNewTextChunk(logicalAttributes->context()->characterStartsNewTextChunk(m_logicalCharacterOffset)); float angle = SVGTextLayoutAttributes::isEmptyValue(data.rotate) ? 0 : data.rotate; // Calculate glyph orientation angle. // Font::width() calculates the resolved FontOrientation for each character, // but is not exposed today to avoid the API complexity. UChar32 currentCharacter = textLineLayout.codepointAt(m_visualMetricsIterator.characterOffset()); FontOrientation fontOrientation = font.fontDescription().orientation(); fontOrientation = adjustOrientationForCharacterInMixedVertical(fontOrientation, currentCharacter); // Calculate glyph advance. // Shaping engine takes care of x/y orientation shifts for different fontOrientation values. float glyphAdvance = visualMetrics.advance(fontOrientation); // Assign current text position to x/y values, if needed. updateCharacterPositionIfNeeded(x, y); // Apply dx/dy value adjustments to current text position, if needed. updateRelativePositionAdjustmentsIfNeeded(data.dx, data.dy); // Calculate CSS 'letter-spacing' and 'word-spacing' for next character, if needed. float spacing = spacingLayout.calculateCSSSpacing(currentCharacter); float textPathOffset = 0; float textPathShiftX = 0; float textPathShiftY = 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 (!SVGTextLayoutAttributes::isEmptyValue(y)) m_textPathCurrentOffset = y + m_textPathStartOffset; m_textPathCurrentOffset += m_dy; m_dy = 0; // Apply dx/dy correction and setup translations that move to the glyph midpoint. textPathShiftX += m_dx + baselineShift; textPathShiftY -= scaledGlyphAdvance / 2; } else { // If there's an absolute x position available, it marks the beginning of a new position along the path. if (!SVGTextLayoutAttributes::isEmptyValue(x)) m_textPathCurrentOffset = x + m_textPathStartOffset; m_textPathCurrentOffset += m_dx; m_dx = 0; // Apply dx/dy correction and setup translations that move to the glyph midpoint. textPathShiftX -= scaledGlyphAdvance / 2; textPathShiftY += 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) { advanceToNextLogicalCharacter(logicalMetrics); m_visualMetricsIterator.next(); continue; } // Stop processing, if the next character lies behind the path. if (textPathOffset > m_textPathLength) break; FloatPoint point; bool ok = m_textPathCalculator->pointAndNormalAtLength(textPathOffset, point, angle); ASSERT_UNUSED(ok, ok); x = point.x(); y = point.y(); // 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; else y -= baselineShift; x += m_dx; y += m_dy; } // Determine whether we have to start a new fragment. bool shouldStartNewFragment = m_dx || m_dy || m_isVerticalText || m_inPathLayout || angle || angle != lastAngle || applySpacingToNextCharacter || m_textLengthSpacingInEffect; // If we already started a fragment, close it now. if (didStartTextFragment && shouldStartNewFragment) { applySpacingToNextCharacter = false; recordTextFragment(textBox); } // Eventually start a new fragment, if not yet done. if (!didStartTextFragment || shouldStartNewFragment) { ASSERT(!m_currentTextFragment.characterOffset); ASSERT(!m_currentTextFragment.length); didStartTextFragment = true; m_currentTextFragment.characterOffset = m_visualMetricsIterator.characterOffset(); m_currentTextFragment.metricsListOffset = m_visualMetricsIterator.metricsListOffset(); m_currentTextFragment.x = x; m_currentTextFragment.y = y; // Build fragment transformation. if (angle) m_currentTextFragment.transform.rotate(angle); if (textPathShiftX || textPathShiftY) m_currentTextFragment.transform.translate(textPathShiftX, textPathShiftY); // In vertical text, always rotate by 90 degrees regardless of fontOrientation. // Shaping engine takes care of the necessary orientation. if (m_isVerticalText) m_currentTextFragment.transform.rotate(90); m_currentTextFragment.isTextOnPath = m_inPathLayout && m_textPathScaling != 1; if (m_currentTextFragment.isTextOnPath) { if (m_isVerticalText) m_currentTextFragment.lengthAdjustTransform.scaleNonUniform(1, m_textPathScaling); else m_currentTextFragment.lengthAdjustTransform.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); } advanceToNextLogicalCharacter(logicalMetrics); m_visualMetricsIterator.next(); lastAngle = angle; } if (!didStartTextFragment) return; // Close last open fragment, if needed. recordTextFragment(textBox); }
void SVGTextChunkBuilder::handleTextChunk(BoxListConstIterator boxStart, BoxListConstIterator boxEnd) { ASSERT(*boxStart); const LineLayoutSVGInlineText textLineLayout = LineLayoutSVGInlineText((*boxStart)->getLineLayoutItem()); const ComputedStyle& style = textLineLayout.styleRef(); // Handle 'lengthAdjust' property. float desiredTextLength = 0; SVGLengthAdjustType lengthAdjust = SVGLengthAdjustUnknown; if (SVGTextContentElement* textContentElement = SVGTextContentElement::elementFromLineLayoutItem( textLineLayout.parent())) { lengthAdjust = textContentElement->lengthAdjust()->currentValue()->enumValue(); SVGLengthContext lengthContext(textContentElement); if (textContentElement->textLengthIsSpecifiedByUser()) desiredTextLength = textContentElement->textLength()->currentValue()->value( lengthContext); else desiredTextLength = 0; } bool processTextLength = desiredTextLength > 0; bool processTextAnchor = needsTextAnchorAdjustment(style); if (!processTextAnchor && !processTextLength) return; bool isVerticalText = !style.isHorizontalWritingMode(); // Calculate absolute length of whole text chunk (starting from text box // 'start', spanning 'length' text boxes). ChunkLengthAccumulator lengthAccumulator(isVerticalText); lengthAccumulator.processRange(boxStart, boxEnd); if (processTextLength) { float chunkLength = lengthAccumulator.length(); if (lengthAdjust == SVGLengthAdjustSpacing) { float textLengthShift = (desiredTextLength - chunkLength) / lengthAccumulator.numCharacters(); unsigned atCharacter = 0; for (auto boxIter = boxStart; boxIter != boxEnd; ++boxIter) { Vector<SVGTextFragment>& fragments = (*boxIter)->textFragments(); if (fragments.isEmpty()) continue; processTextLengthSpacingCorrection(isVerticalText, textLengthShift, fragments, atCharacter); } // Fragments have been adjusted, we have to recalculate the chunk // length, to be able to apply the text-anchor shift. if (processTextAnchor) { lengthAccumulator.reset(); lengthAccumulator.processRange(boxStart, boxEnd); } } else { ASSERT(lengthAdjust == SVGLengthAdjustSpacingAndGlyphs); float textLengthScale = desiredTextLength / chunkLength; float textLengthBias = 0; bool foundFirstFragment = false; for (auto boxIter = boxStart; boxIter != boxEnd; ++boxIter) { SVGInlineTextBox* textBox = *boxIter; Vector<SVGTextFragment>& fragments = textBox->textFragments(); if (fragments.isEmpty()) continue; if (!foundFirstFragment) { foundFirstFragment = true; textLengthBias = computeTextLengthBias(fragments.first(), textLengthScale); } applyTextLengthScaleAdjustment(textLengthScale, textLengthBias, fragments); } } } if (!processTextAnchor) return; float textAnchorShift = calculateTextAnchorShift(style, lengthAccumulator.length()); for (auto boxIter = boxStart; boxIter != boxEnd; ++boxIter) { Vector<SVGTextFragment>& fragments = (*boxIter)->textFragments(); if (fragments.isEmpty()) continue; processTextAnchorCorrection(isVerticalText, textAnchorShift, fragments); } }