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