Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextShape_p.h
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#ifndef KO_SVG_TEXT_SHAPE_P_H
9#define KO_SVG_TEXT_SHAPE_P_H
10
11#include "KoSvgTextShape.h"
12
13#include "KoSvgText.h"
15#include <KoShapePainter.h>
16#include <KoShapeManager.h>
17#include <KoInsets.h>
18
19#include "KoCssTextUtils.h"
20#include <KoShapeGroup.h>
21
22#include <kis_assert.h>
23#include <KisForest.h>
24
25#include <QFont>
26#include <QImage>
27#include <QLineF>
28#include <QPainterPath>
29#include <QPointF>
30#include <QRectF>
31#include <QVector>
32#include <QtMath>
33
34#include <variant>
35
36#include <ft2build.h>
37#include FT_FREETYPE_H
38
39constexpr qreal SHAPE_PRECISION = 1e-6;
40
41class KoPathShape;
42struct raqm_glyph_t;
43
44enum class BreakType {
45 NoBreak,
48};
49
56
65
66struct CursorPos {
67 int cluster;
68 int index;
69 int offset = 0;
70 bool synthetic = false;
71};
72
73namespace Glyph {
74
75struct Outline {
76 QPainterPath path;
77};
78
83
89
90using Variant = std::variant<std::monostate, Outline, Bitmap, ColorLayers>;
91
92} // namespace Glyph
93
95 QPointF finalPosition;
96 qreal rotate = 0.0;
97 bool hidden = false; // whether the character will be drawn.
98 // The original svg specs' notion of addressable character relies on utf16,
99 // but there's an issue to switch to unicode codepoints proper (that is, utf 32), though it was never applied.
100 // https://github.com/w3c/svgwg/issues/537
101 bool addressable = true;
104 bool middle = false;
106 bool anchored_chunk = false;
107
109
111 bool isHorizontal = true;
112 int visualIndex = -1;
114 QPointF cssPosition = QPointF();
115 QPointF textLengthOffset = QPointF();
116 QPointF textPathAndAnchoringOffset = QPointF();
117 QPointF dominantBaselineOffset = QPointF(); // Shift caused by aligning glyphs to dominant baseline.
118 QPointF baselineOffset = QPointF();
120 QPointF advance;
124 bool justifyBefore = false;
125 bool justifyAfter = false;
126 bool isHanging = false;
127 bool textLengthApplied = false;
128 bool overflowWrap = false;
129
130 qreal extraFontScaling = 1.0;
134 qreal scaledAscent{};
136
137 std::optional<qreal> tabSize;
138
139 void calculateAndApplyTabsize(QPointF currentPos, bool isHorizontal, const KoSvgText::ResolutionHandler &resHandler) {
140 if (!tabSize) return;
141 if (*tabSize == qInf() || qIsNaN(*tabSize)) return;
142
143 if (*tabSize > 0) {
144 qreal remainder = *tabSize - (isHorizontal? fmod(currentPos.x(), *tabSize): fmod(currentPos.y(), *tabSize));
145 advance = resHandler.adjust(isHorizontal? QPointF(remainder, advance.y()): QPointF(advance.x(), remainder));
146 }
147 }
148
153 QRectF layoutBox() const {
156 }
161 QRectF lineHeightBox () const {
162 QRectF lBox = layoutBox();
163 return isHorizontal? lBox.adjusted(0, -scaledHalfLeading, 0, scaledHalfLeading)
164 : lBox.adjusted(-scaledHalfLeading, 0, scaledHalfLeading, 0);
165 }
166
172 void translateOrigin(QPointF newOrigin) {
173 if (newOrigin == QPointF()) return;
174 if (Glyph::Outline *outlineGlyph = std::get_if<Glyph::Outline>(&glyph)) {
175 outlineGlyph->path.translate(-newOrigin);
176 } else if (Glyph::Bitmap *bitmapGlyph = std::get_if<Glyph::Bitmap>(&glyph)) {
177 for (int i = 0; i< bitmapGlyph->drawRects.size(); i++) {
178 bitmapGlyph->drawRects[i].translate(-newOrigin);
179 }
180 } else if (Glyph::ColorLayers *colorGlyph = std::get_if<Glyph::ColorLayers>(&glyph)) {
181 for (int i = 0; i< colorGlyph->paths.size(); i++) {
182 colorGlyph->paths[i].translate(-newOrigin);
183 }
184 }
185 cursorInfo.caret.translate(-newOrigin);
186 inkBoundingBox.translate(-newOrigin);
187
188 if (isHorizontal) {
189 scaledDescent -= newOrigin.y();
190 scaledAscent -= newOrigin.y();
191 } else {
192 scaledDescent -= newOrigin.x();
193 scaledAscent -= newOrigin.x();
194 }
195 }
196
203 void scaleCharacterResult(qreal xScale, qreal yScale) {
204 QTransform scale = QTransform::fromScale(xScale, yScale);
205 if (scale.isIdentity()) return;
206 const bool scaleToZero = !(xScale > 0 && yScale > 0);
207
208 if (Glyph::Outline *outlineGlyph = std::get_if<Glyph::Outline>(&glyph)) {
209 if (!outlineGlyph->path.isEmpty()) {
210 if (scaleToZero) {
211 outlineGlyph->path = QPainterPath();
212 } else {
213 outlineGlyph->path = scale.map(outlineGlyph->path);
214 }
215 }
216 } else if (Glyph::Bitmap *bitmapGlyph = std::get_if<Glyph::Bitmap>(&glyph)) {
217 if (scaleToZero) {
218 bitmapGlyph->drawRects.clear();
219 bitmapGlyph->images.clear();
220 } else {
221 for (int i = 0; i< bitmapGlyph->drawRects.size(); i++) {
222 bitmapGlyph->drawRects[i] = scale.mapRect(bitmapGlyph->drawRects[i]);
223 }
224 }
225 } else if (Glyph::ColorLayers *colorGlyph = std::get_if<Glyph::ColorLayers>(&glyph)) {
226 for (int i = 0; i< colorGlyph->paths.size(); i++) {
227 if (scaleToZero) {
228 colorGlyph->paths[i] = QPainterPath();
229 } else {
230 colorGlyph->paths[i] = scale.map(colorGlyph->paths[i]);
231 }
232 }
233 }
234 advance = scale.map(advance);
235 cursorInfo.caret = scale.map(cursorInfo.caret);
236 for (int i = 0; i < cursorInfo.offsets.size(); i++) {
237 cursorInfo.offsets[i] = scale.map(cursorInfo.offsets.at(i));
238 }
239 inkBoundingBox = scale.mapRect(inkBoundingBox);
240
241 if (isHorizontal) {
242 scaledDescent *= yScale;
243 scaledAscent *= yScale;
244 scaledHalfLeading *= yScale;
245 metrics.scaleBaselines(yScale);
246 if (tabSize) {
247 tabSize = scale.map(QPointF(*tabSize, *tabSize)).x();
248 }
249 } else {
250 scaledDescent *= xScale;
251 scaledAscent *= xScale;
252 scaledHalfLeading *= xScale;
253 metrics.scaleBaselines(xScale);
254 if (tabSize) {
255 tabSize = scale.map(QPointF(*tabSize, *tabSize)).y();
256 }
257 }
258 }
259
260 QPointF totalBaselineOffset() const {
262 }
263
264 QFont::Style fontStyle = QFont::StyleNormal;
265 int fontWeight = 400;
266
268
271
272 QTransform finalTransform() const {
273 QTransform tf =
274 QTransform::fromTranslate(finalPosition.x(), finalPosition.y());
275 tf.rotateRadians(rotate);
276 return tf;
277 }
278};
279
280struct LineChunk {
281 QLineF length;
284 QPointF conditionalHangEnd = QPointF();
285};
286
304struct LineBox {
305
307 }
308
309 LineBox(QPointF start, QPointF end, const KoSvgText::ResolutionHandler &resHandler) {
311 chunk.length = QLineF(resHandler.adjustCeil(start), resHandler.adjustFloor(end));
312 chunks.append(chunk);
313 currentChunk = 0;
314 }
315
316 LineBox(QVector<QLineF> lineWidths, bool ltr, QPointF indent, const KoSvgText::ResolutionHandler &resHandler) {
317 textIndent = indent;
318 if (ltr) {
319 Q_FOREACH(QLineF line, lineWidths) {
321 chunk.length = QLineF(resHandler.adjustCeil(line.p1()), resHandler.adjustFloor(line.p2()));
322 chunks.append(chunk);
323 currentChunk = 0;
324 }
325 } else {
326 Q_FOREACH(QLineF line, lineWidths) {
328 chunk.length = QLineF(resHandler.adjustFloor(line.p2()), resHandler.adjustCeil(line.p1()));
329 chunks.insert(0, chunk);
330 currentChunk = 0;
331 }
332 }
333 }
334
336 int currentChunk = -1;
337
338 qreal expectedLineTop = 0;
339 qreal actualLineTop = 0;
341
342 QPointF baselineTop = QPointF();
343 QPointF baselineBottom = QPointF();
344
345 QPointF textIndent = QPointF();
346 bool firstLine = false;
347 bool lastLine = false;
348 bool lineFinalized = false;
349 bool justifyLine = false;
350
352 return chunks.value(currentChunk);
353 }
354
356 currentChunk = qMax(currentChunk, 0);
357 if (currentChunk < chunks.size()) {
359 } else {
360 chunks.append(chunk);
361 }
362 }
363
364 void clearAndAdjust(bool isHorizontal, QPointF current, QPointF indent) {
366 actualLineTop = 0;
368 textIndent = indent;
369 QLineF length = chunks.at(currentChunk).length;
370 if (isHorizontal) {
371 length.setP1(QPointF(length.p1().x(), current.y()));
372 length.setP2(QPointF(length.p2().x(), current.y()));
373 } else {
374 length.setP1(QPointF(current.x(), length.p1().y()));
375 length.setP2(QPointF(current.x(), length.p2().y()));
376 }
377 chunks.clear();
378 currentChunk = 0;
380 chunks.append(chunk);
381 firstLine = false;
382 }
383
384 void setCurrentChunkForPos(QPointF pos, bool isHorizontal) {
385 for (int i=0; i<chunks.size(); i++) {
386 LineChunk chunk = chunks.at(i);
387 if (isHorizontal) {
388 qreal min = qMin(chunk.length.p1().x(), chunk.length.p2().x()) - SHAPE_PRECISION;
389 qreal max = qMax(chunk.length.p1().x(), chunk.length.p2().x()) + SHAPE_PRECISION;
390 if ((pos.x() < max) &&
391 (pos.x() >= min)) {
392 currentChunk = i;
393 break;
394 }
395 } else {
396 qreal min = qMin(chunk.length.p1().y(), chunk.length.p2().y()) - SHAPE_PRECISION;
397 qreal max = qMax(chunk.length.p1().y(), chunk.length.p2().y()) + SHAPE_PRECISION;
398 if ((pos.y() < max) &&
399 (pos.y() >= min)) {
400 currentChunk = i;
401 break;
402 }
403 }
404 }
405 }
406
407 bool isEmpty() {
408 if (chunks.isEmpty()) return true;
409 for (int i =0; i < chunks.size(); i++) {
410 if (!chunks.at(i).chunkIndices.isEmpty()) return false;
411 }
412 return true;
413 }
414
415};
416
437
438class KRITAFLAKE_EXPORT KoSvgTextShape::Private
439{
440public:
441 // NOTE: the cache data is shared between all the instances of
442 // the shape, though it will be reset locally if the
443 // accessing thread changes
444
446 : internalShapesPainter(new KoShapePainter)
447 , shapeGroup(new KoShapeGroup)
448 {
449 shapeGroup->setSelectable(false);
450 }
451
458
459 Private(const Private &rhs)
460 : internalShapesPainter(new KoShapePainter)
461 , textData(rhs.textData)
462 {
463
464 KoShapeGroup *g = dynamic_cast<KoShapeGroup*>(rhs.shapeGroup.data()->cloneShape());
465 shapeGroup.reset(g);
466 shapeGroup->setSelectable(false);
467 handleShapes(shapeGroup->shapes(), rhs.shapeGroup->shapes(), rhs.shapesInside, shapesInside);
468 handleShapes(shapeGroup->shapes(), rhs.shapeGroup->shapes(), rhs.shapesSubtract, shapesSubtract);
469 handleShapes(shapeGroup->shapes(), rhs.shapeGroup->shapes(), rhs.textPaths, textPaths);
470 updateInternalShapesList();
471
472 yRes = rhs.yRes;
473 xRes = rhs.xRes;
474 result = rhs.result;
475 lineBoxes = rhs.lineBoxes;
476
477 cursorPos = rhs.cursorPos;
478 logicalToVisualCursorPos = rhs.logicalToVisualCursorPos;
479 plainText = rhs.plainText;
480 isBidi = rhs.isBidi;
481 initialTextPosition = rhs.initialTextPosition;
482
483 isLoading = rhs.isLoading;
484 disableFontMatching = rhs.disableFontMatching;
485
486 currentTextWrappingAreas = rhs.currentTextWrappingAreas;
487 }
488
489 void handleShapes(const QList<KoShape*> &sourceShapeList, const QList<KoShape*> referenceList2, const QList<KoShape*> referenceShapeList, QList<KoShape*> &destinationShapeList) {
490 for (int i = 0; i<sourceShapeList.size(); i++) {
491 if (referenceShapeList.contains(referenceList2.at(i))) {
492 destinationShapeList.append(sourceShapeList.at(i));
493 }
494 }
495 }
496
498
499 internalShapesPainter.reset();
500 Q_FOREACH(KoShape *shape, shapeGroup->shapes()) {
501 shapeGroup->removeShape(shape);
502 }
503 shapeGroup.reset();
504 qDeleteAll(shapesInside);
505 shapesInside.clear();
506 qDeleteAll(shapesSubtract);
507 shapesSubtract.clear();
508 qDeleteAll(textPaths);
509 textPaths.clear();
510 }
511
512 int xRes = 72;
513 int yRes = 72;
514
515 QScopedPointer<KoShapePainter> internalShapesPainter;
516 QScopedPointer<KoShapeGroup> shapeGroup;
517
519 return internalShapesPainter->internalShapeManager()->shapes();
520 }
521
523 Q_FOREACH(KoShape *shape, shapeGroup->shapes()) {
524 shapeGroup->removeShape(shape);
525 }
526 Q_FOREACH(KoShape *shape, shapesInside) {
527 shapeGroup->addShape(shape);
528 }
529 Q_FOREACH(KoShape *shape, shapesSubtract) {
530 shapeGroup->addShape(shape);
531 }
532 Q_FOREACH(KoShape *shape, textPaths) {
533 shapeGroup->addShape(shape);
534 }
535 updateTextWrappingAreas();
536 updateInternalShapesList();
537 }
539 if (shapeGroup) {
540 internalShapesPainter->setShapes(shapeGroup->shapes());
541 }
542 }
543
545 BulkActionState(QRectF originalBoundingRectArg) : originalBoundingRect(originalBoundingRectArg) {}
546
548 bool contourHasChanged = false;
549 bool layoutHasChanged = false;
550 bool layoutSetFromMemento = false;
551
552 bool changed() const {
553 return contourHasChanged || layoutHasChanged || layoutSetFromMemento;
554 }
555 };
556
557 std::optional<BulkActionState> bulkActionState;
558
562
564
571 static QList<QPainterPath> generateShapes(const QList<KoShape*> shapesInside, const QList<KoShape*> shapesSubtract, const KoSvgTextProperties &properties);
572
573 static KoShape *textPathByName(QString name, QList<KoShape*> textPaths) {
574 auto it = std::find_if(textPaths.begin(), textPaths.end(), [&name](const KoShape *s) -> bool {return s->name() == name;});
575 return it != textPaths.end()? *it: nullptr;
576 }
577
579 bool isLoading = false;
580
581 bool disableFontMatching = false;
582
585
588
589 QString plainText;
590 bool isBidi = false;
591 QPointF initialTextPosition = QPointF();
592
593 void relayout();
594
595 static bool loadGlyph(const KoSvgText::ResolutionHandler &resHandler,
596 const FT_Int32 faceLoadFlags,
597 const bool isHorizontal,
598 const char32_t firstCodepoint,
599 const KoSvgText::TextRendering rendering,
600 raqm_glyph_t &currentGlyph,
601 CharacterResult &charResult,
602 QPointF &totalAdvanceFTFontCoordinates);
603
605 const bool isHorizontal,
606 raqm_glyph_t &currentGlyph);
607
608 static std::pair<QTransform, qreal> loadGlyphOnly(const QTransform &ftTF,
609 FT_Int32 faceLoadFlags,
610 bool isHorizontal,
611 raqm_glyph_t &currentGlyph,
612 CharacterResult &charResult, const KoSvgText::TextRendering rendering);
613
616 QString text, QVector<CharacterResult> &result, int &currentIndex,
617 bool isHorizontal, bool wrapped, bool textInPath, QVector<KoSvgText::CharTransformation> &resolved,
618 QVector<bool> collapsedChars, const KoSvgTextProperties resolvedProps, bool withControls = true);
619
620 void applyTextLength(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, QVector<CharacterResult> &result, int &currentIndex, int &resolvedDescendentNodes, bool isHorizontal,
621 const KoSvgTextProperties resolvedProps, const KoSvgText::ResolutionHandler &resHandler);
622 static void applyAnchoring(QVector<CharacterResult> &result, bool isHorizontal, const KoSvgText::ResolutionHandler resHandler);
623 static qreal anchoredChunkShift(const QVector<CharacterResult> &result, const bool isHorizontal, const int start, int &end);
624 static qreal
625 characterResultOnPath(CharacterResult &cr, qreal length, qreal offset, bool isHorizontal, bool isClosed);
626 static QPainterPath stretchGlyphOnPath(const QPainterPath &glyph,
627 const QPainterPath &path,
628 bool isHorizontal,
629 qreal offset,
630 bool isClosed);
631 static void applyTextPath(KisForest<KoSvgTextContentElement>::child_iterator parent, QVector<CharacterResult> &result, bool isHorizontal, QPointF &startPos, const KoSvgTextProperties resolvedProps, QList<KoShape*> textPaths);
633 const KoSvgText::FontMetrics &parentBaselineTable, const KoSvgText::Baseline parentBaseline,
635 int &currentIndex,
636 const KoSvgText::ResolutionHandler resHandler,
637 const bool isHorizontal,
638 const bool disableFontMatching);
640 QVector<CharacterResult> &result, const QVector<LineBox> lineBoxes,
641 int &currentIndex,
642 const bool isHorizontal, const KoSvgTextProperties resolvedProps);
644 const QVector<CharacterResult>& result,
645 const QMap<int, int>& logicalToVisual,
646 const KoSvgText::ResolutionHandler resHandler,
647 KoPathShape *textPath,
648 qreal textPathoffset,
649 bool side,
650 int &currentIndex,
651 bool isHorizontal,
652 bool ltr,
653 bool wrapping,
654 const KoSvgTextProperties resolvedProps, QList<KoShape*> textPaths);
655 QMap<KoSvgText::TextDecoration, QPainterPath> generateDecorationPaths(const int &start, const int &end,
656 const KoSvgText::ResolutionHandler resHandler,
657 const QVector<CharacterResult> &result,
658 const bool isHorizontal,
659 const KoSvgText::TextDecorations &decor,
661 const bool textDecorationSkipInset = false,
662 const KoPathShape *currentTextPath = nullptr,
663 const qreal currentTextPathOffset = 0.0,
664 const bool textPathSide = false,
667 static void finalizeDecoration (
668 QPainterPath decorationPath,
669 const QPointF offset,
670 const QPainterPathStroker &stroker,
671 const KoSvgText::TextDecoration type,
672 QMap<KoSvgText::TextDecoration, QPainterPath> &decorationPaths,
673 const KoPathShape *currentTextPath,
674 const bool isHorizontal,
675 const qreal currentTextPathOffset,
676 const bool textPathSide
677 );
678
679 void paintTextDecoration(QPainter &painter,
680 const QPainterPath &outlineRect,
681 const KoShape *rootShape,
682 const KoSvgText::TextDecoration type,
683 const KoSvgText::TextRendering rendering);
684 void paintPaths(QPainter &painter,
685 const QPainterPath &outlineRect,
686 const KoShape *rootShape,
687 const QVector<CharacterResult> &result,
688 const KoSvgText::TextRendering rendering,
689 QPainterPath &chunk,
690 int &currentIndex);
691 KoShape* collectPaths(const KoSvgTextShape *rootShape, QVector<CharacterResult> &result, int &currentIndex);
692 void paintDebug(QPainter &painter,
693 const QVector<CharacterResult> &result,
694 int &currentIndex);
695
698 KoSvgTextProperties props = parent->properties;
699 props.inheritFrom(resolvedProps, true);
700 int count = parent->numChars(withControls, props);
701 for (auto it = KisForestDetail::childBegin(parent); it != KisForestDetail::childEnd(parent); it++) {
702 count += numChars(it, withControls, props);
703 }
704 return count;
705 }
706
715
726 int &currentIndex,
727 int sought,
728 bool skipZeroWidth = false)
729 {
730 auto it = tree.depthFirstTailBegin();
731 for (; it != tree.depthFirstTailEnd(); it++) {
732 if (childCount(siblingCurrent(it)) > 0) {
733 continue;
734 }
735 int length = it->numChars(false);
736 if (length == 0 && skipZeroWidth) {
737 continue;
738 }
739
740 if (sought == currentIndex || (sought > currentIndex && sought < currentIndex + length)) {
741 break;
742 } else {
743 currentIndex += length;
744 }
745 }
746 return it;
747 }
757 static bool splitContentElement(KisForest<KoSvgTextContentElement> &tree, int index, bool allowEmptyText = true) {
758 int currentIndex = 0;
759
760 // If there's only a single root element, don't bother searching.
761 auto contentElement = depth(tree) == 1? tree.depthFirstTailBegin(): findTextContentElementForIndex(tree, currentIndex, index, true);
762 if (contentElement == tree.depthFirstTailEnd()) return false;
763
764 bool suitableStartIndex = siblingCurrent(contentElement) == tree.childBegin()? index >= currentIndex: index > currentIndex;
765 bool suitableEndIndex = siblingCurrent(contentElement) == tree.childBegin()? true: index < currentIndex + contentElement->numChars(false);
766
767 if (suitableStartIndex && suitableEndIndex) {
769 duplicate.text = contentElement->text;
770 int start = index - currentIndex;
771 int length = contentElement->numChars(false) - start;
772 int zero = 0;
773 duplicate.removeText(start, length);
774
775 if (!allowEmptyText && (duplicate.text.isEmpty() || length == 0)) {
776 return true;
777 }
778
779 // TODO: handle localtransforms better; annoyingly, this requires whitespace handling
780
781 if (siblingCurrent(contentElement) != tree.childBegin()
782 && contentElement->textPathId.isEmpty()
783 && contentElement->textLength.isAuto
784 && contentElement->localTransformations.isEmpty()) {
785 contentElement->removeText(zero, start);
786 duplicate.properties = contentElement->properties;
787 tree.insert(siblingCurrent(contentElement), duplicate);
788 } else {
790 duplicate2.text = contentElement->text;
791 duplicate2.removeText(zero, start);
792 contentElement->text.clear();
793 tree.insert(childBegin(contentElement), duplicate);
794 tree.insert(childEnd(contentElement), duplicate2);
795 }
796 return true;
797 }
798 return false;
799 }
800
808 static void splitTree(KisForest<KoSvgTextContentElement> &tree, int index, bool textPathAfterSplit) {
809 splitContentElement(tree, index, false);
810 int currentIndex = 0;
811 auto contentElement = depth(tree) == 1? tree.depthFirstTailBegin(): findTextContentElementForIndex(tree, currentIndex, index, true);
812
813 // We're either at the start or end.
814 if (contentElement == tree.depthFirstTailEnd()) return;
815 if (siblingCurrent(contentElement) == tree.childBegin()) return;
816
817 auto lastNode = siblingCurrent(contentElement);
818 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(contentElement));
819 parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(contentElement)); parentIt++) {
820 if (lastNode == siblingCurrent(parentIt)) continue;
821 if (siblingCurrent(parentIt) == tree.childBegin()) {
822 break;
823 }
824
825 if (lastNode != childBegin(siblingCurrent(parentIt))) {
827 duplicate.properties = parentIt->properties;
828 if (textPathAfterSplit) {
829 duplicate.textPathId = parentIt->textPathId;
830 duplicate.textPathInfo = parentIt->textPathInfo;
831 parentIt->textPathId = QString();
832 parentIt->textPathInfo = KoSvgText::TextOnPathInfo();
833 }
834 auto insert = siblingCurrent(parentIt);
835 insert ++;
836 auto it = tree.insert(insert, duplicate);
837
839 for (auto child = lastNode; child != childEnd(siblingCurrent(parentIt)); child++) {
840 movableChildren.append(child);
841 }
842 while(!movableChildren.isEmpty()) {
843 auto child = movableChildren.takeLast();
844 tree.move(child, childBegin(it));
845 }
846 lastNode = it;
847 } else {
848 lastNode = siblingCurrent(parentIt);
849 }
850 }
851 }
852
863 // An earlier version of the code used Hierarchy iterator,
864 // but that had too many exceptions when the child was not in the root.
865 if (!child.node()) return childEnd(root);
866 if (KisForestDetail::parent(child) == root) return child;
867 for (auto rootChild = childBegin(root); rootChild != childEnd(root); rootChild++) {
868 for (auto leaf = KisForestDetail::tailSubtreeBegin(rootChild);
869 leaf != KisForestDetail::tailSubtreeEnd(rootChild); leaf++) {
870 if (siblingCurrent(leaf) == child) {
871 return rootChild;
872 }
873 }
874 }
875 return childEnd(root);
876 }
877
886 static QVector<bool> collapsedWhiteSpacesForText(KisForest<KoSvgTextContentElement> &tree, QString &allText, const bool alsoCollapseLowSurrogate = false, bool includeBidiControls = false) {
887 QMap<int, KoSvgText::TextSpaceCollapse> collapseModes;
888
890 for (auto it = tree.compositionBegin(); it != tree.compositionEnd(); it++) {
891 if (it.state() == KisForestDetail::Enter) {
892 KoSvgTextProperties ownProperties = it->properties;
893 ownProperties.inheritFrom(parentProps.last());
894 parentProps.append(ownProperties);
895
896 const int children = childCount(siblingCurrent(it));
897 if (children == 0) {
898 QString text = it->text;
899 if (includeBidiControls) {
900 KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(parentProps.last().propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt());
901 KoSvgText::Direction direction = KoSvgText::Direction(parentProps.last().propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
902 QVector<QPair<int, int>> positions;
903 QString text = KoCssTextUtils::getBidiOpening(direction, bidi);
904 text += it->getTransformedString(positions, parentProps.last());
905 text += KoCssTextUtils::getBidiClosing(bidi);
906 }
907 KoSvgText::TextSpaceCollapse collapse = KoSvgText::TextSpaceCollapse(parentProps.last().propertyOrDefault(KoSvgTextProperties::TextCollapseId).toInt());
908 collapseModes.insert(allText.size(), collapse);
909 allText += text;
910 }
911 } else {
912 parentProps.pop_back();
913 }
914 }
915 QVector<bool> collapsed = KoCssTextUtils::collapseSpaces(&allText, collapseModes);
916
917 if (alsoCollapseLowSurrogate) {
918 for (int i = 0; i < allText.size(); i++) {
919 if (i > 0 && allText.at(i).isLowSurrogate() && allText.at(i-1).isHighSurrogate()) {
920 collapsed[i] = true;
921 }
922 }
923 }
924 return collapsed;
925 }
926
938 static void removeTransforms(KisForest<KoSvgTextContentElement> &tree, const int start, const int length) {
939 QString all;
940 QVector<bool> collapsedCharacters = collapsedWhiteSpacesForText(tree, all, true);
941
942 auto root = tree.childBegin();
943 removeTransformsImpl(root, 0, start, length, collapsedCharacters);
944 }
945
951 static int removeTransformsImpl(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector<bool> collapsedCharacters) {
952 int currentLength = 0;
953 auto it = childBegin(currentTextElement);
954 if (it != childEnd(currentTextElement)) {
955 for (; it != childEnd(currentTextElement); it++) {
956 currentLength += removeTransformsImpl(it, globalIndex + currentLength, start, length, collapsedCharacters);
957 }
958 } else {
959 currentLength = currentTextElement->text.size();
960 }
961
962 if (!currentTextElement->localTransformations.isEmpty()) {
963 int transformOffset = 0;
964 int transformOffsetEnd = 0;
965
966 for (int i = globalIndex; i < globalIndex + currentLength; i++) {
967 if (i >= collapsedCharacters.size()) break;
968 if (i < start) {
969 transformOffset += collapsedCharacters.at(i)? 0: 1;
970 }
971 if (i < start + length) {
972 transformOffsetEnd += collapsedCharacters.at(i)? 0: 1;
973 } else {
974 break;
975 }
976 }
977 if (transformOffset < currentTextElement->localTransformations.size()) {
978 int length = qBound(0, transformOffsetEnd-transformOffset, qMax(0, currentTextElement->localTransformations.size()-transformOffset));
979 currentTextElement->localTransformations.remove(transformOffset,
980 length);
981 }
982
983 }
984 return currentLength;
985 }
986
996 static void insertTransforms(KisForest<KoSvgTextContentElement> &tree, const int start, const int length, const bool allowSkipFirst) {
997 QString all;
998 QVector<bool> collapsedCharacters = collapsedWhiteSpacesForText(tree, all, true);
999
1000 auto root = tree.childBegin();
1001 insertTransformsImpl(root, 0, start, length, collapsedCharacters, allowSkipFirst);
1002 }
1008 static int insertTransformsImpl(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector<bool> collapsedCharacters, const bool allowSkipFirst) {
1009 int currentLength = 0;
1010 auto it = childBegin(currentTextElement);
1011 if (it != childEnd(currentTextElement)) {
1012 for (; it != childEnd(currentTextElement); it++) {
1013 currentLength += insertTransformsImpl(it, globalIndex + currentLength, start, length, collapsedCharacters, allowSkipFirst);
1014 }
1015 } else {
1016 currentLength = currentTextElement->text.size();
1017 }
1018
1019 if (!currentTextElement->localTransformations.isEmpty()) {
1020 int transformOffset = 0;
1021 int transformOffsetEnd = 0;
1022
1023 for (int i = globalIndex; i < globalIndex + currentLength; i++) {
1024 if (i < start) {
1025 transformOffset += collapsedCharacters.at(i)? 0: 1;
1026 }
1027 if (i < start + length) {
1028 transformOffsetEnd += collapsedCharacters.at(i)? 0: 1;
1029 } else {
1030 break;
1031 }
1032 }
1033
1034 // When at the start, skip the first transform.
1035 if (transformOffset == 0 && allowSkipFirst && currentTextElement->localTransformations.at(0).startsNewChunk()) {
1036 transformOffset += 1;
1037 }
1038
1039 if (transformOffset < currentTextElement->localTransformations.size()) {
1040 for (int i = transformOffset; i < transformOffsetEnd; i++) {
1041 currentTextElement->localTransformations.insert(i, KoSvgText::CharTransformation());
1042 }
1043 }
1044
1045 }
1046 return currentLength;
1047 }
1048
1059 void applyWhiteSpace(KisForest<KoSvgTextContentElement> &tree, const bool convertToPreWrapped = false) {
1060 QString allText;
1061 QVector<bool> collapsed = collapsedWhiteSpacesForText(tree, allText, false);
1062
1063 auto end = std::make_reverse_iterator(tree.childBegin());
1064 auto begin = std::make_reverse_iterator(tree.childEnd());
1065
1066 for (; begin != end; begin++) {
1067 applyWhiteSpaceImpl(begin, collapsed, allText, convertToPreWrapped);
1068 }
1069 if (convertToPreWrapped) {
1070 tree.childBegin()->properties.setProperty(KoSvgTextProperties::TextCollapseId, QVariant::fromValue(KoSvgText::Preserve));
1071 tree.childBegin()->properties.setProperty(KoSvgTextProperties::TextWrapId, QVariant::fromValue(KoSvgText::Wrap));
1072 }
1073 }
1074
1075 void applyWhiteSpaceImpl(std::reverse_iterator<KisForest<KoSvgTextContentElement>::child_iterator> current, QVector<bool> &collapsed, QString &allText, const bool convertToPreWrapped) {
1076 auto base = current.base();
1077 // It seems that .base() refers to the next entry instead of the pointer,
1078 // which is coherent with the template implementation of "operator*" here:
1079 // https://en.cppreference.com/w/cpp/iterator/reverse_iterator.html
1080 base--;
1081 if(base != siblingEnd(base)) {
1082 auto end = std::make_reverse_iterator(childBegin(base));
1083 auto begin = std::make_reverse_iterator(childEnd(base));
1084 for (; begin != end; begin++) {
1085 applyWhiteSpaceImpl(begin, collapsed, allText, convertToPreWrapped);
1086 }
1087 }
1088
1089 if (!current->text.isEmpty()) {
1090 const int total = current->text.size();
1091 QString currentText = allText.right(total);
1092
1093 for (int i = 0; i < total; i++) {
1094 const int j = total - (i+1);
1095 const bool col = collapsed.takeLast();
1096 if (col) {
1097 currentText.remove(j, 1);
1098 }
1099
1100 }
1101 current->text = currentText;
1102 allText.chop(total);
1103 }
1104 if (convertToPreWrapped) {
1105 current->properties.removeProperty(KoSvgTextProperties::TextCollapseId);
1106 current->properties.removeProperty(KoSvgTextProperties::TextWrapId);
1107 }
1108 }
1109
1110 static QVector<KoSvgText::CharTransformation> resolvedTransformsForTree(KisForest<KoSvgTextContentElement> &tree, bool shapesInside = false, bool includeBidiControls = false) {
1111 QString all;
1112 QVector<bool> collapsed = collapsedWhiteSpacesForText(tree, all, false, includeBidiControls);
1113 QVector<CharacterResult> result(all.size());
1114 int globalIndex = 0;
1115 KoSvgTextProperties props = tree.childBegin()->properties;
1119 bool isHorizontal = mode == KoSvgText::HorizontalTB;
1120 bool isWrapped = (props.hasProperty(KoSvgTextProperties::InlineSizeId)
1121 || shapesInside);
1122 QVector<KoSvgText::CharTransformation> resolvedTransforms(all.size());
1123 resolveTransforms(tree.childBegin(), all, result, globalIndex,
1124 isHorizontal, isWrapped, false, resolvedTransforms,
1126 false);
1127 return resolvedTransforms;
1128 }
1129
1137 static void insertNewLinesAtAnchors(KisForest<KoSvgTextContentElement> &tree, bool shapesInside = false) {
1138 QVector<KoSvgText::CharTransformation> resolvedTransforms = resolvedTransformsForTree(tree, shapesInside);
1139
1140 auto end = std::make_reverse_iterator(tree.childBegin());
1141 auto begin = std::make_reverse_iterator(tree.childEnd());
1142
1143 bool inTextPath = false;
1144 for (; begin != end; begin++) {
1145 insertNewLinesAtAnchorsImpl(begin, resolvedTransforms, inTextPath);
1146 }
1147 }
1148
1150 QVector<KoSvgText::CharTransformation> &resolvedTransforms,
1151 bool &inTextPath) {
1152
1153 inTextPath = (!current->textPathId.isEmpty());
1154 auto base = current.base();
1155 base--;
1156 if(base != siblingEnd(base)) {
1157 auto end = std::make_reverse_iterator(childBegin(base));
1158 auto begin = std::make_reverse_iterator(childEnd(base));
1159 for (; begin != end; begin++) {
1160 insertNewLinesAtAnchorsImpl(begin,
1161 resolvedTransforms,
1162 inTextPath);
1163 }
1164 }
1165
1166 if (!current->text.isEmpty()) {
1167 const int total = current->text.size();
1168
1169 for (int i = 0; i < total; i++) {
1170 const int j = total - (i+1);
1171 KoSvgText::CharTransformation transform = resolvedTransforms.takeLast();
1178 bool startsNewChunk = transform.startsNewChunk() && j == 0;
1179
1180 if (inTextPath) {
1181 // First transform in path is always absolute, so we don't insert a newline.
1182 startsNewChunk = false;
1183 inTextPath = false;
1184 }
1185 // When there's no new chunk, we're not at the start of the text and there isn't already a line feed, insert a line feed.
1186 if (startsNewChunk && !resolvedTransforms.isEmpty() && current->text.at(j) != QChar::LineFeed) {
1187 current->text.insert(j, "\n");
1188 }
1189 }
1190 }
1191 current->localTransformations.clear();
1192 }
1193
1195 for (int i = 0; i< layout.size(); i++) {
1196 //split all anchored chunks, so we can set transforms on them.
1197 if (layout.at(i).anchored_chunk) {
1198 int plainTextIndex = layout.at(i).plaintTextIndex;
1199 splitContentElement(tree, plainTextIndex);
1200 }
1201 }
1202
1203 int globalIndex = 0;
1204 for (auto it = tree.childBegin(); it != tree.childEnd(); it++) {
1206 it->properties.propertyOrDefault(KoSvgTextProperties::WritingModeId).toInt());
1207 bool isHorizontal = mode == KoSvgText::HorizontalTB;
1208 setTransformsFromLayoutImpl(it, KoSvgTextProperties::defaultProperties(), layout, globalIndex, isHorizontal);
1209 }
1210 }
1211
1213 const KoSvgTextProperties parentProps, const QVector<CharacterResult> layout,
1214 int &globalIndex, bool isHorizontal) {
1215 KoSvgTextProperties props = current->properties;
1216 props.inheritFrom(parentProps);
1217 if (!current->textPathId.isEmpty()) return; // When we're doing text-on-path, we're already in preformatted mode.
1218 for (auto it = childBegin(current); it!= childEnd(current); it++) {
1219 setTransformsFromLayoutImpl(it, props, layout, globalIndex, isHorizontal);
1220 }
1221
1222 if (current->text.isEmpty()) {
1223 current->localTransformations.clear();
1224 } else {
1226 const int length = current->numChars(true, props);
1227
1228 for (int i = globalIndex; i< globalIndex+length; i++) {
1229 CharacterResult result = layout.value(i);
1230
1231 if (!result.addressable) {
1232 continue;
1233 }
1235
1236 //TODO: Also split up content element if multiple anchored chunks.
1237 if (result.anchored_chunk) {
1238 int endIndex = 0;
1239 qreal shift = anchoredChunkShift(layout, isHorizontal, i, endIndex);
1240 QPointF offset = isHorizontal? QPointF(shift, 0): QPointF(0, shift);
1241 transform.xPos = result.finalPosition.x() - offset.x();
1242 transform.yPos = result.finalPosition.y() - offset.y();
1243 } else if (i > 0) {
1244 CharacterResult resultPrev = layout.value(i-1);
1245 QPointF offset = (result.finalPosition - result.cssPosition) - (resultPrev.finalPosition - resultPrev.cssPosition);
1246
1247 transform.dxPos = offset.x();
1248 transform.dyPos = offset.y();
1249 }
1250 transform.rotate = result.rotate;
1251
1252 transforms.append(transform);
1253 }
1254 current->localTransformations = transforms;
1255 current->text = current->text.split("\n").join(" ");
1256 globalIndex += length;
1257 }
1258 }
1259
1269 const int children = childCount(current);
1270 if (children == 0) {
1271 const int length = current->numChars(false);
1272 if (length == 0 && current != tree.childBegin() && current->textPathId.isEmpty()) {
1273 return true;
1274 } else {
1275 // check if siblings are similar.
1276 auto siblingPrev = current;
1278 QVariant(KoSvgText::BidiNormal)).toInt());
1279 siblingPrev--;
1280 while (!isEnd(siblingPrev) && siblingPrev->text.isEmpty()) {
1281 // By checking whether the text is empty, we can skip previous
1282 // siblings that are already marked for deletion.
1283 siblingPrev--;
1284 }
1285 if (!isEnd(siblingPrev)
1286 && siblingPrev != current
1287 && (siblingPrev->localTransformations.isEmpty() && current->localTransformations.isEmpty())
1288 && (siblingPrev->textPathId.isEmpty() && current->textPathId.isEmpty())
1289 && (siblingPrev->textLength.isAuto && current->textLength.isAuto)
1290 && (siblingPrev->properties == current->properties)
1292 && childCount(siblingPrev) == 0) {
1293 // TODO: handle localtransforms better; annoyingly, this requires whitespace handling
1294 siblingPrev->text += current->text;
1295 current->text = QString();
1296 return true;
1297 }
1298 }
1299 } else if (children == 1) {
1300 // merge single children into parents if possible.
1301 auto child = childBegin(current);
1302 if ((child->localTransformations.isEmpty() && current->localTransformations.isEmpty())
1303 && (child->textPathId.isEmpty() && current->textPathId.isEmpty())
1304 && (child->textLength.isAuto && current->textLength.isAuto)
1305 && (!child->properties.hasNonInheritableProperties() || !current->properties.hasNonInheritableProperties())) {
1306 if (current->properties.hasNonInheritableProperties()) {
1307 KoSvgTextProperties props = current->properties;
1308 props.setAllButNonInheritableProperties(child->properties);
1309 child->properties = props;
1310 } else {
1311 child->properties.inheritFrom(current->properties);
1312 }
1313 *current = *child;
1314 tree.erase(child);
1315 }
1316 } else {
1318 for (auto child = childBegin(current); child != childEnd(current); child++) {
1319 if (cleanUpImpl(tree, child)) {
1320 deleteList.append(child);
1321 }
1322 }
1323 while (!deleteList.isEmpty()) {
1324 auto child = deleteList.takeFirst();
1325 KIS_ASSERT_RECOVER_NOOP(childBegin(child) == childEnd(child));
1326 tree.erase(child);
1327 }
1328 if (childCount(current) <= 1) {
1329 // This checks if, when there's only 0 or 1 children, this node can be cleaned up as well.
1330 return cleanUpImpl(tree, current);
1331 }
1332 }
1333 return false;
1334 }
1335
1345 if (tree.childBegin() != tree.childEnd()) {
1346 cleanUpImpl(tree, tree.childBegin());
1347 }
1348 }
1349
1351 if (treeIndex.isEmpty()) return parent;
1352 QVector<int> idx = treeIndex;
1353 int count = idx.takeFirst();
1354 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1355 if (count == 0) {
1357 return iteratorForTreeIndex(idx, child);
1358 } else {
1359 return child;
1360 }
1361 }
1362 count -= 1;
1363 }
1364 return KisForestDetail::childEnd(parent);
1365 }
1366
1368 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1369 if (child == target) {
1370 return true;
1371 } else if ((KisForestDetail::childBegin(child) != KisForestDetail::childEnd(child))) {
1372 if (startIndexOfIterator(child, target, currentIndex)) {
1373 return true;
1374 }
1375 } else {
1376 currentIndex += numChars(child);
1377 }
1378 }
1379 return false;
1380 }
1381
1383
1389 for (auto it = childBegin(parent); it != childEnd(parent); it++) {
1390 if (it->textPathId == name) {
1391 it->textPathId = QString();
1392 break;
1393 }
1394 }
1395 }
1396
1397 static void makeTextPathNameUnique(QList<KoShape*> textPaths, KoShape *textPath) {
1398 bool textPathNameUnique = false;
1399 int textPathNumber = textPaths.size();
1400 QString newTextPathName = textPath->name();
1401 while(!textPathNameUnique) {
1402 textPathNameUnique = true;
1403 Q_FOREACH(KoShape *shape, textPaths) {
1404 if (shape->name() == newTextPathName) {
1405 textPathNameUnique = false;
1406 textPathNumber += 1;
1407 break;
1408 }
1409 }
1410 if (textPathNameUnique && !newTextPathName.isEmpty()) {
1411 textPath->setName(newTextPathName);
1412 } else {
1413 textPathNameUnique = false;
1414 }
1415 newTextPathName = QString("textPath"+QString::number(textPathNumber));
1416 }
1417 }
1418
1428 static QRectF boundingBoxFromTree(KisForest<KoSvgTextContentElement> tree, const KoSvgTextShape *rootShape, bool includeStrokeInset) {
1429 QRectF result;
1430 QList<KoShapeStrokeModelSP> parentStrokes;
1431 for (auto it = tree.compositionBegin(); it != tree.compositionEnd(); it++) {
1432 if (it.state() == KisForestDetail::Enter) {
1433 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
1434 parentStrokes.append(it->properties.property(KoSvgTextProperties::StrokeId).value<KoSvgText::StrokeProperty>().property);
1435 }
1436 } else {
1437 KoShapeStrokeModelSP stroke = parentStrokes.size() > 0? parentStrokes.last(): nullptr;
1438 QRectF bb = it->associatedOutline.boundingRect();
1439 QMap<KoSvgText::TextDecoration, QPainterPath> decorations = it->textDecorations;
1440 for (int i = 0; i < decorations.values().size(); ++i) {
1441 bb |= decorations.values().at(i).boundingRect();
1442 }
1443 if (!bb.isEmpty()) {
1444 if (stroke && includeStrokeInset) {
1445 KoInsets insets;
1446 stroke->strokeInsets(rootShape, insets);
1447 result |= bb.adjusted(-insets.left, -insets.top, insets.right, insets.bottom);
1448 } else {
1449 result |= bb;
1450 }
1451 }
1452 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
1453 // reset stroke to use parent stroke.
1454 parentStrokes.pop_back();
1455 }
1456 }
1457 }
1458 return result;
1459 }
1460};
1461
1462#endif // KO_SVG_TEXT_SHAPE_P_H
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
KisMagneticGraph::vertex_descriptor target(typename KisMagneticGraph::edge_descriptor e, KisMagneticGraph g)
constexpr qreal SHAPE_PRECISION
Value that indicates the precision for testing coordinates for text-in-shape layout.
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.
NodeType * node() const
Definition KisForest.h:78
child_iterator childEnd()
Definition KisForest.h:880
child_iterator insert(child_iterator pos, X &&value)
Inserts element value into position pos. value becomes the child of the same parent as pos and is pla...
Definition KisForest.h:943
depth_first_tail_iterator depthFirstTailEnd()
Definition KisForest.h:856
child_iterator childBegin()
Definition KisForest.h:876
child_iterator move(child_iterator subtree, child_iterator newPos)
move a subtree to new position Moves subtree into a new position pointer by newPos....
Definition KisForest.h:994
composition_iterator compositionBegin()
Definition KisForest.h:912
composition_iterator compositionEnd()
Definition KisForest.h:916
depth_first_tail_iterator depthFirstTailBegin()
Definition KisForest.h:852
child_iterator erase(child_iterator pos)
Removes element at position pos. If pos is 'end', then result is undefined.
Definition KisForest.h:956
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 QString getBidiClosing(KoSvgText::UnicodeBidi bidi)
getBidiClosing Returns the bidi closing string associated with the given Css unicode-bidi value.
static QString getBidiOpening(bool ltr, KoSvgText::UnicodeBidi bidi)
getBidiOpening Get the bidi opening string associated with the given Css unicode-bidi value and direc...
The position of a path point within a path shape.
Definition KoPathShape.h:63
void setName(const QString &name)
Definition KoShape.cpp:955
void setSelectable(bool selectable)
Definition KoShape.cpp:827
QString name() const
Definition KoShape.cpp:950
The KoSvgTextNodeIndex class.
@ InlineSizeId
KoSvgText::AutoValue.
@ UnicodeBidiId
KoSvgText::UnicodeBidi.
@ TextCollapseId
KoSvgText::TextSpaceCollapse.
@ StrokeId
KoSvgText::StrokeProperty.
@ WritingModeId
KoSvgText::WritingMode.
@ DirectionId
KoSvgText::Direction.
@ TextWrapId
KoSvgText::TextWrap.
QList< PropertyId > properties() const
static const KoSvgTextProperties & defaultProperties()
bool hasProperty(PropertyId id) const
void inheritFrom(const KoSvgTextProperties &parentProperties, bool resolve=false, bool onlyFontAndLineHeight=false)
void setAllButNonInheritableProperties(const KoSvgTextProperties &properties)
Used to merge child properties into parent properties.
QVariant propertyOrDefault(PropertyId id) const
void applyTextLength(KisForest< KoSvgTextContentElement >::child_iterator currentTextElement, QVector< CharacterResult > &result, int &currentIndex, int &resolvedDescendentNodes, bool isHorizontal, const KoSvgTextProperties resolvedProps, const KoSvgText::ResolutionHandler &resHandler)
static qreal anchoredChunkShift(const QVector< CharacterResult > &result, const bool isHorizontal, const int start, int &end)
static int removeTransformsImpl(KisForest< KoSvgTextContentElement >::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector< bool > collapsedCharacters)
removeTransformsImpl recursive function that handles removing local transforms in a certain range....
QMap< KoSvgText::TextDecoration, QPainterPath > generateDecorationPaths(const int &start, const int &end, const KoSvgText::ResolutionHandler resHandler, const QVector< CharacterResult > &result, const bool isHorizontal, const KoSvgText::TextDecorations &decor, const KoSvgText::TextDecorationStyle style=KoSvgText::TextDecorationStyle::Solid, const bool textDecorationSkipInset=false, const KoPathShape *currentTextPath=nullptr, const qreal currentTextPathOffset=0.0, const bool textPathSide=false, const KoSvgText::TextDecorationUnderlinePosition underlinePosH=KoSvgText::TextDecorationUnderlinePosition::UnderlineAuto, const KoSvgText::TextDecorationUnderlinePosition underlinePosV=KoSvgText::TextDecorationUnderlinePosition::UnderlineAuto)
void paintDebug(QPainter &painter, const QVector< CharacterResult > &result, int &currentIndex)
static bool cleanUpImpl(KisForest< KoSvgTextContentElement > &tree, KisForest< KoSvgTextContentElement >::child_iterator current)
cleanUpImpl implementation of recursive function for cleanup. This goes over a child and decides whet...
static void removeTransforms(KisForest< KoSvgTextContentElement > &tree, const int start, const int length)
removeTransforms Remove all local SVG character transforms in a certain range. Local transforms are i...
static void resolveTransforms(KisForest< KoSvgTextContentElement >::child_iterator currentTextElement, QString text, QVector< CharacterResult > &result, int &currentIndex, bool isHorizontal, bool wrapped, bool textInPath, QVector< KoSvgText::CharTransformation > &resolved, QVector< bool > collapsedChars, const KoSvgTextProperties resolvedProps, bool withControls=true)
KisForest< KoSvgTextContentElement > textData
QVector< CursorPos > cursorPos
void updateTextWrappingAreas()
updateShapeContours The current shape contours can be slow to compute, so this function calls computi...
static qreal characterResultOnPath(CharacterResult &cr, qreal length, qreal offset, bool isHorizontal, bool isClosed)
void setTransformsFromLayoutImpl(KisForest< KoSvgTextContentElement >::child_iterator current, const KoSvgTextProperties parentProps, const QVector< CharacterResult > layout, int &globalIndex, bool isHorizontal)
static QVector< KoSvgText::CharTransformation > resolvedTransformsForTree(KisForest< KoSvgTextContentElement > &tree, bool shapesInside=false, bool includeBidiControls=false)
QVector< CharacterResult > result
static void finalizeDecoration(QPainterPath decorationPath, const QPointF offset, const QPainterPathStroker &stroker, const KoSvgText::TextDecoration type, QMap< KoSvgText::TextDecoration, QPainterPath > &decorationPaths, const KoPathShape *currentTextPath, const bool isHorizontal, const qreal currentTextPathOffset, const bool textPathSide)
QScopedPointer< KoShapeGroup > shapeGroup
void applyWhiteSpaceImpl(std::reverse_iterator< KisForest< KoSvgTextContentElement >::child_iterator > current, QVector< bool > &collapsed, QString &allText, const bool convertToPreWrapped)
static KisForest< KoSvgTextContentElement >::depth_first_tail_iterator findTextContentElementForIndex(KisForest< KoSvgTextContentElement > &tree, int &currentIndex, int sought, bool skipZeroWidth=false)
findTextContentElementForIndex Finds the given leaf of the current tree-wide string index.
static void computeFontMetrics(KisForest< KoSvgTextContentElement >::child_iterator parent, const KoSvgTextProperties &parentProps, const KoSvgText::FontMetrics &parentBaselineTable, const KoSvgText::Baseline parentBaseline, QVector< CharacterResult > &result, int &currentIndex, const KoSvgText::ResolutionHandler resHandler, const bool isHorizontal, const bool disableFontMatching)
void paintPaths(QPainter &painter, const QPainterPath &outlineRect, const KoShape *rootShape, const QVector< CharacterResult > &result, const KoSvgText::TextRendering rendering, QPainterPath &chunk, int &currentIndex)
QList< QPainterPath > currentTextWrappingAreas
static int insertTransformsImpl(KisForest< KoSvgTextContentElement >::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector< bool > collapsedCharacters, const bool allowSkipFirst)
insertTransformsImpl Recursive function that handles inserting empty transforms into a given range....
QList< KoShape * > internalShapes() const
void updateInternalShapesList()
QList< KoShape * > shapesInside
static KisForest< KoSvgTextContentElement >::child_iterator iteratorForTreeIndex(const QVector< int > treeIndex, KisForest< KoSvgTextContentElement >::child_iterator parent)
static KoShape * textPathByName(QString name, QList< KoShape * > textPaths)
static int numChars(KisForest< KoSvgTextContentElement >::child_iterator parent, bool withControls=false, KoSvgTextProperties resolvedProps=KoSvgTextProperties::defaultProperties())
Get the number of characters for the whole subtree of this node.
QMap< int, int > logicalToVisualCursorPos
static void cleanUp(KisForest< KoSvgTextContentElement > &tree)
cleanUp This cleans up the tree by...
static void splitTree(KisForest< KoSvgTextContentElement > &tree, int index, bool textPathAfterSplit)
splitTree Split the whole hierarchy of nodes at the given index.
static void applyAnchoring(QVector< CharacterResult > &result, bool isHorizontal, const KoSvgText::ResolutionHandler resHandler)
void computeTextDecorations(KisForest< KoSvgTextContentElement >::child_iterator currentTextElement, const QVector< CharacterResult > &result, const QMap< int, int > &logicalToVisual, const KoSvgText::ResolutionHandler resHandler, KoPathShape *textPath, qreal textPathoffset, bool side, int &currentIndex, bool isHorizontal, bool ltr, bool wrapping, const KoSvgTextProperties resolvedProps, QList< KoShape * > textPaths)
static void insertNewLinesAtAnchorsImpl(std::reverse_iterator< KisForest< KoSvgTextContentElement >::child_iterator > current, QVector< KoSvgText::CharTransformation > &resolvedTransforms, bool &inTextPath)
KoSvgTextNodeIndex createTextNodeIndex(KisForest< KoSvgTextContentElement >::child_iterator textElement) const
static void applyTextPath(KisForest< KoSvgTextContentElement >::child_iterator parent, QVector< CharacterResult > &result, bool isHorizontal, QPointF &startPos, const KoSvgTextProperties resolvedProps, QList< KoShape * > textPaths)
void setTransformsFromLayout(KisForest< KoSvgTextContentElement > &tree, const QVector< CharacterResult > layout)
Private(const Private &rhs)
static int childCount(KisForest< KoSvgTextContentElement >::child_iterator it)
Get the child count of the current node. A node without children is a text node.
void paintTextDecoration(QPainter &painter, const QPainterPath &outlineRect, const KoShape *rootShape, const KoSvgText::TextDecoration type, const KoSvgText::TextRendering rendering)
static void makeTextPathNameUnique(QList< KoShape * > textPaths, KoShape *textPath)
static QRectF boundingBoxFromTree(KisForest< KoSvgTextContentElement > tree, const KoSvgTextShape *rootShape, bool includeStrokeInset)
boundingBoxFromTree get the bounding box of the current textdata tree. We need the bounding box for a...
static QVector< QPointF > getLigatureCarets(const KoSvgText::ResolutionHandler &resHandler, const bool isHorizontal, raqm_glyph_t &currentGlyph)
static void insertNewLinesAtAnchors(KisForest< KoSvgTextContentElement > &tree, bool shapesInside=false)
insertNewLinesAtAnchors Resolves character transforms and then inserts new lines at each transform th...
static KisForest< KoSvgTextContentElement >::child_iterator findTopLevelParent(KisForest< KoSvgTextContentElement >::child_iterator root, KisForest< KoSvgTextContentElement >::child_iterator child)
findTopLevelParent Returns the toplevel parent of child that is not root.
static bool splitContentElement(KisForest< KoSvgTextContentElement > &tree, int index, bool allowEmptyText=true)
splitContentElement split the contentElement in tree at index into two nodes.
QScopedPointer< KoShapePainter > internalShapesPainter
std::optional< BulkActionState > bulkActionState
static std::pair< QTransform, qreal > loadGlyphOnly(const QTransform &ftTF, FT_Int32 faceLoadFlags, bool isHorizontal, raqm_glyph_t &currentGlyph, CharacterResult &charResult, const KoSvgText::TextRendering rendering)
static void insertTransforms(KisForest< KoSvgTextContentElement > &tree, const int start, const int length, const bool allowSkipFirst)
insertTransforms Inserts empty transforms into tree recursively.
void handleShapes(const QList< KoShape * > &sourceShapeList, const QList< KoShape * > referenceList2, const QList< KoShape * > referenceShapeList, QList< KoShape * > &destinationShapeList)
static bool loadGlyph(const KoSvgText::ResolutionHandler &resHandler, const FT_Int32 faceLoadFlags, const bool isHorizontal, const char32_t firstCodepoint, const KoSvgText::TextRendering rendering, raqm_glyph_t &currentGlyph, CharacterResult &charResult, QPointF &totalAdvanceFTFontCoordinates)
static QVector< bool > collapsedWhiteSpacesForText(KisForest< KoSvgTextContentElement > &tree, QString &allText, const bool alsoCollapseLowSurrogate=false, bool includeBidiControls=false)
collapsedWhiteSpacesForText This returns the collapsed spaces for a given piece of text,...
void clearAssociatedOutlines()
static QVector< SubChunk > collectSubChunks(KisForest< KoSvgTextContentElement >::child_iterator it, KoSvgTextProperties parent, bool textInPath, bool &firstTextInPath)
static QList< QPainterPath > generateShapes(const QList< KoShape * > shapesInside, const QList< KoShape * > shapesSubtract, const KoSvgTextProperties &properties)
QList< KoShape * > shapesSubtract
QVector< LineBox > lineBoxes
static QPainterPath stretchGlyphOnPath(const QPainterPath &glyph, const QPainterPath &path, bool isHorizontal, qreal offset, bool isClosed)
static bool startIndexOfIterator(KisForest< KoSvgTextContentElement >::child_iterator parent, KisForest< KoSvgTextContentElement >::child_iterator target, int &currentIndex)
QList< KoShape * > textPaths
static void handleLineBoxAlignment(KisForest< KoSvgTextContentElement >::child_iterator parent, QVector< CharacterResult > &result, const QVector< LineBox > lineBoxes, int &currentIndex, const bool isHorizontal, const KoSvgTextProperties resolvedProps)
KoShape * collectPaths(const KoSvgTextShape *rootShape, QVector< CharacterResult > &result, int &currentIndex)
static void removeTextPathId(KisForest< KoSvgTextContentElement >::child_iterator parent, const QString &name)
removeTextPathId Remove the text path id with the given name from the toplevel elements.
void applyWhiteSpace(KisForest< KoSvgTextContentElement > &tree, const bool convertToPreWrapped=false)
applyWhiteSpace CSS Whitespace processes whitespaces so that duplicate white spaces and unnecessary h...
#define KIS_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:97
std::variant< std::monostate, Outline, Bitmap, ColorLayers > Variant
ResultIterator hierarchyBegin(Iterator it)
Definition KisForest.h:419
ChildIterator< value_type, is_const > childBegin(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:290
ResultIterator hierarchyEnd(Iterator it)
Definition KisForest.h:427
ResultIterator tailSubtreeBegin(Iterator it)
Definition KisForest.h:706
ChildIterator< value_type, is_const > parent(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:327
ResultIterator tailSubtreeEnd(Iterator it)
Definition KisForest.h:714
ChildIterator< value_type, is_const > childEnd(const ChildIterator< value_type, is_const > &it)
Definition KisForest.h:300
TextAnchor
Where the text is anchored for SVG 1.1 text and 'inline-size'.
Definition KoSvgText.h:79
@ AnchorStart
Anchor left for LTR, right for RTL.
Definition KoSvgText.h:80
TextDecorationStyle
Style of the text-decoration.
Definition KoSvgText.h:265
@ Solid
Draw a solid line.Ex: --—.
Definition KoSvgText.h:266
TextDecorationUnderlinePosition
Which location to choose for the underline.
Definition KoSvgText.h:275
@ UnderlineAuto
Use Font metrics.
Definition KoSvgText.h:276
TextDecoration
Flags for text-decoration, for underline, overline and strikethrough.
Definition KoSvgText.h:257
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionLeftToRight
Definition KoSvgText.h:49
Baseline
Baseline values used by dominant-baseline and baseline-align.
Definition KoSvgText.h:213
@ HorizontalTB
Definition KoSvgText.h:38
@ BidiNormal
No new bidi-level is started.
Definition KoSvgText.h:57
@ BidiIsolate
Content is ordered as if in a separate paragraph.
Definition KoSvgText.h:61
@ BidiIsolateOverride
Definition KoSvgText.h:62
TextSpaceCollapse
Definition KoSvgText.h:96
@ Preserve
Do not collapse any space.
Definition KoSvgText.h:99
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.
std::optional< qreal > tabSize
If present, this is a tab and it should align to multiples of this tabSize value.
QRectF lineHeightBox() const
lineHeightBox
qreal extraFontScaling
Freetype doesn't allow us to scale below 1pt, so we need to do an extra transformation in these cases...
bool anchored_chunk
whether this is the start of a new chunk.
QPointF dominantBaselineOffset
LineEdgeBehaviour lineEnd
void scaleCharacterResult(qreal xScale, qreal yScale)
scaleCharacterResult convenience function to scale the whole character result.
qreal scaledAscent
Ascender, in pt.
void calculateAndApplyTabsize(QPointF currentPos, bool isHorizontal, const KoSvgText::ResolutionHandler &resHandler)
bool isHorizontal
Whether the current glyph lays out horizontal or vertical. Currently same as paragraph,...
QPointF textPathAndAnchoringOffset
Offset caused by textPath and anchoring.
QPointF cssPosition
the position in accordance with the CSS specs, as opossed to the SVG spec.
QFont::Style fontStyle
qreal scaledHalfLeading
Leading for both sides, can be either negative or positive, in pt.
qreal fontHalfLeading
Leading for both sides, can be either negative or positive.
QRectF layoutBox() const
layoutBox
void translateOrigin(QPointF newOrigin)
translateOrigin For dominant baseline, we want to move the glyph origin. This encompassed the glyph,...
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.
QVector< QImage > images
QVector< QRectF > drawRects
QVector< bool > replaceWithForeGroundColor
QVector< QPainterPath > paths
QVector< QBrush > colors
QPainterPath path
qreal bottom
Bottom inset.
Definition KoInsets.h:50
qreal right
Right inset.
Definition KoInsets.h:52
qreal top
Top inset.
Definition KoInsets.h:49
qreal left
Left inset.
Definition KoInsets.h:51
The KoSvgTextContentElement struct.
void removeText(int &start, int length)
removeText removes text,
KoSvgTextProperties properties
The textProperties. This includes.
QString textPathId
The textpath's name, if any.
QString text
Plain text of the current node. Use insertText and removeText to manipulate it.
KoSvgText::TextOnPathInfo textPathInfo
Text path info for the text-on-path algorithm.
BulkActionState(QRectF originalBoundingRectArg)
boost::optional< qreal > yPos
Definition KoSvgText.h:606
boost::optional< qreal > dxPos
Definition KoSvgText.h:607
boost::optional< qreal > dyPos
Definition KoSvgText.h:608
boost::optional< qreal > rotate
Definition KoSvgText.h:609
boost::optional< qreal > xPos
Definition KoSvgText.h:605
The FontMetrics class A class to keep track of a variety of font metrics. Note that values are in Fre...
Definition KoSvgText.h:327
void scaleBaselines(const qreal multiplier)
The ResolutionHandler class.
Definition KoSvgText.h:1084
QPointF adjustCeil(const QPointF point) const
Adjusts the point to ceiled pixel values.
QPointF adjustFloor(const QPointF point) const
Adjusts the point to floored pixel values.
QPointF adjust(const QPointF point) const
Adjusts the point to rounded pixel values, based on whether roundToPixelHorizontal or roundToPixelVer...
StrokeProperty is a special wrapper around KoShapeStrokeModel for managing it in KoSvgTextProperties.
Definition KoSvgText.h:733
The LineBox struct.
LineBox(QVector< QLineF > lineWidths, bool ltr, QPointF indent, const KoSvgText::ResolutionHandler &resHandler)
void setCurrentChunk(LineChunk chunk)
qreal actualLineBottom
QPointF baselineTop
Used to identify the top of the line for baseline-alignment.
void setCurrentChunkForPos(QPointF pos, bool isHorizontal)
qreal actualLineTop
LineChunk chunk()
void clearAndAdjust(bool isHorizontal, QPointF current, QPointF indent)
QPointF textIndent
qreal expectedLineTop
Because fonts can affect lineheight mid-line, and this affects wrapping, this estimates the line-heig...
QVector< LineChunk > chunks
QPointF baselineBottom
Used to identify the bottom of the line for baseline-alignment.
LineBox(QPointF start, QPointF end, const KoSvgText::ResolutionHandler &resHandler)
QPointF conditionalHangEnd
QVector< int > chunkIndices
charResult indices that belong to this chunk.
QLineF length
Used to measure how long the current line is allowed to be.
KisForest< KoSvgTextContentElement >::child_iterator associatedLeaf
SubChunk(KisForest< KoSvgTextContentElement >::child_iterator leaf)
QSharedPointer< KoShapeBackground > bg
KoSvgTextProperties inheritedProps
QVector< QPair< int, int > > newToOldPositions
For transformed strings, we need to know which.
QString originalText