Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextShape_p_layout.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
3 * SPDX-FileCopyrightText: 2022 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include "KoSvgTextShape.h"
9#include "KoSvgTextShape_p.h"
11
12#include "KoCssTextUtils.h"
14#include "KoFontRegistry.h"
15#include "KoSvgTextProperties.h"
16#include "KoColorBackground.h"
18
19#include <FlakeDebug.h>
20#include <KoPathShape.h>
21
22#include <kis_global.h>
23
24#include <QPainterPath>
25#include <QtMath>
26
27#include <variant>
28
29#include <graphemebreak.h>
30#include <wordbreak.h>
31#include <linebreak.h>
32
33#include <ft2build.h>
34#include FT_FREETYPE_H
35#include FT_TRUETYPE_TABLES_H
36
37#include <hb.h>
38#include <hb-ft.h>
39#include <hb-ot.h>
40
41#include <raqm.h>
42
44
49
50
55static QMap<int, int> logicalToVisualCursorPositions(const QVector<CursorPos> &cursorPos
56 , const QVector<CharacterResult> &result
57 , const QVector<LineBox> &lines
58 , const bool &ltr = false) {
59 QMap<int, int> logicalToVisual;
60 for (int i = 0; i < lines.size(); i++) {
61 Q_FOREACH(const LineChunk chunk, lines.at(i).chunks) {
62 QMap<int, int> visualToLogical;
63 QVector<int> visual;
64 Q_FOREACH(const int j, chunk.chunkIndices) {
65 visualToLogical.insert(result.at(j).visualIndex, j);
66 }
67 Q_FOREACH(const int j, visualToLogical.values()) {
68 QMap<int, int> relevant;
69 for (int k = 0; k < cursorPos.size(); k++) {
70 if (j == cursorPos.at(k).cluster) {
71 relevant.insert(cursorPos.at(k).offset, k);
72 }
73 }
74 Q_FOREACH(const int k, relevant.keys()) {
75 int final = result.at(j).cursorInfo.rtl? relevant.size()-1-k: k;
76 visual.append(relevant.value(final));
77 }
78 }
79
80 if (ltr) {
81 for (int k = 0; k < visual.size(); k++) {
82 logicalToVisual.insert(visual.at(k), logicalToVisual.size());
83 }
84 } else {
85 for (int k = visual.size()-1; k > -1; k--) {
86 logicalToVisual.insert(visual.at(k), logicalToVisual.size());
87 }
88 }
89 }
90 }
91
92 return logicalToVisual;
93}
94
95QString langToLibUnibreakLang(const QString lang) {
96 // Libunibreak only tests against "ko", "ja" and "zh".
98 if (locale.language() == QLocale::Japanese || locale.script() == QLocale::JapaneseScript) {
99 return "ja";
100 } else if (locale.language() == QLocale::Korean || locale.script() == QLocale::KoreanScript || locale.script() == QLocale::HangulScript) {
101 return "ko";
102 } else if (locale.script() == QLocale::SimplifiedChineseScript
103 || locale.script() == QLocale::TraditionalChineseScript
104 || locale.script() == QLocale::HanWithBopomofoScript
105 || locale.script() == QLocale::HanScript
106 || locale.script() == QLocale::BopomofoScript) {
107 return "zh";
108 }
109 return lang;
110}
111
112// NOLINTNEXTLINE(readability-function-cognitive-complexity)
113void KoSvgTextShape::Private::relayout()
114{
115 clearAssociatedOutlines();
116 this->initialTextPosition = QPointF();
117 this->result.clear();
118 this->cursorPos.clear();
119 this->logicalToVisualCursorPos.clear();
120
121 bool disableFontMatching = this->disableFontMatching;
122
123 if (KisForestDetail::size(textData) == 0) {
124 return;
125 }
126 // The following is based on the text-layout algorithm in SVG 2.
127 KoSvgTextProperties rootProperties = textData.childBegin()->properties;
133 QString lang = rootProperties.property(KoSvgTextProperties::TextLanguage).toString();
134
135 const bool isHorizontal = writingMode == KoSvgText::HorizontalTB;
136
137 const auto getUcs4At = [](const QString &s, int i) -> char32_t {
138 const QChar high = s.at(i);
139 if (!high.isSurrogate()) {
140 return high.unicode();
141 }
142 if (high.isHighSurrogate() && s.length() > i + 1) {
143 const QChar low = s[i + 1];
144 if (low.isLowSurrogate()) {
145 return QChar::surrogateToUcs4(high, low);
146 }
147 }
148 // Don't return U+FFFD replacement character but return the
149 // unpaired surrogate itself, so that if we want to we can draw
150 // a tofu block for it.
151 return high.unicode();
152 };
153
154 // Setup the resolution handler.
155 const bool horzSnapping = textRendering == KoSvgText::RenderingOptimizeSpeed || (textRendering == KoSvgText::RenderingOptimizeLegibility && !isHorizontal);
156 const bool vertSnapping = textRendering != KoSvgText::RenderingGeometricPrecision && textRendering != KoSvgText::RenderingAuto;
157 const KoSvgText::ResolutionHandler resHandler(this->xRes, this->yRes, horzSnapping, vertSnapping);
158
159 // First, get text. We use the subChunks because that handles bidi-insertion for us.
160
161 bool _ignore = false;
162 const QVector<SubChunk> textChunks =
163 collectSubChunks(textData.childBegin(), KoSvgTextProperties::defaultProperties(),false, _ignore);
164 QString text;
165 QVector<QPair<int, int>> clusterToOriginalString;
166 QMap<int, KoSvgText::TextSpaceCollapse> collapseModes;
167 QString plainText;
168 Q_FOREACH (const SubChunk &chunk, textChunks) {
169 for (int i = 0; i < chunk.newToOldPositions.size(); i++) {
170 QPair<int, int> pos = chunk.newToOldPositions.at(i);
171 int a = pos.second < 0? -1: text.size()+pos.second;
172 int b = pos.first < 0? -1: plainText.size()+pos.first;
173 QPair<int, int> newPos = QPair<int, int> (a, b);
174 clusterToOriginalString.append(newPos);
175 }
177 collapseModes.insert(text.size(), collapse);
178 text.append(chunk.text);
179 plainText.append(chunk.originalText);
180 }
181 QVector<bool> collapseChars = KoCssTextUtils::collapseSpaces(&text, collapseModes);
182 debugFlake << "Laying out the following text: " << text;
183
184 // 1. Setup.
186 KoSvgText::LineBreak linebreakStrictness = KoSvgText::LineBreak(rootProperties.property(KoSvgTextProperties::LineBreakId).toInt());
187
189 QVector<char> lineBreaks(text.size());
190 QVector<char> wordBreaks(text.size());
191 QVector<char> graphemeBreaks(text.size());
192 if (text.size() > 0) {
193 // TODO: Figure out how to gracefully skip all the next steps when the text-size is 0.
194 // can't currently remember if removing the associated outlines was all that is necessary.
195 QString unibreakLang = langToLibUnibreakLang(lang);
196 if (!lang.isEmpty()) {
197 // Libunibreak currently only has support for strict, and even then only
198 // for very specific cases.
199 if (linebreakStrictness == KoSvgText::LineBreakStrict) {
200 unibreakLang += "-strict";
201 }
202 }
203 set_linebreaks_utf16(text.utf16(), static_cast<size_t>(text.size()), unibreakLang.toUtf8().data(), lineBreaks.data());
204 set_wordbreaks_utf16(text.utf16(), static_cast<size_t>(text.size()), unibreakLang.toUtf8().data(), wordBreaks.data());
205 set_graphemebreaks_utf16(text.utf16(), static_cast<size_t>(text.size()), unibreakLang.toUtf8().data(), graphemeBreaks.data());
206 justify = KoCssTextUtils::justificationOpportunities(text, lang);
207 }
208
209
210 int globalIndex = 0;
211 QVector<CharacterResult> result(text.size());
212 // HACK ALERT!
213 // Apparently feeding a bidi algorithm a hardbreak makes it go 'ok, not doing any
214 // bidi', which makes sense, Bidi is supposed to be done 'after' line breaking.
215 // Without replacing hardbreaks with spaces, hardbreaks in rtl will break the bidi.
216 for (int i = 0; i < text.size(); i++) {
217 if (lineBreaks[i] == LINEBREAK_MUSTBREAK) {
218 text[i] = QChar::Space;
219 }
220 }
221 for (int i=0; i < clusterToOriginalString.size(); i++) {
222 QPair<int, int> mapping = clusterToOriginalString.at(i);
223 if (mapping.first < 0) {
224 continue;
225 } else {
226 if (mapping.first < result.size()) {
227 result[mapping.first].plaintTextIndex = mapping.second;
228 }
229 }
230 }
231
232
233 // 3. Resolve character positioning.
234 // According to SVG 2.0 algorithm, you'd first put everything into a css-compatible-renderer,
235 // so, apply https://www.w3.org/TR/css-text-3/#order and then the rest of the SVG 2 text algorithm.
236 // However, SVG 1.1 requires Textchunks to have separate shaping (and separate bidi), so you need to
237 // resolve the transforms first to find the absolutely positioned chunks, but because that relies on
238 // white-space collapse, we need to do that first, and then apply the collapse.
239 // https://github.com/w3c/svgwg/issues/631 and https://github.com/w3c/svgwg/issues/635
240 // argue shaping across multiple text-chunks is undefined behaviour, but it breaks SVG 1.1 text
241 // to consider it anything but required to have both shaping and bidi-reorder break.
242 QVector<KoSvgText::CharTransformation> resolvedTransforms(text.size());
243 globalIndex = 0;
244 bool wrapped = !(inlineSize.isAuto && this->shapesInside.isEmpty());
245 if (!resolvedTransforms.isEmpty()) {
246 resolvedTransforms[0].xPos = 0;
247 resolvedTransforms[0].yPos = 0;
248 }
249 this->resolveTransforms(textData.childBegin(), text, result, globalIndex, isHorizontal, wrapped, false, resolvedTransforms, collapseChars, KoSvgTextProperties::defaultProperties(), true);
250
251 // pass everything to a css-compatible text-layout algorithm.
252 raqm_t_sp layout(raqm_create());
253
254 if (raqm_set_text_utf16(layout.data(), text.utf16(), static_cast<size_t>(text.size()))) {
255 if (writingMode == KoSvgText::VerticalRL || writingMode == KoSvgText::VerticalLR) {
256 raqm_set_par_direction(layout.data(), raqm_direction_t::RAQM_DIRECTION_TTB);
257 } else if (direction == KoSvgText::DirectionRightToLeft) {
258 raqm_set_par_direction(layout.data(), raqm_direction_t::RAQM_DIRECTION_RTL);
259 } else {
260 raqm_set_par_direction(layout.data(), raqm_direction_t::RAQM_DIRECTION_LTR);
261 }
262
263 int start = 0;
264 Q_FOREACH (const SubChunk &chunk, textChunks) {
265 int length = chunk.text.size();
266 KoSvgTextProperties properties = chunk.inheritedProps;
267
268 // In this section we retrieve the resolved transforms and
269 // direction/anchoring that we can get from the subchunks.
273 KoSvgText::HangingPunctuations hang =
274 properties.propertyOrDefault(KoSvgTextProperties::HangingPunctuationId).value<KoSvgText::HangingPunctuations>();
280
281 KoSvgText::LineBreak localLinebreakStrictness = KoSvgText::LineBreak(properties.property(KoSvgTextProperties::LineBreakId).toInt());
282 QString localLang = properties.property(KoSvgTextProperties::TextLanguage).toString();
283
284 if ((localLinebreakStrictness != linebreakStrictness && localLinebreakStrictness != KoSvgText::LineBreakAnywhere) || localLang != lang ) {
285 QString unibreakLang = langToLibUnibreakLang(localLang);
286 if (localLinebreakStrictness == KoSvgText::LineBreakStrict && !unibreakLang.isEmpty()) {
287 unibreakLang += "-strict";
288 }
289 int localLineBreakStart = qMax(0, start -1);
290 int localLineBreakEnd = qMin(text.size(), start+chunk.text.size());
291 QVector<char> localLineBreaks(localLineBreakEnd - localLineBreakStart);
292 set_linebreaks_utf16(text.mid(localLineBreakStart, localLineBreaks.size()).utf16(),
293 static_cast<size_t>(localLineBreaks.size()),
294 unibreakLang.toUtf8().data(),
295 localLineBreaks.data());
296 for (int i = 0; i < localLineBreaks.size(); i++) {
297 if (i < (start - localLineBreakStart)) continue;
298 if (i+localLineBreakStart > start+chunk.text.size()) break;
299 lineBreaks[i+localLineBreakStart] = localLineBreaks[i];
300 }
301 }
302
303 KoColorBackground *b = dynamic_cast<KoColorBackground *>(chunk.bg.data());
304 QColor fillColor;
305 if (b)
306 {
307 fillColor = b->color();
308 }
309 if (!letterSpacing.isAuto) {
310 tabInfo.extraSpacing += letterSpacing.length.value;
311 }
312 if (!wordSpacing.isAuto) {
313 tabInfo.extraSpacing += wordSpacing.length.value;
314 }
315
316 for (int i = 0; i < length; i++) {
317 CharacterResult cr = result[start + i];
318 cr.anchor = anchor;
319 cr.direction = direction;
320 QPair<bool, bool> canJustify = justify.value(start + i, QPair<bool, bool>(false, false));
321 cr.justifyBefore = canJustify.first;
322 cr.justifyAfter = canJustify.second;
323 cr.overflowWrap = overflowWrap;
324 if (lineBreaks[start + i] == LINEBREAK_MUSTBREAK) {
328 } else if (lineBreaks[start + i] == LINEBREAK_ALLOWBREAK && wrap != KoSvgText::NoWrap) {
330
331 if (KoCssTextUtils::collapseLastSpace(text.at(start + i), collapse)) {
334 }
335 }
337 const auto isFollowedByForcedLineBreak = [&]() {
338 if (result.size() <= start + i + 1) {
339 // End of the text block, consider it a forced line break
340 return true;
341 }
342 if (lineBreaks[start + i+ 1] == LINEBREAK_MUSTBREAK) {
343 // Next character is a forced line break
344 return true;
345 }
346 if (resolvedTransforms.at(start + i + 1).startsNewChunk()) {
347 // Next character is another chunk, consider it a forced line break
348 return true;
349 }
350 return false;
351 };
352 bool forceHang = false;
353 if (KoCssTextUtils::hangLastSpace(text.at(start + i), collapse, wrap, forceHang, isFollowedByForcedLineBreak())) {
355
356 } else if (collapse == KoSvgText::BreakSpaces && wrap != KoSvgText::NoWrap && KoCssTextUtils::IsCssWordSeparator(QString(text.at(start + i)))) {
358 }
359 }
360
361 if ((wordBreakStrictness == KoSvgText::WordBreakBreakAll ||
362 localLinebreakStrictness == KoSvgText::LineBreakAnywhere)
363 && wrap != KoSvgText::NoWrap) {
364 if (graphemeBreaks[start + i] == GRAPHEMEBREAK_BREAK && cr.breakType == BreakType::NoBreak) {
366 }
367 } else if (wordBreakStrictness == KoSvgText::WordBreakKeepAll) {
368 cr.breakType = (wordBreaks[start + i] == WORDBREAK_BREAK)? cr.breakType: BreakType::NoBreak;
369 }
370 if (cr.lineStart != LineEdgeBehaviour::Collapse && hang.testFlag(KoSvgText::HangFirst)) {
373 : cr.lineEnd;
374 }
376 if (hang.testFlag(KoSvgText::HangLast)) {
379 : cr.lineEnd;
380 }
381 if (hang.testFlag(KoSvgText::HangEnd)) {
382 LineEdgeBehaviour edge = hang.testFlag(KoSvgText::HangForce)
385 cr.lineEnd = KoCssTextUtils::characterCanHang(text.at(start + i), KoSvgText::HangEnd) ? edge : cr.lineEnd;
386 }
387 }
388
389 cr.cursorInfo.isWordBoundary = (wordBreaks[start + i] == WORDBREAK_BREAK);
390 cr.cursorInfo.color = fillColor;
391
392
393
394 if (resolvedTransforms.at(start + i).startsNewChunk()) {
395 raqm_set_arbitrary_run_break(layout.data(), static_cast<size_t>(start + i), true);
396 }
397
398 if (chunk.firstTextInPath && i == 0) {
399 cr.anchored_chunk = true;
400 }
401 result[start + i] = cr;
402 }
403
404 QVector<int> lengths;
405 QStringList fontFeatures = properties.fontFeaturesForText(start, length);
406
408 bool synthesizeWeight = properties.propertyOrDefault(KoSvgTextProperties::FontSynthesisBoldId).toBool();
409 bool synthesizeStyle = properties.propertyOrDefault(KoSvgTextProperties::FontSynthesisItalicId).toBool();
410
411 const std::vector<FT_FaceSP> faces = KoFontRegistry::instance()->facesForCSSValues(
412 lengths,
413 properties.cssFontInfo(),
414 chunk.text,
415 static_cast<quint32>(resHandler.xRes),
416 static_cast<quint32>(resHandler.yRes),
417 disableFontMatching);
418 const qreal fontSize = properties.cssFontInfo().size;
420 raqm_set_language(layout.data(),
421 properties.property(KoSvgTextProperties::TextLanguage).toString().toUtf8(),
422 static_cast<size_t>(start),
423 static_cast<size_t>(length));
424 }
425 Q_FOREACH (const QString &feature, fontFeatures) {
426 debugFlake << "adding feature" << feature;
427 raqm_add_font_feature(layout.data(), feature.toUtf8(), feature.toUtf8().size());
428 }
429
430 if (!letterSpacing.isAuto) {
431 raqm_set_letter_spacing_range(layout.data(),
432 static_cast<int>(letterSpacing.length.value * resHandler.freeTypePixel * resHandler.pointToPixelFactor(isHorizontal)),
433 static_cast<size_t>(start),
434 static_cast<size_t>(length));
435 }
436
437 if (!wordSpacing.isAuto) {
438 raqm_set_word_spacing_range(layout.data(),
439 static_cast<int>(wordSpacing.length.value * resHandler.freeTypePixel * resHandler.pointToPixelFactor(isHorizontal)),
440 static_cast<size_t>(start),
441 static_cast<size_t>(length));
442 }
443
444 for (int i = 0; i < lengths.size(); i++) {
445 length = lengths.at(i);
446 const FT_FaceSP &face = faces.at(static_cast<size_t>(i));
447 const FT_Int32 faceLoadFlags = KoFontRegistry::loadFlagsForFace(face.data(), isHorizontal, 0, textRendering);
448 if (start == 0) {
449 raqm_set_freetype_face(layout.data(), face.data());
450 raqm_set_freetype_load_flags(layout.data(), faceLoadFlags);
451 }
452 if (length > 0) {
453 raqm_set_freetype_face_range(layout.data(),
454 face.data(),
455 static_cast<size_t>(start),
456 static_cast<size_t>(length));
457 raqm_set_freetype_load_flags_range(layout.data(),
458 faceLoadFlags,
459 static_cast<size_t>(start),
460 static_cast<size_t>(length));
461 }
462
463 QHash<QChar::Script, KoSvgText::FontMetrics> metricsList;
464 for (int j=start; j<start+length; j++) {
465 const QChar::Script currentScript = QChar::script(getUcs4At(text, j));
466 if (!metricsList.contains(currentScript)) {
467 metricsList.insert(currentScript, KoFontRegistry::generateFontMetrics(face, isHorizontal, KoWritingSystemUtils::scriptTagForQCharScript(currentScript), textRendering));
468 }
469 result[j].metrics = metricsList.value(currentScript);
470 if (fontSize < 1.0) {
471 result[j].extraFontScaling = fontSize;
472 }
473
474 const KoSvgText::FontMetrics currentMetrics = properties.applyLineHeight(result[j].metrics);
475
476 if (text.at(j) == QChar::Tabulation) {
477 qreal tabSize = 0;
478 if (tabInfo.isNumber) {
479 // Try to avoid Nan situations.
480 if (result[j].metrics.spaceAdvance > 0) {
481 tabSize = (result[j].metrics.spaceAdvance + (tabInfo.extraSpacing*resHandler.freeTypePixel)) * tabInfo.value;
482 } else {
483 tabSize = ((result[j].metrics.fontSize/2) + (tabInfo.extraSpacing*resHandler.freeTypePixel)) * tabInfo.value;
484 }
485 } else {
486 tabSize = tabInfo.length.value * resHandler.freeTypePixel;
487 }
488 result[j].tabSize = tabSize;
489 }
490
491 result[j].fontHalfLeading = currentMetrics.lineGap / 2;
492 result[j].fontStyle = synthesizeStyle? style.style: QFont::StyleNormal;
493 result[j].fontWeight = synthesizeWeight? properties.propertyOrDefault(KoSvgTextProperties::FontWeightId).toInt(): 400;
494 }
495
496 start += length;
497 }
498 }
499 debugFlake << "text-length:" << text.size();
500 }
501 // set very first character as anchored chunk.
502 if (!result.empty()) {
503 result[0].anchored_chunk = true;
504 }
505
506 if (raqm_layout(layout.data())) {
507 debugFlake << "layout succeeded";
508 }
509
510 // 2. Set flags and assign initial positions
511 // We also retrieve a glyph path here.
512 size_t count = 0;
513 const raqm_glyph_t *glyphs = raqm_get_glyphs(layout.data(), &count);
514 if (!glyphs) {
515 return;
516 }
517
518 QPointF totalAdvanceFTFontCoordinates;
519 QMap<int, int> logicalToVisual;
520 this->isBidi = false;
521
522
523 KIS_ASSERT(count <= INT32_MAX);
524
525 for (int i = 0; i < static_cast<int>(count); i++) {
526 raqm_glyph_t currentGlyph = glyphs[i];
527 KIS_ASSERT(currentGlyph.cluster <= INT32_MAX);
528 const int cluster = static_cast<int>(currentGlyph.cluster);
529 if (!result[cluster].addressable) {
530 continue;
531 }
532 CharacterResult charResult = result[cluster];
533
534 const FT_Int32 faceLoadFlags = KoFontRegistry::loadFlagsForFace(currentGlyph.ftface, isHorizontal, 0, textRendering);
535
536
537 const char32_t codepoint = getUcs4At(text, cluster);
538 debugFlake << "glyph" << i << "cluster" << cluster << currentGlyph.index << codepoint;
539
540 charResult.cursorInfo.rtl = raqm_get_direction_at_index(layout.data(), cluster) == RAQM_DIRECTION_RTL;
541 if (charResult.cursorInfo.rtl != (charResult.direction == KoSvgText::DirectionRightToLeft)) {
542 this->isBidi = true;
543 }
544
545 if (!this->loadGlyph(resHandler,
546 faceLoadFlags,
547 isHorizontal,
548 codepoint,
549 textRendering,
550 currentGlyph,
551 charResult,
552 totalAdvanceFTFontCoordinates)) {
553 continue;
554 }
555
556 charResult.visualIndex = i;
557 logicalToVisual.insert(cluster, i);
558
559 charResult.middle = false;
560
561 result[cluster] = charResult;
562 }
563
564 // fix it so that characters that are in the 'middle' due to either being
565 // surrogates or part of a ligature, are marked as such. Also set the css
566 // position so that anchoring will work correctly later.
567 int firstCluster = -1;
568 bool graphemeBreakNext = false;
569 for (int i = 0; i < result.size(); i++) {
570 result[i].middle = result.at(i).visualIndex == -1;
571 if (result[i].addressable && !result.at(i).middle) {
572 if (result.at(i).plaintTextIndex > -1 && firstCluster > -1) {
573 CursorInfo info = result.at(firstCluster).cursorInfo;
574 // ensure the advance gets added to the ligature carets if we found them,
575 // so they don't get overwritten by the synthesis code.
576 if (!info.offsets.isEmpty()) {
577 info.offsets.append(result.at(firstCluster).advance);
578 }
579 info.graphemeIndices.append(result.at(i).plaintTextIndex);
580 result[firstCluster].cursorInfo = info;
581 }
582 firstCluster = i;
583 } else {
584 int fC = qMax(0, firstCluster);
585 if (text[fC].isSpace() == text[i].isSpace()) {
586 if (result[fC].breakType != BreakType::HardBreak) {
587 result[fC].breakType = result.at(i).breakType;
588 }
589 if (result[fC].lineStart == LineEdgeBehaviour::NoChange) {
590 result[fC].lineStart = result.at(i).lineStart;
591 }
592 if (result[fC].lineEnd == LineEdgeBehaviour::NoChange) {
593 result[fC].lineEnd = result.at(i).lineEnd;
594 }
595 }
596 if (graphemeBreakNext && result[i].addressable && result.at(i).plaintTextIndex > -1) {
597 result[fC].cursorInfo.graphemeIndices.append(result.at(i).plaintTextIndex);
598 }
599 result[i].cssPosition = result.at(fC).cssPosition + result.at(fC).advance;
600 result[i].hidden = true;
601 }
602 graphemeBreakNext = graphemeBreaks[i] == GRAPHEMEBREAK_BREAK;
603 }
604 int fC = qMax(0, firstCluster);
605 if (result.at(fC).cursorInfo.graphemeIndices.isEmpty() || graphemeBreakNext) {
606 result[fC].cursorInfo.graphemeIndices.append(plainText.size());
607 }
608
609 // Add a dummy charResult at the end when the last non-collapsed position
610 // is a hard breaks, so the new line is laid out.
611 int dummyIndex = -1;
612 if (result.at(fC).breakType == BreakType::HardBreak) {
613 CharacterResult hardbreak = result.at(fC);
614 dummyIndex = fC +1;
615 CharacterResult dummy;
616 //dummy.hidden = true;
617 dummy.addressable = true;
618 dummy.visualIndex = hardbreak.visualIndex + 1;
619 dummy.scaledAscent = hardbreak.scaledAscent;
620 dummy.scaledDescent = hardbreak.scaledDescent;
621 dummy.scaledHalfLeading = hardbreak.scaledHalfLeading;
622 dummy.cssPosition = hardbreak.cssPosition + hardbreak.advance;
623 dummy.finalPosition = dummy.cssPosition;
624 dummy.inkBoundingBox = hardbreak.inkBoundingBox;
625 if (isHorizontal) {
626 dummy.scaleCharacterResult(0.0, 1.0);
627 } else {
628 dummy.scaleCharacterResult(1.0, 0.0);
629 }
630 dummy.plaintTextIndex = hardbreak.cursorInfo.graphemeIndices.last();
631 dummy.cursorInfo.caret = hardbreak.cursorInfo.caret;
632 dummy.cursorInfo.rtl = hardbreak.cursorInfo.rtl;
633 dummy.direction = hardbreak.direction;
634 result.insert(dummyIndex, dummy);
635 logicalToVisual.insert(dummyIndex, dummy.visualIndex);
636 resolvedTransforms.insert(dummyIndex, KoSvgText::CharTransformation());
637 }
638
639 debugFlake << "Glyphs retrieved";
640
641 // Compute baseline alignment.
642 globalIndex = 0;
643 this->computeFontMetrics(textData.childBegin(), KoSvgTextProperties::defaultProperties(), KoSvgText::FontMetrics(), KoSvgText::BaselineAuto, QPointF(), QPointF(), result, globalIndex, resHandler, isHorizontal, disableFontMatching);
644
645 // Handle linebreaking.
646 QPointF startPos = resolvedTransforms.value(0).absolutePos() - result.value(0).dominantBaselineOffset;
647 if (!this->shapesInside.isEmpty()) {
648 QList<QPainterPath> shapes = getShapes(this->shapesInside, this->shapesSubtract, rootProperties);
649 this->lineBoxes = flowTextInShapes(rootProperties, logicalToVisual, result, shapes, startPos, resHandler);
650 } else {
651 this->lineBoxes = breakLines(rootProperties, logicalToVisual, result, startPos, resHandler);
652 }
653
654 // Handle baseline alignment.
655 globalIndex = 0;
656 this->handleLineBoxAlignment(textData.childBegin(), result, this->lineBoxes, globalIndex, isHorizontal, KoSvgTextProperties::defaultProperties());
657
658 if (inlineSize.isAuto && this->shapesInside.isEmpty()) {
659 debugFlake << "Starting with SVG 1.1 specific portion";
660 debugFlake << "4. Adjust positions: dx, dy";
661 // 4. Adjust positions: dx, dy
662 QPointF shift = QPointF();
663 bool setAnchoredChunk = false;
664 for (int i = 0; i < result.size(); i++) {
665 if (result.at(i).addressable) {
666 KoSvgText::CharTransformation transform = resolvedTransforms[i];
667 if (transform.hasRelativeOffset()) {
668 shift += transform.relativeOffset();
669 }
670 CharacterResult charResult = result[i];
671 if (transform.rotate) {
672 charResult.rotate = *transform.rotate;
673 }
674 charResult.finalPosition = charResult.cssPosition + shift;
675
676 // ensure that anchored chunks aren't set in the middle of a ligature.
677 if (setAnchoredChunk) {
678 charResult.anchored_chunk = true;
679 setAnchoredChunk = false;
680 }
681 if (transform.startsNewChunk()) {
682 if(charResult.middle) {
683 setAnchoredChunk = true;
684 } else {
685 charResult.anchored_chunk = true;
686 }
687 }
688 result[i] = charResult;
689 }
690 }
691
692 // 5. Apply ‘textLength’ attribute
693 debugFlake << "5. Apply ‘textLength’ attribute";
694 globalIndex = 0;
695 int resolved = 0;
696 this->applyTextLength(textData.childBegin(), result, globalIndex, resolved, isHorizontal, KoSvgTextProperties::defaultProperties(), resHandler);
697
698 // 6. Adjust positions: x, y
699 debugFlake << "6. Adjust positions: x, y";
700 // https://github.com/w3c/svgwg/issues/617
701 shift = QPointF();
702 for (int i = 0; i < result.size(); i++) {
703 if (result.at(i).addressable) {
704 KoSvgText::CharTransformation transform = resolvedTransforms[i];
705 CharacterResult charResult = result[i];
706 if (transform.xPos) {
707 const qreal delta = charResult.baselineOffset.x() + (transform.dxPos ? *transform.dxPos : 0.0);
708 shift.setX(*transform.xPos + (delta - charResult.finalPosition.x()));
709 }
710 if (transform.yPos) {
711 const qreal delta = charResult.baselineOffset.y() + (transform.dyPos ? *transform.dyPos : 0.0);
712 shift.setY(*transform.yPos + (delta - charResult.finalPosition.y()));
713 }
714 charResult.finalPosition += shift;
715 if (charResult.middle && i-1 >=0) {
716 charResult.finalPosition = result.at(i-1).finalPosition;
717 }
718
719 result[i] = charResult;
720 }
721 }
722
723 // 7. Apply anchoring
724 debugFlake << "7. Apply anchoring";
725 applyAnchoring(result, isHorizontal, resHandler);
726
727 // Computing the textDecorations needs to happen before applying the
728 // textPath to the results, as we need the unapplied result vector for
729 // positioning.
730 debugFlake << "Now Computing text-decorations";
731 globalIndex = 0;
732 this->computeTextDecorations(textData.childBegin(),
733 result,
734 logicalToVisual,
735 resHandler,
736 nullptr,
737 0.0,
738 false,
739 globalIndex,
740 isHorizontal,
743
744 // 8. Position on path
745
746 debugFlake << "8. Position on path";
747 this->applyTextPath(textData.childBegin(), result, isHorizontal, startPos, KoSvgTextProperties::defaultProperties());
748 } else {
749 globalIndex = 0;
750 debugFlake << "Computing text-decorationsfor inline-size";
751 this->computeTextDecorations(textData.childBegin(),
752 result,
753 logicalToVisual,
754 resHandler,
755 nullptr,
756 0.0,
757 false,
758 globalIndex,
759 isHorizontal,
762 }
763
764 // 9. return result.
765 debugFlake << "9. return result.";
766 globalIndex = 0;
767 QVector<CursorPos> cursorPos;
768 for (auto chunk = textChunks.begin(); chunk != textChunks.end(); chunk++) {
769 const int j = chunk->text.size();
770 chunk->associatedLeaf->finalResultIndex = (globalIndex+j);
771 for (int i = globalIndex; i < chunk->associatedLeaf->finalResultIndex; i++) {
772 if (result.at(i).addressable && !result.at(i).middle) {
773
774 if (result.at(i).plaintTextIndex > -1) {
775 QVector<QPointF> positions;
776 bool insertFirst = false;
777 if (result.at(i).anchored_chunk) {
778 CursorPos pos;
779 pos.cluster = i;
780 pos.index = result.at(i).plaintTextIndex;
781 insertFirst = true;
782 QPointF newOffset = result.at(i).cursorInfo.rtl? result.at(i).advance: QPointF();
783 result[i].cursorInfo.offsets.insert(0, newOffset);
784 positions.append(newOffset);
785 pos.offset = 0;
786 pos.synthetic = true;
787 cursorPos.append(pos);
788 }
789
790 int graphemes = result.at(i).cursorInfo.graphemeIndices.size();
791 for (int k = 0; k < graphemes; k++) {
792 if (result.at(i).breakType == BreakType::HardBreak && k+1 == graphemes) {
793 continue;
794 }
795 CursorPos pos;
796 pos.cluster = i;
797 pos.index = result.at(i).cursorInfo.graphemeIndices.at(k);
798 pos.offset = insertFirst? k+1: k;
799 cursorPos.append(pos);
800 QPointF offset = (k+1) * (result.at(i).advance/graphemes);
801 positions.append(result.at(i).cursorInfo.rtl? result.at(i).advance - offset: offset);
802 }
803 if (insertFirst) {
804 result[i].cursorInfo.graphemeIndices.insert(0, result[i].plaintTextIndex);
805 }
806 if (result.at(i).cursorInfo.offsets.size() < positions.size()) {
807 result[i].cursorInfo.offsets = positions;
808 }
809 }
810
811
812 if (!result.at(i).hidden) {
813 const QTransform tf = result.at(i).finalTransform();
814 chunk->associatedLeaf->associatedOutline.addRect(tf.mapRect(result.at(i).inkBoundingBox));
815 }
816 }
817 }
818 globalIndex += j;
819 }
820 // figure out if we added a dummy, and if so add a pos for it.
821 if (dummyIndex > -1 && dummyIndex < result.size()) {
822 if (result.at(dummyIndex).anchored_chunk) {
823 CursorPos pos;
824 pos.cluster = dummyIndex;
825 pos.index = result.at(dummyIndex).plaintTextIndex;
826 result[dummyIndex].plaintTextIndex -= 1;
827 result[dummyIndex].cursorInfo.offsets.insert(0, QPointF());
828 pos.offset = 0;
829 pos.synthetic = true;
830 cursorPos.append(pos);
831 if (!textChunks.isEmpty()) {
832 textChunks.last().associatedLeaf->associatedOutline.addRect(result.at(dummyIndex).finalTransform().mapRect(result[dummyIndex].inkBoundingBox));
833 textChunks.last().associatedLeaf->finalResultIndex = result.size();
834 }
835 }
836 }
837 this->initialTextPosition = startPos;
838 this->plainText = plainText;
839 this->result = result;
840 this->cursorPos = cursorPos;
841 this->logicalToVisualCursorPos = logicalToVisualCursorPositions(cursorPos, result, this->lineBoxes, direction == KoSvgText::DirectionLeftToRight);
842}
843
844void KoSvgTextShape::Private::clearAssociatedOutlines()
845{
846 for (auto it = textData.depthFirstTailBegin(); it != textData.depthFirstTailEnd(); it++) {
847 it->associatedOutline = QPainterPath();
848 it->textDecorations.clear();
849 it->finalResultIndex = -1;
850 }
851}
852
857const QString bidiControls = "\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069";
858void KoSvgTextShape::Private::resolveTransforms(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement,
859 QString text, QVector<CharacterResult> &result, int &currentIndex,
860 bool isHorizontal, bool wrapped, bool textInPath,
862 const KoSvgTextProperties resolvedProps, bool withControls) {
863 QVector<KoSvgText::CharTransformation> local = currentTextElement->localTransformations;
864
865
866 int index = currentIndex;
867 int j = index + numChars(currentTextElement, withControls, resolvedProps);
868
869 if (currentTextElement->textPath) {
870 textInPath = true;
871 } else {
872 int i = 0;
873 for (int k = index; k < j; k++ ) {
874 if (k >= text.size()) {
875 continue;
876 }
877
878 bool bidi = bidiControls.contains(text.at(k));
879 bool softHyphen = text.at(k) == QChar::SoftHyphen;
880
881 // Apparently when there's bidi controls in the text, they participate in line-wrapping,
882 // so we don't check for it when wrapping.
883 if (collapsedChars[k] || (bidi && !wrapped) || softHyphen) {
884 result[k].addressable = false;
885 continue;
886 }
887 if (k > 0 && text.at(k).isLowSurrogate() && text.at(k-1).isHighSurrogate()) {
888 // transforms apply per-undicode codepoint, not per utf16.
889 result[k].addressable = false;
890 continue;
891 }
892
893 if (i < local.size()) {
894
895 KoSvgText::CharTransformation newTransform = local.at(i);
896 newTransform.mergeInParentTransformation(resolved[k]);
897 resolved[k] = newTransform;
898 i += 1;
899 } else if (k > 0) {
900 if (resolved[k - 1].rotate && !resolved[k].rotate) {
901 resolved[k].rotate = resolved[k - 1].rotate;
902 }
903 }
904 }
905 }
906
907 KoSvgTextProperties props = currentTextElement->properties;
908 props.inheritFrom(resolvedProps, true);
909 for (auto child = KisForestDetail::childBegin(currentTextElement); child != KisForestDetail::childEnd(currentTextElement); child++) {
910 resolveTransforms(child, text, result, currentIndex, isHorizontal, false, textInPath, resolved, collapsedChars, props, withControls);
911
912 }
913
914 if (currentTextElement->textPath) {
915 bool first = true;
916 for (int k = index; k < j; k++ ) {
917
918 if (!result[k].addressable) {
919 continue;
920 }
921
922 // Also unset the first transform on a textPath to avoid breakage with rtl text.
923 if (first) {
924 if (isHorizontal) {
925 resolved[k].xPos = 0.0;
926 } else {
927 resolved[k].yPos = 0.0;
928 }
929 first = false;
930 }
931 // x and y attributes are officially 'ignored' for text on path, though the algorithm
932 // suggests this is only if a child of a path... In reality, not resetting this will
933 // break text-on-path with rtl.
934 if (isHorizontal) {
935 resolved[k].yPos.reset();
936 } else {
937 resolved[k].xPos.reset();
938 }
939 }
940 }
941
942 currentIndex = j;
943
944}
945
946// NOLINTNEXTLINE(readability-function-cognitive-complexity)
947void KoSvgTextShape::Private::applyTextLength(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement,
949 int &currentIndex,
950 int &resolvedDescendentNodes,
951 bool isHorizontal,
952 const KoSvgTextProperties resolvedProps, const KoSvgText::ResolutionHandler &resHandler)
953{
954
955 int i = currentIndex;
956 int j = i + numChars(currentTextElement, true, resolvedProps);
957 int resolvedChildren = 0;
958
960 props.inheritFrom(resolvedProps, true);
961 for (auto child = KisForestDetail::childBegin(currentTextElement); child != KisForestDetail::childEnd(currentTextElement); child++) {
962 applyTextLength(child, result, currentIndex, resolvedChildren, isHorizontal, props, resHandler);
963 }
964 // Raqm handles bidi reordering for us, but this algorithm does not
965 // anticipate that, so we need to keep track of which typographic item
966 // belongs where.
967 QMap<int, int> visualToLogical;
968 if (!currentTextElement->textLength.isAuto) {
969 qreal a = 0.0;
970 qreal b = 0.0;
971 int n = 0;
972 for (int k = i; k < j; k++) {
973 if (result.at(k).addressable) {
974 if (result.at(k).visualIndex > -1) {
975 visualToLogical.insert(result.at(k).visualIndex, k);
976 }
977 // if character is linebreak, return;
978
979 qreal pos = result.at(k).finalPosition.x();
980 qreal advance = result.at(k).advance.x();
981 if (!isHorizontal) {
982 pos = result.at(k).finalPosition.y();
983 advance = result.at(k).advance.y();
984 }
985 if (k == i) {
986 a = qMin(pos, pos + advance);
987 b = qMax(pos, pos + advance);
988 } else {
989 a = qMin(a, qMin(pos, pos + advance));
990 b = qMax(b, qMax(pos, pos + advance));
991 }
992 if (!result.at(k).textLengthApplied) {
993 n += 1;
994 }
995 }
996 }
997 n += resolvedChildren;
998 bool spacingAndGlyphs = (currentTextElement->lengthAdjust == KoSvgText::LengthAdjustSpacingAndGlyphs);
999 if (!spacingAndGlyphs) {
1000 n -= 1;
1001 }
1002 const qreal delta = currentTextElement->textLength.customValue - (b - a);
1003
1004 const QPointF d = isHorizontal ? QPointF(delta / n, 0) : QPointF(0, delta / n);
1005
1006 QPointF shift;
1007 bool secondTextLengthApplied = false;
1008 Q_FOREACH (int k, visualToLogical.keys()) {
1009 CharacterResult cr = result[visualToLogical.value(k)];
1010 if (cr.addressable) {
1011 cr.finalPosition += shift;
1012 if (spacingAndGlyphs) {
1013 QPointF scale(d.x() != 0 ? (d.x() / cr.advance.x()) + 1 : 1.0, d.y() != 0 ? (d.y() / cr.advance.y()) + 1 : 1.0);
1014 cr.scaleCharacterResult(scale.x(), scale.y());
1015 }
1016 bool last = spacingAndGlyphs ? false : k == visualToLogical.keys().last();
1017
1018 if (!(cr.textLengthApplied && secondTextLengthApplied) && !last) {
1019 shift = resHandler.adjust(shift+d);
1020 }
1021 secondTextLengthApplied = cr.textLengthApplied;
1022 cr.textLengthApplied = true;
1023 }
1024 result[visualToLogical.value(k)] = cr;
1025 }
1026 resolvedDescendentNodes += 1;
1027
1028 // apply the shift to all consecutive chars as long as they don't start
1029 // a new chunk.
1030 int lastVisualValue = visualToLogical.keys().last();
1031 visualToLogical.clear();
1032
1033 for (int k = j; k < result.size(); k++) {
1034 if (result.at(k).anchored_chunk) {
1035 break;
1036 }
1037 visualToLogical.insert(result.at(k).visualIndex, k);
1038 }
1039 // And also backwards for rtl.
1040 for (int k = i; k > -1; k--) {
1041 visualToLogical.insert(result.at(k).visualIndex, k);
1042 if (result.at(k).anchored_chunk) {
1043 break;
1044 }
1045 }
1046 Q_FOREACH (int k, visualToLogical.keys()) {
1047 if (k > lastVisualValue) {
1048 result[visualToLogical.value(k)].finalPosition += shift;
1049 }
1050 }
1051 }
1052
1053 currentIndex = j;
1054}
1055
1061void KoSvgTextShape::Private::computeFontMetrics(// NOLINT(readability-function-cognitive-complexity)
1063 const KoSvgTextProperties &parentProps,
1064 const KoSvgText::FontMetrics &parentBaselineTable,
1065 const KoSvgText::Baseline parentBaseline,
1066 const QPointF superScript,
1067 const QPointF subScript,
1069 int &currentIndex,
1070 const KoSvgText::ResolutionHandler resHandler,
1071 const bool isHorizontal,
1072 const bool disableFontMatching)
1073{
1074 const int i = currentIndex;
1075 const int j = qMin(i + numChars(parent, true, parentProps), result.size());
1076
1077 KoSvgTextProperties properties = parent->properties;
1078 properties.inheritFrom(parentProps, true);
1079
1081 QPointF baselineShiftTotal;
1083
1084 if (baselineShiftMode == KoSvgText::ShiftSuper) {
1085 baselineShiftTotal = isHorizontal ? superScript : QPointF(-superScript.y(), superScript.x());
1086 } else if (baselineShiftMode == KoSvgText::ShiftSub) {
1087 baselineShiftTotal = isHorizontal ? subScript : QPointF(-subScript.y(), subScript.x());
1088 } else if (baselineShiftMode == KoSvgText::ShiftLengthPercentage) {
1089 // Positive baseline-shift goes up in the inline-direction, which is up in horizontal and right in vertical.
1090 baselineShiftTotal = isHorizontal ? QPointF(0, -baselineShift.value) : QPointF(baselineShift.value, 0);
1091 }
1092 baselineShiftTotal = resHandler.adjust(baselineShiftTotal);
1093
1094 QVector<int> lengths;
1095 const std::vector<FT_FaceSP> faces = KoFontRegistry::instance()->facesForCSSValues(
1096 lengths,
1097 properties.cssFontInfo(),
1098 QString(),
1099 static_cast<quint32>(resHandler.xRes),
1100 static_cast<quint32>(resHandler.yRes),
1101 disableFontMatching);
1102
1103
1105
1107 KoSvgText::FontMetrics metrics;
1108
1109 // In SVG 2 and CSS-inline-3, metrics are always recalculated per box.
1110 metrics = KoFontRegistry::generateFontMetrics(faces.front(), isHorizontal);
1111 if (dominantBaseline == KoSvgText::BaselineResetSize || dominantBaseline == KoSvgText::BaselineNoChange) {
1112 dominantBaseline = KoSvgText::BaselineAuto;
1113 }
1114
1115 if (dominantBaseline == KoSvgText::BaselineAuto) {
1116 dominantBaseline = defaultBaseline;
1117 }
1118
1119 // Get underline and super/subscripts.
1120 const QTransform ftTf = resHandler.freeTypeToPointTransform();
1121 const QPointF newSuperScript = ftTf.map(QPointF(metrics.superScriptOffset.first, metrics.superScriptOffset.second));
1122 const QPointF newSubScript = ftTf.map(QPointF(metrics.subScriptOffset.first, -metrics.subScriptOffset.second));
1123
1124 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1125 computeFontMetrics(child, properties, metrics, dominantBaseline, newSuperScript, newSubScript, result, currentIndex, resHandler, isHorizontal, disableFontMatching);
1126 }
1127
1129
1130 if (baselineAdjust == KoSvgText::BaselineDominant) {
1131 baselineAdjust = parentBaseline;
1132 }
1133 if (baselineAdjust == KoSvgText::BaselineAuto || baselineAdjust == KoSvgText::BaselineUseScript) {
1134 // UseScript got deprecated in CSS-Inline-3.
1135 baselineAdjust = defaultBaseline;
1136 }
1137
1139 const int boxOffset = parentBaselineTable.valueForBaselineValue(baselineAdjust) - metrics.valueForBaselineValue(baselineAdjust);
1140 const QPointF shift = resHandler.adjust(ftTf.map(isHorizontal? QPointF(0, (boxOffset)): QPointF((boxOffset), 0)));
1141 if (baselineShiftMode == KoSvgText::ShiftSuper || baselineShiftMode == KoSvgText::ShiftSub) {
1142 // We need to remove the additional alignment shift to ensure that super and sub shifts don't get too dramatic.
1143 baselineShiftTotal -= shift;
1144 }
1145
1146 /*
1147 * The actual alignment process.
1148 * What the CSS-inline-3 spec wants us to do is to have inline-boxes aligned to their parent by their alignment-baseline
1149 * (which defaults to dominant baseline), while glyphs are aligned to their parent by the dominant baseline. This means
1150 * alignment baseline can nest, which makes sense since it does not inherit.
1151 * Glyphs' parent is the inline box they're in, so we're using the main metrics of the inline box to align them via their
1152 * per-glyph metrics to the inline box. This only happens if we're at a text-content element.
1153 * The inline box itself is then aligned to the parent inline box with the alignment baseline, using the parent
1154 * and inline main metrics.
1155 * Then, finally baseline shift is applied, if any.
1156 */
1157 for (int k = i; k < j; k++) {
1158 if (applyGlyphAlignment) {
1159 // We offset the whole glyph so that the origin is at the dominant baseline.
1160 // This will simplify having svg per-char transforms apply as per spec.
1161 const int originOffset = result[k].metrics.valueForBaselineValue(dominantBaseline);
1162 const QPointF newOrigin = resHandler.adjust(ftTf.map(isHorizontal? QPointF(0, originOffset): QPointF(originOffset, 0)));
1163 const int uniqueOffset = metrics.valueForBaselineValue(dominantBaseline);
1164 const QPointF uniqueShift = resHandler.adjust(ftTf.map(isHorizontal? QPointF(0, uniqueOffset): QPointF(uniqueOffset, 0)));
1165 result[k].translateOrigin(newOrigin);
1166 result[k].metrics.offsetMetricsToNewOrigin(dominantBaseline);
1167 result[k].dominantBaselineOffset = uniqueShift;
1168 }
1169 result[k].dominantBaselineOffset += shift;
1170 result[k].baselineOffset += baselineShiftTotal;
1171 }
1172
1173 currentIndex = j;
1174}
1175
1176void KoSvgTextShape::Private::handleLineBoxAlignment(KisForest<KoSvgTextContentElement>::child_iterator parent,
1178 const QVector<LineBox> lineBoxes,
1179 int &currentIndex,
1180 const bool isHorizontal,
1181 const KoSvgTextProperties resolvedProps)
1182{
1183
1184 const int i = currentIndex;
1185 const int j = qMin(i + numChars(parent, true, resolvedProps), result.size());
1186
1187 KoSvgTextProperties properties = parent->properties;
1189
1190 properties.inheritFrom(resolvedProps);
1191 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1192 handleLineBoxAlignment(child, result, lineBoxes, currentIndex, isHorizontal, properties);
1193 }
1194
1195 if (baselineShiftMode == KoSvgText::ShiftLineTop || baselineShiftMode == KoSvgText::ShiftLineBottom) {
1196
1197 LineBox relevantLine;
1198 Q_FOREACH(LineBox lineBox, lineBoxes) {
1199 Q_FOREACH(LineChunk chunk, lineBox.chunks) {
1200 if (chunk.chunkIndices.contains(i)) {
1201 relevantLine = lineBox;
1202 }
1203 }
1204 }
1205 QPointF shift = QPointF();
1206 double ascent = 0.0;
1207 double descent = 0.0;
1208 for (int k = i; k < j; k++) {
1209 // The height calculation here is to remove the shifted-part height
1210 // from the top (or bottom) of the line.
1211 calculateLineHeight(result[k], ascent, descent, isHorizontal, true);
1212 }
1213
1214 if (baselineShiftMode == KoSvgText::ShiftLineTop) {
1215 shift = relevantLine.baselineTop;
1216 shift -= isHorizontal? QPointF(0, ascent):QPointF(ascent, 0);
1217 } else if (baselineShiftMode == KoSvgText::ShiftLineBottom) {
1218 shift = relevantLine.baselineBottom;
1219 shift -= isHorizontal? QPointF(0, descent):QPointF(descent, 0);
1220 }
1221
1222
1223 for (int k = i; k < j; k++) {
1224 CharacterResult cr = result[k];
1225 cr.cssPosition += shift;
1226 cr.finalPosition = cr.cssPosition;
1227 result[k] = cr;
1228 }
1229 }
1230
1231 currentIndex = j;
1232}
1233
1241void KoSvgTextShape::Private::computeTextDecorations(// NOLINT(readability-function-cognitive-complexity)
1243 const QVector<CharacterResult> &result,
1244 const QMap<int, int> &logicalToVisual,
1245 const KoSvgText::ResolutionHandler resHandler,
1246 KoPathShape *textPath,
1247 qreal textPathoffset,
1248 bool side,
1249 int &currentIndex,
1250 bool isHorizontal,
1251 bool ltr,
1252 bool wrapping,
1253 const KoSvgTextProperties resolvedProps)
1254{
1255
1256 const int i = currentIndex;
1257 const int j = qMin(i + numChars(currentTextElement, true, resolvedProps), result.size());
1258 using namespace KoSvgText;
1259
1260 KoPathShape *currentTextPath = nullptr;
1261 qreal currentTextPathOffset = textPathoffset;
1262 bool textPathSide = side;
1263 if (!wrapping) {
1264 currentTextPath = textPath ? textPath : dynamic_cast<KoPathShape *>(currentTextElement->textPath.data());
1265
1266 if (currentTextElement->textPath) {
1267 textPathSide = currentTextElement->textPathInfo.side == TextPathSideRight;
1268 if (currentTextElement->textPathInfo.startOffsetIsPercentage) {
1269 KIS_ASSERT(currentTextPath);
1270 currentTextPathOffset = currentTextPath->outline().length() * (0.01 * currentTextElement->textPathInfo.startOffset);
1271 } else {
1272 currentTextPathOffset = currentTextElement->textPathInfo.startOffset;
1273 }
1274 }
1275 }
1276 KoSvgTextProperties properties = currentTextElement->properties;
1277 properties.inheritFrom(resolvedProps);
1278
1282
1283 for (auto child = KisForestDetail::childBegin(currentTextElement); child != KisForestDetail::childEnd(currentTextElement); child++) {
1284 computeTextDecorations(child,
1285 result,
1286 logicalToVisual,
1287 resHandler,
1288 currentTextPath,
1289 currentTextPathOffset,
1290 textPathSide,
1291 currentIndex,
1292 isHorizontal,
1293 ltr,
1294 wrapping,
1295 properties
1296 );
1297 }
1298
1299 TextDecorations decor = currentTextElement->properties.propertyOrDefault(KoSvgTextProperties::TextDecorationLineId).value<TextDecorations>();
1300 if (decor != DecorationNone && currentTextElement->properties.hasProperty(KoSvgTextProperties::TextDecorationLineId)) {
1301
1303 properties.propertyOrDefault(
1305
1306 QMap<TextDecoration, QPainterPath> decorationPaths =
1307 generateDecorationPaths(i, j, resHandler,
1308 result, isHorizontal, decor, style, false, currentTextPath,
1309 currentTextPathOffset, textPathSide, newUnderlinePosH, newUnderlinePosV
1310 );
1311
1312 // And finally add the paths to the chunkshape.
1313
1314 Q_FOREACH (TextDecoration type, decorationPaths.keys()) {
1315 QPainterPath decorationPath = decorationPaths.value(type);
1316 if (!decorationPath.isEmpty()) {
1317 currentTextElement->textDecorations.insert(type, decorationPath.simplified());
1318 }
1319 }
1320 }
1321 currentIndex = j;
1322}
1323
1324QPair<QPainterPath, QPointF> generateDecorationPath (
1325 const QLineF length,
1326 const qreal strokeWidth,
1328 const bool isHorizontal,
1329 const bool onTextPath,
1330 const qreal minimumDecorationThickness
1331 ) {
1332 QPainterPath p;
1333 QPointF pathWidth;
1334 if (style != KoSvgText::Wavy) {
1335 p.moveTo(QPointF());
1336 // We're segmenting the path here so it'll be easier to warp
1337 // when text-on-path is happening.
1338 if (onTextPath) {
1339 const int total = std::floor(length.length() / (strokeWidth * 2));
1340 const qreal segment = qreal(length.length() / total);
1341 if (isHorizontal) {
1342 for (int i = 0; i < total; i++) {
1343 p.lineTo(p.currentPosition() + QPointF(segment, 0));
1344 }
1345 } else {
1346 for (int i = 0; i < total; i++) {
1347 p.lineTo(p.currentPosition() + QPointF(0, segment));
1348 }
1349 }
1350 } else {
1351 if (isHorizontal) {
1352 p.lineTo(length.length(), 0);
1353 } else {
1354 p.lineTo(0, length.length());
1355 }
1356 }
1357 }
1358 if (style == KoSvgText::Double) {
1359 qreal linewidthOffset = qMax(strokeWidth * 1.5, minimumDecorationThickness * 2);
1360 if (isHorizontal) {
1361 p.addPath(p.translated(0, linewidthOffset));
1362 pathWidth = QPointF(0, -linewidthOffset);
1363 } else {
1364 p.addPath(p.translated(linewidthOffset, 0));
1365 pathWidth = QPointF(linewidthOffset, 0);
1366 }
1367
1368 } else if (style == KoSvgText::Wavy) {
1369 qreal width = length.length();
1370 qreal height = strokeWidth * 2;
1371
1372 bool down = true;
1373 p.moveTo(QPointF());
1374
1375 for (int i = 0; i < qFloor(width / height); i++) {
1376 if (down) {
1377 p.lineTo(p.currentPosition().x() + height, height);
1378 } else {
1379 p.lineTo(p.currentPosition().x() + height, 0);
1380 }
1381 down = !down;
1382 }
1383 qreal offset = fmod(width, height);
1384 if (down) {
1385 p.lineTo(width, offset);
1386 } else {
1387 p.lineTo(width, height - offset);
1388 }
1389 pathWidth = QPointF(0, -strokeWidth);
1390
1391 // Rotate for vertical.
1392 if (!isHorizontal) {
1393 for (int i = 0; i < p.elementCount(); i++) {
1394 p.setElementPositionAt(i, p.elementAt(i).y - (strokeWidth * 2), p.elementAt(i).x);
1395 }
1396 pathWidth = QPointF(strokeWidth, 0);
1397 }
1398 }
1399 return qMakePair(p, pathWidth);
1400}
1401
1402void KoSvgTextShape::Private::finalizeDecoration (
1403 QPainterPath decorationPath,
1404 const QPointF offset,
1405 const QPainterPathStroker &stroker,
1406 const KoSvgText::TextDecoration type,
1407 QMap<KoSvgText::TextDecoration, QPainterPath> &decorationPaths,
1408 const KoPathShape *currentTextPath,
1409 const bool isHorizontal,
1410 const qreal currentTextPathOffset,
1411 const bool textPathSide
1412 ) {
1413 if (currentTextPath) {
1414 QPainterPath path = currentTextPath->outline();
1415 path = currentTextPath->transformation().map(path);
1416 if (textPathSide) {
1417 path = path.toReversed();
1418 }
1419
1420 decorationPath = stretchGlyphOnPath(decorationPath.translated(offset), path, isHorizontal, currentTextPathOffset, currentTextPath->isClosedSubpath(0));
1421 decorationPaths[type].addPath(stroker.createStroke(decorationPath));
1422 } else {
1423 decorationPaths[type].addPath(stroker.createStroke(decorationPath.translated(offset)));
1424 }
1425 decorationPaths[type].setFillRule(Qt::WindingFill);
1426}
1427
1428QMap<KoSvgText::TextDecoration, QPainterPath>
1429KoSvgTextShape::Private::generateDecorationPaths(const int &start, const int &end,
1430 const KoSvgText::ResolutionHandler resHandler,
1431 const QVector<CharacterResult> &result,
1432 const bool isHorizontal,
1433 const KoSvgText::TextDecorations &decor,
1435 const bool textDecorationSkipInset,
1436 const KoPathShape *currentTextPath,
1437 const qreal currentTextPathOffset,
1438 const bool textPathSide,
1440 const KoSvgText::TextDecorationUnderlinePosition underlinePosV) {
1441 using namespace KoSvgText;
1442
1443 const qreal freetypePixelsToPt = resHandler.freeTypePixelToPointFactor(isHorizontal);
1444 const qreal minimumDecorationThickness = resHandler.pixelToPointFactor(isHorizontal);
1445
1446 QMap<TextDecoration, QPainterPath> decorationPaths;
1447
1448 decorationPaths.insert(DecorationUnderline, QPainterPath());
1449 decorationPaths.insert(DecorationOverline, QPainterPath());
1450 decorationPaths.insert(DecorationLineThrough, QPainterPath());
1451
1452 QPainterPathStroker stroker;
1453 stroker.setCapStyle(Qt::FlatCap);
1454 if (style == Dotted) {
1455 QPen pen;
1456 pen.setStyle(Qt::DotLine);
1457 stroker.setDashPattern(pen.dashPattern());
1458 } else if (style == Dashed) {
1459 QPen pen;
1460 pen.setStyle(Qt::DashLine);
1461 stroker.setDashPattern(pen.dashPattern());
1462 }
1463
1464 struct DecorationBox {
1465 QRectF decorationRect;
1466 QVector<qreal> underlineOffsets;
1467 qint32 thickness = 0;
1468 int start = 0;
1469 int end = 0;
1470 };
1471
1472 struct LineThrough {
1473 QPolygonF line;
1474 qint32 thickness = 0;
1475 };
1476
1477 QVector<DecorationBox> decorationBoxes;
1478 QVector<LineThrough> lineThroughLines;
1479 DecorationBox currentBox;
1480 currentBox.start = start;
1481
1482 // First we get all the ranges between anchored chunks and "trim" the whitespaces.
1483 for (int k = start; k < end; k++) {
1484 CharacterResult charResult = result.at(k);
1485 const bool atEnd = k+1 >= end;
1486 if ((charResult.anchored_chunk || atEnd) && currentBox.start < k) {
1487 // walk backwards to remove empties.
1488 if (k > start) {
1489 for (int l = atEnd? k: k-1; l > currentBox.start; l--) {
1490 if (!result.at(l).inkBoundingBox.isEmpty()
1491 && result.at(l).addressable
1492 && !result.at(l).hidden) {
1493 currentBox.end = l;
1494 break;
1495 }
1496 }
1497 }
1498 decorationBoxes.append(currentBox);
1499 currentBox = DecorationBox();
1500 currentBox.start = k;
1501 }
1502 if (currentBox.start == k && (charResult.inkBoundingBox.isEmpty()
1503 || !charResult.addressable
1504 || charResult.hidden)) {
1505 currentBox.start = k+1;
1506 }
1507 }
1508
1509 qint32 lastFontSize = 0;
1510 QPointF lastBaselineOffset;
1511 QPointF lastLineThroughOffset;
1512
1513 // Then we go over each range, and calculate their decoration box,
1514 // as well as calculate the linethroughs.
1515 for (int b = 0; b < decorationBoxes.size(); b++) {
1516 DecorationBox currentBox = decorationBoxes.at(b);
1517 LineThrough currentLineThrough = LineThrough();
1518 lastFontSize = result.at(currentBox.start).metrics.fontSize;
1519 lastBaselineOffset = result.at(currentBox.start).totalBaselineOffset();
1520
1521 for (int k = currentBox.start; k <= currentBox.end; k++) {
1522 CharacterResult charResult = result.at(k);
1523
1524 if (currentTextPath) {
1525 characterResultOnPath(charResult,
1526 currentTextPath->outline().length(),
1527 currentTextPathOffset,
1528 isHorizontal,
1529 currentTextPath->isClosedSubpath(0));
1530 }
1531
1532 if (charResult.hidden || !charResult.addressable) {
1533 continue;
1534 }
1535 // Adjustments to "vertical align" will not affect the baseline offset, so no new stroke needs to be created.
1536 const bool baselineIsOffset = charResult.totalBaselineOffset() != lastBaselineOffset;
1537 const bool newLineThrough = charResult.metrics.fontSize != lastFontSize && !baselineIsOffset;
1538 const bool ignoreMetrics = !newLineThrough && baselineIsOffset && !currentLineThrough.line.isEmpty();
1539
1540 if (newLineThrough) {
1541 lineThroughLines.append(currentLineThrough);
1542 currentLineThrough = LineThrough();
1543
1544 lastFontSize = charResult.metrics.fontSize;
1545 lastBaselineOffset = charResult.totalBaselineOffset();
1546 }
1547
1548 const qreal alphabetic = charResult.metrics.valueForBaselineValue(BaselineAlphabetic)*freetypePixelsToPt;
1549 const QPointF alphabeticOffset = isHorizontal? QPointF(0, -alphabetic): QPointF(alphabetic, 0);
1550
1551 if (!ignoreMetrics) {
1552 currentBox.thickness += charResult.metrics.underlineThickness;
1553 currentLineThrough.thickness += charResult.metrics.lineThroughThickness;
1554 lastLineThroughOffset = isHorizontal? QPointF(0, -(charResult.metrics.lineThroughOffset*freetypePixelsToPt)) + alphabeticOffset
1555 : QPointF(charResult.metrics.valueForBaselineValue(BaselineCentral)*freetypePixelsToPt, 0);
1556 currentBox.underlineOffsets.append((-charResult.metrics.underlineOffset)*freetypePixelsToPt);
1557 }
1558 QPointF lastLineThroughPoint = charResult.finalPosition + charResult.advance + lastLineThroughOffset;
1559
1560 if (ignoreMetrics) {
1561 QPointF lastP = currentLineThrough.line.last();
1562 if (isHorizontal) {
1563 lastLineThroughPoint.setY(lastP.y());
1564 } else {
1565 lastLineThroughPoint.setX(lastP.x());
1566 }
1567 }
1568
1569 if ((isHorizontal && underlinePosH == UnderlineAuto)) {
1570 QRectF bbox = charResult.layoutBox().translated(-alphabeticOffset);
1571 bbox.setBottom(0);
1572 currentBox.decorationRect |= bbox.translated(charResult.finalPosition + alphabeticOffset);
1573 } else {
1574 currentBox.decorationRect |= charResult.layoutBox().translated(charResult.finalPosition);
1575 }
1576 if (currentLineThrough.line.isEmpty()) {
1577 currentLineThrough.line.append(charResult.finalPosition + lastLineThroughOffset);
1578 }
1579 currentLineThrough.line.append(lastLineThroughPoint);
1580 }
1581 decorationBoxes[b] = currentBox;
1582 lineThroughLines.append(currentLineThrough);
1583 }
1584
1585 // Now to create a QPainterPath for the given style that stretches
1586 // over a single decoration rect,
1587 // transform that and add it to the general paths.
1588
1589 const bool textOnPath = (currentTextPath)? true: false;
1590
1591 for (int i = 0; i < decorationBoxes.size(); i++) {
1592 DecorationBox box = decorationBoxes.at(i);
1593 if (box.underlineOffsets.size() > 0) {
1594 stroker.setWidth(qMax(minimumDecorationThickness, (box.thickness/(box.underlineOffsets.size()))*freetypePixelsToPt));
1595 if (isHorizontal) {
1596 stroker.setWidth(resHandler.adjust(QPointF(0, stroker.width())).y());
1597 } else {
1598 stroker.setWidth(resHandler.adjust(QPointF(stroker.width(), 0)).x());
1599 }
1600 } else {
1601 stroker.setWidth(minimumDecorationThickness);
1602 }
1603 QRectF rect = box.decorationRect;
1604 if (textDecorationSkipInset) {
1605 qreal inset = stroker.width() * 0.5;
1606 rect.adjust(-inset, -inset, inset, inset);
1607 }
1608 QLineF length;
1609 length.setP1(isHorizontal? QPointF(rect.left(), 0): QPointF(0, rect.top()));
1610 length.setP2(isHorizontal? QPointF(rect.right(), 0): QPointF(0, rect.bottom()));
1611
1612 QPair<QPainterPath, QPointF> generatedPath = generateDecorationPath(length, stroker.width(), style, isHorizontal, textOnPath, minimumDecorationThickness);
1613 QPainterPath p = generatedPath.first;
1614 QPointF pathWidth = generatedPath.second;
1615
1616 QMap<TextDecoration, QPointF> decorationOffsets;
1617 if (isHorizontal) {
1618 const qreal startX = rect.left();
1619 decorationOffsets[DecorationOverline] = resHandler.adjustWithOffset(QPointF(startX, box.decorationRect.top()) + pathWidth, pathWidth*0.5);
1620 decorationOffsets[DecorationUnderline] = resHandler.adjustWithOffset(QPointF(startX, box.decorationRect.bottom()), pathWidth*0.5);
1621 if (underlinePosH == UnderlineAuto) {
1622 qreal average = 0;
1623 for (int j = 0; j < box.underlineOffsets.size(); j++) {
1624 average += box.underlineOffsets.at(j);
1625 }
1626 average = average > 0? (average/box.underlineOffsets.size()): 0;
1627 decorationOffsets[DecorationUnderline] += resHandler.adjustWithOffset(QPointF(0, average), pathWidth*0.5);
1628 }
1629 } else {
1630 const qreal startY = rect.top();
1631 if (underlinePosV == UnderlineRight) {
1632 decorationOffsets[DecorationOverline] = resHandler.adjustWithOffset(QPointF(box.decorationRect.left(), startY), pathWidth*0.5);
1633 decorationOffsets[DecorationUnderline] = resHandler.adjustWithOffset(QPointF(box.decorationRect.right(), startY) +pathWidth, pathWidth*0.5);
1634 } else {
1635 decorationOffsets[DecorationOverline] = resHandler.adjustWithOffset(QPointF(box.decorationRect.right(), startY) +pathWidth, pathWidth*0.5);
1636 decorationOffsets[DecorationUnderline] = resHandler.adjustWithOffset(QPointF(box.decorationRect.left(), startY), pathWidth*0.5);
1637 }
1638 }
1639
1640 if (decor.testFlag(DecorationUnderline)) {
1641 finalizeDecoration(p, decorationOffsets.value(DecorationUnderline), stroker, DecorationUnderline, decorationPaths, currentTextPath, isHorizontal, currentTextPathOffset, textPathSide);
1642 }
1643 if (decor.testFlag(DecorationOverline)) {
1644 finalizeDecoration(p, decorationOffsets.value(DecorationOverline), stroker, DecorationOverline, decorationPaths, currentTextPath, isHorizontal, currentTextPathOffset, textPathSide);
1645 }
1646 }
1647 if (decor.testFlag(DecorationLineThrough)) {
1648 for (int i = 0; i < lineThroughLines.size(); i++) {
1649 LineThrough l = lineThroughLines.at(i);
1650 QPolygonF poly = l.line;
1651 if (poly.isEmpty()) continue;
1652 stroker.setWidth(qMax(minimumDecorationThickness, (l.thickness/(poly.size()))*freetypePixelsToPt));
1653 QPointF pathWidth;
1654 if (isHorizontal) {
1655 pathWidth = resHandler.adjust(QPointF(0, stroker.width()));
1656 stroker.setWidth(pathWidth.y());
1657 } else {
1658 pathWidth = resHandler.adjust(QPointF(stroker.width(), 0));
1659 stroker.setWidth(pathWidth.x());
1660 }
1661 qreal average = 0.0;
1662 for (int j = 0; j < poly.size(); j++) {
1663 average += isHorizontal? poly.at(j).y(): poly.at(j).x();
1664 }
1665 average /= poly.size();
1666 QLineF line = isHorizontal? QLineF(poly.first().x(), average, poly.last().x(), average)
1667 : QLineF(average, poly.first().y(), average, poly.last().y());
1668 line.setP1(resHandler.adjustWithOffset(line.p1(), pathWidth*0.5));
1669 line.setP2(resHandler.adjustWithOffset(line.p2(), pathWidth*0.5));
1670 QPair<QPainterPath, QPointF> generatedPath = generateDecorationPath(line, stroker.width(), style, isHorizontal, textOnPath, minimumDecorationThickness);
1671 QPainterPath p = generatedPath.first;
1672 finalizeDecoration(p, line.p1() + (generatedPath.second * 0.5), stroker, DecorationLineThrough, decorationPaths, currentTextPath, isHorizontal, currentTextPathOffset, textPathSide);
1673 }
1674 }
1675
1676 return decorationPaths;
1677}
1678
1679// NOLINTNEXTLINE(readability-function-cognitive-complexity)
1680void KoSvgTextShape::Private::applyAnchoring(QVector<CharacterResult> &result, bool isHorizontal, const KoSvgText::ResolutionHandler resHandler)
1681{
1682 int end = 0;
1683 int start = 0;
1684
1685 while (start < result.size()) {
1686
1687 qreal shift = anchoredChunkShift(result, isHorizontal, start, end);
1688
1689 const QPointF shiftP = resHandler.adjust(isHorizontal ? QPointF(shift, 0) : QPointF(0, shift));
1690
1691 for (int j = start; j < end; j++) {
1692 result[j].finalPosition += shiftP;
1693 }
1694 start = end;
1695 }
1696}
1697
1698qreal KoSvgTextShape::Private::anchoredChunkShift(const QVector<CharacterResult> &result, const bool isHorizontal, const int start, int &end)
1699{
1700 qreal a = 0;
1701 qreal b = 0;
1702 int i = start;
1703 for (; i < result.size(); i++) {
1704 if (!result.at(i).addressable) {
1705 continue;
1706 }
1707 if (result.at(i).anchored_chunk && i > start) {
1708 break;
1709 }
1710 qreal pos = isHorizontal ? result.at(i).finalPosition.x() : result.at(i).finalPosition.y();
1711 qreal advance = isHorizontal ? result.at(i).advance.x() : result.at(i).advance.y();
1712
1713 if (result.at(i).anchored_chunk) {
1714 a = qMin(pos, pos + advance);
1715 b = qMax(pos, pos + advance);
1716 } else {
1717 a = qMin(a, qMin(pos, pos + advance));
1718 b = qMax(b, qMax(pos, pos + advance));
1719 }
1720 }
1721
1722 const CharacterResult startRes = result.at(start);
1723
1724 const bool rtl = startRes.direction == KoSvgText::DirectionRightToLeft;
1725 qreal shift = isHorizontal ? startRes.finalPosition.x() : startRes.finalPosition.y();
1726
1727 if ((startRes.anchor == KoSvgText::AnchorStart && !rtl)
1728 || (startRes.anchor == KoSvgText::AnchorEnd && rtl)) {
1729 shift = shift - a;
1730
1731 } else if ((startRes.anchor == KoSvgText::AnchorEnd && !rtl)
1732 || (startRes.anchor == KoSvgText::AnchorStart && rtl)) {
1733 shift = shift - b;
1734 } else {
1735 shift = shift - (a + b) * 0.5;
1736 }
1737 end = i;
1738 return shift;
1739}
1740
1741// NOLINTNEXTLINE(readability-function-cognitive-complexity)
1742qreal KoSvgTextShape::Private::characterResultOnPath(CharacterResult &cr,
1743 qreal length,
1744 qreal offset,
1745 bool isHorizontal,
1746 bool isClosed)
1747{
1748 const bool rtl = (cr.direction == KoSvgText::DirectionRightToLeft);
1749 qreal mid = cr.finalPosition.x() + (cr.advance.x() * 0.5) + offset;
1750 if (!isHorizontal) {
1751 mid = cr.finalPosition.y() + (cr.advance.y() * 0.5) + offset;
1752 }
1753 if (isClosed) {
1754 if ((cr.anchor == KoSvgText::AnchorStart && !rtl) || (cr.anchor == KoSvgText::AnchorEnd && rtl)) {
1755 if (mid - offset < 0 || mid - offset > length) {
1756 cr.hidden = true;
1757 }
1758 } else if ((cr.anchor == KoSvgText::AnchorEnd && !rtl) || (cr.anchor == KoSvgText::AnchorStart && rtl)) {
1759 if (mid - offset < -length || mid - offset > 0) {
1760 cr.hidden = true;
1761 }
1762 } else {
1763 if (mid - offset < -(length * 0.5) || mid - offset > (length * 0.5)) {
1764 cr.hidden = true;
1765 }
1766 }
1767 if (mid < 0) {
1768 mid += length;
1769 }
1770 mid = fmod(mid, length);
1771 } else {
1772 if (mid < 0 || mid > length) {
1773 cr.hidden = true;
1774 }
1775 }
1776 return mid;
1777}
1778
1779QPainterPath KoSvgTextShape::Private::stretchGlyphOnPath(const QPainterPath &glyph,
1780 const QPainterPath &path,
1781 bool isHorizontal,
1782 qreal offset,
1783 bool isClosed)
1784{
1785 QPainterPath p = glyph;
1786 for (int i = 0; i < glyph.elementCount(); i++) {
1787 qreal mid = isHorizontal ? glyph.elementAt(i).x + offset : glyph.elementAt(i).y + offset;
1788 qreal midUnbound = mid;
1789 if (isClosed) {
1790 if (mid < 0) {
1791 mid += path.length();
1792 }
1793 mid = fmod(mid, qreal(path.length()));
1794 midUnbound = mid;
1795 } else {
1796 mid = qBound(0.0, mid, qreal(path.length()));
1797 }
1798 const qreal percent = path.percentAtLength(mid);
1799 const QPointF pos = path.pointAtPercent(percent);
1800 qreal tAngle = path.angleAtPercent(percent);
1801 if (tAngle > 180) {
1802 tAngle = 0 - (360 - tAngle);
1803 }
1804 const QPointF vectorT(qCos(qDegreesToRadians(tAngle)), -qSin(qDegreesToRadians(tAngle)));
1805 QPointF finalPos = pos;
1806 if (isHorizontal) {
1807 QPointF vectorN(-vectorT.y(), vectorT.x());
1808 const qreal o = mid - (midUnbound);
1809 finalPos = pos - (o * vectorT) + (glyph.elementAt(i).y * vectorN);
1810 } else {
1811 QPointF vectorN(vectorT.y(), -vectorT.x());
1812 const qreal o = mid - (midUnbound);
1813 finalPos = pos - (o * vectorT) + (glyph.elementAt(i).x * vectorN);
1814 }
1815 p.setElementPositionAt(i, finalPos.x(), finalPos.y());
1816 }
1817 return p;
1818}
1819
1820// NOLINTNEXTLINE(readability-function-cognitive-complexity)
1821void KoSvgTextShape::Private::applyTextPath(KisForest<KoSvgTextContentElement>::child_iterator root,
1823 bool isHorizontal,
1824 QPointF &startPos,
1825 const KoSvgTextProperties resolvedProps)
1826{
1827 // Unlike all the other applying functions, this one only iterates over the
1828 // top-level. SVG is not designed to have nested textPaths. Source:
1829 // https://github.com/w3c/svgwg/issues/580
1830 bool inPath = false;
1831 bool afterPath = false;
1832 int currentIndex = 0;
1833 QPointF pathEnd;
1834 for (auto textShapeElement = KisForestDetail::childBegin(root); textShapeElement != KisForestDetail::childEnd(root); textShapeElement++) {
1835 int endIndex = currentIndex + numChars(textShapeElement, true, resolvedProps);
1836
1837 KoPathShape *shape = dynamic_cast<KoPathShape *>(textShapeElement->textPath.data());
1838 if (shape) {
1839 QPainterPath path = shape->outline();
1840 path = shape->transformation().map(path);
1841 inPath = true;
1842 if (textShapeElement->textPathInfo.side == KoSvgText::TextPathSideRight) {
1843 path = path.toReversed();
1844 }
1845 qreal length = path.length();
1846 qreal offset = 0.0;
1847 bool isClosed = (shape->isClosedSubpath(0) && shape->subpathCount() == 1);
1848 if (textShapeElement->textPathInfo.startOffsetIsPercentage) {
1849 offset = length * (0.01 * textShapeElement->textPathInfo.startOffset);
1850 } else {
1851 offset = textShapeElement->textPathInfo.startOffset;
1852 }
1853 bool stretch = textShapeElement->textPathInfo.method == KoSvgText::TextPathStretch;
1854
1855 if (textShapeElement == KisForestDetail::childBegin(root)) {
1856 const qreal percent = path.percentAtLength(offset);
1857 startPos = path.pointAtPercent(percent);
1858 }
1859
1860 for (int i = currentIndex; i < endIndex; i++) {
1861 CharacterResult cr = result[i];
1862
1863 if (!cr.middle) {
1864 const qreal mid = characterResultOnPath(cr, length, offset, isHorizontal, isClosed);
1865 if (!cr.hidden) {
1866 auto *outlineGlyph = std::get_if<Glyph::Outline>(&cr.glyph);
1867 // FIXME: What about other glyph formats?
1868 if (stretch && outlineGlyph) {
1869 const QTransform tf = cr.finalTransform();
1870 QPainterPath glyph = stretchGlyphOnPath(tf.map(outlineGlyph->path), path, isHorizontal, offset, isClosed);
1871 outlineGlyph->path = glyph;
1872 }
1873 const qreal percent = path.percentAtLength(mid);
1874 const QPointF pos = path.pointAtPercent(percent);
1875 qreal tAngle = path.angleAtPercent(percent);
1876 if (tAngle > 180) {
1877 tAngle = 0 - (360 - tAngle);
1878 }
1879 const QPointF vectorT(qCos(qDegreesToRadians(tAngle)), -qSin(qDegreesToRadians(tAngle)));
1880 if (isHorizontal) {
1881 cr.rotate -= qDegreesToRadians(tAngle);
1882 QPointF vectorN(-vectorT.y(), vectorT.x());
1883 const qreal o = (cr.advance.x() * 0.5);
1884 cr.finalPosition = pos - (o * vectorT) + (cr.finalPosition.y() * vectorN);
1885 } else {
1886 cr.rotate -= qDegreesToRadians(tAngle + 90);
1887 QPointF vectorN(vectorT.y(), -vectorT.x());
1888 const qreal o = (cr.advance.y() * 0.5);
1889 cr.finalPosition = pos - (o * vectorT) + (cr.finalPosition.x() * vectorN);
1890 }
1891 // FIXME: What about other glyph formats?
1892 if (stretch && outlineGlyph) {
1893 const QTransform tf = cr.finalTransform();
1894 outlineGlyph->path = tf.inverted().map(outlineGlyph->path);
1895 }
1896 }
1897 }
1898 result[i] = cr;
1899 }
1900 pathEnd = path.pointAtPercent(1.0);
1901 } else {
1902 if (inPath) {
1903 inPath = false;
1904 afterPath = true;
1905 pathEnd -= result.at(currentIndex).finalPosition;
1906 }
1907 if (afterPath) {
1908 for (int i = currentIndex; i < endIndex; i++) {
1909 CharacterResult cr = result[i];
1910 if (cr.anchored_chunk) {
1911 afterPath = false;
1912 } else {
1913 cr.finalPosition += pathEnd;
1914 result[i] = cr;
1915 }
1916 }
1917 }
1918 }
1919 currentIndex = endIndex;
1920 }
1921}
1922QVector<SubChunk> KoSvgTextShape::Private::collectSubChunks(KisForest<KoSvgTextContentElement>::child_iterator it, KoSvgTextProperties parentProps, bool textInPath, bool &firstTextInPath)
1923{
1924 QVector<SubChunk> result;
1925 if (it->textPath) {
1926 textInPath = true;
1927 firstTextInPath = true;
1928 }
1929
1930 KoSvgTextProperties currentProps = it->properties;
1931 currentProps.inheritFrom(parentProps, true);
1932
1933
1934 if (childCount(it)) {
1935 for (auto child = KisForestDetail::childBegin(it); child != KisForestDetail::childEnd(it); child++) {
1936 result += collectSubChunks(child, currentProps, textInPath, firstTextInPath);
1937 }
1938 } else {
1939 SubChunk chunk(it);
1940 chunk.inheritedProps = currentProps;
1941 chunk.bg = chunk.inheritedProps.background();
1942
1943
1944 KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(it->properties.propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt());
1945 KoSvgText::Direction direction = KoSvgText::Direction(it->properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
1946 const QString bidiOpening = KoCssTextUtils::getBidiOpening(direction == KoSvgText::DirectionLeftToRight, bidi);
1947 const QString bidiClosing = KoCssTextUtils::getBidiClosing(bidi);
1948
1949
1950 if (!bidiOpening.isEmpty()) {
1951 chunk.text = bidiOpening;
1952 chunk.originalText = QString();
1953 chunk.newToOldPositions.clear();
1954 result.append(chunk);
1955 firstTextInPath = false;
1956 }
1957
1958 chunk.originalText = it->text;
1959 chunk.text = it->getTransformedString(chunk.newToOldPositions, chunk.inheritedProps);
1960 result.append(chunk);
1961
1962 if (!bidiClosing.isEmpty()) {
1963 chunk.text = bidiClosing;
1964 chunk.originalText = QString();
1965 chunk.newToOldPositions.clear();
1966 result.append(chunk);
1967 }
1968
1969 firstTextInPath = false;
1970 }
1971
1972 if (it->textPath) {
1973 textInPath = false;
1974 firstTextInPath = false;
1975 }
1976
1977 return result;
1978}
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
#define debugFlake
Definition FlakeDebug.h:15
float value(const T *src, size_t ch)
const Params2D p
BreakType
LineEdgeBehaviour
@ ConditionallyHang
Only hang if no space otherwise, only measured for justification if not hanging.
@ Collapse
Collapse if first or last in line.
@ ForceHang
Force hanging at the start or end of a line, never measured for justification.
@ NoChange
Do nothing special.
const QString bidiControls
KoSvgTextShape::Private::resolveTransforms This resolves transforms and applies whitespace collapse.
static QMap< int, int > logicalToVisualCursorPositions(const QVector< CursorPos > &cursorPos, const QVector< CharacterResult > &result, const QVector< LineBox > &lines, const bool &ltr=false)
logicalToVisualCursorPositions Create a map that sorts the cursor positions by the visual index of th...
QPair< QPainterPath, QPointF > generateDecorationPath(const QLineF length, const qreal strokeWidth, const KoSvgText::TextDecorationStyle style, const bool isHorizontal, const bool onTextPath, const qreal minimumDecorationThickness)
QString langToLibUnibreakLang(const QString lang)
A simple solid color shape background.
static QVector< bool > collapseSpaces(QString *text, QMap< int, KoSvgText::TextSpaceCollapse > collapseMethods)
collapseSpaces Some versions of CSS-Text 'white-space' or 'text-space-collapse' will collapse or tran...
static bool IsCssWordSeparator(QString grapheme)
IsCssWordSeparator CSS has a number of characters it considers word-separators, which are used in jus...
static bool characterCanHang(QChar c, KoSvgText::HangingPunctuations hangType)
characterCanHang The function returns whether the character qualifies for 'hanging-punctuation',...
static QString getBidiClosing(KoSvgText::UnicodeBidi bidi)
getBidiClosing Returns the bidi closing string associated with the given Css unicode-bidi value.
static bool collapseLastSpace(QChar c, KoSvgText::TextSpaceCollapse collapseMethod)
collapseLastSpace Some versions of CSS-Text 'white-space' or 'text-space-collapse' will collapse the ...
static bool hangLastSpace(const QChar c, KoSvgText::TextSpaceCollapse collapseMethod, KoSvgText::TextWrap wrapMethod, bool &force, bool nextCharIsHardBreak)
hangLastSpace Some versions of CSS-Text 'white-space' or 'text-space-collapse' will hang the final sp...
static QString getBidiOpening(bool ltr, KoSvgText::UnicodeBidi bidi)
getBidiOpening Get the bidi opening string associated with the given Css unicode-bidi value and direc...
static QVector< QPair< bool, bool > > justificationOpportunities(QString text, QString langCode)
justificationOpportunities mark justification opportunities in the text. Opportunities are between ch...
std::vector< FT_FaceSP > facesForCSSValues(QVector< int > &lengths, KoCSSFontInfo info=KoCSSFontInfo(), const QString &text="", quint32 xRes=72, quint32 yRes=72, bool disableFontMatching=false, const QString &language=QString())
facesForCSSValues This selects a font with fontconfig using the given values. If "text" is not empty ...
static int32_t loadFlagsForFace(FT_Face face, bool isHorizontal=true, int32_t loadFlags=0, const KoSvgText::TextRendering rendering=KoSvgText::RenderingAuto)
static KoFontRegistry * instance()
static KoSvgText::FontMetrics generateFontMetrics(FT_FaceSP face, bool isHorizontal=true, QString script=QString(), const KoSvgText::TextRendering rendering=KoSvgText::RenderingAuto)
The position of a path point within a path shape.
Definition KoPathShape.h:63
bool isClosedSubpath(int subpathIndex) const
Checks if a subpath is closed.
QPainterPath outline() const override
reimplemented
int subpathCount() const
Returns the number of subpaths in the path.
QScopedPointer< Private > d
Definition KoShape.h:1135
KoShapeAnchor * anchor() const
void rotate(qreal angle)
Rotate the shape (relative)
Definition KoShape.cpp:250
KoShapeContainer * parent() const
Definition KoShape.cpp:1039
void scale(qreal sx, qreal sy)
Scale the shape using the zero-point which is the top-left corner.
Definition KoShape.cpp:237
QTransform transformation() const
Returns the shapes local transformation matrix.
Definition KoShape.cpp:424
QSharedDataPointer< SharedData > s
Definition KoShape.h:1138
QTransform transform() const
return the current matrix that contains the rotation/scale/position of this shape
Definition KoShape.cpp:1145
@ TextAnchorId
KoSvgText::TextAnchor.
@ InlineSizeId
KoSvgText::AutoValue.
@ UnicodeBidiId
KoSvgText::UnicodeBidi.
@ DominantBaselineId
KoSvgText::Baseline.
@ AlignmentBaselineId
KoSvgText::Baseline.
@ WordSpacingId
KoSvgText::AutoLengthPercentage.
@ LineBreakId
KoSvgText::LineBreak.
@ LetterSpacingId
KoSvgText::AutoLengthPercentage.
@ TextCollapseId
KoSvgText::TextSpaceCollapse.
@ TextDecorationStyleId
KoSvgText::TextDecorationStyle.
@ FontStyleId
KoSvgText::CssSlantData.
@ WritingModeId
KoSvgText::WritingMode.
@ DirectionId
KoSvgText::Direction.
@ TextWrapId
KoSvgText::TextWrap.
@ HangingPunctuationId
Flags, KoSvgText::HangingPunctuations.
@ BaselineShiftModeId
KoSvgText::BaselineShiftMode.
@ TextDecorationPositionId
KoSvgText::TextDecorationUnderlinePosition.
@ WordBreakId
KoSvgText::WordBreak.
@ TextLanguage
a language string.
@ TextDecorationLineId
Flags, KoSvgText::TextDecorations.
QSharedPointer< KoShapeBackground > background() const
QList< PropertyId > properties() const
QVariant property(PropertyId id, const QVariant &defaultValue=QVariant()) const
static const KoSvgTextProperties & defaultProperties()
QStringList fontFeaturesForText(int start, int length) const
fontFeaturesForText Returns a harfbuzz friendly list of opentype font-feature settings using the vari...
KoCSSFontInfo cssFontInfo() const
cssFontInfo
bool hasProperty(PropertyId id) const
KoSvgText::FontMetrics applyLineHeight(KoSvgText::FontMetrics metrics) const
applyLineHeight Calculate the linegap for the current linegap property.
QVariant propertyOrDefault(PropertyId id) const
void inheritFrom(const KoSvgTextProperties &parentProperties, bool resolve=false)
static QString scriptTagForQCharScript(QChar::Script script)
static QLocale localeFromBcp47Locale(const Bcp47Locale &locale)
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
ChildIterator< value_type, is_const > childBegin(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:290
int size(const Forest< T > &forest)
Definition KisForest.h:1232
ChildIterator< value_type, is_const > childEnd(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:300
void calculateLineHeight(CharacterResult cr, double &ascent, double &descent, bool isHorizontal, bool compare=false)
calculateLineHeight calculate the total ascent and descent (including baseline-offset) of a charResul...
QVector< LineBox > flowTextInShapes(const KoSvgTextProperties &properties, const QMap< int, int > &logicalToVisual, QVector< CharacterResult > &result, QList< QPainterPath > shapes, QPointF &startPos, const KoSvgText::ResolutionHandler &resHandler)
QList< QPainterPath > getShapes(QList< KoShape * > shapesInside, QList< KoShape * > shapesSubtract, const KoSvgTextProperties &properties)
QVector< LineBox > breakLines(const KoSvgTextProperties &properties, const QMap< int, int > &logicalToVisual, QVector< CharacterResult > &result, QPointF startPos, const KoSvgText::ResolutionHandler &resHandler)
BaselineShiftMode
Mode of the baseline shift.
Definition KoSvgText.h:240
@ ShiftLineBottom
this handles css-inline-3 vertical-align:bottom. Not exposed to ui
Definition KoSvgText.h:246
@ ShiftSuper
Use parent font metric for 'superscript'.
Definition KoSvgText.h:243
@ ShiftLineTop
this handles css-inline-3 vertical-align:top. Not exposed to ui
Definition KoSvgText.h:245
@ ShiftLengthPercentage
Css Length Percentage, percentage is lh.
Definition KoSvgText.h:244
@ ShiftSub
Use parent font metric for 'subscript'.
Definition KoSvgText.h:242
@ LineBreakStrict
Use strict method, language specific.
Definition KoSvgText.h:145
@ LineBreakAnywhere
Break between any typographic clusters.
Definition KoSvgText.h:146
TextAnchor
Where the text is anchored for SVG 1.1 text and 'inline-size'.
Definition KoSvgText.h:79
@ AnchorEnd
Anchor right for LTR, left for RTL.
Definition KoSvgText.h:82
@ AnchorStart
Anchor left for LTR, right for RTL.
Definition KoSvgText.h:80
TextDecorationStyle
Style of the text-decoration.
Definition KoSvgText.h:265
@ Double
Draw two lines. Ex: =====.
Definition KoSvgText.h:267
@ Wavy
Draw a wavy line. We currently make a zigzag, ex: ^^^^^.
Definition KoSvgText.h:270
OverflowWrap
What to do with words that cannot be broken, but still overflow.
Definition KoSvgText.h:151
@ OverflowWrapNormal
Definition KoSvgText.h:152
TextDecorationUnderlinePosition
Which location to choose for the underline.
Definition KoSvgText.h:275
WordBreak
Whether to break words.
Definition KoSvgText.h:132
@ WordBreakBreakAll
Always break inside words.
Definition KoSvgText.h:135
@ WordBreakKeepAll
Never break inside words.
Definition KoSvgText.h:134
@ LengthAdjustSpacingAndGlyphs
Stretches the glyphs as well.
Definition KoSvgText.h:252
TextWrap
Part of "white-space", in practice we only support wrap and nowrap.
Definition KoSvgText.h:109
@ NoWrap
Do not do any text wrapping.
Definition KoSvgText.h:112
TextDecoration
Flags for text-decoration, for underline, overline and strikethrough.
Definition KoSvgText.h:257
@ DecorationOverline
Definition KoSvgText.h:260
@ DecorationUnderline
Definition KoSvgText.h:259
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionLeftToRight
Definition KoSvgText.h:49
@ DirectionRightToLeft
Definition KoSvgText.h:50
Baseline
Baseline values used by dominant-baseline and baseline-align.
Definition KoSvgText.h:213
@ BaselineAlphabetic
Use 'romn' or the baseline for LCG scripts.
Definition KoSvgText.h:225
@ BaselineDominant
Definition KoSvgText.h:218
@ BaselineUseScript
Definition KoSvgText.h:216
@ BaselineResetSize
Definition KoSvgText.h:221
@ BaselineNoChange
Use parent baseline table.
Definition KoSvgText.h:220
@ BaselineCentral
Use the center between the ideographic over and under.
Definition KoSvgText.h:231
@ TextPathSideRight
Definition KoSvgText.h:301
@ HorizontalTB
Definition KoSvgText.h:38
@ RenderingGeometricPrecision
Definition KoSvgText.h:318
@ RenderingAuto
Definition KoSvgText.h:315
@ RenderingOptimizeLegibility
Definition KoSvgText.h:317
@ RenderingOptimizeSpeed
Definition KoSvgText.h:316
@ HangForce
Whether to force hanging stops or commas.
Definition KoSvgText.h:209
@ HangLast
Hang closing brackets and quotes.
Definition KoSvgText.h:207
@ HangFirst
Hang opening brackets and quotes.
Definition KoSvgText.h:206
@ HangEnd
Hang stops and commas. Force/Allow is a separate boolean.
Definition KoSvgText.h:208
TextSpaceCollapse
Definition KoSvgText.h:96
@ BreakSpaces
Same as preserve, except each white space and wordseperate is breakable.
Definition KoSvgText.h:104
@ TextPathStretch
Definition KoSvgText.h:288
wrap(obj, force=False)
Definition mikro.py:126
LineEdgeBehaviour lineStart
QPointF totalBaselineOffset() const
QRectF inkBoundingBox
The bounds of the drawn glyph. Different from the bounds the charresult takes up in the layout,...
KoSvgText::TextAnchor anchor
qreal scaledDescent
Descender, in pt.
QPointF finalPosition
the final position, taking into account both CSS and SVG positioning considerations.
Glyph::Variant glyph
KoSvgText::FontMetrics metrics
Fontmetrics for current font, in Freetype scanline coordinates.
bool justifyAfter
Justification Opportunity follows this character.
KoSvgText::Direction direction
bool justifyBefore
Justification Opportunity precedes this character.
bool anchored_chunk
whether this is the start of a new chunk.
LineEdgeBehaviour lineEnd
void scaleCharacterResult(qreal xScale, qreal yScale)
scaleCharacterResult convenience function to scale the whole character result.
qreal scaledAscent
Ascender, in pt.
QPointF cssPosition
the position in accordance with the CSS specs, as opossed to the SVG spec.
qreal scaledHalfLeading
Leading for both sides, can be either negative or positive, in pt.
QRectF layoutBox() const
layoutBox
QTransform finalTransform() const
QLineF caret
Caret for this characterResult.
bool rtl
Whether the current glyph is right-to-left, as opposed to the markup.
QVector< int > graphemeIndices
The text-string indices of graphemes starting here, starting grapheme is not present.
QColor color
Which color the current position has.
QVector< QPointF > offsets
The advance offsets for each grapheme index.
int cluster
Which character result this position belongs in.
bool synthetic
Whether this position was inserted to have a visual indicator.
int index
Which grapheme this position belongs with.
int offset
Which offset this position belongs with.
void mergeInParentTransformation(const CharTransformation &t)
When style is oblique, a custom slant value can be specified for variable fonts.
Definition KoSvgText.h:475
The FontMetrics class A class to keep track of a variety of font metrics. Note that values are in Fre...
Definition KoSvgText.h:327
QPair< qint32, qint32 > subScriptOffset
subscript baseline height, defaults to 1/5th em below alphabetic.
Definition KoSvgText.h:336
qint32 lineThroughThickness
strikethrough thickness, from font.
Definition KoSvgText.h:359
qint32 underlineThickness
underline thickness from font.
Definition KoSvgText.h:357
qint32 lineThroughOffset
offset of strike-through from alphabetic baseline.
Definition KoSvgText.h:358
qint32 lineGap
additional linegap between consecutive lines.
Definition KoSvgText.h:341
qint32 underlineOffset
underline offset from alphabetic, positive.
Definition KoSvgText.h:356
qint32 fontSize
Currently set size, CSS unit 'em'.
Definition KoSvgText.h:329
int valueForBaselineValue(Baseline baseline) const
QPair< qint32, qint32 > superScriptOffset
superscript baseline height, defaults to 2/3rd above alphabetic.
Definition KoSvgText.h:337
The ResolutionHandler class.
Definition KoSvgText.h:1084
qreal freeTypePixelToPointFactor(const bool x=true) const
qreal pixelToPointFactor(const bool x=true) const
QTransform freeTypeToPointTransform() const
QPointF adjustWithOffset(const QPointF point, const QPointF offset) const
QPointF adjust(const QPointF point) const
Adjusts the point to rounded pixel values, based on whether roundToPixelHorizontal or roundToPixelVer...
TextDecorationUnderlinePosition verticalPosition
Definition KoSvgText.h:1070
TextDecorationUnderlinePosition horizontalPosition
Definition KoSvgText.h:1069
The LineBox struct.
QPointF baselineTop
Used to identify the top of the line for baseline-alignment.
QVector< LineChunk > chunks
QPointF baselineBottom
Used to identify the bottom of the line for baseline-alignment.
QVector< int > chunkIndices
charResult indices that belong to this chunk.
KisForest< KoSvgTextContentElement >::child_iterator associatedLeaf
QSharedPointer< KoShapeBackground > bg
KoSvgTextProperties inheritedProps
QVector< QPair< int, int > > newToOldPositions
For transformed strings, we need to know which.
QString originalText