File indexing completed on 2024-06-16 04:11:25
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 #ifndef KO_SVG_TEXT_SHAPE_P_H 0009 #define KO_SVG_TEXT_SHAPE_P_H 0010 0011 #include "KoSvgTextShape.h" 0012 0013 #include "KoSvgText.h" 0014 #include "KoSvgTextContentElement.h" 0015 0016 #include <kis_assert.h> 0017 #include <KisForest.h> 0018 0019 #include <QFont> 0020 #include <QImage> 0021 #include <QLineF> 0022 #include <QPainterPath> 0023 #include <QPointF> 0024 #include <QRectF> 0025 #include <QVector> 0026 0027 #include <variant> 0028 0029 #include <ft2build.h> 0030 #include FT_FREETYPE_H 0031 0032 constexpr qreal SHAPE_PRECISION = 1e-6; ///< Value that indcates the precision for testing coordinates for text-in-shape layout. 0033 0034 class KoPathShape; 0035 struct raqm_glyph_t; 0036 0037 enum class BreakType { 0038 NoBreak, 0039 SoftBreak, 0040 HardBreak 0041 }; 0042 0043 enum class LineEdgeBehaviour { 0044 NoChange, ///< Do nothing special. 0045 Collapse, ///< Collapse if first or last in line. 0046 ForceHang, ///< Force hanging at the start or end of a line, never measured for justfication. 0047 ConditionallyHang ///< Only hang if no space otherwise, only measured for justification if not hanging. 0048 }; 0049 0050 struct CursorInfo { 0051 QLineF caret; ///< Caret for this characterResult 0052 QVector<int> graphemeIndices; ///< The text-string indices of graphemes starting here, starting grapheme is not present. 0053 QVector<QPointF> offsets; ///< The advance offsets for each grapheme index. 0054 bool rtl = false; ///< Whether the current glyph is right-to-left, as opposed to the markup. 0055 bool isWordBoundary = false; 0056 QColor color; ///< Which color the current position has. 0057 }; 0058 0059 struct CursorPos { 0060 int cluster; ///< Which character result this position belongs in. 0061 int index; ///< Which grapheme this position belongs with. 0062 int offset; ///< Which offset this position belongs with. 0063 bool synthetic = false; ///< Whether this position was inserted to have a visual indicator. 0064 }; 0065 0066 namespace Glyph { 0067 0068 struct Outline { 0069 QPainterPath path; 0070 }; 0071 0072 struct Bitmap { 0073 QImage image; 0074 QRectF drawRect; 0075 }; 0076 0077 struct ColorLayers { 0078 QVector<QPainterPath> paths; 0079 QVector<QBrush> colors; 0080 QVector<bool> replaceWithForeGroundColor; 0081 }; 0082 0083 using Variant = std::variant<std::monostate, Outline, Bitmap, ColorLayers>; 0084 0085 } // namespace Glyph 0086 0087 struct CharacterResult { 0088 QPointF finalPosition; ///< the final position, taking into account both CSS and SVG positioning considerations. 0089 qreal rotate = 0.0; 0090 bool hidden = false; // whether the character will be drawn. 0091 // The original svg specs' notion of addressable character relies on utf16, 0092 // but there's an issue to switch to unicode codepoints proper (that is, utf 32), though it was never applied. 0093 // https://github.com/w3c/svgwg/issues/537 0094 bool addressable = true; ///< whether the character is not discarded for various reasons, 0095 ///< this is necessary for determining to which index a given x/y transform is applied, 0096 ///< and in this code we also use it to determine if a character is considered for line-breaking. 0097 bool middle = false; ///< whether the character is the second of last of a 0098 ///< typographic character. 0099 bool anchored_chunk = false; ///< whether this is the start of a new chunk. 0100 0101 Glyph::Variant glyph; 0102 0103 QRectF boundingBox; 0104 int visualIndex = -1; 0105 int plaintTextIndex = -1; 0106 QPointF cssPosition = QPointF(); ///< the position in accordance with the CSS specs, as opossed to the SVG spec. 0107 QPointF baselineOffset = QPointF(); ///< The computed baseline offset, will be applied 0108 ///< when calculating the line-offset during line breaking. 0109 QPointF advance; 0110 BreakType breakType = BreakType::NoBreak; 0111 LineEdgeBehaviour lineEnd = LineEdgeBehaviour::NoChange; 0112 LineEdgeBehaviour lineStart = LineEdgeBehaviour::NoChange; 0113 bool justifyBefore = false;///< Justification Opportunity precedes this character. 0114 bool justifyAfter = false; ///< Justification Opportunity follows this character. 0115 bool isHanging = false; 0116 bool textLengthApplied = false; 0117 bool overflowWrap = false; 0118 0119 qreal fontHalfLeading; ///< Leading for both sides, can be either negative or positive. 0120 int fontAscent; ///< Ascender, in scanline coordinates 0121 int fontDescent; ///< Descender, in scanline coordinates 0122 qreal scaledHalfLeading{}; ///< Leading for both sides, can be either negative or positive, in pt 0123 qreal scaledAscent{}; ///< Ascender, in pt 0124 qreal scaledDescent{}; ///< Descender, in pt 0125 QRectF lineHeightBox; ///< The box representing the line height of this char 0126 QFont::Style fontStyle = QFont::StyleNormal; 0127 int fontWeight = 400; 0128 0129 CursorInfo cursorInfo; 0130 0131 KoSvgText::TextAnchor anchor = KoSvgText::AnchorStart; 0132 KoSvgText::Direction direction = KoSvgText::DirectionLeftToRight; 0133 0134 QTransform finalTransform() const { 0135 QTransform tf = 0136 QTransform::fromTranslate(finalPosition.x(), finalPosition.y()); 0137 tf.rotateRadians(rotate); 0138 return tf; 0139 } 0140 }; 0141 0142 struct LineChunk { 0143 QLineF length; 0144 QVector<int> chunkIndices; 0145 QRectF boundingBox; 0146 QPointF conditionalHangEnd = QPointF(); 0147 }; 0148 0149 /** 0150 * @brief The LineBox struct 0151 * 0152 * The line box struct is to simplify keeping track of lines inside the wrapping 0153 * functions. It somewhat corresponds to CSS line boxes, with the caveat that formally, 0154 * a line split in two in CSS/SVG would be two line boxes, while we instead have two 0155 * line chunks in a single line box. This is necessary to ensure we can calculate the 0156 * same line height for boxes split by a shape. 0157 * 0158 * CSS-Inline-3 defines Line Boxes here: https://www.w3.org/TR/css-inline-3/#line-box 0159 * CSS-Text-3 briefly talks about them here: https://www.w3.org/TR/css-text-3/#bidi-linebox 0160 * SVG-2 chapter text talks about them here: https://svgwg.org/svg2-draft/text.html#TextLayoutAutoNotes 0161 * 0162 * What is important to us is that all the above specifications, when they talk about Bidi-reordering, 0163 * agree that the order is dependant on the paragraph/block level direction, and is not affected by 0164 * the inline content changing direction. Which is good, because that'd make things super hard. 0165 */ 0166 struct LineBox { 0167 0168 LineBox() { 0169 } 0170 0171 LineBox(QPointF start, QPointF end) { 0172 LineChunk chunk; 0173 chunk.length = QLineF(start, end); 0174 chunks.append(chunk); 0175 currentChunk = 0; 0176 } 0177 0178 LineBox(QVector<QLineF> lineWidths, bool ltr, QPointF indent) { 0179 textIndent = indent; 0180 if (ltr) { 0181 Q_FOREACH(QLineF line, lineWidths) { 0182 LineChunk chunk; 0183 chunk.length = line; 0184 chunks.append(chunk); 0185 currentChunk = 0; 0186 } 0187 } else { 0188 Q_FOREACH(QLineF line, lineWidths) { 0189 LineChunk chunk; 0190 chunk.length = QLineF(line.p2(), line.p1()); 0191 chunks.insert(0, chunk); 0192 currentChunk = 0; 0193 } 0194 } 0195 } 0196 0197 QVector<LineChunk> chunks; 0198 int currentChunk = -1; 0199 0200 qreal expectedLineTop = 0; 0201 qreal actualLineTop = 0; 0202 qreal actualLineBottom = 0; 0203 0204 QPointF baselineTop = QPointF(); 0205 QPointF baselineBottom = QPointF(); 0206 0207 QPointF textIndent = QPointF(); 0208 bool firstLine = false; 0209 bool lastLine = false; 0210 bool lineFinalized = false; 0211 bool justifyLine = false; 0212 0213 LineChunk chunk() { 0214 return chunks.value(currentChunk); 0215 } 0216 0217 void setCurrentChunk(LineChunk chunk) { 0218 currentChunk = qMax(currentChunk, 0); 0219 if (currentChunk < chunks.size()) { 0220 chunks[currentChunk] = chunk; 0221 } else { 0222 chunks.append(chunk); 0223 } 0224 } 0225 0226 void clearAndAdjust(bool isHorizontal, QPointF current, QPointF indent) { 0227 actualLineBottom = 0; 0228 actualLineTop = 0; 0229 LineChunk chunk; 0230 textIndent = indent; 0231 QLineF length = chunks.at(currentChunk).length; 0232 if (isHorizontal) { 0233 length.setP1(QPointF(length.p1().x(), current.y())); 0234 length.setP2(QPointF(length.p2().x(), current.y())); 0235 } else { 0236 length.setP1(QPointF(current.x(), length.p1().y())); 0237 length.setP2(QPointF(current.x(), length.p2().y())); 0238 } 0239 chunks.clear(); 0240 currentChunk = 0; 0241 chunk.length = length; 0242 chunks.append(chunk); 0243 firstLine = false; 0244 } 0245 0246 void setCurrentChunkForPos(QPointF pos, bool isHorizontal) { 0247 for (int i=0; i<chunks.size(); i++) { 0248 LineChunk chunk = chunks.at(i); 0249 if (isHorizontal) { 0250 qreal min = qMin(chunk.length.p1().x(), chunk.length.p2().x()) - SHAPE_PRECISION; 0251 qreal max = qMax(chunk.length.p1().x(), chunk.length.p2().x()) + SHAPE_PRECISION; 0252 if ((pos.x() < max) && 0253 (pos.x() >= min)) { 0254 currentChunk = i; 0255 break; 0256 } 0257 } else { 0258 qreal min = qMin(chunk.length.p1().y(), chunk.length.p2().y()) - SHAPE_PRECISION; 0259 qreal max = qMax(chunk.length.p1().y(), chunk.length.p2().y()) + SHAPE_PRECISION; 0260 if ((pos.y() < max) && 0261 (pos.y() >= min)) { 0262 currentChunk = i; 0263 break; 0264 } 0265 } 0266 } 0267 } 0268 0269 bool isEmpty() { 0270 if (chunks.isEmpty()) return true; 0271 return chunks.at(currentChunk).chunkIndices.isEmpty(); 0272 } 0273 0274 }; 0275 0276 /** 0277 * A representation of a single leaf of the KisForest<KoTextContentElement> 0278 **/ 0279 struct SubChunk { 0280 0281 SubChunk(KisForest<KoSvgTextContentElement>::child_iterator leaf): 0282 associatedLeaf(leaf){ 0283 0284 } 0285 0286 QString text; 0287 QString originalText; 0288 KisForest<KoSvgTextContentElement>::child_iterator associatedLeaf; 0289 QVector<QPair<int, int>> newToOldPositions; ///< For transformed strings, we need to know which 0290 bool textInPath = false; 0291 bool firstTextInPath = false; ///< We need to mark the first text in path as an anchored chunk. 0292 ///< original index matches which new index; 0293 QSharedPointer<KoShapeBackground> bg; 0294 }; 0295 0296 class KRITAFLAKE_EXPORT KoSvgTextShape::Private 0297 { 0298 public: 0299 // NOTE: the cache data is shared between all the instances of 0300 // the shape, though it will be reset locally if the 0301 // accessing thread changes 0302 0303 Private() = default; 0304 0305 Private(const Private &rhs) 0306 : textData(rhs.textData) { 0307 Q_FOREACH (KoShape *shape, rhs.shapesInside) { 0308 KoShape *clonedShape = shape->cloneShape(); 0309 KIS_ASSERT_RECOVER(clonedShape) { continue; } 0310 0311 shapesInside.append(clonedShape); 0312 } 0313 Q_FOREACH (KoShape *shape, rhs.shapesSubtract) { 0314 KoShape *clonedShape = shape->cloneShape(); 0315 KIS_ASSERT_RECOVER(clonedShape) { continue; } 0316 0317 shapesSubtract.append(clonedShape); 0318 } 0319 textRendering = rhs.textRendering; 0320 yRes = rhs.yRes; 0321 xRes = rhs.xRes; 0322 result = rhs.result; 0323 lineBoxes = rhs.lineBoxes; 0324 }; 0325 0326 TextRendering textRendering = Auto; 0327 int xRes = 72; 0328 int yRes = 72; 0329 QList<KoShape*> shapesInside; 0330 QList<KoShape*> shapesSubtract; 0331 0332 KisForest<KoSvgTextContentElement> textData; 0333 0334 0335 QVector<CharacterResult> result; 0336 QVector<LineBox> lineBoxes; 0337 0338 QVector<CursorPos> cursorPos; 0339 QMap<int, int> logicalToVisualCursorPos; 0340 0341 QString plainText; 0342 bool isBidi = false; 0343 QPointF initialTextPosition = QPointF(); 0344 0345 void relayout(); 0346 0347 bool loadGlyph(const QTransform &ftTF, 0348 const QMap<int, KoSvgText::TabSizeInfo> &tabSizeInfo, 0349 FT_Int32 faceLoadFlags, 0350 bool isHorizontal, 0351 char32_t firstCodepoint, 0352 raqm_glyph_t ¤tGlyph, 0353 CharacterResult &charResult, 0354 QPointF &totalAdvanceFTFontCoordinates) const; 0355 0356 std::pair<QTransform, qreal> loadGlyphOnly(const QTransform &ftTF, 0357 FT_Int32 faceLoadFlags, 0358 bool isHorizontal, 0359 raqm_glyph_t ¤tGlyph, 0360 CharacterResult &charResult) const; 0361 0362 void clearAssociatedOutlines(); 0363 void resolveTransforms(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, QString text, QVector<CharacterResult> &result, int ¤tIndex, bool isHorizontal, bool wrapped, bool textInPath, QVector<KoSvgText::CharTransformation> &resolved, QVector<bool> collapsedChars); 0364 0365 void applyTextLength(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, QVector<CharacterResult> &result, int ¤tIndex, int &resolvedDescendentNodes, bool isHorizontal); 0366 static void applyAnchoring(QVector<CharacterResult> &result, bool isHorizontal); 0367 static qreal 0368 characterResultOnPath(CharacterResult &cr, qreal length, qreal offset, bool isHorizontal, bool isClosed); 0369 static QPainterPath stretchGlyphOnPath(const QPainterPath &glyph, 0370 const QPainterPath &path, 0371 bool isHorizontal, 0372 qreal offset, 0373 bool isClosed); 0374 static void applyTextPath(KisForest<KoSvgTextContentElement>::child_iterator parent, QVector<CharacterResult> &result, bool isHorizontal, QPointF &startPos); 0375 void computeFontMetrics(KisForest<KoSvgTextContentElement>::child_iterator parent, 0376 const QMap<int, int> &parentBaselineTable, 0377 qreal parentFontSize, 0378 QPointF superScript, 0379 QPointF subScript, 0380 QVector<CharacterResult> &result, 0381 int ¤tIndex, 0382 qreal res, 0383 bool isHorizontal); 0384 void handleLineBoxAlignment(KisForest<KoSvgTextContentElement>::child_iterator parent, 0385 QVector<CharacterResult> &result, QVector<LineBox> lineBoxes, 0386 int ¤tIndex, 0387 bool isHorizontal); 0388 void computeTextDecorations(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, 0389 const QVector<CharacterResult>& result, 0390 const QMap<int, int>& logicalToVisual, 0391 qreal minimumDecorationThickness, 0392 KoPathShape *textPath, 0393 qreal textPathoffset, 0394 bool side, 0395 int ¤tIndex, 0396 bool isHorizontal, 0397 bool ltr, 0398 bool wrapping); 0399 QMap<KoSvgText::TextDecoration, QPainterPath> generateDecorationPaths(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, 0400 const int &start, const int &end, 0401 const QVector<CharacterResult> &result, 0402 QPainterPathStroker &stroker, 0403 const bool isHorizontal, 0404 const KoSvgText::TextDecorations &decor, 0405 const qreal &minimumDecorationThickness, 0406 const KoSvgText::TextDecorationStyle style = KoSvgText::TextDecorationStyle::Solid, 0407 const bool textDecorationSkipInset = false, 0408 const KoPathShape *currentTextPath = nullptr, 0409 const qreal currentTextPathOffset = 0.0, 0410 const bool textPathSide = false, 0411 const KoSvgText::TextDecorationUnderlinePosition underlinePosH = KoSvgText::TextDecorationUnderlinePosition::UnderlineAuto, 0412 const KoSvgText::TextDecorationUnderlinePosition underlinePosV = KoSvgText::TextDecorationUnderlinePosition::UnderlineAuto); 0413 void paintPaths(QPainter &painter, 0414 const QPainterPath &outlineRect, 0415 const KoShape *rootShape, 0416 const QVector<CharacterResult> &result, 0417 QPainterPath &chunk, 0418 int ¤tIndex); 0419 QList<KoShape *> collectPaths(const KoShape *rootShape, QVector<CharacterResult> &result, int ¤tIndex); 0420 void paintDebug(QPainter &painter, 0421 const QVector<CharacterResult> &result, 0422 int ¤tIndex); 0423 0424 /// Get the number of characters for the whole subtree of this node. 0425 static int numChars(KisForest<KoSvgTextContentElement>::child_iterator parent, bool withControls = false) { 0426 int count = parent->numChars(withControls); 0427 for (auto it = KisForestDetail::childBegin(parent); it != KisForestDetail::childEnd(parent); it++) { 0428 count += numChars(it, withControls); 0429 } 0430 return count; 0431 } 0432 0433 /// Get the child count of the current node. A node without children is a text node. 0434 static int childCount(KisForest<KoSvgTextContentElement>::child_iterator it) { 0435 return std::distance(KisForestDetail::childBegin(it), KisForestDetail::childEnd(it)); 0436 } 0437 /** 0438 * Return a linearized representation of a subtree of text "subchunks". 0439 */ 0440 static QVector<SubChunk> collectSubChunks(KisForest<KoSvgTextContentElement>::child_iterator it, bool textInPath, bool &firstTextInPath); 0441 }; 0442 0443 #endif // KO_SVG_TEXT_SHAPE_P_H