File indexing completed on 2024-12-22 04:09:12

0001 /*
0002  *  SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
0003  *  SPDX-FileCopyrightText: 2022 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "KoSvgTextShapeLayoutFunc.h"
0009 
0010 #include "KoPolygonUtils.h"
0011 #include "KoSvgTextProperties.h"
0012 
0013 #include <KoClipMaskPainter.h>
0014 #include <KoColorBackground.h>
0015 #include <KoPathShape.h>
0016 #include <KoShapeStroke.h>
0017 
0018 #include <kis_global.h>
0019 
0020 #include <QPainter>
0021 #include <QtMath>
0022 
0023 namespace KoSvgTextShapeLayoutFunc {
0024 
0025 QList<QPainterPath>
0026 getShapes(QList<KoShape *> shapesInside, QList<KoShape *> shapesSubtract, const KoSvgTextProperties &properties)
0027 {
0028     // the boost polygon method requires (and gives best result) on a inter-based polygon,
0029     // so we need to scale up. The scale selected here is the size freetype coordinates give to a single pixel.
0030     qreal scale = 64.0;
0031     QTransform precisionTF = QTransform::fromScale(scale, scale);
0032 
0033     qreal shapePadding = scale * properties.propertyOrDefault(KoSvgTextProperties::ShapePaddingId).toReal();
0034     qreal shapeMargin = scale * properties.propertyOrDefault(KoSvgTextProperties::ShapeMarginId).toReal();
0035 
0036     QPainterPath subtract;
0037     Q_FOREACH(const KoShape *shape, shapesSubtract) {
0038         const KoPathShape *path = dynamic_cast<const KoPathShape*>(shape);
0039         if (path) {
0040             QPainterPath p = path->transformation().map(path->outline());
0041             p.setFillRule(path->fillRule());
0042             // grow each polygon here with the shape margin size.
0043             if (shapeMargin > 0) {
0044                 QList<QPolygon> subpathPolygons;
0045                 Q_FOREACH(QPolygonF subPath, p.toSubpathPolygons()) {
0046                     subpathPolygons.append(precisionTF.map(subPath).toPolygon());
0047                 }
0048                 subpathPolygons = KoPolygonUtils::offsetPolygons(subpathPolygons, shapeMargin);
0049 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
0050                 p.clear();
0051 #else
0052                 p = QPainterPath();
0053 #endif
0054 
0055                 Q_FOREACH (const QPolygon poly, subpathPolygons) {
0056                     p.addPolygon(poly);
0057                 }
0058             } else {
0059                 p = precisionTF.map(p);
0060             }
0061             subtract.addPath(p);
0062         }
0063     }
0064 
0065     QList<QPainterPath> shapes;
0066     Q_FOREACH(const KoShape *shape, shapesInside) {
0067         const KoPathShape *path = dynamic_cast<const KoPathShape*>(shape);
0068         if (path) {
0069             QPainterPath p = path->transformation().map(path->outline());
0070             p.setFillRule(path->fillRule());
0071             QPainterPath p2;
0072             p2.setFillRule(path->fillRule());
0073 
0074             QList<QPolygon> subpathPolygons;
0075             Q_FOREACH(QPolygonF subPath, p.toSubpathPolygons()) {
0076                 subpathPolygons.append(precisionTF.map(subPath).toPolygon());
0077             }
0078             subpathPolygons = KoPolygonUtils::offsetPolygons(subpathPolygons, -shapePadding);
0079 
0080             for (int i=0; i < subpathPolygons.size(); i++) {
0081                 QPolygonF subpathPoly = subpathPolygons.at(i);
0082                 Q_FOREACH(QPolygonF subtractPoly, subtract.toSubpathPolygons()) {
0083                     if (subpathPoly.intersects(subtractPoly)) {
0084                         subpathPoly = subpathPoly.subtracted(subtractPoly);
0085                     }
0086                 }
0087                 p2.addPolygon(subpathPoly);
0088             }
0089             shapes.append(precisionTF.inverted().map(p2));
0090         }
0091     }
0092     return shapes;
0093 }
0094 
0095 static bool getFirstPosition(QPointF &firstPoint,
0096                              QPainterPath p,
0097                              QRectF wordBox,
0098                              QPointF terminator,
0099                              KoSvgText::WritingMode writingMode,
0100                              bool ltr)
0101 {
0102     if (wordBox.isEmpty()) {
0103         // line-height: 0 will give us empty rects, make them slightly tall/wide
0104         // to avoid issues with complex QRectF operations.
0105         if (writingMode == KoSvgText::HorizontalTB) {
0106             wordBox.setHeight(1e-3);
0107         } else {
0108             wordBox.setWidth(1e-3);
0109         }
0110     }
0111 
0112     /// Precision of fitting the word box into the polygon to account for
0113     /// floating-point precision error.
0114 
0115     QVector<QPointF> candidatePositions;
0116     QRectF word = wordBox.normalized();
0117     word.translate(-wordBox.topLeft());
0118     // Slightly shrink the box to account for precision error.
0119     word.adjust(SHAPE_PRECISION, SHAPE_PRECISION, -SHAPE_PRECISION, -SHAPE_PRECISION);
0120 
0121     QPointF terminatorAdjusted = terminator;
0122     Q_FOREACH(const QPolygonF polygon, p.toFillPolygons()) {
0123         QVector<QLineF> offsetPoly;
0124         for(int i = 0; i < polygon.size()-1; i++) {
0125             QLineF line;
0126             line.setP1(polygon.at(i));
0127             line.setP2(polygon.at(i+1));
0128 
0129             if (line.angle() == 0.0 || line.angle() == 180.0) {
0130                 qreal offset = word.center().y();
0131                 offsetPoly.append(line.translated(0, offset));
0132                 offsetPoly.append(line.translated(0, -offset));
0133             } else if (line.angle() == 90.0 || line.angle() == 270.0) {
0134                 qreal offset = word.center().x();
0135                 offsetPoly.append(line.translated(offset, 0));
0136                 offsetPoly.append(line.translated(-offset, 0));
0137             } else {
0138                 qreal tAngle = fmod(line.angle(), 180.0);
0139                 QPointF cPos = tAngle > 90? line.center() + QPointF(-word.center().x(), word.center().y()): line.center() + word.center();
0140                 qreal offset = kisDistanceToLine(cPos, line);
0141                 const QPointF vectorT(qCos(qDegreesToRadians(tAngle)), -qSin(qDegreesToRadians(tAngle)));
0142                 QPointF vectorN(-vectorT.y(), vectorT.x());
0143                 QPointF offsetP = QPointF() - (0.0 * vectorT) + (offset * vectorN);
0144                 offsetPoly.append(line.translated(offsetP));
0145                 offsetPoly.append(line.translated(-offsetP));
0146             }
0147         }
0148         if (writingMode == KoSvgText::HorizontalTB) {
0149             terminatorAdjusted = terminator + word.center();
0150             QLineF top(polygon.boundingRect().topLeft(), polygon.boundingRect().topRight());
0151             offsetPoly.append(top.translated(0, terminatorAdjusted.y()));
0152         } else if (writingMode == KoSvgText::VerticalRL) {
0153             terminatorAdjusted = terminator - word.center();
0154             QLineF top(terminatorAdjusted.x(), polygon.boundingRect().top(),
0155                        terminatorAdjusted.x(), polygon.boundingRect().bottom());
0156             offsetPoly.append(top);
0157         } else{
0158             terminatorAdjusted = terminator + word.center();
0159             QLineF top(terminatorAdjusted.x(), polygon.boundingRect().top(),
0160                        terminatorAdjusted.x(), polygon.boundingRect().bottom());
0161             offsetPoly.append(top);
0162         }
0163         for (int i=0; i < offsetPoly.size(); i++) {
0164             QLineF line = offsetPoly.at(i);
0165             for (int j=i; j< offsetPoly.size(); j++){
0166                 QLineF line2 = offsetPoly.at(j);
0167                 QPointF intersectPoint;
0168 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
0169                 QLineF::IntersectType intersect = line.intersects(line2, &intersectPoint);
0170 #else
0171                 QLineF::IntersectType intersect = line.intersect(line2, &intersectPoint);
0172 #endif
0173                 if (intersect != QLineF::NoIntersection) {
0174                     // should proly handle 'reflex' vertices better.
0175                     if (!p.contains(intersectPoint)) {
0176                         continue;
0177                     }
0178                     if(!p.contains(word.translated(intersectPoint-word.center()))) {
0179                         continue;
0180                     }
0181                     if (!candidatePositions.contains(intersectPoint)) {
0182                         candidatePositions.append(intersectPoint);
0183                     }
0184                 }
0185             }
0186         }
0187     }
0188     if (candidatePositions.isEmpty()) {
0189         return false;
0190     }
0191 
0192     QPointF firstPointC = writingMode == KoSvgText::VerticalRL? p.boundingRect().bottomLeft(): p.boundingRect().bottomRight();
0193     Q_FOREACH(const QPointF candidate, candidatePositions) {
0194         if (writingMode == KoSvgText::HorizontalTB) {
0195             if (terminatorAdjusted.y() - candidate.y() < SHAPE_PRECISION) {
0196 
0197                 if (firstPointC.y() - candidate.y() > SHAPE_PRECISION) {
0198                     firstPointC = candidate;
0199                 } else if (firstPointC.y() - candidate.y() > -SHAPE_PRECISION) {
0200                     if (ltr) {
0201                         if (candidate.x() < firstPointC.x()) {
0202                             firstPointC = candidate;
0203                         }
0204                     } else {
0205                         if (candidate.x() > firstPointC.x()) {
0206                             firstPointC = candidate;
0207                         }
0208                     }
0209                 }
0210             }
0211         } else if (writingMode == KoSvgText::VerticalRL) {
0212             if (terminatorAdjusted.x() - candidate.x() >= -SHAPE_PRECISION) {
0213 
0214                 if (firstPointC.x() - candidate.x() < -SHAPE_PRECISION) {
0215                     firstPointC = candidate;
0216                 } else if (firstPointC.x() - candidate.x() < SHAPE_PRECISION) {
0217                     if (ltr) {
0218                         if (candidate.y() < firstPointC.y()) {
0219                             firstPointC = candidate;
0220                         }
0221                     } else {
0222                         if (candidate.y() > firstPointC.y()) {
0223                             firstPointC = candidate;
0224                         }
0225                     }
0226                 }
0227             }
0228         } else {
0229             if (terminatorAdjusted.x() - candidate.x() < SHAPE_PRECISION) {
0230 
0231                 if (firstPointC.x() - candidate.x() > SHAPE_PRECISION) {
0232                     firstPointC = candidate;
0233                 } else if (firstPointC.x() - candidate.x() > -SHAPE_PRECISION) {
0234                     if (ltr) {
0235                         if (candidate.y() < firstPointC.y()) {
0236                             firstPointC = candidate;
0237                         }
0238                     } else {
0239                         if (candidate.y() > firstPointC.y()) {
0240                             firstPointC = candidate;
0241                         }
0242                     }
0243                 }
0244             }
0245         }
0246     }
0247     if (!p.contains(firstPointC)) {
0248         return false;
0249     }
0250     firstPointC -= word.center();
0251     firstPointC -= wordBox.topLeft();
0252     firstPoint = firstPointC;
0253 
0254     return true;
0255 }
0256 
0257 static bool pointLessThan(const QPointF &a, const QPointF &b)
0258 {
0259     return a.x() < b.x();
0260 }
0261 
0262 static bool pointLessThanVertical(const QPointF &a, const QPointF &b)
0263 {
0264     return a.y() < b.y();
0265 }
0266 
0267 static QVector<QLineF>
0268 findLineBoxesForFirstPos(QPainterPath shape, QPointF firstPos, QRectF wordBox, KoSvgText::WritingMode writingMode)
0269 {
0270     QVector<QLineF> lines;
0271 
0272     QLineF baseLine;
0273     QPointF lineTop;
0274     QPointF lineBottom;
0275     QRectF word = wordBox.normalized();
0276     word.adjust(SHAPE_PRECISION, SHAPE_PRECISION, -SHAPE_PRECISION, -SHAPE_PRECISION);
0277 
0278     if (writingMode == KoSvgText::HorizontalTB) {
0279         baseLine = QLineF(shape.boundingRect().left()-5, firstPos.y(), shape.boundingRect().right()+5, firstPos.y());
0280         lineTop = QPointF(0, word.top());
0281         lineBottom = QPointF(0, word.bottom());
0282     } else {
0283         baseLine = QLineF(firstPos.x(), shape.boundingRect().top()-5, firstPos.x(), shape.boundingRect().bottom()+5);
0284         if (writingMode == KoSvgText::VerticalRL) {
0285             lineTop = QPointF(word.left(), 0);
0286             lineBottom = QPointF(word.right(), 0);
0287         } else {
0288             lineTop = QPointF(word.right(), 0);
0289             lineBottom = QPointF(word.left(), 0);
0290         }
0291     }
0292 
0293     QPolygonF polygon = shape.toFillPolygon();
0294     QList<QPointF> intersects;
0295     QLineF topLine = baseLine.translated(lineTop);
0296     QLineF bottomLine = baseLine.translated(lineBottom);
0297     for(int i = 0; i < polygon.size()-1; i++) {
0298         QLineF line(polygon.at(i), polygon.at(i+1));
0299         bool addedA = false;
0300         QPointF intersectA;
0301         QPointF intersectB;
0302         QPointF intersect;
0303 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
0304         if (topLine.intersects(line, &intersect) == QLineF::BoundedIntersection) {
0305 #else
0306         if (topLine.intersect(line, &intersect) == QLineF::BoundedIntersection) {
0307 #endif
0308             intersectA = intersect-lineTop;
0309             intersects.append(intersectA);
0310             addedA = true;
0311         }
0312 #if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
0313         if (bottomLine.intersects(line, &intersect) == QLineF::BoundedIntersection) {
0314 #else
0315         if (bottomLine.intersect(line, &intersect) == QLineF::BoundedIntersection) {
0316 #endif
0317             intersectB = intersect-lineBottom;
0318             if (intersectA != intersectB || !addedA) {
0319                 intersects.append(intersectB);
0320             }
0321         }
0322     }
0323     if (!intersects.isEmpty()) {
0324         intersects.append(baseLine.p1());
0325         intersects.append(baseLine.p2());
0326     }
0327     if (writingMode == KoSvgText::HorizontalTB) {
0328         std::sort(intersects.begin(), intersects.end(), pointLessThan);
0329     } else {
0330         std::sort(intersects.begin(), intersects.end(), pointLessThanVertical);
0331     }
0332 
0333 
0334     for (int i = 0; i< intersects.size()-1; i++) {
0335         QLineF line(intersects.at(i), intersects.at(i+1));
0336 
0337         if (!(shape.contains(line.translated(lineTop).center())
0338               && shape.contains(line.translated(lineBottom).center()))
0339                 || line.length() == 0) {
0340             continue;
0341         }
0342 
0343         QRectF lineBox = QRectF(line.p1() + lineTop, line.p2() + lineBottom).normalized();
0344 
0345         QVector<QPointF> relevant;
0346         for(int i = 0; i < polygon.size()-1; i++) {
0347 
0348             QLineF edgeLine(polygon.at(i), polygon.at(i+1));
0349 
0350             if (lineBox.contains(polygon.at(i))) {
0351                 relevant.append(polygon.at(i));
0352             }
0353         }
0354         qreal start = writingMode == KoSvgText::HorizontalTB? lineBox.left(): lineBox.top();
0355         qreal end = writingMode == KoSvgText::HorizontalTB? lineBox.right(): lineBox.bottom();
0356         for(int j = 0; j < relevant.size(); j++) {
0357             QPointF current = relevant.at(j);
0358 
0359             if (writingMode == KoSvgText::HorizontalTB) {
0360                 if (current.x() < line.center().x()) {
0361                     start = qMax(current.x(), start);
0362                 } else if (current.x() > line.center().x()) {
0363                     end = qMin(current.x(), end);
0364                 }
0365             } else {
0366                 if (current.y() < line.center().y()) {
0367                     start = qMax(current.y(), start);
0368                 } else if (current.y() > line.center().y()) {
0369                     end = qMin(current.y(), end);
0370                 }
0371             }
0372         }
0373         if (writingMode == KoSvgText::HorizontalTB) {
0374 
0375             QLineF newLine(start, line.p1().y(), end, line.p2().y());
0376             if (!lines.isEmpty()) {
0377                 if (lines.last().p2() == intersects.at(i)) {
0378                     newLine.setP1(lines.last().p1());
0379                     lines.removeLast();
0380                 }
0381             }
0382             lines.append(newLine);
0383         } else {
0384             QLineF newLine(line.p1().x(), start, line.p2().x(), end);
0385             if (!lines.isEmpty()) {
0386                 if (lines.last().p2() == intersects.at(i)) {
0387                     newLine.setP1(lines.last().p1());
0388                     lines.removeLast();
0389                 }
0390             }
0391             lines.append(newLine);
0392         }
0393     }
0394 
0395     return lines;
0396 }
0397 
0398 static void getEstimatedHeight(QVector<CharacterResult> &result,
0399                                int index,
0400                                QRectF &wordBox,
0401                                QRectF boundingBox,
0402                                KoSvgText::WritingMode writingMode)
0403 {
0404     bool isHorizontal = writingMode == KoSvgText::HorizontalTB;
0405     QPointF totalAdvance = wordBox.bottomRight() - wordBox.topLeft();
0406     qreal maxAscent = isHorizontal? wordBox.top(): wordBox.right();
0407     qreal maxDescent = isHorizontal? wordBox.bottom(): wordBox.left();
0408 
0409     for (int i=index; i<result.size(); i++) {
0410         if (!result.at(i).addressable || result.at(i).hidden) {
0411             continue;
0412         }
0413         totalAdvance += result.at(i).advance;
0414         if ((totalAdvance.x() > boundingBox.width() && isHorizontal) ||
0415                 (totalAdvance.y() > boundingBox.height() && !isHorizontal)) {
0416             break;
0417         }
0418         calculateLineHeight(result.at(i), maxAscent, maxDescent, isHorizontal, true);
0419     }
0420     if (writingMode == KoSvgText::HorizontalTB) {
0421         wordBox.setTop(maxAscent);
0422         wordBox.setBottom(maxDescent);
0423     } else {
0424         // vertical lr has top at the right even though block flow is also to the right.
0425         wordBox.setRight(maxAscent);
0426         wordBox.setLeft(maxDescent);
0427     }
0428 }
0429 
0430 static KoSvgText::TextAnchor
0431 textAnchorForTextAlign(KoSvgText::TextAlign align, KoSvgText::TextAlign alignLast, bool ltr)
0432 {
0433     KoSvgText::TextAlign compare = align;
0434     if (align == KoSvgText::AlignJustify) {
0435         compare = alignLast;
0436     }
0437     if (compare == KoSvgText::AlignStart) {
0438         return KoSvgText::AnchorStart;
0439     } else if (compare == KoSvgText::AlignCenter) {
0440         return KoSvgText::AnchorMiddle;
0441     } else if (compare == KoSvgText::AlignEnd) {
0442         return KoSvgText::AnchorEnd;
0443     } else if (compare == KoSvgText::AlignLeft) {
0444         return ltr? KoSvgText::AnchorStart: KoSvgText::AnchorEnd;
0445     } else if (compare == KoSvgText::AlignRight) {
0446         return ltr? KoSvgText::AnchorEnd: KoSvgText::AnchorStart;
0447     } else if (align == KoSvgText::AlignJustify) {
0448         return KoSvgText::AnchorMiddle;
0449     }
0450     return KoSvgText::AnchorStart;
0451 }
0452 
0453 QVector<LineBox> flowTextInShapes(const KoSvgTextProperties &properties,
0454                                   const QMap<int, int> &logicalToVisual,
0455                                   QVector<CharacterResult> &result,
0456                                   QList<QPainterPath> shapes,
0457                                   QPointF &startPos)
0458 {
0459     QVector<LineBox> lineBoxes;
0460     KoSvgText::WritingMode writingMode = KoSvgText::WritingMode(properties.propertyOrDefault(KoSvgTextProperties::WritingModeId).toInt());
0461     KoSvgText::Direction direction = KoSvgText::Direction(properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
0462     bool ltr = direction == KoSvgText::DirectionLeftToRight;
0463     bool isHorizontal = writingMode == KoSvgText::HorizontalTB;
0464     KoSvgText::TextAlign align = KoSvgText::TextAlign(properties.propertyOrDefault(KoSvgTextProperties::TextAlignAllId).toInt());
0465     KoSvgText::TextAlign alignLast = KoSvgText::TextAlign(properties.propertyOrDefault(KoSvgTextProperties::TextAlignLastId).toInt());
0466     KoSvgText::TextAnchor anchor = textAnchorForTextAlign(align, alignLast, ltr);
0467 
0468     QPointF textIndent; ///< The textIndent.
0469     KoSvgText::TextIndentInfo textIndentInfo = properties.propertyOrDefault(KoSvgTextProperties::TextIndentId).value<KoSvgText::TextIndentInfo>();
0470 
0471     QVector<int> wordIndices; ///< 'word' in this case meaning characters
0472                               ///< inbetween softbreaks.
0473     QRectF wordBox; ///< Approximated box of the currrent word;
0474     QPointF wordAdvance;
0475 
0476     LineBox currentLine;
0477     bool firstLine = true; ///< First line will be created proper after we get our first wordbox, this tracks if it's the first.
0478     bool indentLine = true;
0479 
0480     QPointF currentPos = writingMode == KoSvgText::VerticalRL? shapes.at(0).boundingRect().topRight()
0481                                                              :shapes.at(0).boundingRect().topLeft(); ///< Current position with advances of each character.
0482     QPointF lineOffset = currentPos; ///< Current line offset.
0483 
0484     QListIterator<int> it(logicalToVisual.keys());
0485     QListIterator<QPainterPath> shapesIt(shapes);
0486     if (shapes.isEmpty()) {
0487         return lineBoxes;
0488     }
0489     {
0490         // Calculate the default pos.
0491         qreal fontSize = properties.propertyOrDefault(KoSvgTextProperties::FontSizeId).toReal();
0492         QRectF wordBox = isHorizontal? QRectF(0, fontSize * -0.8, SHAPE_PRECISION, fontSize)
0493                                      : QRectF(fontSize * -0.5, 0, fontSize, SHAPE_PRECISION);
0494         getFirstPosition(startPos, shapes.first(), wordBox, currentPos, writingMode, ltr);
0495     }
0496     QPainterPath currentShape;
0497     while (it.hasNext()) {
0498         int index = it.next();
0499         CharacterResult charResult = result.at(index);
0500         if (!charResult.addressable) {
0501             continue;
0502         }
0503 
0504         bool softBreak = false; ///< Whether to break a line.
0505         bool doNotCountAdvance =
0506             ((charResult.lineEnd != LineEdgeBehaviour::NoChange)
0507              && !(currentLine.isEmpty() && wordIndices.isEmpty()));
0508         if (!doNotCountAdvance) {
0509             if (wordIndices.isEmpty()) {
0510                 wordBox = charResult.lineHeightBox.translated(charResult.baselineOffset);
0511                 wordAdvance = charResult.advance;
0512             } else {
0513                 wordBox |= charResult.lineHeightBox.translated(wordAdvance+charResult.baselineOffset);
0514                 wordAdvance += charResult.advance;
0515             }
0516         }
0517         wordIndices.append(index);
0518         currentLine.lastLine = !it.hasNext();
0519         if (currentLine.lastLine) {
0520             currentLine.justifyLine = alignLast == KoSvgText::AlignJustify;
0521         }
0522 
0523         if (charResult.breakType != BreakType::NoBreak || currentLine.lastLine) {
0524             if (currentLine.chunks.isEmpty() || currentLine.lastLine) {
0525                 softBreak = true;
0526             }
0527 
0528             for (int i = currentLine.currentChunk; i < currentLine.chunks.size(); i++) {
0529                 if (i == -1) {
0530                     currentLine.currentChunk = 0;
0531                     i = 0;
0532                 }
0533                 QLineF line = currentLine.chunks.value(i).length;
0534                 qreal lineLength = isHorizontal ? (currentPos - line.p1() + wordAdvance).x()
0535                                                 : (currentPos - line.p1() + wordAdvance).y();
0536                 if (qRound((abs(lineLength) - line.length())) > 0) {
0537                     if (i == currentLine.chunks.size()-1) {
0538                         softBreak = true;
0539                         break;
0540                     } else {
0541                         QLineF nextLine = currentLine.chunks.value(i+1).length;
0542                         if (isHorizontal) {
0543                             currentPos.setX(ltr? qMax(nextLine.p1().x(), currentPos.x()):
0544                                                  qMin(nextLine.p1().x(), currentPos.x()));
0545                         } else {
0546                             currentPos.setY(nextLine.p1().y());
0547                         }
0548                     }
0549                 } else {
0550                     currentLine.currentChunk = i;
0551                     addWordToLine(result, currentPos, wordIndices, currentLine, ltr, isHorizontal);
0552                     break;
0553                 }
0554             }
0555         }
0556 
0557         if (softBreak) {
0558             if (!currentLine.isEmpty()) {
0559                 finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true);
0560                 lineBoxes.append(currentLine);
0561                 firstLine = false;
0562                 indentLine = false;
0563             }
0564             // Not adding indent to the (first) word box means it'll overflow if there's no room,
0565             // but being too strict might end with the whole text dissapearing. Given Krita's text layout is
0566             // in an interactive context, ugly result might be more communicative than all text dissapearing.
0567             bool ind = textIndentInfo.hanging? !indentLine: indentLine;
0568             QPointF indent = ind? textIndent: QPointF();
0569             bool foundFirst = false;
0570             bool needNewLine = true;
0571             // add text indent to wordbox.
0572             getEstimatedHeight(result, index, wordBox, currentShape.boundingRect(), writingMode);
0573             if (!currentShape.isEmpty()) {
0574                 // we're going to try and get an offset line first before trying get first pos.
0575                 // This gives more stable results on curved shapes.
0576                 currentPos -= writingMode == KoSvgText::VerticalRL? wordBox.topRight(): wordBox.topLeft();
0577                 currentLine = LineBox(findLineBoxesForFirstPos(currentShape, currentPos, wordBox, writingMode), ltr, indent);
0578                 qreal length = isHorizontal? wordBox.width(): wordBox.height();
0579                 for (int i = 0; i < currentLine.chunks.size(); i++) {
0580                     if (currentLine.chunks.at(i).length.length() > length) {
0581                         currentLine.currentChunk = i;
0582                         foundFirst = true;
0583                         needNewLine = false;
0584                         break;
0585                     }
0586                 }
0587             }
0588             /*
0589              * In theory we could have overflow wrap for shapes, but it'd require either generalizing
0590              * the line-filling portion above and this new line seeking portion, or somehow reverting
0591              * the itterator over the results to be on the last fitted glyph (which'd still require
0592              * generalizing the line-filling portion about), and I am unsure how to do that.
0593              * Either way, this place here is where you'd check for overflow wrap.
0594              */
0595             while(!foundFirst) {
0596                 foundFirst = getFirstPosition(currentPos, currentShape, wordBox, lineOffset, writingMode, ltr);
0597                 if (foundFirst || !shapesIt.hasNext()) {
0598                     break;
0599                 }
0600                 currentShape = shapesIt.next();
0601                 qreal textIdentValue = textIndentInfo.value;
0602                 if (isHorizontal) {
0603                     if (textIndentInfo.isPercentage) {
0604                         textIndent *= currentShape.boundingRect().width();
0605                     }
0606                     textIndent = QPointF(textIdentValue, 0);
0607                 } else {
0608                     if (textIndentInfo.isPercentage) {
0609                         textIndent *= currentShape.boundingRect().height();
0610                     }
0611                     textIndent = QPointF(0, textIdentValue);
0612                 }
0613                 bool ind = textIndentInfo.hanging? !indentLine: indentLine;
0614                 indent = ind? textIndent: QPointF();
0615                 currentPos = writingMode == KoSvgText::VerticalRL? currentShape.boundingRect().topRight(): currentShape.boundingRect().topLeft();
0616                 lineOffset = currentPos;
0617             }
0618 
0619             bool lastDitch = false;
0620             if (!foundFirst && firstLine && !wordIndices.isEmpty() && !currentShape.isEmpty()) {
0621                 // Last-ditch attempt to get any kind of positioning to happen.
0622                 // This typically happens when wrapping has been disabled.
0623                 wordBox = result[wordIndices.first()].lineHeightBox.translated(result[wordIndices.first()].baselineOffset);
0624                 foundFirst = getFirstPosition(currentPos, currentShape, wordBox, lineOffset, writingMode, ltr);
0625                 lastDitch = true;
0626             }
0627 
0628             if (foundFirst) {
0629                 if (needNewLine) {
0630                     currentLine = LineBox(findLineBoxesForFirstPos(currentShape, currentPos, wordBox, writingMode), ltr, indent);
0631                     // We could set this to find the first fitting width, but it's better to try and improve the precision of the firstpos algorithm,
0632                     // as this gives more stable results.
0633                     currentLine.setCurrentChunkForPos(currentPos, isHorizontal);
0634                 }
0635                 currentLine.firstLine = firstLine;
0636                 currentLine.expectedLineTop = isHorizontal? -wordBox.top():
0637                                                             writingMode == KoSvgText::VerticalRL? wordBox.right(): wordBox.left();
0638                 currentLine.justifyLine = align == KoSvgText::AlignJustify;
0639                 currentPos = currentLine.chunk().length.p1() + indent;
0640                 lineOffset = currentPos;
0641 
0642                 if (lastDitch) {
0643                     QVector<int> wordNew;
0644                     QPointF advance = currentPos;
0645                     Q_FOREACH(const int i, wordIndices) {
0646                         advance += result[i].advance;
0647                         if (currentShape.contains(advance)) {
0648                             wordNew.append(i);
0649                         } else {
0650                             result[i].hidden = true;
0651                             result[i].cssPosition = advance-result[i].advance;
0652                         }
0653                         wordIndices = wordNew;
0654                     }
0655                 }
0656                 addWordToLine(result, currentPos, wordIndices, currentLine, ltr, isHorizontal);
0657             } else {
0658                 currentLine = LineBox();
0659                 QPointF advance = currentPos;
0660                 Q_FOREACH (const int j, wordIndices) {
0661                     result[j].cssPosition = advance;
0662                     advance += result[j].advance;
0663                     result[j].hidden = true;
0664                 }
0665             }
0666         }
0667 
0668         if (charResult.breakType == BreakType::HardBreak) {
0669             finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true);
0670             lineBoxes.append(currentLine);
0671             currentLine = LineBox();
0672             indentLine = textIndentInfo.hanging? false: textIndentInfo.eachLine;
0673         }
0674     }
0675     finalizeLine(result, currentPos, currentLine, lineOffset, anchor, writingMode, ltr, true, true);
0676     lineBoxes.append(currentLine);
0677     return lineBoxes;
0678 }
0679 
0680 } // namespace KoSvgTextShapeLayoutFunc