580 lines
21 KiB
C++
580 lines
21 KiB
C++
#include "svgtextelement.h"
|
|
#include "svglayoutstate.h"
|
|
#include "svgrenderstate.h"
|
|
|
|
#include <cassert>
|
|
|
|
namespace lunasvg {
|
|
|
|
inline const SVGTextNode* toSVGTextNode(const SVGNode* node)
|
|
{
|
|
assert(node && node->isTextNode());
|
|
return static_cast<const SVGTextNode*>(node);
|
|
}
|
|
|
|
inline const SVGTextPositioningElement* toSVGTextPositioningElement(const SVGNode* node)
|
|
{
|
|
assert(node && node->isTextPositioningElement());
|
|
return static_cast<const SVGTextPositioningElement*>(node);
|
|
}
|
|
|
|
static AlignmentBaseline resolveDominantBaseline(const SVGTextPositioningElement* element)
|
|
{
|
|
switch(element->dominant_baseline()) {
|
|
case DominantBaseline::Auto:
|
|
if(element->isVerticalWritingMode())
|
|
return AlignmentBaseline::Central;
|
|
return AlignmentBaseline::Alphabetic;
|
|
case DominantBaseline::UseScript:
|
|
case DominantBaseline::NoChange:
|
|
case DominantBaseline::ResetSize:
|
|
return AlignmentBaseline::Auto;
|
|
case DominantBaseline::Ideographic:
|
|
return AlignmentBaseline::Ideographic;
|
|
case DominantBaseline::Alphabetic:
|
|
return AlignmentBaseline::Alphabetic;
|
|
case DominantBaseline::Hanging:
|
|
return AlignmentBaseline::Hanging;
|
|
case DominantBaseline::Mathematical:
|
|
return AlignmentBaseline::Mathematical;
|
|
case DominantBaseline::Central:
|
|
return AlignmentBaseline::Central;
|
|
case DominantBaseline::Middle:
|
|
return AlignmentBaseline::Middle;
|
|
case DominantBaseline::TextAfterEdge:
|
|
return AlignmentBaseline::TextAfterEdge;
|
|
case DominantBaseline::TextBeforeEdge:
|
|
return AlignmentBaseline::TextBeforeEdge;
|
|
default:
|
|
assert(false);
|
|
}
|
|
|
|
return AlignmentBaseline::Auto;
|
|
}
|
|
|
|
static float calculateBaselineOffset(const SVGTextPositioningElement* element)
|
|
{
|
|
auto offset = element->baseline_offset();
|
|
auto parent = element->parentElement();
|
|
while(parent->isTextPositioningElement()) {
|
|
offset += toSVGTextPositioningElement(parent)->baseline_offset();
|
|
parent = parent->parentElement();
|
|
}
|
|
|
|
auto baseline = element->alignment_baseline();
|
|
if(baseline == AlignmentBaseline::Auto || baseline == AlignmentBaseline::Baseline) {
|
|
baseline = resolveDominantBaseline(element);
|
|
}
|
|
|
|
const auto& font = element->font();
|
|
switch(baseline) {
|
|
case AlignmentBaseline::BeforeEdge:
|
|
case AlignmentBaseline::TextBeforeEdge:
|
|
offset -= font.ascent();
|
|
break;
|
|
case AlignmentBaseline::Middle:
|
|
offset -= font.xHeight() / 2.f;
|
|
break;
|
|
case AlignmentBaseline::Central:
|
|
offset -= (font.ascent() + font.descent()) / 2.f;
|
|
break;
|
|
case AlignmentBaseline::AfterEdge:
|
|
case AlignmentBaseline::TextAfterEdge:
|
|
case AlignmentBaseline::Ideographic:
|
|
offset -= font.descent();
|
|
break;
|
|
case AlignmentBaseline::Hanging:
|
|
offset -= font.ascent() * 8.f / 10.f;
|
|
break;
|
|
case AlignmentBaseline::Mathematical:
|
|
offset -= font.ascent() / 2.f;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
static bool needsTextAnchorAdjustment(const SVGTextPositioningElement* element)
|
|
{
|
|
auto direction = element->direction();
|
|
switch(element->text_anchor()) {
|
|
case TextAnchor::Start:
|
|
return direction == Direction::Rtl;
|
|
case TextAnchor::Middle:
|
|
return true;
|
|
case TextAnchor::End:
|
|
return direction == Direction::Ltr;
|
|
default:
|
|
assert(false);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static float calculateTextAnchorOffset(const SVGTextPositioningElement* element, float width)
|
|
{
|
|
auto direction = element->direction();
|
|
switch(element->text_anchor()) {
|
|
case TextAnchor::Start:
|
|
if(direction == Direction::Ltr)
|
|
return 0.f;
|
|
return -width;
|
|
case TextAnchor::Middle:
|
|
return -width / 2.f;
|
|
case TextAnchor::End:
|
|
if(direction == Direction::Ltr)
|
|
return -width;
|
|
return 0.f;
|
|
default:
|
|
assert(false);
|
|
}
|
|
|
|
return 0.f;
|
|
}
|
|
|
|
using SVGTextFragmentIterator = SVGTextFragmentList::iterator;
|
|
|
|
static float calculateTextChunkLength(SVGTextFragmentIterator begin, SVGTextFragmentIterator end, bool isVerticalText)
|
|
{
|
|
float chunkLength = 0;
|
|
const SVGTextFragment* lastFragment = nullptr;
|
|
for(auto it = begin; it != end; ++it) {
|
|
const SVGTextFragment& fragment = *it;
|
|
chunkLength += isVerticalText ? fragment.height : fragment.width;
|
|
if(!lastFragment) {
|
|
lastFragment = &fragment;
|
|
continue;
|
|
}
|
|
|
|
if(isVerticalText) {
|
|
chunkLength += fragment.y - (lastFragment->y + lastFragment->height);
|
|
} else {
|
|
chunkLength += fragment.x - (lastFragment->x + lastFragment->width);
|
|
}
|
|
|
|
lastFragment = &fragment;
|
|
}
|
|
|
|
return chunkLength;
|
|
}
|
|
|
|
static void handleTextChunk(SVGTextFragmentIterator begin, SVGTextFragmentIterator end)
|
|
{
|
|
const SVGTextFragment& firstFragment = *begin;
|
|
const auto isVerticalText = firstFragment.element->isVerticalWritingMode();
|
|
if(firstFragment.element->hasAttribute(PropertyID::TextLength)) {
|
|
LengthContext lengthContext(firstFragment.element);
|
|
auto textLength = lengthContext.valueForLength(firstFragment.element->textLength());
|
|
auto chunkLength = calculateTextChunkLength(begin, end, isVerticalText);
|
|
if(textLength > 0.f && chunkLength > 0.f) {
|
|
size_t numCharacters = 0;
|
|
for(auto it = begin; it != end; ++it) {
|
|
const SVGTextFragment& fragment = *it;
|
|
numCharacters += fragment.length;
|
|
}
|
|
|
|
if(firstFragment.element->lengthAdjust() == LengthAdjust::SpacingAndGlyphs) {
|
|
auto textLengthScale = textLength / chunkLength;
|
|
auto lengthAdjustTransform = Transform::translated(firstFragment.x, firstFragment.y);
|
|
if(isVerticalText) {
|
|
lengthAdjustTransform.scale(1.f, textLengthScale);
|
|
} else {
|
|
lengthAdjustTransform.scale(textLengthScale, 1.f);
|
|
}
|
|
|
|
lengthAdjustTransform.translate(-firstFragment.x, -firstFragment.y);
|
|
for(auto it = begin; it != end; ++it) {
|
|
SVGTextFragment& fragment = *it;
|
|
fragment.lengthAdjustTransform = lengthAdjustTransform;
|
|
}
|
|
} else if(numCharacters > 1) {
|
|
assert(firstFragment.element->lengthAdjust() == LengthAdjust::Spacing);
|
|
size_t characterOffset = 0;
|
|
auto textLengthShift = (textLength - chunkLength) / (numCharacters - 1);
|
|
for(auto it = begin; it != end; ++it) {
|
|
SVGTextFragment& fragment = *it;
|
|
if(isVerticalText) {
|
|
fragment.y += textLengthShift * characterOffset;
|
|
} else {
|
|
fragment.x += textLengthShift * characterOffset;
|
|
}
|
|
|
|
characterOffset += fragment.length;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(needsTextAnchorAdjustment(firstFragment.element)) {
|
|
auto chunkLength = calculateTextChunkLength(begin, end, isVerticalText);
|
|
auto chunkOffset = calculateTextAnchorOffset(firstFragment.element, chunkLength);
|
|
for(auto it = begin; it != end; ++it) {
|
|
SVGTextFragment& fragment = *it;
|
|
if(isVerticalText) {
|
|
fragment.y += chunkOffset;
|
|
} else {
|
|
fragment.x += chunkOffset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SVGTextFragmentsBuilder::SVGTextFragmentsBuilder(std::u32string& text, SVGTextFragmentList& fragments)
|
|
: m_text(text), m_fragments(fragments)
|
|
{
|
|
m_text.clear();
|
|
m_fragments.clear();
|
|
}
|
|
|
|
void SVGTextFragmentsBuilder::build(const SVGTextElement* textElement)
|
|
{
|
|
handleElement(textElement);
|
|
for(const auto& position : m_textPositions) {
|
|
fillCharacterPositions(position);
|
|
}
|
|
|
|
std::u32string_view wholeText(m_text);
|
|
for(const auto& textPosition : m_textPositions) {
|
|
if(!textPosition.node->isTextNode())
|
|
continue;
|
|
auto element = toSVGTextPositioningElement(textPosition.node->parentElement());
|
|
const auto isVerticalText = element->isVerticalWritingMode();
|
|
const auto isUprightText = element->isUprightTextOrientation();
|
|
const auto& font = element->font();
|
|
|
|
SVGTextFragment fragment(element);
|
|
auto recordTextFragment = [&](auto startOffset, auto endOffset) {
|
|
auto text = wholeText.substr(startOffset, endOffset - startOffset);
|
|
fragment.offset = startOffset;
|
|
fragment.length = text.length();
|
|
fragment.width = font.measureText(text);
|
|
fragment.height = font.height() + font.lineGap();
|
|
if(isVerticalText) {
|
|
m_y += isUprightText ? fragment.height : fragment.width;
|
|
} else {
|
|
m_x += fragment.width;
|
|
}
|
|
|
|
m_fragments.push_back(fragment);
|
|
};
|
|
|
|
auto needsTextLengthSpacing = element->lengthAdjust() == LengthAdjust::Spacing && element->hasAttribute(PropertyID::TextLength);
|
|
auto baselineOffset = calculateBaselineOffset(element);
|
|
auto startOffset = textPosition.startOffset;
|
|
auto textOffset = textPosition.startOffset;
|
|
auto didStartTextFragment = false;
|
|
auto applySpacingToNextCharacter = false;
|
|
auto lastCharacter = 0u;
|
|
auto lastAngle = 0.f;
|
|
while(textOffset < textPosition.endOffset) {
|
|
SVGCharacterPosition characterPosition;
|
|
if(auto it = m_characterPositions.find(m_characterOffset); it != m_characterPositions.end()) {
|
|
characterPosition = it->second;
|
|
}
|
|
|
|
auto currentCharacter = wholeText.at(textOffset);
|
|
auto angle = characterPosition.rotate.value_or(0);
|
|
auto dx = characterPosition.dx.value_or(0);
|
|
auto dy = characterPosition.dy.value_or(0);
|
|
|
|
auto shouldStartNewFragment = needsTextLengthSpacing || isVerticalText || applySpacingToNextCharacter
|
|
|| characterPosition.x || characterPosition.y || dx || dy || angle || angle != lastAngle;
|
|
if(shouldStartNewFragment && didStartTextFragment) {
|
|
recordTextFragment(startOffset, textOffset);
|
|
applySpacingToNextCharacter = false;
|
|
startOffset = textOffset;
|
|
}
|
|
|
|
auto startsNewTextChunk = (characterPosition.x || characterPosition.y) && textOffset == textPosition.startOffset;
|
|
if(startsNewTextChunk || shouldStartNewFragment || !didStartTextFragment) {
|
|
m_x = dx + characterPosition.x.value_or(m_x);
|
|
m_y = dy + characterPosition.y.value_or(m_y);
|
|
fragment.x = isVerticalText ? m_x + baselineOffset : m_x;
|
|
fragment.y = isVerticalText ? m_y : m_y - baselineOffset;
|
|
fragment.angle = angle;
|
|
if(isVerticalText) {
|
|
if(isUprightText) {
|
|
fragment.y += font.height();
|
|
} else {
|
|
fragment.angle += 90.f;
|
|
}
|
|
}
|
|
|
|
fragment.startsNewTextChunk = startsNewTextChunk;
|
|
didStartTextFragment = true;
|
|
}
|
|
|
|
auto spacing = element->letter_spacing();
|
|
if(currentCharacter && lastCharacter && element->word_spacing()) {
|
|
if(currentCharacter == ' ' && lastCharacter != ' ') {
|
|
spacing += element->word_spacing();
|
|
}
|
|
}
|
|
|
|
if(spacing) {
|
|
applySpacingToNextCharacter = true;
|
|
if(isVerticalText) {
|
|
m_y += spacing;
|
|
} else {
|
|
m_x += spacing;
|
|
}
|
|
}
|
|
|
|
lastAngle = angle;
|
|
lastCharacter = currentCharacter;
|
|
++textOffset;
|
|
++m_characterOffset;
|
|
}
|
|
|
|
recordTextFragment(startOffset, textOffset);
|
|
}
|
|
|
|
if(m_fragments.empty())
|
|
return;
|
|
auto it = m_fragments.begin();
|
|
auto begin = m_fragments.begin();
|
|
auto end = m_fragments.end();
|
|
for(++it; it != end; ++it) {
|
|
const SVGTextFragment& fragment = *it;
|
|
if(!fragment.startsNewTextChunk)
|
|
continue;
|
|
handleTextChunk(begin, it);
|
|
begin = it;
|
|
}
|
|
|
|
handleTextChunk(begin, it);
|
|
}
|
|
|
|
void SVGTextFragmentsBuilder::handleText(const SVGTextNode* node)
|
|
{
|
|
const auto& text = node->data();
|
|
if(text.empty())
|
|
return;
|
|
auto element = toSVGTextPositioningElement(node->parentElement());
|
|
const auto startOffset = m_text.length();
|
|
uint32_t lastCharacter = ' ';
|
|
if(!m_text.empty()) {
|
|
lastCharacter = m_text.back();
|
|
}
|
|
|
|
plutovg_text_iterator_t it;
|
|
plutovg_text_iterator_init(&it, text.data(), text.length(), PLUTOVG_TEXT_ENCODING_UTF8);
|
|
while(plutovg_text_iterator_has_next(&it)) {
|
|
auto currentCharacter = plutovg_text_iterator_next(&it);
|
|
if(currentCharacter == '\t' || currentCharacter == '\n' || currentCharacter == '\r')
|
|
currentCharacter = ' ';
|
|
if(currentCharacter == ' ' && lastCharacter == ' ' && element->white_space() == WhiteSpace::Default)
|
|
continue;
|
|
m_text.push_back(currentCharacter);
|
|
lastCharacter = currentCharacter;
|
|
}
|
|
|
|
if(startOffset < m_text.length()) {
|
|
m_textPositions.emplace_back(node, startOffset, m_text.length());
|
|
}
|
|
}
|
|
|
|
void SVGTextFragmentsBuilder::handleElement(const SVGTextPositioningElement* element)
|
|
{
|
|
if(element->isDisplayNone())
|
|
return;
|
|
const auto itemIndex = m_textPositions.size();
|
|
m_textPositions.emplace_back(element, m_text.length(), m_text.length());
|
|
for(const auto& child : element->children()) {
|
|
if(child->isTextNode()) {
|
|
handleText(toSVGTextNode(child.get()));
|
|
} else if(child->isTextPositioningElement()) {
|
|
handleElement(toSVGTextPositioningElement(child.get()));
|
|
}
|
|
}
|
|
|
|
auto& position = m_textPositions[itemIndex];
|
|
assert(position.node == element);
|
|
position.endOffset = m_text.length();
|
|
}
|
|
|
|
void SVGTextFragmentsBuilder::fillCharacterPositions(const SVGTextPosition& position)
|
|
{
|
|
if(!position.node->isTextPositioningElement())
|
|
return;
|
|
auto element = toSVGTextPositioningElement(position.node);
|
|
const auto& xList = element->x();
|
|
const auto& yList = element->y();
|
|
const auto& dxList = element->dx();
|
|
const auto& dyList = element->dy();
|
|
const auto& rotateList = element->rotate();
|
|
|
|
auto xListSize = xList.size();
|
|
auto yListSize = yList.size();
|
|
auto dxListSize = dxList.size();
|
|
auto dyListSize = dyList.size();
|
|
auto rotateListSize = rotateList.size();
|
|
if(!xListSize && !yListSize && !dxListSize && !dyListSize && !rotateListSize) {
|
|
return;
|
|
}
|
|
|
|
LengthContext lengthContext(element);
|
|
std::optional<float> lastRotation;
|
|
for(auto offset = position.startOffset; offset < position.endOffset; ++offset) {
|
|
auto index = offset - position.startOffset;
|
|
if(index >= xListSize && index >= yListSize && index >= dxListSize && index >= dyListSize && index >= rotateListSize)
|
|
break;
|
|
auto& characterPosition = m_characterPositions[offset];
|
|
if(index < xListSize)
|
|
characterPosition.x = lengthContext.valueForLength(xList[index], LengthDirection::Horizontal);
|
|
if(index < yListSize)
|
|
characterPosition.y = lengthContext.valueForLength(yList[index], LengthDirection::Vertical);
|
|
if(index < dxListSize)
|
|
characterPosition.dx = lengthContext.valueForLength(dxList[index], LengthDirection::Horizontal);
|
|
if(index < dyListSize)
|
|
characterPosition.dy = lengthContext.valueForLength(dyList[index], LengthDirection::Vertical);
|
|
if(index < rotateListSize) {
|
|
characterPosition.rotate = rotateList[index];
|
|
lastRotation = characterPosition.rotate;
|
|
}
|
|
}
|
|
|
|
if(lastRotation == std::nullopt)
|
|
return;
|
|
auto offset = position.startOffset + rotateList.size();
|
|
while(offset < position.endOffset) {
|
|
m_characterPositions[offset++].rotate = lastRotation;
|
|
}
|
|
}
|
|
|
|
SVGTextPositioningElement::SVGTextPositioningElement(Document* document, ElementID id)
|
|
: SVGGraphicsElement(document, id)
|
|
, m_x(PropertyID::X, LengthDirection::Horizontal, LengthNegativeMode::Allow)
|
|
, m_y(PropertyID::Y, LengthDirection::Vertical, LengthNegativeMode::Allow)
|
|
, m_dx(PropertyID::Dx, LengthDirection::Horizontal, LengthNegativeMode::Allow)
|
|
, m_dy(PropertyID::Dy, LengthDirection::Vertical, LengthNegativeMode::Allow)
|
|
, m_rotate(PropertyID::Rotate)
|
|
, m_textLength(PropertyID::TextLength, LengthDirection::Horizontal, LengthNegativeMode::Forbid)
|
|
, m_lengthAdjust(PropertyID::LengthAdjust, LengthAdjust::Spacing)
|
|
{
|
|
addProperty(m_x);
|
|
addProperty(m_y);
|
|
addProperty(m_dx);
|
|
addProperty(m_dy);
|
|
addProperty(m_rotate);
|
|
addProperty(m_textLength);
|
|
addProperty(m_lengthAdjust);
|
|
}
|
|
|
|
void SVGTextPositioningElement::layoutElement(const SVGLayoutState& state)
|
|
{
|
|
m_font = state.font();
|
|
m_fill = getPaintServer(state.fill(), state.fill_opacity());
|
|
m_stroke = getPaintServer(state.stroke(), state.stroke_opacity());
|
|
SVGGraphicsElement::layoutElement(state);
|
|
|
|
LengthContext lengthContext(this);
|
|
m_stroke_width = lengthContext.valueForLength(state.stroke_width(), LengthDirection::Diagonal);
|
|
m_letter_spacing = lengthContext.valueForLength(state.letter_spacing(), LengthDirection::Diagonal);
|
|
m_word_spacing = lengthContext.valueForLength(state.word_spacing(), LengthDirection::Diagonal);
|
|
|
|
m_baseline_offset = convertBaselineOffset(state.baseline_shift());
|
|
m_alignment_baseline = state.alignment_baseline();
|
|
m_dominant_baseline = state.dominant_baseline();
|
|
m_text_anchor = state.text_anchor();
|
|
m_white_space = state.white_space();
|
|
m_writing_mode = state.writing_mode();
|
|
m_text_orientation = state.text_orientation();
|
|
m_direction = state.direction();
|
|
}
|
|
|
|
float SVGTextPositioningElement::convertBaselineOffset(const BaselineShift& baselineShift) const
|
|
{
|
|
if(baselineShift.type() == BaselineShift::Type::Baseline)
|
|
return 0.f;
|
|
if(baselineShift.type() == BaselineShift::Type::Sub)
|
|
return -m_font.height() / 2.f;
|
|
if(baselineShift.type() == BaselineShift::Type::Super) {
|
|
return m_font.height() / 2.f;
|
|
}
|
|
|
|
const auto& length = baselineShift.length();
|
|
if(length.units() == LengthUnits::Percent)
|
|
return length.value() * m_font.size() / 100.f;
|
|
if(length.units() == LengthUnits::Ex)
|
|
return length.value() * m_font.size() / 2.f;
|
|
if(length.units() == LengthUnits::Em)
|
|
return length.value() * m_font.size();
|
|
return length.value();
|
|
}
|
|
|
|
SVGTSpanElement::SVGTSpanElement(Document* document)
|
|
: SVGTextPositioningElement(document, ElementID::Tspan)
|
|
{
|
|
}
|
|
|
|
SVGTextElement::SVGTextElement(Document* document)
|
|
: SVGTextPositioningElement(document, ElementID::Text)
|
|
{
|
|
}
|
|
|
|
void SVGTextElement::layout(SVGLayoutState& state)
|
|
{
|
|
SVGTextPositioningElement::layout(state);
|
|
SVGTextFragmentsBuilder(m_text, m_fragments).build(this);
|
|
}
|
|
|
|
void SVGTextElement::render(SVGRenderState& state) const
|
|
{
|
|
if(m_fragments.empty() || isVisibilityHidden() || isDisplayNone())
|
|
return;
|
|
SVGBlendInfo blendInfo(this);
|
|
SVGRenderState newState(this, state, localTransform());
|
|
newState.beginGroup(blendInfo);
|
|
if(newState.mode() == SVGRenderMode::Clipping) {
|
|
newState->setColor(Color::White);
|
|
}
|
|
|
|
std::u32string_view wholeText(m_text);
|
|
for(const auto& fragment : m_fragments) {
|
|
if(fragment.element->isVisibilityHidden())
|
|
continue;
|
|
auto transform = newState.currentTransform() * Transform::rotated(fragment.angle, fragment.x, fragment.y) * fragment.lengthAdjustTransform;
|
|
auto text = wholeText.substr(fragment.offset, fragment.length);
|
|
auto origin = Point(fragment.x, fragment.y);
|
|
|
|
const auto& font = fragment.element->font();
|
|
if(newState.mode() == SVGRenderMode::Clipping) {
|
|
newState->fillText(text, font, origin, transform);
|
|
} else {
|
|
const auto& fill = fragment.element->fill();
|
|
const auto& stroke = fragment.element->stroke();
|
|
auto stroke_width = fragment.element->stroke_width();
|
|
if(fill.applyPaint(newState))
|
|
newState->fillText(text, font, origin, transform);
|
|
if(stroke.applyPaint(newState)) {
|
|
newState->strokeText(text, stroke_width, font, origin, transform);
|
|
}
|
|
}
|
|
}
|
|
|
|
newState.endGroup(blendInfo);
|
|
}
|
|
|
|
Rect SVGTextElement::boundingBox(bool includeStroke) const
|
|
{
|
|
auto boundingBox = Rect::Invalid;
|
|
for(const auto& fragment : m_fragments) {
|
|
const auto& font = fragment.element->font();
|
|
const auto& stroke = fragment.element->stroke();
|
|
auto fragmentTranform = Transform::rotated(fragment.angle, fragment.x, fragment.y) * fragment.lengthAdjustTransform;
|
|
auto fragmentRect = Rect(fragment.x, fragment.y - font.ascent(), fragment.width, fragment.height);
|
|
if(includeStroke && stroke.isRenderable())
|
|
fragmentRect.inflate(fragment.element->stroke_width() / 2.f);
|
|
boundingBox.unite(fragmentTranform.mapRect(fragmentRect));
|
|
}
|
|
|
|
if(!boundingBox.isValid())
|
|
boundingBox = Rect::Empty;
|
|
return boundingBox;
|
|
}
|
|
|
|
} // namespace lunasvg
|