Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextShapeLayoutFunc_inShape.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
3 * SPDX-FileCopyrightText: 2022 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9
10#include "KoPolygonUtils.h"
11#include "KoSvgTextProperties.h"
12
13#include <KoClipMaskPainter.h>
14#include <KoColorBackground.h>
15#include <KoPathShape.h>
16#include <KoShapeStroke.h>
17
18#include <kis_global.h>
19#include <kis_algebra_2d.h>
20
21#include <QPainter>
22#include <QtMath>
23
25
27getShapes(QList<KoShape *> shapesInside, QList<KoShape *> shapesSubtract, const KoSvgTextProperties &properties)
28{
29 // the boost polygon method requires (and gives best result) on a inter-based polygon,
30 // so we need to scale up. The scale selected here is the size freetype coordinates give to a single pixel.
31 qreal scale = 64.0;
32 QTransform precisionTF = QTransform::fromScale(scale, scale);
33
34 KoSvgTextProperties resolved = properties;
36
39
40 QPainterPath subtract;
41 Q_FOREACH(const KoShape *shape, shapesSubtract) {
42 const KoPathShape *path = dynamic_cast<const KoPathShape*>(shape);
43 if (path) {
44 QPainterPath p = path->transformation().map(path->outline());
45 p.setFillRule(path->fillRule());
46 // grow each polygon here with the shape margin size.
47 if (shapeMargin > 0) {
48 QList<QPolygon> subpathPolygons;
49 Q_FOREACH(QPolygonF subPath, p.toSubpathPolygons()) {
50 subpathPolygons.append(precisionTF.map(subPath).toPolygon());
51 }
52 subpathPolygons = KoPolygonUtils::offsetPolygons(subpathPolygons, shapeMargin);
53 p.clear();
54 Q_FOREACH (const QPolygon poly, subpathPolygons) {
55 p.addPolygon(poly);
56 }
57 } else {
58 p = precisionTF.map(p);
59 }
60
61 subtract.addPath(p);
62 }
63 }
64
66 Q_FOREACH(const KoShape *shape, shapesInside) {
67 const KoPathShape *path = dynamic_cast<const KoPathShape*>(shape);
68 if (path) {
69 QPainterPath p = path->transformation().map(path->outline());
70 p.setFillRule(path->fillRule());
71 QPainterPath p2;
72 p2.setFillRule(path->fillRule());
73
74 QList<QPolygon> subpathPolygons;
75 Q_FOREACH(QPolygonF subPath, p.toSubpathPolygons()) {
76 subpathPolygons.append(precisionTF.map(subPath).toPolygon());
77 }
78 subpathPolygons = KoPolygonUtils::offsetPolygons(subpathPolygons, -shapePadding);
79
80 for (int i=0; i < subpathPolygons.size(); i++) {
81 QPolygonF subpathPoly = subpathPolygons.at(i);
82 Q_FOREACH(QPolygonF subtractPoly, subtract.toSubpathPolygons()) {
83 if (subpathPoly.intersects(subtractPoly)) {
84 subpathPoly = subpathPoly.subtracted(subtractPoly);
85 }
86 }
87 p2.addPolygon(subpathPoly);
88 }
89 p2 = KisAlgebra2D::trySimplifyPath(precisionTF.inverted().map(p2), 1.0);
90 p2.closeSubpath();
91 shapes.append(p2);
92 }
93 }
94 return shapes;
95}
96
97bool pastTerminator(const QPointF point, const QPointF terminator, const KoSvgText::WritingMode writingMode) {
98 if (writingMode == KoSvgText::HorizontalTB) {
99 return (terminator.y() - point.y() < SHAPE_PRECISION);
100 } else if (writingMode == KoSvgText::VerticalRL) {
101 return (point.x() - terminator.x() < SHAPE_PRECISION);
102 } else {
103 return (terminator.x() - point.x() < SHAPE_PRECISION);
104 }
105 return false;
106}
107
108static bool getFirstPosition(QPointF &firstPoint,
109 QPainterPath p,
110 QRectF wordBox,
111 QPointF terminator,
112 KoSvgText::WritingMode writingMode,
113 bool ltr)
114{
115 if (wordBox.isEmpty()) {
116 // line-height: 0 will give us empty rects, make them slightly tall/wide
117 // to avoid issues with complex QRectF operations.
118 if (writingMode == KoSvgText::HorizontalTB) {
119 wordBox.setHeight(1e-3);
120 } else {
121 wordBox.setWidth(1e-3);
122 }
123 }
124
127
128 QVector<QPointF> candidatePositions;
129 QRectF word = wordBox.normalized();
130 word.translate(-wordBox.topLeft());
131 // Slightly shrink the box to account for precision error.
133
134 QPointF terminatorAdjusted = terminator;
135 Q_FOREACH(const QPolygonF polygon, p.toFillPolygons()) {
136 QVector<QLineF> offsetPoly;
137 for(int i = 0; i < polygon.size()-1; i++) {
138 QLineF line;
139 line.setP1(polygon.at(i));
140 line.setP2(polygon.at(i+1));
141 if (!(pastTerminator(line.p1(), terminatorAdjusted, writingMode)
142 || pastTerminator(line.p2(), terminatorAdjusted, writingMode))) continue;
143
144 if (line.angle() == 0.0 || line.angle() == 180.0) {
145 qreal offset = word.center().y();
146 offsetPoly.append(line.translated(0, offset));
147 offsetPoly.append(line.translated(0, -offset));
148 } else if (line.angle() == 90.0 || line.angle() == 270.0) {
149 qreal offset = word.center().x();
150 offsetPoly.append(line.translated(offset, 0));
151 offsetPoly.append(line.translated(-offset, 0));
152 } else {
153 qreal tAngle = fmod(line.angle(), 180.0);
154 QPointF cPos = tAngle > 90? line.center() + QPointF(-word.center().x(), word.center().y()): line.center() + word.center();
155 qreal offset = kisDistanceToLine(cPos, line);
156 const QPointF vectorT(qCos(qDegreesToRadians(tAngle)), -qSin(qDegreesToRadians(tAngle)));
157 QPointF vectorN(-vectorT.y(), vectorT.x());
158 QPointF offsetP = QPointF() - (0.0 * vectorT) + (offset * vectorN);
159 offsetPoly.append(line.translated(offsetP));
160 offsetPoly.append(line.translated(-offsetP));
161 }
162 }
163 if (writingMode == KoSvgText::HorizontalTB) {
164 terminatorAdjusted = terminator + word.center();
165 QLineF top(polygon.boundingRect().topLeft(), polygon.boundingRect().topRight());
166 offsetPoly.append(top.translated(0, terminatorAdjusted.y()));
167 } else if (writingMode == KoSvgText::VerticalRL) {
168 terminatorAdjusted = terminator - word.center();
169 QLineF top(terminatorAdjusted.x(), polygon.boundingRect().top(),
170 terminatorAdjusted.x(), polygon.boundingRect().bottom());
171 offsetPoly.append(top);
172 } else{
173 terminatorAdjusted = terminator + word.center();
174 QLineF top(terminatorAdjusted.x(), polygon.boundingRect().top(),
175 terminatorAdjusted.x(), polygon.boundingRect().bottom());
176 offsetPoly.append(top);
177 }
178 for (int i=0; i < offsetPoly.size(); i++) {
179 QLineF line = offsetPoly.at(i);
180 for (int j=i; j< offsetPoly.size(); j++){
181 QLineF line2 = offsetPoly.at(j);
182 QPointF intersectPoint;
183 QLineF::IntersectType intersect = line.intersects(line2, &intersectPoint);
184 if (intersect != QLineF::NoIntersection) {
185 // should proly handle 'reflex' vertices better.
186 if (!p.contains(intersectPoint)) {
187 continue;
188 }
189 if(!p.contains(word.translated(intersectPoint-word.center()))) {
190 continue;
191 }
192 if (!candidatePositions.contains(intersectPoint)) {
193 candidatePositions.append(intersectPoint);
194 }
195 }
196 }
197 }
198 }
199 if (candidatePositions.isEmpty()) {
200
201 return false;
202 }
203 QPointF firstPointC = writingMode == KoSvgText::VerticalRL? p.boundingRect().bottomLeft(): p.boundingRect().bottomRight();
204 Q_FOREACH(const QPointF candidate, candidatePositions) {
205 if (writingMode == KoSvgText::HorizontalTB) {
206 if (terminatorAdjusted.y() - candidate.y() < SHAPE_PRECISION) {
207
208 if (firstPointC.y() - candidate.y() > SHAPE_PRECISION) {
209 firstPointC = candidate;
210 } else if (firstPointC.y() - candidate.y() > -SHAPE_PRECISION) {
211 if (ltr) {
212 if (candidate.x() < firstPointC.x()) {
213 firstPointC = candidate;
214 }
215 } else {
216 if (candidate.x() > firstPointC.x()) {
217 firstPointC = candidate;
218 }
219 }
220 }
221 }
222 } else if (writingMode == KoSvgText::VerticalRL) {
223 if (terminatorAdjusted.x() - candidate.x() >= -SHAPE_PRECISION) {
224
225 if (firstPointC.x() - candidate.x() < -SHAPE_PRECISION) {
226 firstPointC = candidate;
227 } else if (firstPointC.x() - candidate.x() < SHAPE_PRECISION) {
228 if (ltr) {
229 if (candidate.y() < firstPointC.y()) {
230 firstPointC = candidate;
231 }
232 } else {
233 if (candidate.y() > firstPointC.y()) {
234 firstPointC = candidate;
235 }
236 }
237 }
238 }
239 } else {
240 if (terminatorAdjusted.x() - candidate.x() < SHAPE_PRECISION) {
241
242 if (firstPointC.x() - candidate.x() > SHAPE_PRECISION) {
243 firstPointC = candidate;
244 } else if (firstPointC.x() - candidate.x() > -SHAPE_PRECISION) {
245 if (ltr) {
246 if (candidate.y() < firstPointC.y()) {
247 firstPointC = candidate;
248 }
249 } else {
250 if (candidate.y() > firstPointC.y()) {
251 firstPointC = candidate;
252 }
253 }
254 }
255 }
256 }
257 }
258 if (!p.contains(firstPointC)) {
259 return false;
260 }
261 firstPointC -= word.center();
262 firstPointC -= wordBox.topLeft();
263 firstPoint = firstPointC;
264
265 return true;
266}
267
268static bool pointLessThan(const QPointF &a, const QPointF &b)
269{
270 return a.x() < b.x();
271}
272
273static bool pointLessThanVertical(const QPointF &a, const QPointF &b)
274{
275 return a.y() < b.y();
276}
277
278static QVector<QLineF>
279findLineBoxesForFirstPos(QPainterPath shape, QPointF firstPos, const QRectF wordBox, KoSvgText::WritingMode writingMode)
280{
281 QVector<QLineF> lines;
282
283 QLineF baseLine;
284 QPointF lineTop;
285 QPointF lineBottom;
286 QRectF word = wordBox.normalized();
288
289 if (writingMode == KoSvgText::HorizontalTB) {
290 baseLine = QLineF(shape.boundingRect().left()-5, firstPos.y(), shape.boundingRect().right()+5, firstPos.y());
291 lineTop = QPointF(0, word.top());
292 lineBottom = QPointF(0, word.bottom());
293 } else {
294 baseLine = QLineF(firstPos.x(), shape.boundingRect().top()-5, firstPos.x(), shape.boundingRect().bottom()+5);
295 if (writingMode == KoSvgText::VerticalRL) {
296 lineTop = QPointF(word.left(), 0);
297 lineBottom = QPointF(word.right(), 0);
298 } else {
299 lineTop = QPointF(word.right(), 0);
300 lineBottom = QPointF(word.left(), 0);
301 }
302 }
303
304 QPolygonF polygon = shape.toFillPolygon();
305 QList<QPointF> intersects;
306 QLineF topLine = baseLine.translated(lineTop);
307 QLineF bottomLine = baseLine.translated(lineBottom);
308 for(int i = 0; i < polygon.size()-1; i++) {
309 QLineF line(polygon.at(i), polygon.at(i+1));
310 bool addedA = false;
311 QPointF intersectA;
312 QPointF intersectB;
313 QPointF intersect;
314 if (topLine.intersects(line, &intersect) == QLineF::BoundedIntersection) {
315 intersectA = intersect-lineTop;
316 intersects.append(intersectA);
317 addedA = true;
318 }
319 if (bottomLine.intersects(line, &intersect) == QLineF::BoundedIntersection) {
320 intersectB = intersect-lineBottom;
321 if (intersectA != intersectB || !addedA) {
322 intersects.append(intersectB);
323 }
324 }
325 }
326 if (!intersects.isEmpty()) {
327 intersects.append(baseLine.p1());
328 intersects.append(baseLine.p2());
329 }
330 if (writingMode == KoSvgText::HorizontalTB) {
331 std::sort(intersects.begin(), intersects.end(), pointLessThan);
332 } else {
333 std::sort(intersects.begin(), intersects.end(), pointLessThanVertical);
334 }
335
336
337 for (int i = 0; i< intersects.size()-1; i++) {
338 QLineF line(intersects.at(i), intersects.at(i+1));
339
340 if (!(shape.contains(line.translated(lineTop).center())
341 && shape.contains(line.translated(lineBottom).center()))
342 || line.length() == 0) {
343 continue;
344 }
345
346 QRectF lineBox = QRectF(line.p1() + lineTop, line.p2() + lineBottom).normalized();
347
348 QVector<QPointF> relevant;
349 for(int i = 0; i < polygon.size()-1; i++) {
350
351 if (lineBox.contains(polygon.at(i))) {
352 relevant.append(polygon.at(i));
353 }
354 }
355 qreal start = writingMode == KoSvgText::HorizontalTB? lineBox.left(): lineBox.top();
356 qreal end = writingMode == KoSvgText::HorizontalTB? lineBox.right(): lineBox.bottom();
357 for(int j = 0; j < relevant.size(); j++) {
358 QPointF current = relevant.at(j);
359
360 if (writingMode == KoSvgText::HorizontalTB) {
361 if (current.x() < line.center().x()) {
362 start = qMax(current.x(), start);
363 } else if (current.x() > line.center().x()) {
364 end = qMin(current.x(), end);
365 }
366 } else {
367 if (current.y() < line.center().y()) {
368 start = qMax(current.y(), start);
369 } else if (current.y() > line.center().y()) {
370 end = qMin(current.y(), end);
371 }
372 }
373 }
374 if (writingMode == KoSvgText::HorizontalTB) {
375
376 QLineF newLine(start, line.p1().y(), end, line.p2().y());
377 if (!lines.isEmpty()) {
378 if (lines.last().p2() == intersects.at(i)) {
379 newLine.setP1(lines.last().p1());
380 lines.removeLast();
381 }
382 }
383 lines.append(newLine);
384 } else {
385 QLineF newLine(line.p1().x(), start, line.p2().x(), end);
386 if (!lines.isEmpty()) {
387 if (lines.last().p2() == intersects.at(i)) {
388 newLine.setP1(lines.last().p1());
389 lines.removeLast();
390 }
391 }
392 lines.append(newLine);
393 }
394 }
395
396 return lines;
397}
398
405 const int index,
406 QRectF &wordBox,
407 const QRectF boundingBox,
408 KoSvgText::WritingMode writingMode)
409{
410 bool isHorizontal = writingMode == KoSvgText::HorizontalTB;
411 QPointF totalAdvance = wordBox.bottomRight() - wordBox.topLeft();
412 qreal maxAscent = isHorizontal? wordBox.top(): wordBox.right();
413 qreal maxDescent = isHorizontal? wordBox.bottom(): wordBox.left();
414
415 for (int i=index; i<result.size(); i++) {
416 if (!result.at(i).addressable || result.at(i).hidden) {
417 continue;
418 }
419 totalAdvance += result.at(i).advance;
420 if ((totalAdvance.x() > boundingBox.width() && isHorizontal) ||
421 (totalAdvance.y() > boundingBox.height() && !isHorizontal)) {
422 break;
423 }
424 calculateLineHeight(result.at(i), maxAscent, maxDescent, isHorizontal, true);
425 }
426 if (writingMode == KoSvgText::HorizontalTB) {
427 wordBox.setTop(maxAscent);
428 wordBox.setBottom(maxDescent);
429 } else {
430 // vertical lr has top at the right even though block flow is also to the right.
431 wordBox.setRight(maxAscent);
432 wordBox.setLeft(maxDescent);
433 }
434}
435
438{
439 KoSvgText::TextAlign compare = align;
440 if (align == KoSvgText::AlignJustify) {
441 compare = alignLast;
442 }
443 if (compare == KoSvgText::AlignStart) {
445 } else if (compare == KoSvgText::AlignCenter) {
447 } else if (compare == KoSvgText::AlignEnd) {
449 } else if (compare == KoSvgText::AlignLeft) {
451 } else if (compare == KoSvgText::AlignRight) {
453 } else if (align == KoSvgText::AlignJustify) {
455 }
457}
458
460 const QMap<int, int> &logicalToVisual,
462 QList<QPainterPath> shapes,
463 QPointF &startPos,
464 const KoSvgText::ResolutionHandler &resHandler)
465{
466 QVector<LineBox> lineBoxes;
469 bool ltr = direction == KoSvgText::DirectionLeftToRight;
470 bool isHorizontal = writingMode == KoSvgText::HorizontalTB;
473 KoSvgText::TextAnchor anchor = textAnchorForTextAlign(align, alignLast, ltr);
474
475 QPointF textIndent;
477
478 QVector<int> wordIndices;
480 QRectF wordBox;
481 QPointF wordAdvance;
482
483 LineBox currentLine;
484 bool indentLine = true;
485
486 QPointF currentPos = writingMode == KoSvgText::VerticalRL? shapes.first().boundingRect().topRight()
487 : shapes.first().boundingRect().topLeft();
488 QPointF lineOffset = currentPos;
489
490 QListIterator<int> it(logicalToVisual.keys());
491 QListIterator<QPainterPath> shapesIt(shapes);
492 if (shapes.isEmpty()) {
493 return lineBoxes;
494 }
495 {
496 // Calculate the default pos.
497 qreal fontSize = properties.fontSize().value;
498 QRectF wordBox = isHorizontal? QRectF(0, fontSize * -0.8, SHAPE_PRECISION, fontSize)
499 : QRectF(fontSize * -0.5, 0, fontSize, SHAPE_PRECISION);
500 getFirstPosition(startPos, shapes.first(), wordBox, currentPos, writingMode, ltr);
501 }
502 QPainterPath currentShape;
503 while (it.hasNext()) {
504 int index = it.next();
505 result[index].calculateAndApplyTabsize(wordAdvance + currentPos, isHorizontal, resHandler);
506 CharacterResult charResult = result.at(index);
507 if (!charResult.addressable) {
508 continue;
509 }
510
511 bool softBreak = false;
512 bool doNotCountAdvance =
513 ((charResult.lineEnd != LineEdgeBehaviour::NoChange)
514 && !(currentLine.isEmpty() && wordIndices.isEmpty()));
515 if (!doNotCountAdvance) {
516 if (wordIndices.isEmpty()) {
517 wordBox = charResult.lineHeightBox().translated(charResult.totalBaselineOffset());
518 wordAdvance = charResult.advance;
519 } else {
520 wordBox |= charResult.lineHeightBox().translated(wordAdvance+charResult.totalBaselineOffset());
521 wordAdvance += charResult.advance;
522 }
523 }
524 wordIndices.append(index);
525 currentLine.lastLine = !it.hasNext();
526 if (currentLine.lastLine) {
527 currentLine.justifyLine = alignLast == KoSvgText::AlignJustify;
528 }
529
530 if (charResult.breakType != BreakType::NoBreak || currentLine.lastLine) {
531 if (currentLine.chunks.isEmpty() || currentLine.lastLine) {
532 softBreak = true;
533 }
534
535 for (int i = currentLine.currentChunk; i < currentLine.chunks.size(); i++) {
536 if (i == -1) {
537 currentLine.currentChunk = 0;
538 i = 0;
539 }
540 QLineF line = currentLine.chunks.value(i).length;
541 qreal lineLength = isHorizontal ? (currentPos - line.p1() + wordAdvance).x()
542 : (currentPos - line.p1() + wordAdvance).y();
543 if (qRound((abs(lineLength) - line.length())) > 0) {
544 if (i == currentLine.chunks.size()-1) {
545 softBreak = true;
546 break;
547 } else {
548 QLineF nextLine = currentLine.chunks.value(i+1).length;
549 if (isHorizontal) {
550 currentPos.setX(ltr? qMax(nextLine.p1().x(), currentPos.x()):
551 qMin(nextLine.p1().x(), currentPos.x()));
552 } else {
553 currentPos.setY(nextLine.p1().y());
554 }
555 }
556 } else {
557 currentLine.currentChunk = i;
558 addWordToLine(result, currentPos, wordIndices, currentLine, ltr, isHorizontal);
559 break;
560 }
561 }
562 }
563
564 if (softBreak) {
565 if (!currentLine.isEmpty()) {
566 finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true, resHandler);
567 lineBoxes.append(currentLine);
568 indentLine = false;
569 }
570 // Not adding indent to the (first) word box means it'll overflow if there's no room,
571 // but being too strict might end with the whole text disappearing. Given Krita's text layout is
572 // in an interactive context, ugly result might be more communicative than all text disappearing.
573 bool ind = textIndentInfo.hanging? !indentLine: indentLine;
574 QPointF indent = ind? textIndent: QPointF();
575 bool foundFirst = false;
576 bool needNewLine = true;
577 // add text indent to wordbox.
578 if (!currentShape.isEmpty()) {
579 // we're going to try and get an offset line first before trying get first pos.
580 // This gives more stable results on curved shapes.
581 getEstimatedHeight(result, index, wordBox, currentShape.boundingRect(), writingMode);
582 currentPos -= writingMode == KoSvgText::VerticalRL? wordBox.topRight(): wordBox.topLeft();
583
584 currentLine = LineBox(findLineBoxesForFirstPos(currentShape, currentPos, wordBox, writingMode), ltr, indent, resHandler);
585 qreal length = isHorizontal? wordBox.width(): wordBox.height();
586 for (int i = 0; i < currentLine.chunks.size(); i++) {
587 const LineChunk chunk = currentLine.chunks.at(i);
588 // We have a new line, so we find the first chunk that fits the word.
589 if ( chunk.length.length() > length ) {
590 currentLine.currentChunk = i;
591 foundFirst = true;
592 needNewLine = false;
593 break;
594 }
595 }
596 }
597 /*
598 * In theory we could have overflow wrap for shapes, but it'd require either generalizing
599 * the line-filling portion above and this new line seeking portion, or somehow reverting
600 * the iterator over the results to be on the last fitted glyph (which'd still require
601 * generalizing the line-filling portion about), and I am unsure how to do that.
602 * Either way, this place here is where you'd check for overflow wrap.
603 */
604 while(!foundFirst) {
605 foundFirst = getFirstPosition(currentPos, currentShape, wordBox, lineOffset, writingMode, ltr);
606 if (foundFirst || !shapesIt.hasNext()) {
607 break;
608 }
609 currentShape = shapesIt.next();
610 getEstimatedHeight(result, index, wordBox, currentShape.boundingRect(), writingMode);
611
612 bool indentPercent = textIndentInfo.length.unit == KoSvgText::CssLengthPercentage::Percentage;
613 qreal textIdentValue = textIndentInfo.length.value;
614 if (isHorizontal) {
615 if (indentPercent) {
616 textIdentValue *= currentShape.boundingRect().width();
617 }
618 textIndent = resHandler.adjust(QPointF(textIdentValue, 0));
619 } else {
620 if (indentPercent) {
621 textIdentValue *= currentShape.boundingRect().height();
622 }
623 textIndent = resHandler.adjust(QPointF(0, textIdentValue));
624 }
625 bool ind = textIndentInfo.hanging? !indentLine: indentLine;
626 indent = ind? textIndent: QPointF();
627 currentPos = writingMode == KoSvgText::VerticalRL? currentShape.boundingRect().topRight(): currentShape.boundingRect().topLeft();
628 lineOffset = currentPos;
629 }
630
631 bool lastDitch = false;
632 if (!foundFirst && lineBoxes.isEmpty() && !wordIndices.isEmpty() && !currentShape.isEmpty()) {
633 // Last-ditch attempt to get any kind of positioning to happen.
634 // This typically happens when wrapping has been disabled.
635 wordBox = result[wordIndices.first()].lineHeightBox().translated(result[wordIndices.first()].totalBaselineOffset());
636 foundFirst = getFirstPosition(currentPos, currentShape, wordBox, lineOffset, writingMode, ltr);
637 lastDitch = true;
638 }
639
640 if (foundFirst) {
641
642 if (needNewLine) {
643 currentLine = LineBox(findLineBoxesForFirstPos(currentShape, currentPos, wordBox, writingMode), ltr, indent, resHandler);
644 // We could set this to find the first fitting width, but it's better to try and improve the precision of the firstpos algorithm,
645 // as this gives more stable results.
646 currentLine.setCurrentChunkForPos(currentPos, isHorizontal);
647 }
648 const qreal expectedLineT = isHorizontal? wordBox.top():
649 writingMode == KoSvgText::VerticalRL? wordBox.right(): wordBox.left();
650
651 currentLine.firstLine = lineBoxes.isEmpty();
652 currentLine.expectedLineTop = expectedLineT;
653 currentLine.justifyLine = align == KoSvgText::AlignJustify;
654 currentPos = currentLine.chunk().length.p1();
655 lineOffset = currentPos;
656 currentPos += needNewLine? currentLine.textIndent: QPointF();
657
658 if (lastDitch) {
659 QVector<int> wordNew;
660 QPointF advance = currentPos;
661 Q_FOREACH(const int i, wordIndices) {
662 advance += result[i].advance;
663 if (currentShape.contains(advance)) {
664 wordNew.append(i);
665 } else {
666 result[i].hidden = true;
667 result[i].cssPosition = advance-result[i].advance;
668 }
669 wordIndices = wordNew;
670 }
671 }
672 addWordToLine(result, currentPos, wordIndices, currentLine, ltr, isHorizontal);
673 } else {
674 currentLine = LineBox();
675 QPointF advance = currentPos;
676 Q_FOREACH (const int j, wordIndices) {
677 result[j].cssPosition = advance;
678 advance += result[j].advance;
679 result[j].hidden = true;
680 }
681 }
682 }
683
684 if (charResult.breakType == BreakType::HardBreak) {
685 finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true, resHandler);
686 lineBoxes.append(currentLine);
687 currentLine = LineBox();
688 indentLine = textIndentInfo.hanging? false: textIndentInfo.eachLine;
689 }
690 }
691 finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true, resHandler);
692 lineBoxes.append(currentLine);
693 return lineBoxes;
694}
695
696} // namespace KoSvgTextShapeLayoutFunc
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
const Params2D p
QPointF p2
constexpr qreal SHAPE_PRECISION
Value that indicates the precision for testing coordinates for text-in-shape layout.
@ NoChange
Do nothing special.
The position of a path point within a path shape.
Definition KoPathShape.h:63
static QList< QPolygon > offsetPolygons(const QList< QPolygon > polygons, int offset, bool rounded=true, int circleSegments=16)
QTransform transformation() const
Returns the shapes local transformation matrix.
Definition KoShape.cpp:383
@ TextAlignAllId
KoSvgText::TextAlign.
@ WritingModeId
KoSvgText::WritingMode.
@ DirectionId
KoSvgText::Direction.
@ TextAlignLastId
KoSvgText::TextAlign.
@ TextIndentId
KoSvgText::TextIndentInfo Struct.
static const KoSvgTextProperties & defaultProperties()
QVariant propertyOrDefault(PropertyId id) const
KoSvgText::CssLengthPercentage fontSize() const
void inheritFrom(const KoSvgTextProperties &parentProperties, bool resolve=false)
qreal kisDistanceToLine(const QPointF &m, const QLineF &line)
Definition kis_global.h:234
QPainterPath trySimplifyPath(const QPainterPath &path, qreal lengthThreshold)
trySimplifyPath Tries to simplify a QPainterPath
static bool pointLessThan(const QPointF &a, const QPointF &b)
static KoSvgText::TextAnchor textAnchorForTextAlign(KoSvgText::TextAlign align, KoSvgText::TextAlign alignLast, bool ltr)
void calculateLineHeight(CharacterResult cr, double &ascent, double &descent, bool isHorizontal, bool compare=false)
calculateLineHeight calculate the total ascent and descent (including baseline-offset) of a charResul...
static bool getFirstPosition(QPointF &firstPoint, QPainterPath p, QRectF wordBox, QPointF terminator, KoSvgText::WritingMode writingMode, bool ltr)
QVector< LineBox > flowTextInShapes(const KoSvgTextProperties &properties, const QMap< int, int > &logicalToVisual, QVector< CharacterResult > &result, QList< QPainterPath > shapes, QPointF &startPos, const KoSvgText::ResolutionHandler &resHandler)
static bool pointLessThanVertical(const QPointF &a, const QPointF &b)
static QVector< QLineF > findLineBoxesForFirstPos(QPainterPath shape, QPointF firstPos, const QRectF wordBox, KoSvgText::WritingMode writingMode)
void finalizeLine(QVector< CharacterResult > &result, QPointF &currentPos, LineBox &currentLine, QPointF &lineOffset, const KoSvgText::TextAnchor anchor, const KoSvgText::WritingMode writingMode, const bool ltr, const bool inlineSize, const bool textInShape, const KoSvgText::ResolutionHandler &resHandler)
bool pastTerminator(const QPointF point, const QPointF terminator, const KoSvgText::WritingMode writingMode)
QList< QPainterPath > getShapes(QList< KoShape * > shapesInside, QList< KoShape * > shapesSubtract, const KoSvgTextProperties &properties)
static void getEstimatedHeight(QVector< CharacterResult > &result, const int index, QRectF &wordBox, const QRectF boundingBox, KoSvgText::WritingMode writingMode)
getEstimatedHeight Adjust the wordbox with the estimated height.
void addWordToLine(QVector< CharacterResult > &result, QPointF &currentPos, QVector< int > &wordIndices, LineBox &currentLine, bool ltr, bool isHorizontal)
addWordToLine Small function used in break lines to quickly add a 'word' to the current line....
@ AlignCenter
Center text in line.
Definition KoSvgText.h:173
TextAnchor
Where the text is anchored for SVG 1.1 text and 'inline-size'.
Definition KoSvgText.h:79
@ AnchorEnd
Anchor right for LTR, left for RTL.
Definition KoSvgText.h:82
@ AnchorStart
Anchor left for LTR, right for RTL.
Definition KoSvgText.h:80
@ AnchorMiddle
Anchor to the middle.
Definition KoSvgText.h:81
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionLeftToRight
Definition KoSvgText.h:49
@ HorizontalTB
Definition KoSvgText.h:38
QPointF totalBaselineOffset() const
QRectF lineHeightBox() const
lineHeightBox
LineEdgeBehaviour lineEnd
The ResolutionHandler class.
Definition KoSvgText.h:1084
QPointF adjust(const QPointF point) const
Adjusts the point to rounded pixel values, based on whether roundToPixelHorizontal or roundToPixelVer...
bool hanging
Flip the lines to which text-indent is applied.
Definition KoSvgText.h:659
bool eachLine
Apply the text-indent to each line following a hardbreak.
Definition KoSvgText.h:660
CssLengthPercentage length
Definition KoSvgText.h:658
The LineBox struct.
void setCurrentChunkForPos(QPointF pos, bool isHorizontal)
LineChunk chunk()
QPointF textIndent
qreal expectedLineTop
Because fonts can affect lineheight mid-line, and this affects wrapping, this estimates the line-heig...
QVector< LineChunk > chunks
QLineF length
Used to measure how long the current line is allowed to be.