Files
autosample/vendor/lunasvg/source/svgtextelement.cpp

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