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 &currentGlyph,
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 &currentGlyph,
0360                                                CharacterResult &charResult) const;
0361 
0362     void clearAssociatedOutlines();
0363     void resolveTransforms(KisForest<KoSvgTextContentElement>::child_iterator currentTextElement, QString text, QVector<CharacterResult> &result, int &currentIndex, 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 &currentIndex, 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 &currentIndex,
0382                             qreal res,
0383                             bool isHorizontal);
0384     void handleLineBoxAlignment(KisForest<KoSvgTextContentElement>::child_iterator parent,
0385                             QVector<CharacterResult> &result, QVector<LineBox> lineBoxes,
0386                             int &currentIndex,
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 &currentIndex,
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 &currentIndex);
0419     QList<KoShape *> collectPaths(const KoShape *rootShape, QVector<CharacterResult> &result, int &currentIndex);
0420     void paintDebug(QPainter &painter,
0421                     const QVector<CharacterResult> &result,
0422                     int &currentIndex);
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