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