File indexing completed on 2024-12-22 04:09:11
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 "KoSvgTextShape.h" 0009 #include "KoSvgTextShape_p.h" 0010 0011 #include "KoSvgTextProperties.h" 0012 0013 #include <KoClipMaskPainter.h> 0014 #include <KoColorBackground.h> 0015 #include <KoPathShape.h> 0016 #include <KoShapeStroke.h> 0017 0018 #include <QPainter> 0019 #include <QtMath> 0020 0021 #include <variant> 0022 0023 void inheritPaintProperties(KisForest<KoSvgTextContentElement>::composition_iterator it, 0024 KoShapeStrokeModelSP &stroke, 0025 QSharedPointer<KoShapeBackground> &background, 0026 QVector<KoShape::PaintOrder> &paintOrder) { 0027 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it)); parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) { 0028 if (parentIt->properties.hasProperty(KoSvgTextProperties::StrokeId)) { 0029 stroke = parentIt->properties.stroke(); 0030 break; 0031 } 0032 } 0033 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it)); parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) { 0034 if (parentIt->properties.hasProperty(KoSvgTextProperties::FillId)) { 0035 background = parentIt->properties.background(); 0036 break; 0037 } 0038 } 0039 for (auto parentIt = KisForestDetail::hierarchyBegin(siblingCurrent(it)); parentIt != KisForestDetail::hierarchyEnd(siblingCurrent(it)); parentIt++) { 0040 if (parentIt->properties.hasProperty(KoSvgTextProperties::PaintOrder)) { 0041 paintOrder = parentIt->properties.propertyOrDefault(KoSvgTextProperties::PaintOrder).value<QVector<KoShape::PaintOrder>>(); 0042 break; 0043 } 0044 } 0045 } 0046 0047 0048 // NOLINTNEXTLINE(readability-function-cognitive-complexity) 0049 void KoSvgTextShape::Private::paintPaths(QPainter &painter, 0050 const QPainterPath &rootOutline, 0051 const KoShape *rootShape, 0052 const QVector<CharacterResult> &result, 0053 QPainterPath &chunk, 0054 int ¤tIndex) 0055 { 0056 0057 KoShapeStrokeModelSP stroke = rootShape->stroke(); 0058 QSharedPointer<KoShapeBackground> background = rootShape->background(); 0059 QVector<KoShape::PaintOrder> paintOrder = rootShape->paintOrder(); 0060 0061 for (auto it = compositionBegin(textData); it != compositionEnd(textData); it++) { 0062 inheritPaintProperties(it, stroke, background, paintOrder); 0063 0064 if (it.state() == KisForestDetail::Enter) { 0065 QMap<KoSvgText::TextDecoration, QPainterPath> textDecorations = it->textDecorations; 0066 QColor textDecorationColor = it->properties.propertyOrDefault(KoSvgTextProperties::TextDecorationColorId).value<QColor>(); 0067 if (textDecorations.contains(KoSvgText::DecorationUnderline)) { 0068 Q_FOREACH(const KoShape::PaintOrder p, paintOrder) { 0069 if (p == KoShape::Fill) { 0070 if (background && !textDecorationColor.isValid() && textDecorationColor != Qt::transparent) { 0071 background->paint(painter, textDecorations.value(KoSvgText::DecorationUnderline)); 0072 } else if (textDecorationColor.isValid()) { 0073 painter.fillPath(textDecorations.value(KoSvgText::DecorationUnderline), textDecorationColor); 0074 } 0075 } else if (p == KoShape::Stroke) { 0076 if (stroke) { 0077 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationUnderline))); 0078 stroke->paint(shape.data(), painter); 0079 } 0080 } 0081 } 0082 0083 } 0084 if (textDecorations.contains(KoSvgText::DecorationOverline)) { 0085 Q_FOREACH(const KoShape::PaintOrder p, paintOrder) { 0086 if (p == KoShape::Fill) { 0087 if (background && !textDecorationColor.isValid() && textDecorationColor != Qt::transparent) { 0088 background->paint(painter, textDecorations.value(KoSvgText::DecorationOverline)); 0089 } else if (textDecorationColor.isValid()) { 0090 painter.fillPath(textDecorations.value(KoSvgText::DecorationOverline), textDecorationColor); 0091 } 0092 } else if (p == KoShape::Stroke) { 0093 if (stroke) { 0094 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationOverline))); 0095 stroke->paint(shape.data(), painter); 0096 } 0097 } 0098 } 0099 } 0100 0101 if (childCount(siblingCurrent(it)) == 0) { 0102 const int j = currentIndex + it->numChars(true); 0103 0104 const QRect shapeGlobalClipRect = painter.transform().mapRect(it->associatedOutline.boundingRect()).toAlignedRect(); 0105 0106 if (shapeGlobalClipRect.isValid()) { 0107 KoClipMaskPainter fillPainter(&painter, shapeGlobalClipRect); 0108 if (background) { 0109 background->paint(*fillPainter.shapePainter(), rootOutline); 0110 fillPainter.maskPainter()->fillPath(rootOutline, Qt::black); 0111 if (textRendering != OptimizeSpeed) { 0112 fillPainter.maskPainter()->setRenderHint(QPainter::Antialiasing, true); 0113 fillPainter.maskPainter()->setRenderHint(QPainter::SmoothPixmapTransform, true); 0114 } else { 0115 fillPainter.maskPainter()->setRenderHint(QPainter::Antialiasing, false); 0116 fillPainter.maskPainter()->setRenderHint(QPainter::SmoothPixmapTransform, false); 0117 } 0118 } 0119 QPainterPath textDecorationsRest; 0120 textDecorationsRest.setFillRule(Qt::WindingFill); 0121 0122 for (int i = currentIndex; i < j; i++) { 0123 if (result.at(i).addressable && !result.at(i).hidden) { 0124 const QTransform tf = result.at(i).finalTransform(); 0125 0126 /** 0127 * Make sure the character touches the painter's clip rect, 0128 * otherwise we can just skip it 0129 */ 0130 const QRectF boundingRect = tf.mapRect(result.at(i).boundingBox); 0131 const QRectF clipRect = painter.clipBoundingRect(); 0132 if (boundingRect.isEmpty() || 0133 (!clipRect.contains(boundingRect) && 0134 !clipRect.intersects(boundingRect))) continue; 0135 0136 /** 0137 * There's an annoying problem here that officially speaking 0138 * the chunks need to be unified into one single path before 0139 * drawing, so there's no weirdness with the stroke, but 0140 * QPainterPath's union function will frequently lead to 0141 * reduced quality of the paths because of 'numerical 0142 * instability'. 0143 */ 0144 0145 if (const auto *colorGlyph = std::get_if<Glyph::ColorLayers>(&result.at(i).glyph)) { 0146 for (int c = 0; c < colorGlyph->paths.size(); c++) { 0147 QBrush color = colorGlyph->colors.at(c); 0148 bool replace = colorGlyph->replaceWithForeGroundColor.at(c); 0149 // In theory we can use the pattern or gradient as well 0150 // for ColorV0 fonts, but ColorV1 fonts can have 0151 // gradients, so I am hesitant. 0152 KoColorBackground *b = dynamic_cast<KoColorBackground *>(background.data()); 0153 if (b && replace) { 0154 color = b->brush(); 0155 } 0156 painter.fillPath(tf.map(colorGlyph->paths.at(c)), color); 0157 } 0158 } else if (const auto *outlineGlyph = std::get_if<Glyph::Outline>(&result.at(i).glyph)) { 0159 chunk.addPath(tf.map(outlineGlyph->path)); 0160 } else if (const auto *bitmapGlyph = std::get_if<Glyph::Bitmap>(&result.at(i).glyph)) { 0161 if (bitmapGlyph->image.isGrayscale() || bitmapGlyph->image.format() == QImage::Format_Mono) { 0162 fillPainter.maskPainter()->save(); 0163 fillPainter.maskPainter()->translate(result.at(i).finalPosition.x(), result.at(i).finalPosition.y()); 0164 fillPainter.maskPainter()->rotate(qRadiansToDegrees(result.at(i).rotate)); 0165 fillPainter.maskPainter()->setCompositionMode(QPainter::CompositionMode_Plus); 0166 fillPainter.maskPainter()->drawImage(bitmapGlyph->drawRect, bitmapGlyph->image); 0167 fillPainter.maskPainter()->restore(); 0168 } else { 0169 painter.save(); 0170 painter.translate(result.at(i).finalPosition.x(), result.at(i).finalPosition.y()); 0171 painter.rotate(qRadiansToDegrees(result.at(i).rotate)); 0172 painter.setRenderHint(QPainter::SmoothPixmapTransform, true); 0173 painter.drawImage(bitmapGlyph->drawRect, bitmapGlyph->image); 0174 painter.restore(); 0175 } 0176 } 0177 } 0178 } 0179 Q_FOREACH(const KoShape::PaintOrder p, paintOrder) { 0180 if (p == KoShape::Fill) { 0181 if (background) { 0182 chunk.setFillRule(Qt::WindingFill); 0183 fillPainter.maskPainter()->fillPath(chunk, Qt::white); 0184 } 0185 if (!textDecorationsRest.isEmpty()) { 0186 fillPainter.maskPainter()->fillPath(textDecorationsRest.simplified(), Qt::white); 0187 } 0188 fillPainter.renderOnGlobalPainter(); 0189 } else if (p == KoShape::Stroke) { 0190 KoShapeStrokeSP maskStroke; 0191 if (stroke) { 0192 KoShapeStrokeSP strokeSP = qSharedPointerDynamicCast<KoShapeStroke>(stroke); 0193 0194 if (strokeSP) { 0195 if (strokeSP->lineBrush().gradient()) { 0196 KoClipMaskPainter strokePainter(&painter, shapeGlobalClipRect); 0197 strokePainter.shapePainter()->fillRect(rootOutline.boundingRect(), strokeSP->lineBrush()); 0198 maskStroke = KoShapeStrokeSP(new KoShapeStroke(*strokeSP.data())); 0199 maskStroke->setColor(Qt::white); 0200 maskStroke->setLineBrush(Qt::white); 0201 strokePainter.maskPainter()->fillPath(rootOutline, Qt::black); 0202 if (textRendering != OptimizeSpeed) { 0203 strokePainter.maskPainter()->setRenderHint(QPainter::Antialiasing, true); 0204 } else { 0205 strokePainter.maskPainter()->setRenderHint(QPainter::Antialiasing, false); 0206 } 0207 { 0208 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(chunk)); 0209 maskStroke->paint(shape.data(), *strokePainter.maskPainter()); 0210 } 0211 if (!textDecorationsRest.isEmpty()) { 0212 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(textDecorationsRest)); 0213 maskStroke->paint(shape.data(), *strokePainter.maskPainter()); 0214 } 0215 strokePainter.renderOnGlobalPainter(); 0216 } else { 0217 { 0218 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(chunk)); 0219 stroke->paint(shape.data(), painter); 0220 } 0221 if (!textDecorationsRest.isEmpty()) { 0222 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(textDecorationsRest)); 0223 stroke->paint(shape.data(), painter); 0224 } 0225 } 0226 } 0227 } 0228 } 0229 } 0230 } 0231 chunk = QPainterPath(); 0232 currentIndex = j; 0233 } 0234 } 0235 0236 if (it.state() == KisForestDetail::Leave) { 0237 inheritPaintProperties(it, stroke, background, paintOrder); 0238 QMap<KoSvgText::TextDecoration, QPainterPath> textDecorations = it->textDecorations; 0239 QColor textDecorationColor = it->properties.propertyOrDefault(KoSvgTextProperties::TextDecorationColorId).value<QColor>(); 0240 if (textDecorations.contains(KoSvgText::DecorationLineThrough)) { 0241 Q_FOREACH(const KoShape::PaintOrder p, paintOrder) { 0242 if (p == KoShape::Fill) { 0243 if (background && !textDecorationColor.isValid() && textDecorationColor != Qt::transparent) { 0244 background->paint(painter, textDecorations.value(KoSvgText::DecorationLineThrough)); 0245 } else if (textDecorationColor.isValid()) { 0246 painter.fillPath(textDecorations.value(KoSvgText::DecorationLineThrough), textDecorationColor); 0247 } 0248 } else if (p == KoShape::Stroke) { 0249 if (stroke) { 0250 QScopedPointer<KoShape> shape(KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationLineThrough))); 0251 stroke->paint(shape.data(), painter); 0252 } 0253 } 0254 } 0255 } 0256 } 0257 } 0258 } 0259 0260 // NOLINTNEXTLINE(readability-function-cognitive-complexity) 0261 QList<KoShape *> 0262 KoSvgTextShape::Private::collectPaths(const KoShape *rootShape, QVector<CharacterResult> &result, int ¤tIndex) 0263 { 0264 0265 QList<KoShape *> shapes; 0266 KoShapeStrokeModelSP stroke = rootShape->stroke(); 0267 QSharedPointer<KoShapeBackground> background = rootShape->background(); 0268 QVector<KoShape::PaintOrder> paintOrder = rootShape->paintOrder(); 0269 0270 for (auto it = compositionBegin(textData); it != compositionEnd(textData); it++) { 0271 bool hasPaintOrder = it->properties.hasProperty(KoSvgTextProperties::PaintOrder); 0272 inheritPaintProperties(it, stroke, background, paintOrder); 0273 QMap<KoSvgText::TextDecoration, QPainterPath> textDecorations = it->textDecorations; 0274 QColor textDecorationColor = it->properties.propertyOrDefault(KoSvgTextProperties::TextDecorationColorId).value<QColor>(); 0275 QSharedPointer<KoShapeBackground> decorationColor = background; 0276 if (textDecorationColor.isValid()) { 0277 decorationColor = QSharedPointer<KoColorBackground>(new KoColorBackground(textDecorationColor)); 0278 } 0279 0280 KoShape::PaintOrder first = paintOrder.at(0); 0281 KoShape::PaintOrder second = paintOrder.at(1); 0282 0283 if (it.state() == KisForestDetail::Enter) { 0284 0285 0286 if (textDecorations.contains(KoSvgText::DecorationUnderline)) { 0287 KoPathShape *shape = KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationUnderline)); 0288 shape->setBackground(decorationColor); 0289 shape->setStroke(stroke); 0290 shape->setZIndex(shapes.size()); 0291 shape->setFillRule(Qt::WindingFill); 0292 if (hasPaintOrder) 0293 shape->setPaintOrder(first, second); 0294 shapes.append(shape); 0295 } 0296 if (textDecorations.contains(KoSvgText::DecorationOverline)) { 0297 KoPathShape *shape = KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationOverline)); 0298 shape->setBackground(decorationColor); 0299 shape->setStroke(stroke); 0300 shape->setZIndex(shapes.size()); 0301 shape->setFillRule(Qt::WindingFill); 0302 if (hasPaintOrder) 0303 shape->setPaintOrder(first, second); 0304 shapes.append(shape); 0305 } 0306 0307 if (childCount(siblingCurrent(it)) == 0) { 0308 QPainterPath chunk; 0309 0310 const int j = currentIndex + it->numChars(true); 0311 for (int i = currentIndex; i < j; i++) { 0312 if (result.at(i).addressable && !result.at(i).hidden) { 0313 const QTransform tf = result.at(i).finalTransform(); 0314 if (const auto *colorGlyph = std::get_if<Glyph::ColorLayers>(&result.at(i).glyph)) { 0315 for (int c = 0; c < colorGlyph->paths.size(); c++) { 0316 QBrush color = colorGlyph->colors.at(c); 0317 bool replace = colorGlyph->replaceWithForeGroundColor.at(c); 0318 // In theory we can use the pattern or gradient as well 0319 // for ColorV0 fonts, but ColorV1 fonts can have 0320 // gradients, so I am hesitant. 0321 KoColorBackground *b = dynamic_cast<KoColorBackground *>(background.data()); 0322 if (b && replace) { 0323 color = b->brush(); 0324 } 0325 KoPathShape *shape = KoPathShape::createShapeFromPainterPath(tf.map(colorGlyph->paths.at(c))); 0326 shape->setBackground(QSharedPointer<KoColorBackground>(new KoColorBackground(color.color()))); 0327 shape->setZIndex(shapes.size()); 0328 shape->setFillRule(Qt::WindingFill); 0329 shape->setPaintOrder(first, second); 0330 shapes.append(shape); 0331 } 0332 } else if (const auto *outlineGlyph = std::get_if<Glyph::Outline>(&result.at(i).glyph)) { 0333 chunk.addPath(tf.map(outlineGlyph->path)); 0334 } 0335 } 0336 } 0337 KoPathShape *shape = KoPathShape::createShapeFromPainterPath(chunk); 0338 shape->setBackground(background); 0339 shape->setStroke(stroke); 0340 shape->setZIndex(shapes.size()); 0341 shape->setFillRule(Qt::WindingFill); 0342 if (hasPaintOrder) 0343 shape->setPaintOrder(first, second); 0344 shapes.append(shape); 0345 currentIndex = j; 0346 0347 } 0348 } 0349 if (it.state() ==KisForestDetail::Leave) { 0350 inheritPaintProperties(it, stroke, background, paintOrder); 0351 if (textDecorations.contains(KoSvgText::DecorationLineThrough)) { 0352 KoPathShape *shape = KoPathShape::createShapeFromPainterPath(textDecorations.value(KoSvgText::DecorationLineThrough)); 0353 shape->setBackground(decorationColor); 0354 shape->setStroke(stroke); 0355 shape->setZIndex(shapes.size()); 0356 shape->setFillRule(Qt::WindingFill); 0357 shape->setPaintOrder(first, second); 0358 shapes.append(shape); 0359 } 0360 } 0361 } 0362 return shapes; 0363 } 0364 0365 // NOLINTNEXTLINE(readability-function-cognitive-complexity) 0366 void KoSvgTextShape::Private::paintDebug(QPainter &painter, 0367 const QVector<CharacterResult> &result, 0368 int ¤tIndex) 0369 { 0370 0371 for (auto it = textData.depthFirstTailBegin(); it != textData.depthFirstTailEnd(); it++) { 0372 const int j = currentIndex + it->numChars(true); 0373 0374 const QRect shapeGlobalClipRect = painter.transform().mapRect(it->associatedOutline.boundingRect()).toAlignedRect(); 0375 0376 painter.save(); 0377 0378 QFont font(QFont(), painter.device()); 0379 font.setPointSizeF(16.0); 0380 0381 if (shapeGlobalClipRect.isValid() && childCount(siblingCurrent(it)) == 0) { 0382 for (int i = currentIndex; i < j; i++) { 0383 if (result.at(i).addressable && !result.at(i).hidden) { 0384 const QTransform tf = result.at(i).finalTransform(); 0385 0386 #if 1 // Debug: draw character bounding boxes 0387 painter.setBrush(Qt::transparent); 0388 QPen pen(QColor(0, 0, 0, 50)); 0389 pen.setCosmetic(true); 0390 pen.setWidth(2); 0391 painter.setPen(pen); 0392 if (const auto *bitmapGlyph = std::get_if<Glyph::Bitmap>(&result.at(i).glyph)) { 0393 painter.drawPolygon(tf.map(bitmapGlyph->drawRect)); 0394 } else if (const auto *colorGlyph = std::get_if<Glyph::ColorLayers>(&result.at(i).glyph)) { 0395 QRectF boundingRect; 0396 Q_FOREACH (const QPainterPath &p, colorGlyph->paths) { 0397 boundingRect |= p.boundingRect(); 0398 } 0399 painter.drawPolygon(tf.map(boundingRect)); 0400 } else if (const auto *outlineGlyph = std::get_if<Glyph::Outline>(&result.at(i).glyph)) { 0401 painter.drawPolygon(tf.map(outlineGlyph->path.boundingRect())); 0402 } 0403 QColor penColor = result.at(i).anchored_chunk ? result.at(i).isHanging ? Qt::red : Qt::magenta 0404 : result.at(i).lineEnd == LineEdgeBehaviour::NoChange ? Qt::cyan 0405 : Qt::yellow; 0406 penColor.setAlpha(192); 0407 pen.setColor(penColor); 0408 painter.setPen(pen); 0409 painter.drawPolygon(tf.map(result.at(i).boundingBox)); 0410 0411 penColor.setAlpha(96); 0412 pen.setColor(penColor); 0413 pen.setWidth(1); 0414 pen.setStyle(Qt::DotLine); 0415 painter.setPen(pen); 0416 painter.drawPolygon(tf.map(result.at(i).lineHeightBox)); 0417 0418 pen.setWidth(2); 0419 pen.setStyle(Qt::SolidLine); 0420 0421 const QPointF center = tf.mapRect(result.at(i).boundingBox).center(); 0422 QString text = "#"; 0423 text += QString::number(i); 0424 { 0425 // Find the range of this typographic character 0426 int end = i + 1; 0427 while (end < result.size() && result[end].middle) { 0428 end++; 0429 } 0430 end--; 0431 if (end != i) { 0432 text += "~"; 0433 text += QString::number(end); 0434 } 0435 } 0436 text += QString("\n(%1)").arg(result.at(i).plaintTextIndex); 0437 painter.setWorldMatrixEnabled(false); 0438 painter.setPen(Qt::red); 0439 painter.drawText(QRectF(painter.transform().map(center), QSizeF(0, 64)).adjusted(-128, 0, 128, 0), 0440 Qt::AlignHCenter | Qt::AlignTop, 0441 text); 0442 painter.setWorldMatrixEnabled(true); 0443 0444 pen.setWidth(6); 0445 const BreakType breakType = result.at(i).breakType; 0446 if (breakType == BreakType::SoftBreak || breakType == BreakType::HardBreak) { 0447 if (breakType == BreakType::SoftBreak) { 0448 penColor = Qt::blue; 0449 } else if (breakType == BreakType::HardBreak) { 0450 penColor = Qt::red; 0451 } 0452 penColor.setAlpha(128); 0453 pen.setColor(penColor); 0454 painter.setPen(pen); 0455 painter.drawPoint(center); 0456 } 0457 //ligature carets 0458 penColor = Qt::darkGreen; 0459 penColor.setAlpha(192); 0460 pen.setColor(penColor); 0461 painter.setPen(pen); 0462 QVector<QPointF> offset = result.at(i).cursorInfo.offsets; 0463 for (int k=0; k<offset.size(); k++) { 0464 painter.drawPoint(tf.map(offset.at(k))); 0465 } 0466 // Finalpos 0467 penColor = Qt::red; 0468 penColor.setAlpha(192); 0469 pen.setColor(penColor); 0470 painter.setPen(pen); 0471 painter.drawPoint(result.at(i).finalPosition); 0472 #endif 0473 } 0474 } 0475 } 0476 painter.restore(); 0477 currentIndex = j; 0478 } 0479 }