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
578 // Convenience function to ensure the bounds checking is in place.
580 return cursorPos.at(qBound(0, pos, cursorPos.size()-1));
581 }
582
584 bool isLoading = false;
585
586 bool disableFontMatching = false;
587
590
593
594 QString plainText;
595 bool isBidi = false;
596 QPointF initialTextPosition = QPointF();
597
598 void relayout();
599
600 static bool loadGlyph(const KoSvgText::ResolutionHandler &resHandler,
601 const FT_Int32 faceLoadFlags,
602 const bool isHorizontal,
603 const char32_t firstCodepoint,
604 const KoSvgText::TextRendering rendering,
605 raqm_glyph_t &currentGlyph,
606 CharacterResult &charResult,
607 QPointF &totalAdvanceFTFontCoordinates);
608
610 const bool isHorizontal,
611 raqm_glyph_t &currentGlyph);
612
613 static std::pair<QTransform, qreal> loadGlyphOnly(const QTransform &ftTF,
614 FT_Int32 faceLoadFlags,
615 bool isHorizontal,
616 raqm_glyph_t &currentGlyph,
617 CharacterResult &charResult, const KoSvgText::TextRendering rendering);
618
621 QString text, QVector<CharacterResult> &result, int &currentIndex,
622 bool isHorizontal, bool wrapped, bool textInPath, QVector<KoSvgText::CharTransformation> &resolved,
623 QVector<bool> collapsedChars, const KoSvgTextProperties resolvedProps, bool withControls = true);
624
625 void applyTextLength(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, QVector<CharacterResult> &result, int &currentIndex, int &resolvedDescendentNodes, bool isHorizontal,
626 const KoSvgTextProperties resolvedProps, const KoSvgText::ResolutionHandler &resHandler);
627 static void applyAnchoring(QVector<CharacterResult> &result, bool isHorizontal, const KoSvgText::ResolutionHandler resHandler);
628 static qreal anchoredChunkShift(const QVector<CharacterResult> &result, const bool isHorizontal, const int start, int &end);
629 static qreal
630 characterResultOnPath(CharacterResult &cr, qreal length, qreal offset, bool isHorizontal, bool isClosed);
631 static QPainterPath stretchGlyphOnPath(const QPainterPath &glyph,
632 const QPainterPath &path,
633 bool isHorizontal,
634 qreal offset,
635 bool isClosed);
636 static void applyTextPath(KisForest<KoSvgTextContentElement>::child_iterator parent, QVector<CharacterResult> &result, bool isHorizontal, QPointF &startPos, const KoSvgTextProperties resolvedProps, QList<KoShape*> textPaths);
638 const KoSvgText::FontMetrics &parentBaselineTable, const KoSvgText::Baseline parentBaseline,
640 int &currentIndex,
641 const KoSvgText::ResolutionHandler resHandler,
642 const bool isHorizontal,
643 const bool disableFontMatching);
645 QVector<CharacterResult> &result, const QVector<LineBox> lineBoxes,
646 int &currentIndex,
647 const bool isHorizontal, const KoSvgTextProperties resolvedProps);
649 const QVector<CharacterResult>& result,
650 const QMap<int, int>& logicalToVisual,
651 const KoSvgText::ResolutionHandler resHandler,
652 KoPathShape *textPath,
653 qreal textPathoffset,
654 bool side,
655 int &currentIndex,
656 bool isHorizontal,
657 bool ltr,
658 bool wrapping,
659 const KoSvgTextProperties resolvedProps, QList<KoShape*> textPaths);
660 QMap<KoSvgText::TextDecoration, QPainterPath> generateDecorationPaths(const int &start, const int &end,
661 const KoSvgText::ResolutionHandler resHandler,
662 const QVector<CharacterResult> &result,
663 const bool isHorizontal,
664 const KoSvgText::TextDecorations &decor,
666 const bool textDecorationSkipInset = false,
667 const KoPathShape *currentTextPath = nullptr,
668 const qreal currentTextPathOffset = 0.0,
669 const bool textPathSide = false,
672 static void finalizeDecoration (
673 QPainterPath decorationPath,
674 const QPointF offset,
675 const QPainterPathStroker &stroker,
676 const KoSvgText::TextDecoration type,
677 QMap<KoSvgText::TextDecoration, QPainterPath> &decorationPaths,
678 const KoPathShape *currentTextPath,
679 const bool isHorizontal,
680 const qreal currentTextPathOffset,
681 const bool textPathSide
682 );
683
684 void paintTextDecoration(QPainter &painter,
685 const QPainterPath &outlineRect,
686 const KoShape *rootShape,
687 const KoSvgText::TextDecoration type,
688 const KoSvgText::TextRendering rendering);
689 void paintPaths(QPainter &painter,
690 const QPainterPath &outlineRect,
691 const KoShape *rootShape,
692 const QVector<CharacterResult> &result,
693 const KoSvgText::TextRendering rendering,
694 QPainterPath &chunk,
695 int &currentIndex);
696 KoShape* collectPaths(const KoSvgTextShape *rootShape, QVector<CharacterResult> &result, int &currentIndex);
697 void paintDebug(QPainter &painter,
698 const QVector<CharacterResult> &result,
699 int &currentIndex);
700
703 KoSvgTextProperties props = parent->properties;
704 props.inheritFrom(resolvedProps, true);
705 int count = parent->numChars(withControls, props);
706 for (auto it = KisForestDetail::childBegin(parent); it != KisForestDetail::childEnd(parent); it++) {
707 count += numChars(it, withControls, props);
708 }
709 return count;
710 }
711
720
731 int &currentIndex,
732 int sought,
733 bool skipZeroWidth = false)
734 {
735 auto it = tree.depthFirstTailBegin();
736 for (; it != tree.depthFirstTailEnd(); it++) {
737 if (childCount(siblingCurrent(it)) > 0) {
738 continue;
739 }
740 int length = it->numChars(false);
741 if (length == 0 && skipZeroWidth) {
742 continue;
743 }
744
745 if (sought == currentIndex || (sought > currentIndex && sought < currentIndex + length)) {
746 break;
747 } else {
748 currentIndex += length;
749 }
750 }
751 return it;
752 }
762 static bool splitContentElement(KisForest<KoSvgTextContentElement> &tree, int index, bool allowEmptyText = true) {
763 int currentIndex = 0;
764
765 // If there's only a single root element, don't bother searching.
766 auto contentElement = depth(tree) == 1? tree.depthFirstTailBegin(): findTextContentElementForIndex(tree, currentIndex, index, true);
767 if (contentElement == tree.depthFirstTailEnd()) return false;
768
769 bool suitableStartIndex = siblingCurrent(contentElement) == tree.childBegin()? index >= currentIndex: index > currentIndex;
770 bool suitableEndIndex = siblingCurrent(contentElement) == tree.childBegin()? true: index < currentIndex + contentElement->numChars(false);
771
772 if (suitableStartIndex && suitableEndIndex) {
774 duplicate.text = contentElement->text;
775 int start = index - currentIndex;
776 int length = contentElement->numChars(false) - start;
777 int zero = 0;
778 duplicate.removeText(start, length);
779
780 if (!allowEmptyText && (duplicate.text.isEmpty() || length == 0)) {
781 return true;
782 }
783
784 // TODO: handle localtransforms better; annoyingly, this requires whitespace handling
785
786 if (siblingCurrent(contentElement) != tree.childBegin()
787 && contentElement->textPathId.isEmpty()
788 && contentElement->textLength.isAuto
789 && contentElement->localTransformations.isEmpty()) {
790 contentElement->removeText(zero, start);
791 duplicate.properties = contentElement->properties;
792 tree.insert(siblingCurrent(contentElement), duplicate);
793 } else {
795 duplicate2.text = contentElement->text;
796 duplicate2.removeText(zero, start);
797 contentElement->text.clear();
798 tree.insert(childBegin(contentElement), duplicate);
799 tree.insert(childEnd(contentElement), duplicate2);
800 }
801 return true;
802 }
803 return false;
804 }
805
813 static void splitTree(KisForest<KoSvgTextContentElement> &tree, int index, bool textPathAfterSplit) {
814 splitContentElement(tree, index, false);
815 int currentIndex = 0;
816 auto contentElement = depth(tree) == 1? tree.depthFirstTailBegin(): findTextContentElementForIndex(tree, currentIndex, index, true);
817
818 // We're either at the start or end.
819 if (contentElement == tree.depthFirstTailEnd()) return;
820 if (siblingCurrent(contentElement) == tree.childBegin()) return;
821
822 auto lastNode = siblingCurrent(contentElement);
823 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(contentElement));
824 parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(contentElement)); parentIt++) {
825 if (lastNode == siblingCurrent(parentIt)) continue;
826 if (siblingCurrent(parentIt) == tree.childBegin()) {
827 break;
828 }
829
830 if (lastNode != childBegin(siblingCurrent(parentIt))) {
832 duplicate.properties = parentIt->properties;
833 if (textPathAfterSplit) {
834 duplicate.textPathId = parentIt->textPathId;
835 duplicate.textPathInfo = parentIt->textPathInfo;
836 parentIt->textPathId = QString();
837 parentIt->textPathInfo = KoSvgText::TextOnPathInfo();
838 }
839 auto insert = siblingCurrent(parentIt);
840 insert ++;
841 auto it = tree.insert(insert, duplicate);
842
844 for (auto child = lastNode; child != childEnd(siblingCurrent(parentIt)); child++) {
845 movableChildren.append(child);
846 }
847 while(!movableChildren.isEmpty()) {
848 auto child = movableChildren.takeLast();
849 tree.move(child, childBegin(it));
850 }
851 lastNode = it;
852 } else {
853 lastNode = siblingCurrent(parentIt);
854 }
855 }
856 }
857
868 // An earlier version of the code used Hierarchy iterator,
869 // but that had too many exceptions when the child was not in the root.
870 if (!child.node()) return childEnd(root);
871 if (KisForestDetail::parent(child) == root) return child;
872 for (auto rootChild = childBegin(root); rootChild != childEnd(root); rootChild++) {
873 for (auto leaf = KisForestDetail::tailSubtreeBegin(rootChild);
874 leaf != KisForestDetail::tailSubtreeEnd(rootChild); leaf++) {
875 if (siblingCurrent(leaf) == child) {
876 return rootChild;
877 }
878 }
879 }
880 return childEnd(root);
881 }
882
891 static QVector<bool> collapsedWhiteSpacesForText(KisForest<KoSvgTextContentElement> &tree, QString &allText, const bool alsoCollapseLowSurrogate = false, bool includeBidiControls = false) {
892 QMap<int, KoSvgText::TextSpaceCollapse> collapseModes;
893
895 for (auto it = tree.compositionBegin(); it != tree.compositionEnd(); it++) {
896 if (it.state() == KisForestDetail::Enter) {
897 KoSvgTextProperties ownProperties = it->properties;
898 ownProperties.inheritFrom(parentProps.last());
899 parentProps.append(ownProperties);
900
901 const int children = childCount(siblingCurrent(it));
902 if (children == 0) {
903 QString text = it->text;
904 if (includeBidiControls) {
905 KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(parentProps.last().propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt());
906 KoSvgText::Direction direction = KoSvgText::Direction(parentProps.last().propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
907 QVector<QPair<int, int>> positions;
908 QString text = KoCssTextUtils::getBidiOpening(direction, bidi);
909 text += it->getTransformedString(positions, parentProps.last());
910 text += KoCssTextUtils::getBidiClosing(bidi);
911 }
912 KoSvgText::TextSpaceCollapse collapse = KoSvgText::TextSpaceCollapse(parentProps.last().propertyOrDefault(KoSvgTextProperties::TextCollapseId).toInt());
913 collapseModes.insert(allText.size(), collapse);
914 allText += text;
915 }
916 } else {
917 parentProps.pop_back();
918 }
919 }
920 QVector<bool> collapsed = KoCssTextUtils::collapseSpaces(&allText, collapseModes);
921
922 if (alsoCollapseLowSurrogate) {
923 for (int i = 0; i < allText.size(); i++) {
924 if (i > 0 && allText.at(i).isLowSurrogate() && allText.at(i-1).isHighSurrogate()) {
925 collapsed[i] = true;
926 }
927 }
928 }
929 return collapsed;
930 }
931
943 static void removeTransforms(KisForest<KoSvgTextContentElement> &tree, const int start, const int length) {
944 QString all;
945 QVector<bool> collapsedCharacters = collapsedWhiteSpacesForText(tree, all, true);
946
947 auto root = tree.childBegin();
948 removeTransformsImpl(root, 0, start, length, collapsedCharacters);
949 }
950
956 static int removeTransformsImpl(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector<bool> collapsedCharacters) {
957 int currentLength = 0;
958 auto it = childBegin(currentTextElement);
959 if (it != childEnd(currentTextElement)) {
960 for (; it != childEnd(currentTextElement); it++) {
961 currentLength += removeTransformsImpl(it, globalIndex + currentLength, start, length, collapsedCharacters);
962 }
963 } else {
964 currentLength = currentTextElement->text.size();
965 }
966
967 if (!currentTextElement->localTransformations.isEmpty()) {
968 int transformOffset = 0;
969 int transformOffsetEnd = 0;
970
971 for (int i = globalIndex; i < globalIndex + currentLength; i++) {
972 if (i >= collapsedCharacters.size()) break;
973 if (i < start) {
974 transformOffset += collapsedCharacters.at(i)? 0: 1;
975 }
976 if (i < start + length) {
977 transformOffsetEnd += collapsedCharacters.at(i)? 0: 1;
978 } else {
979 break;
980 }
981 }
982 if (transformOffset < currentTextElement->localTransformations.size()) {
983 int length = qBound(0, transformOffsetEnd-transformOffset, qMax(0, currentTextElement->localTransformations.size()-transformOffset));
984 currentTextElement->localTransformations.remove(transformOffset,
985 length);
986 }
987
988 }
989 return currentLength;
990 }
991
1001 static void insertTransforms(KisForest<KoSvgTextContentElement> &tree, const int start, const int length, const bool allowSkipFirst) {
1002 QString all;
1003 QVector<bool> collapsedCharacters = collapsedWhiteSpacesForText(tree, all, true);
1004
1005 auto root = tree.childBegin();
1006 insertTransformsImpl(root, 0, start, length, collapsedCharacters, allowSkipFirst);
1007 }
1013 static int insertTransformsImpl(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, const int globalIndex, const int start, const int length, const QVector<bool> collapsedCharacters, const bool allowSkipFirst) {
1014 int currentLength = 0;
1015 auto it = childBegin(currentTextElement);
1016 if (it != childEnd(currentTextElement)) {
1017 for (; it != childEnd(currentTextElement); it++) {
1018 currentLength += insertTransformsImpl(it, globalIndex + currentLength, start, length, collapsedCharacters, allowSkipFirst);
1019 }
1020 } else {
1021 currentLength = currentTextElement->text.size();
1022 }
1023
1024 if (!currentTextElement->localTransformations.isEmpty()) {
1025 int transformOffset = 0;
1026 int transformOffsetEnd = 0;
1027
1028 for (int i = globalIndex; i < globalIndex + currentLength; i++) {
1029 if (i < start) {
1030 transformOffset += collapsedCharacters.at(i)? 0: 1;
1031 }
1032 if (i < start + length) {
1033 transformOffsetEnd += collapsedCharacters.at(i)? 0: 1;
1034 } else {
1035 break;
1036 }
1037 }
1038
1039 // When at the start, skip the first transform.
1040 if (transformOffset == 0 && allowSkipFirst && currentTextElement->localTransformations.at(0).startsNewChunk()) {
1041 transformOffset += 1;
1042 }
1043
1044 if (transformOffset < currentTextElement->localTransformations.size()) {
1045 for (int i = transformOffset; i < transformOffsetEnd; i++) {
1046 currentTextElement->localTransformations.insert(i, KoSvgText::CharTransformation());
1047 }
1048 }
1049
1050 }
1051 return currentLength;
1052 }
1053
1064 void applyWhiteSpace(KisForest<KoSvgTextContentElement> &tree, const bool convertToPreWrapped = false) {
1065 QString allText;
1066 QVector<bool> collapsed = collapsedWhiteSpacesForText(tree, allText, false);
1067
1068 auto end = std::make_reverse_iterator(tree.childBegin());
1069 auto begin = std::make_reverse_iterator(tree.childEnd());
1070
1071 for (; begin != end; begin++) {
1072 applyWhiteSpaceImpl(begin, collapsed, allText, convertToPreWrapped);
1073 }
1074 if (convertToPreWrapped) {
1075 tree.childBegin()->properties.setProperty(KoSvgTextProperties::TextCollapseId, QVariant::fromValue(KoSvgText::Preserve));
1076 tree.childBegin()->properties.setProperty(KoSvgTextProperties::TextWrapId, QVariant::fromValue(KoSvgText::Wrap));
1077 }
1078 }
1079
1080 void applyWhiteSpaceImpl(std::reverse_iterator<KisForest<KoSvgTextContentElement>::child_iterator> current, QVector<bool> &collapsed, QString &allText, const bool convertToPreWrapped) {
1081 auto base = current.base();
1082 // It seems that .base() refers to the next entry instead of the pointer,
1083 // which is coherent with the template implementation of "operator*" here:
1084 // https://en.cppreference.com/w/cpp/iterator/reverse_iterator.html
1085 base--;
1086 if(base != siblingEnd(base)) {
1087 auto end = std::make_reverse_iterator(childBegin(base));
1088 auto begin = std::make_reverse_iterator(childEnd(base));
1089 for (; begin != end; begin++) {
1090 applyWhiteSpaceImpl(begin, collapsed, allText, convertToPreWrapped);
1091 }
1092 }
1093
1094 if (!current->text.isEmpty()) {
1095 const int total = current->text.size();
1096 QString currentText = allText.right(total);
1097
1098 for (int i = 0; i < total; i++) {
1099 const int j = total - (i+1);
1100 const bool col = collapsed.takeLast();
1101 if (col) {
1102 currentText.remove(j, 1);
1103 }
1104
1105 }
1106 current->text = currentText;
1107 allText.chop(total);
1108 }
1109 if (convertToPreWrapped) {
1110 current->properties.removeProperty(KoSvgTextProperties::TextCollapseId);
1111 current->properties.removeProperty(KoSvgTextProperties::TextWrapId);
1112 }
1113 }
1114
1115 static QVector<KoSvgText::CharTransformation> resolvedTransformsForTree(KisForest<KoSvgTextContentElement> &tree, bool shapesInside = false, bool includeBidiControls = false) {
1116 QString all;
1117 QVector<bool> collapsed = collapsedWhiteSpacesForText(tree, all, false, includeBidiControls);
1118 QVector<CharacterResult> result(all.size());
1119 int globalIndex = 0;
1120 KoSvgTextProperties props = tree.childBegin()->properties;
1124 bool isHorizontal = mode == KoSvgText::HorizontalTB;
1125 bool isWrapped = (props.hasProperty(KoSvgTextProperties::InlineSizeId)
1126 || shapesInside);
1127 QVector<KoSvgText::CharTransformation> resolvedTransforms(all.size());
1128 resolveTransforms(tree.childBegin(), all, result, globalIndex,
1129 isHorizontal, isWrapped, false, resolvedTransforms,
1131 false);
1132 return resolvedTransforms;
1133 }
1134
1142 static void insertNewLinesAtAnchors(KisForest<KoSvgTextContentElement> &tree, bool shapesInside = false) {
1143 QVector<KoSvgText::CharTransformation> resolvedTransforms = resolvedTransformsForTree(tree, shapesInside);
1144
1145 auto end = std::make_reverse_iterator(tree.childBegin());
1146 auto begin = std::make_reverse_iterator(tree.childEnd());
1147
1148 bool inTextPath = false;
1149 for (; begin != end; begin++) {
1150 insertNewLinesAtAnchorsImpl(begin, resolvedTransforms, inTextPath);
1151 }
1152 }
1153
1155 QVector<KoSvgText::CharTransformation> &resolvedTransforms,
1156 bool &inTextPath) {
1157
1158 inTextPath = (!current->textPathId.isEmpty());
1159 auto base = current.base();
1160 base--;
1161 if(base != siblingEnd(base)) {
1162 auto end = std::make_reverse_iterator(childBegin(base));
1163 auto begin = std::make_reverse_iterator(childEnd(base));
1164 for (; begin != end; begin++) {
1165 insertNewLinesAtAnchorsImpl(begin,
1166 resolvedTransforms,
1167 inTextPath);
1168 }
1169 }
1170
1171 if (!current->text.isEmpty()) {
1172 const int total = current->text.size();
1173
1174 for (int i = 0; i < total; i++) {
1175 const int j = total - (i+1);
1176 KoSvgText::CharTransformation transform = resolvedTransforms.takeLast();
1183 bool startsNewChunk = transform.startsNewChunk() && j == 0;
1184
1185 if (inTextPath) {
1186 // First transform in path is always absolute, so we don't insert a newline.
1187 startsNewChunk = false;
1188 inTextPath = false;
1189 }
1190 // 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.
1191 if (startsNewChunk && !resolvedTransforms.isEmpty() && current->text.at(j) != QChar::LineFeed) {
1192 current->text.insert(j, "\n");
1193 }
1194 }
1195 }
1196 current->localTransformations.clear();
1197 }
1198
1200 for (int i = 0; i< layout.size(); i++) {
1201 //split all anchored chunks, so we can set transforms on them.
1202 if (layout.at(i).anchored_chunk) {
1203 int plainTextIndex = layout.at(i).plaintTextIndex;
1204 splitContentElement(tree, plainTextIndex);
1205 }
1206 }
1207
1208 int globalIndex = 0;
1209 for (auto it = tree.childBegin(); it != tree.childEnd(); it++) {
1211 it->properties.propertyOrDefault(KoSvgTextProperties::WritingModeId).toInt());
1212 bool isHorizontal = mode == KoSvgText::HorizontalTB;
1213 setTransformsFromLayoutImpl(it, KoSvgTextProperties::defaultProperties(), layout, globalIndex, isHorizontal);
1214 }
1215 }
1216
1218 const KoSvgTextProperties parentProps, const QVector<CharacterResult> layout,
1219 int &globalIndex, bool isHorizontal) {
1220 KoSvgTextProperties props = current->properties;
1221 props.inheritFrom(parentProps);
1222 if (!current->textPathId.isEmpty()) return; // When we're doing text-on-path, we're already in preformatted mode.
1223 for (auto it = childBegin(current); it!= childEnd(current); it++) {
1224 setTransformsFromLayoutImpl(it, props, layout, globalIndex, isHorizontal);
1225 }
1226
1227 if (current->text.isEmpty()) {
1228 current->localTransformations.clear();
1229 } else {
1231 const int length = current->numChars(true, props);
1232
1233 for (int i = globalIndex; i< globalIndex+length; i++) {
1234 CharacterResult result = layout.value(i);
1235
1236 if (!result.addressable) {
1237 continue;
1238 }
1240
1241 //TODO: Also split up content element if multiple anchored chunks.
1242 if (result.anchored_chunk) {
1243 int endIndex = 0;
1244 qreal shift = anchoredChunkShift(layout, isHorizontal, i, endIndex);
1245 QPointF offset = isHorizontal? QPointF(shift, 0): QPointF(0, shift);
1246 transform.xPos = result.finalPosition.x() - offset.x();
1247 transform.yPos = result.finalPosition.y() - offset.y();
1248 } else if (i > 0) {
1249 CharacterResult resultPrev = layout.value(i-1);
1250 QPointF offset = (result.finalPosition - result.cssPosition) - (resultPrev.finalPosition - resultPrev.cssPosition);
1251
1252 transform.dxPos = offset.x();
1253 transform.dyPos = offset.y();
1254 }
1255 transform.rotate = result.rotate;
1256
1257 transforms.append(transform);
1258 }
1259 current->localTransformations = transforms;
1260 current->text = current->text.split("\n").join(" ");
1261 globalIndex += length;
1262 }
1263 }
1264
1274 const int children = childCount(current);
1275 if (children == 0) {
1276 const int length = current->numChars(false);
1277 if (length == 0 && current != tree.childBegin() && current->textPathId.isEmpty()) {
1278 return true;
1279 } else {
1280 // check if siblings are similar.
1281 auto siblingPrev = current;
1283 QVariant(KoSvgText::BidiNormal)).toInt());
1284 siblingPrev--;
1285 while (!isEnd(siblingPrev) && siblingPrev->text.isEmpty()) {
1286 // By checking whether the text is empty, we can skip previous
1287 // siblings that are already marked for deletion.
1288 siblingPrev--;
1289 }
1290 if (!isEnd(siblingPrev)
1291 && siblingPrev != current
1292 && (siblingPrev->localTransformations.isEmpty() && current->localTransformations.isEmpty())
1293 && (siblingPrev->textPathId.isEmpty() && current->textPathId.isEmpty())
1294 && (siblingPrev->textLength.isAuto && current->textLength.isAuto)
1295 && (siblingPrev->properties == current->properties)
1297 && childCount(siblingPrev) == 0) {
1298 // TODO: handle localtransforms better; annoyingly, this requires whitespace handling
1299 siblingPrev->text += current->text;
1300 current->text = QString();
1301 return true;
1302 }
1303 }
1304 } else if (children == 1) {
1305 // merge single children into parents if possible.
1306 auto child = childBegin(current);
1307 if ((child->localTransformations.isEmpty() && current->localTransformations.isEmpty())
1308 && (child->textPathId.isEmpty() && current->textPathId.isEmpty())
1309 && (child->textLength.isAuto && current->textLength.isAuto)
1310 && (!child->properties.hasNonInheritableProperties() || !current->properties.hasNonInheritableProperties())) {
1311 if (current->properties.hasNonInheritableProperties()) {
1312 KoSvgTextProperties props = current->properties;
1313 props.setAllButNonInheritableProperties(child->properties);
1314 child->properties = props;
1315 } else {
1316 child->properties.inheritFrom(current->properties);
1317 }
1318 *current = *child;
1319 tree.erase(child);
1320 }
1321 } else {
1323 for (auto child = childBegin(current); child != childEnd(current); child++) {
1324 if (cleanUpImpl(tree, child)) {
1325 deleteList.append(child);
1326 }
1327 }
1328 while (!deleteList.isEmpty()) {
1329 auto child = deleteList.takeFirst();
1330 KIS_ASSERT_RECOVER_NOOP(childBegin(child) == childEnd(child));
1331 tree.erase(child);
1332 }
1333 if (childCount(current) <= 1) {
1334 // This checks if, when there's only 0 or 1 children, this node can be cleaned up as well.
1335 return cleanUpImpl(tree, current);
1336 }
1337 }
1338 return false;
1339 }
1340
1350 if (tree.childBegin() != tree.childEnd()) {
1351 cleanUpImpl(tree, tree.childBegin());
1352 }
1353 }
1354
1356 if (treeIndex.isEmpty()) return parent;
1357 QVector<int> idx = treeIndex;
1358 int count = idx.takeFirst();
1359 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1360 if (count == 0) {
1362 return iteratorForTreeIndex(idx, child);
1363 } else {
1364 return child;
1365 }
1366 }
1367 count -= 1;
1368 }
1369 return KisForestDetail::childEnd(parent);
1370 }
1371
1373 for (auto child = KisForestDetail::childBegin(parent); child != KisForestDetail::childEnd(parent); child++) {
1374 if (child == target) {
1375 return true;
1376 } else if ((KisForestDetail::childBegin(child) != KisForestDetail::childEnd(child))) {
1377 if (startIndexOfIterator(child, target, currentIndex)) {
1378 return true;
1379 }
1380 } else {
1381 currentIndex += numChars(child);
1382 }
1383 }
1384 return false;
1385 }
1386
1388
1394 for (auto it = childBegin(parent); it != childEnd(parent); it++) {
1395 if (it->textPathId == name) {
1396 it->textPathId = QString();
1397 break;
1398 }
1399 }
1400 }
1401
1402 static void makeTextPathNameUnique(QList<KoShape*> textPaths, KoShape *textPath) {
1403 bool textPathNameUnique = false;
1404 int textPathNumber = textPaths.size();
1405 QString newTextPathName = textPath->name();
1406 while(!textPathNameUnique) {
1407 textPathNameUnique = true;
1408 Q_FOREACH(KoShape *shape, textPaths) {
1409 if (shape->name() == newTextPathName) {
1410 textPathNameUnique = false;
1411 textPathNumber += 1;
1412 break;
1413 }
1414 }
1415 if (textPathNameUnique && !newTextPathName.isEmpty()) {
1416 textPath->setName(newTextPathName);
1417 } else {
1418 textPathNameUnique = false;
1419 }
1420 newTextPathName = QString("textPath"+QString::number(textPathNumber));
1421 }
1422 }
1423
1433 static QRectF boundingBoxFromTree(KisForest<KoSvgTextContentElement> tree, const KoSvgTextShape *rootShape, bool includeStrokeInset) {
1434 QRectF result;
1435 QList<KoShapeStrokeModelSP> parentStrokes;
1436 for (auto it = tree.compositionBegin(); it != tree.compositionEnd(); it++) {
1437 if (it.state() == KisForestDetail::Enter) {
1438 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
1439 parentStrokes.append(it->properties.property(KoSvgTextProperties::StrokeId).value<KoSvgText::StrokeProperty>().property);
1440 }
1441 } else {
1442 KoShapeStrokeModelSP stroke = parentStrokes.size() > 0? parentStrokes.last(): nullptr;
1443 QRectF bb = it->associatedOutline.boundingRect();
1444 QMap<KoSvgText::TextDecoration, QPainterPath> decorations = it->textDecorations;
1445 for (int i = 0; i < decorations.values().size(); ++i) {
1446 bb |= decorations.values().at(i).boundingRect();
1447 }
1448 if (!bb.isEmpty()) {
1449 if (stroke && includeStrokeInset) {
1450 KoInsets insets;
1451 stroke->strokeInsets(rootShape, insets);
1452 result |= bb.adjusted(-insets.left, -insets.top, insets.right, insets.bottom);
1453 } else {
1454 result |= bb;
1455 }
1456 }
1457 if (it->properties.hasProperty(KoSvgTextProperties::StrokeId)) {
1458 // reset stroke to use parent stroke.
1459 parentStrokes.pop_back();
1460 }
1461 }
1462 }
1463 return result;
1464 }
1465};
1466
1467#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)
CursorPos getCursorPos(int pos)
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