File indexing completed on 2024-05-12 16:33:19

0001 /* This file is part of the KDE project
0002  * Copyright (C) 2007-2009,2011 Jan Hambrecht <jaham@gmx.net>
0003  * Copyright (C) 2008 Rob Buis <buis@kde.org>
0004  *
0005  * This library is free software; you can redistribute it and/or
0006  * modify it under the terms of the GNU Library General Public
0007  * License as published by the Free Software Foundation; either
0008  * version 2 of the License, or (at your option) any later version.
0009  *
0010  * This library is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013  * Library General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU Library General Public License
0016  * along with this library; see the file COPYING.LIB.  If not, write to
0017  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018  * Boston, MA 02110-1301, USA.
0019  */
0020 
0021 #include "ArtisticTextShape.h"
0022 #include "ArtisticTextLoadingContext.h"
0023 
0024 #include <KoPathShape.h>
0025 #include <KoShapeSavingContext.h>
0026 #include <KoShapeLoadingContext.h>
0027 #include <KoXmlNS.h>
0028 #include <KoXmlWriter.h>
0029 #include <KoXmlReader.h>
0030 #include <KoPathShapeLoader.h>
0031 #include <KoShapeBackground.h>
0032 #include <KoEmbeddedDocumentSaver.h>
0033 #include <SvgSavingContext.h>
0034 #include <SvgLoadingContext.h>
0035 #include <SvgGraphicContext.h>
0036 #include <SvgUtil.h>
0037 #include <SvgStyleParser.h>
0038 #include <SvgWriter.h>
0039 #include <SvgStyleWriter.h>
0040 
0041 #include <klocalizedstring.h>
0042 
0043 #include <QDebug>
0044 #include <QBuffer>
0045 #include <QPen>
0046 #include <QPainter>
0047 #include <QFont>
0048 
0049 ArtisticTextShape::ArtisticTextShape()
0050     : m_path(0), m_startOffset(0.0)
0051     , m_textAnchor( AnchorStart ), m_textUpdateCounter(0)
0052     , m_defaultFont("ComicSans", 20)
0053     , m_drawBoundaryLines(false)
0054 {
0055     setShapeId( ArtisticTextShapeID );
0056     updateSizeAndPosition();
0057 }
0058 
0059 ArtisticTextShape::~ArtisticTextShape()
0060 {
0061     if (m_path) {
0062         m_path->removeDependee( this );
0063     }
0064 }
0065 
0066 void ArtisticTextShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext)
0067 {
0068     applyConversion( painter, converter );
0069     if( background() ) {
0070         if (!m_drawBoundaryLines) {
0071             painter.setPen(Qt::NoPen);
0072         }
0073         background()->paint( painter, converter, paintContext, m_outline );
0074     }
0075 }
0076 
0077 void ArtisticTextShape::saveOdf(KoShapeSavingContext &context) const
0078 {
0079     SvgWriter svgWriter(QList<KoShape*>() << const_cast<ArtisticTextShape*>(this), size());
0080     QByteArray fileContent;
0081     QBuffer fileContentDevice(&fileContent);
0082     if (!fileContentDevice.open(QIODevice::WriteOnly))
0083         return;
0084 
0085     if(!svgWriter.save(fileContentDevice)) {
0086         qWarning() << "ArtisticTextShape::saveOdf: Could not write svg content";
0087         return;
0088     }
0089 
0090     const QString fileName = context.embeddedSaver().getFilename("SvgImages/Image");
0091     const QString mimeType = "image/svg+xml";
0092 
0093     context.xmlWriter().startElement("draw:frame");
0094     saveOdfAttributes(context, OdfAllAttributes);
0095     context.embeddedSaver().embedFile(context.xmlWriter(), "draw:image", fileName, mimeType.toLatin1(), fileContent);
0096     context.xmlWriter().endElement(); // draw:frame
0097 }
0098 
0099 bool ArtisticTextShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context)
0100 {
0101     return loadOdfAttributes(element, context, OdfAllAttributes);
0102 }
0103 
0104 QSizeF ArtisticTextShape::size() const
0105 {
0106     if( m_ranges.isEmpty() )
0107         return nullBoundBox().size();
0108     else
0109         return m_outline.boundingRect().size();
0110 }
0111 
0112 void ArtisticTextShape::setSize( const QSizeF &newSize )
0113 {
0114     QSizeF oldSize = size();
0115     if ( !oldSize.isNull() ) {
0116         qreal zoomX = newSize.width() / oldSize.width();
0117         qreal zoomY = newSize.height() / oldSize.height();
0118         QTransform matrix( zoomX, 0, 0, zoomY, 0, 0 );
0119 
0120         update();
0121         applyTransformation( matrix );
0122         update();
0123     }
0124     KoShape::setSize(newSize);
0125 }
0126 
0127 QPainterPath ArtisticTextShape::outline() const
0128 {
0129     return m_outline;
0130 }
0131 
0132 QRectF ArtisticTextShape::nullBoundBox() const
0133 {
0134     QFontMetrics metrics(defaultFont());
0135     QPointF tl(0.0, -metrics.ascent());
0136     QPointF br(metrics.averageCharWidth(), metrics.descent());
0137     return QRectF(tl, br);
0138 }
0139 
0140 QFont ArtisticTextShape::defaultFont() const
0141 {
0142     return m_defaultFont;
0143 }
0144 
0145 qreal baselineShiftForFontSize(const ArtisticTextRange &range, qreal fontSize)
0146 {
0147     switch(range.baselineShift()) {
0148     case ArtisticTextRange::Sub:
0149         return fontSize/3.; // taken from wikipedia
0150     case ArtisticTextRange::Super:
0151         return -fontSize/3.; // taken from wikipedia
0152     case ArtisticTextRange::Percent:
0153         return range.baselineShiftValue() * fontSize;
0154     case ArtisticTextRange::Length:
0155         return range.baselineShiftValue();
0156     default:
0157         return 0.0;
0158     }
0159 }
0160 
0161 QVector<QPointF> ArtisticTextShape::calculateAbstractCharacterPositions()
0162 {
0163     const int totalTextLength = plainText().length();
0164 
0165     QVector<QPointF> charPositions;
0166 
0167     // one more than the number of characters for position after the last character
0168     charPositions.resize(totalTextLength+1);
0169 
0170     // the character index within the text shape
0171     int globalCharIndex = 0;
0172 
0173     QPointF charPos(0, 0);
0174     QPointF advance(0, 0);
0175 
0176     const bool attachedToPath = isOnPath();
0177 
0178     foreach(const ArtisticTextRange &range, m_ranges) {
0179         QFontMetricsF metrics(QFont(range.font(), &m_paintDevice));
0180         const QString textRange = range.text();
0181         const qreal letterSpacing = range.letterSpacing();
0182         const int localTextLength = textRange.length();
0183 
0184         const bool absoluteXOffset = range.xOffsetType() == ArtisticTextRange::AbsoluteOffset;
0185         const bool absoluteYOffset = range.yOffsetType() == ArtisticTextRange::AbsoluteOffset;
0186 
0187         // set baseline shift
0188         const qreal baselineShift = baselineShiftForFontSize(range, defaultFont().pointSizeF());
0189 
0190         for(int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) {
0191             // apply offset to character
0192             if (range.hasXOffset(localCharIndex)) {
0193                 if (absoluteXOffset)
0194                     charPos.rx() = range.xOffset(localCharIndex);
0195                 else
0196                     charPos.rx() += range.xOffset(localCharIndex);
0197             } else {
0198                 charPos.rx() += advance.x();
0199             }
0200             if (range.hasYOffset(localCharIndex)) {
0201                 if (absoluteYOffset) {
0202                     // when attached to a path, absolute y-offsets are ignored
0203                     if (!attachedToPath)
0204                         charPos.ry() = range.yOffset(localCharIndex);
0205                 } else {
0206                     charPos.ry() += range.yOffset(localCharIndex);
0207                 }
0208             } else {
0209                 charPos.ry() += advance.y();
0210             }
0211 
0212             // apply baseline shift
0213             charPos.ry() += baselineShift;
0214 
0215             // save character position of current character
0216             charPositions[globalCharIndex] = charPos;
0217             // advance character position
0218             advance = QPointF(metrics.width(textRange[localCharIndex])+letterSpacing, 0.0);
0219 
0220             charPos.ry() -= baselineShift;
0221         }
0222     }
0223     charPositions[globalCharIndex] = charPos + advance;
0224 
0225     return charPositions;
0226 }
0227 
0228 void ArtisticTextShape::createOutline()
0229 {
0230     // reset relevant data
0231     m_outline = QPainterPath();
0232     m_charPositions.clear();
0233     m_charOffsets.clear();
0234     m_drawBoundaryLines = false;
0235 
0236     // calculate character positions in baseline coordinates
0237     m_charPositions = calculateAbstractCharacterPositions();
0238 
0239     // the character index within the text shape
0240     int globalCharIndex = 0;
0241 
0242     if( isOnPath() ) {
0243         m_drawBoundaryLines = true;
0244         // one more than the number of characters for offset after the last character
0245         m_charOffsets.insert(0, m_charPositions.size(), -1);
0246         // the current character position
0247         qreal startCharOffset = m_startOffset * m_baseline.length();
0248         // calculate total text width
0249         qreal totalTextWidth = 0.0;
0250         foreach (const ArtisticTextRange &range, m_ranges) {
0251             QFontMetricsF metrics(QFont(range.font(), &m_paintDevice));
0252             totalTextWidth += metrics.width(range.text());
0253         }
0254         // adjust starting character position to anchor point
0255         if( m_textAnchor == AnchorMiddle )
0256             startCharOffset -= 0.5 * totalTextWidth;
0257         else if( m_textAnchor == AnchorEnd )
0258             startCharOffset -= totalTextWidth;
0259 
0260         QPointF pathPoint;
0261         qreal rotation = 0.0;
0262         qreal charOffset;
0263 
0264         foreach (const ArtisticTextRange &range, m_ranges) {
0265             const QFont rangeFont(range.font(), &m_paintDevice);
0266             const QFontMetricsF metrics(rangeFont);
0267             const QString localText = range.text();
0268             const int localTextLength = localText.length();
0269 
0270             for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) {
0271                 QPointF charPos = m_charPositions[globalCharIndex];
0272 
0273                 // apply advance along baseline
0274                 charOffset = startCharOffset + charPos.x();
0275 
0276                 const qreal charMidPoint = charOffset + 0.5 * metrics.width(localText[localCharIndex]);
0277                 // get the normalized position of the middle of the character
0278                 const qreal midT = m_baseline.percentAtLength(charMidPoint);
0279                 // is the character midpoint beyond the baseline ends?
0280                 if (midT <= 0.0 || midT >= 1.0) {
0281                     if (midT >= 1.0) {
0282                         pathPoint = m_baseline.pointAtPercent(1.0);
0283                         for (int i = globalCharIndex; i < m_charPositions.size(); ++i) {
0284                             m_charPositions[i] = pathPoint;
0285                             m_charOffsets[i] = 1.0;
0286                         }
0287                         break;
0288                     } else {
0289                         m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(0.0);
0290                         m_charOffsets[globalCharIndex] = 0.0;
0291                         continue;
0292                     }
0293                 }
0294                 // get the percent value of the actual char position
0295                 qreal t = m_baseline.percentAtLength(charOffset);
0296 
0297                 // get the path point of the given path position
0298                 pathPoint = m_baseline.pointAtPercent(t);
0299 
0300                 // save character offset as fraction of baseline length
0301                 m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(charOffset);
0302                 // save character position as point
0303                 m_charPositions[globalCharIndex] = pathPoint;
0304 
0305                 // get the angle at the given path position
0306                 const qreal angle = m_baseline.angleAtPercent(midT);
0307                 if (range.hasRotation(localCharIndex))
0308                     rotation = range.rotation(localCharIndex);
0309 
0310                 QTransform m;
0311                 m.translate(pathPoint.x(), pathPoint.y());
0312                 m.rotate(360. - angle + rotation);
0313                 m.translate(0.0, charPos.y());
0314                 QPainterPath charOutline;
0315                 charOutline.addText(QPointF(), rangeFont, localText[localCharIndex]);
0316                 m_outline.addPath(m.map(charOutline));
0317             }
0318         }
0319         // save offset and position after last character
0320         m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(startCharOffset + m_charPositions[globalCharIndex].x());
0321         m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(m_charOffsets[globalCharIndex]);
0322     } else {
0323         foreach(const ArtisticTextRange &range, m_ranges) {
0324             const QString localText = range.text();
0325             const int localTextLength = localText.length();
0326             const QFont rangeFont(range.font(), &m_paintDevice);
0327             if (!range.hasRotations()) {
0328                 m_outline.addText(QPointF(), rangeFont, localText);
0329                 m_outline = m_outline.simplified();
0330                 continue;
0331             }
0332             qreal rotation = 0.0;
0333             m_drawBoundaryLines = true;
0334             for(int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) {
0335                 const QPointF &charPos = m_charPositions[globalCharIndex];
0336                 if (range.hasRotation(localCharIndex))
0337                     rotation = range.rotation(localCharIndex);
0338 
0339                 QTransform m;
0340                 m.translate(charPos.x(), charPos.y());
0341                 m.rotate(rotation);
0342                 QPainterPath charOutline;
0343                 charOutline.addText(QPointF(), rangeFont, localText[localCharIndex]);
0344                 m_outline.addPath(m.map(charOutline));
0345             }
0346         }
0347     }
0348 }
0349 
0350 void ArtisticTextShape::setPlainText(const QString &newText)
0351 {
0352     if( plainText() == newText )
0353         return;
0354 
0355     beginTextUpdate();
0356 
0357     if (newText.isEmpty()) {
0358         // remove all text ranges
0359         m_ranges.clear();
0360     } else if (isEmpty()) {
0361         // create new text range
0362         m_ranges.append(ArtisticTextRange(newText, defaultFont()));
0363     } else {
0364         // set text to first range
0365         m_ranges.first().setText(newText);
0366         // remove all ranges except the first
0367         while(m_ranges.count() > 1)
0368             m_ranges.pop_back();
0369     }
0370 
0371     finishTextUpdate();
0372 }
0373 
0374 QString ArtisticTextShape::plainText() const
0375 {
0376     QString allText;
0377     foreach(const ArtisticTextRange &range, m_ranges) {
0378         allText += range.text();
0379     }
0380 
0381     return allText;
0382 }
0383 
0384 QList<ArtisticTextRange> ArtisticTextShape::text() const
0385 {
0386     return m_ranges;
0387 }
0388 
0389 bool ArtisticTextShape::isEmpty() const
0390 {
0391     return m_ranges.isEmpty();
0392 }
0393 
0394 void ArtisticTextShape::clear()
0395 {
0396     beginTextUpdate();
0397     m_ranges.clear();
0398     finishTextUpdate();
0399 }
0400 
0401 void ArtisticTextShape::setFont(const QFont &newFont)
0402 {
0403     // no text
0404     if(isEmpty())
0405         return;
0406 
0407     const int rangeCount = m_ranges.count();
0408     // only one text range with the same font
0409     if(rangeCount == 1 && m_ranges.first().font() == newFont)
0410         return;
0411 
0412     beginTextUpdate();
0413 
0414     // set font on ranges
0415     for (int i = 0; i < rangeCount; ++i) {
0416         m_ranges[i].setFont(newFont);
0417     }
0418 
0419     m_defaultFont = newFont;
0420 
0421     finishTextUpdate();
0422 }
0423 
0424 void ArtisticTextShape::setFont(int charIndex, int charCount, const QFont &font)
0425 {
0426     if (isEmpty() || charCount <= 0)
0427         return;
0428 
0429     if (charIndex == 0 && charCount == plainText().length()) {
0430         setFont(font);
0431         return;
0432     }
0433 
0434     CharIndex charPos = indexOfChar(charIndex);
0435     if (charPos.first < 0 || charPos.first >= m_ranges.count())
0436         return;
0437 
0438     beginTextUpdate();
0439 
0440     int remainingCharCount = charCount;
0441     while(remainingCharCount > 0) {
0442         ArtisticTextRange &currRange = m_ranges[charPos.first];
0443         // does this range have a different font ?
0444         if (currRange.font() != font) {
0445             if (charPos.second == 0 && currRange.text().length() < remainingCharCount) {
0446                 // set font on all characters of this range
0447                 currRange.setFont(font);
0448                 remainingCharCount -= currRange.text().length();
0449             } else {
0450                 ArtisticTextRange changedRange = currRange.extract(charPos.second, remainingCharCount);
0451                 changedRange.setFont(font);
0452                 if (charPos.second == 0) {
0453                     m_ranges.insert(charPos.first, changedRange);
0454                 } else if (charPos.second >= currRange.text().length()) {
0455                     m_ranges.insert(charPos.first+1, changedRange);
0456                 } else {
0457                     ArtisticTextRange remainingRange = currRange.extract(charPos.second);
0458                     m_ranges.insert(charPos.first+1, changedRange);
0459                     m_ranges.insert(charPos.first+2, remainingRange);
0460                 }
0461                 charPos.first++;
0462                 remainingCharCount -= changedRange.text().length();
0463             }
0464         }
0465         charPos.first++;
0466         if(charPos.first >= m_ranges.count())
0467             break;
0468         charPos.second = 0;
0469     }
0470 
0471     finishTextUpdate();
0472 }
0473 
0474 QFont ArtisticTextShape::fontAt(int charIndex) const
0475 {
0476     if (isEmpty())
0477         return defaultFont();
0478     if (charIndex < 0)
0479         return m_ranges.first().font();
0480     const int rangeIndex = indexOfChar(charIndex).first;
0481     if (rangeIndex < 0)
0482         return m_ranges.last().font();
0483 
0484     return m_ranges[rangeIndex].font();
0485 }
0486 
0487 void ArtisticTextShape::setStartOffset( qreal offset )
0488 {
0489     if( m_startOffset == offset )
0490         return;
0491 
0492     update();
0493     m_startOffset = qBound<qreal>(0.0, offset, 1.0);
0494     updateSizeAndPosition();
0495     update();
0496     notifyChanged();
0497 }
0498 
0499 qreal ArtisticTextShape::startOffset() const
0500 {
0501     return m_startOffset;
0502 }
0503 
0504 qreal ArtisticTextShape::baselineOffset() const
0505 {
0506     return m_charPositions.value(0).y();
0507 }
0508 
0509 void ArtisticTextShape::setTextAnchor( TextAnchor anchor )
0510 {
0511     if( anchor == m_textAnchor )
0512         return;
0513 
0514     qreal totalTextWidth = 0.0;
0515     foreach (const ArtisticTextRange &range, m_ranges) {
0516         QFontMetricsF metrics(QFont(range.font(), &m_paintDevice));
0517         totalTextWidth += metrics.width(range.text());
0518     }
0519 
0520     qreal oldOffset = 0.0;
0521     if( m_textAnchor == AnchorMiddle )
0522         oldOffset = -0.5 * totalTextWidth;
0523     else if( m_textAnchor == AnchorEnd )
0524         oldOffset = -totalTextWidth;
0525 
0526     m_textAnchor = anchor;
0527 
0528     qreal newOffset = 0.0;
0529     if( m_textAnchor == AnchorMiddle )
0530         newOffset = -0.5 * totalTextWidth;
0531     else if( m_textAnchor == AnchorEnd )
0532         newOffset = -totalTextWidth;
0533 
0534 
0535     update();
0536     updateSizeAndPosition();
0537     if( ! isOnPath() ) {
0538         QTransform m;
0539         m.translate( newOffset-oldOffset, 0.0 );
0540         setTransformation( transformation() * m );
0541     }
0542     update();
0543     notifyChanged();
0544 }
0545 
0546 ArtisticTextShape::TextAnchor ArtisticTextShape::textAnchor() const
0547 {
0548     return m_textAnchor;
0549 }
0550 
0551 bool ArtisticTextShape::putOnPath( KoPathShape * path )
0552 {
0553     if( ! path )
0554         return false;
0555 
0556     if( path->outline().isEmpty() )
0557         return false;
0558 
0559     if( ! path->addDependee( this ) )
0560         return false;
0561 
0562     update();
0563 
0564     m_path = path;
0565 
0566     // use the paths outline converted to document coordinates as the baseline
0567     m_baseline = m_path->absoluteTransformation(0).map( m_path->outline() );
0568 
0569     // reset transformation
0570     setTransformation( QTransform() );
0571     updateSizeAndPosition();
0572     // move to correct position
0573     setAbsolutePosition( m_outlineOrigin, KoFlake::TopLeftCorner );
0574     update();
0575 
0576     return true;
0577 }
0578 
0579 bool ArtisticTextShape::putOnPath( const QPainterPath &path )
0580 {
0581     if( path.isEmpty() )
0582         return false;
0583 
0584     update();
0585     if( m_path )
0586         m_path->removeDependee( this );
0587     m_path = 0;
0588     m_baseline = path;
0589 
0590     // reset transformation
0591     setTransformation( QTransform() );
0592     updateSizeAndPosition();
0593     // move to correct position
0594     setAbsolutePosition( m_outlineOrigin, KoFlake::TopLeftCorner );
0595     update();
0596 
0597     return true;
0598 }
0599 
0600 void ArtisticTextShape::removeFromPath()
0601 {
0602     update();
0603     if( m_path )
0604         m_path->removeDependee( this );
0605     m_path = 0;
0606     m_baseline = QPainterPath();
0607     updateSizeAndPosition();
0608     update();
0609 }
0610 
0611 bool ArtisticTextShape::isOnPath() const
0612 {
0613     return (m_path != 0 || ! m_baseline.isEmpty() );
0614 }
0615 
0616 ArtisticTextShape::LayoutMode ArtisticTextShape::layout() const
0617 {
0618     if( m_path )
0619         return OnPathShape;
0620     else if( ! m_baseline.isEmpty() )
0621         return OnPath;
0622     else
0623         return Straight;
0624 }
0625 
0626 
0627 QPainterPath ArtisticTextShape::baseline() const
0628 {
0629     return m_baseline;
0630 }
0631 
0632 KoPathShape * ArtisticTextShape::baselineShape() const
0633 {
0634     return m_path;
0635 }
0636 
0637 QList<ArtisticTextRange> ArtisticTextShape::removeText(int charIndex, int charCount)
0638 {
0639     QList<ArtisticTextRange> extractedRanges;
0640     if (!charCount)
0641         return extractedRanges;
0642 
0643     if (charIndex == 0 && charCount >= plainText().length()) {
0644         beginTextUpdate();
0645         extractedRanges = m_ranges;
0646         m_ranges.clear();
0647         finishTextUpdate();
0648         return extractedRanges;
0649     }
0650 
0651     CharIndex charPos = indexOfChar(charIndex);
0652     if (charPos.first < 0 || charPos.first >= m_ranges.count())
0653         return extractedRanges;
0654 
0655     beginTextUpdate();
0656 
0657     int extractedTextLength = 0;
0658     while(extractedTextLength < charCount) {
0659         ArtisticTextRange r = m_ranges[charPos.first].extract(charPos.second, charCount-extractedTextLength);
0660         extractedTextLength += r.text().length();
0661         extractedRanges.append(r);
0662         if (extractedTextLength == charCount)
0663             break;
0664         charPos.first++;
0665         if(charPos.first >= m_ranges.count())
0666             break;
0667         charPos.second = 0;
0668     }
0669 
0670     // now remove all empty ranges
0671     const int rangeCount = m_ranges.count();
0672     for (int i = charPos.first; i < rangeCount; ++i) {
0673         if (m_ranges[charPos.first].text().isEmpty()) {
0674             m_ranges.removeAt(charPos.first);
0675         }
0676     }
0677 
0678     finishTextUpdate();
0679 
0680     return extractedRanges;
0681 }
0682 
0683 QList<ArtisticTextRange> ArtisticTextShape::copyText(int charIndex, int charCount)
0684 {
0685     QList<ArtisticTextRange> extractedRanges;
0686     if (!charCount)
0687         return extractedRanges;
0688 
0689     CharIndex charPos = indexOfChar(charIndex);
0690     if (charPos.first < 0 || charPos.first >= m_ranges.count())
0691         return extractedRanges;
0692 
0693     int extractedTextLength = 0;
0694     while(extractedTextLength < charCount) {
0695         ArtisticTextRange copy = m_ranges[charPos.first];
0696         ArtisticTextRange r = copy.extract(charPos.second, charCount-extractedTextLength);
0697         extractedTextLength += r.text().length();
0698         extractedRanges.append(r);
0699         if (extractedTextLength == charCount)
0700             break;
0701         charPos.first++;
0702         if(charPos.first >= m_ranges.count())
0703             break;
0704         charPos.second = 0;
0705     }
0706 
0707     return extractedRanges;
0708 }
0709 
0710 void ArtisticTextShape::insertText(int charIndex, const QString &str)
0711 {
0712     if (isEmpty()) {
0713         appendText(str);
0714         return;
0715     }
0716 
0717     CharIndex charPos = indexOfChar(charIndex);
0718     if (charIndex < 0) {
0719         // insert before first character
0720         charPos = CharIndex(0, 0);
0721     } else if (charIndex >= plainText().length()) {
0722         // insert after last character
0723         charPos = CharIndex(m_ranges.count()-1, m_ranges.last().text().length());
0724     }
0725 
0726     // check range index, just in case
0727     if (charPos.first < 0)
0728         return;
0729 
0730     beginTextUpdate();
0731 
0732     m_ranges[charPos.first].insertText(charPos.second, str);
0733 
0734     finishTextUpdate();
0735 }
0736 
0737 void ArtisticTextShape::insertText(int charIndex, const ArtisticTextRange &textRange)
0738 {
0739     QList<ArtisticTextRange> ranges;
0740     ranges.append(textRange);
0741     insertText(charIndex, ranges);
0742 }
0743 
0744 void ArtisticTextShape::insertText(int charIndex, const QList<ArtisticTextRange> &textRanges)
0745 {
0746     if (isEmpty()) {
0747         beginTextUpdate();
0748         m_ranges = textRanges;
0749         finishTextUpdate();
0750         return;
0751     }
0752 
0753     CharIndex charPos = indexOfChar(charIndex);
0754     if (charIndex < 0) {
0755         // insert before first character
0756         charPos = CharIndex(0, 0);
0757     } else if (charIndex >= plainText().length()) {
0758         // insert after last character
0759         charPos = CharIndex(m_ranges.count()-1, m_ranges.last().text().length());
0760     }
0761 
0762     // check range index, just in case
0763     if (charPos.first < 0)
0764         return;
0765 
0766     beginTextUpdate();
0767 
0768     ArtisticTextRange &hitRange = m_ranges[charPos.first];
0769     if (charPos.second == 0) {
0770         // insert ranges before the hit range
0771         foreach(const ArtisticTextRange &range, textRanges) {
0772             m_ranges.insert(charPos.first, range);
0773             charPos.first++;
0774         }
0775     } else if (charPos.second == hitRange.text().length()) {
0776         // insert ranges after the hit range
0777         foreach(const ArtisticTextRange &range, textRanges) {
0778             m_ranges.insert(charPos.first+1, range);
0779             charPos.first++;
0780         }
0781     } else {
0782         // insert ranges inside hit range
0783         ArtisticTextRange right = hitRange.extract(charPos.second, hitRange.text().length());
0784         m_ranges.insert(charPos.first+1, right);
0785         // now insert after the left part of hit range
0786         foreach(const ArtisticTextRange &range, textRanges) {
0787             m_ranges.insert(charPos.first+1, range);
0788             charPos.first++;
0789         }
0790     }
0791 
0792     for (auto it = m_ranges.begin(); it != m_ranges.end(); ++it) {
0793         for (auto itNext = it + 1; itNext != m_ranges.end();) {
0794             if (it->hasEqualStyle(*itNext)) {
0795                 it->appendText(itNext->text());
0796                 itNext = m_ranges.erase(itNext);
0797             } else {
0798                 ++itNext;
0799             }
0800         }
0801     }
0802 
0803     finishTextUpdate();
0804 }
0805 
0806 void ArtisticTextShape::appendText(const QString &text)
0807 {
0808     beginTextUpdate();
0809 
0810     if (isEmpty()) {
0811         m_ranges.append(ArtisticTextRange(text, defaultFont()));
0812     } else {
0813         m_ranges.last().appendText(text);
0814     }
0815 
0816     finishTextUpdate();
0817 }
0818 
0819 void ArtisticTextShape::appendText(const ArtisticTextRange &text)
0820 {
0821     beginTextUpdate();
0822 
0823     bool merged = false;
0824     for (ArtisticTextRange &textRange : m_ranges) {
0825         if (textRange.hasEqualStyle(text)) {
0826             textRange.appendText(text.text());
0827             merged = true;
0828             break;
0829         }
0830     }
0831 
0832     if (!merged) {
0833         m_ranges.append(text);
0834     }
0835 
0836     finishTextUpdate();
0837 }
0838 
0839 bool ArtisticTextShape::replaceText(int charIndex, int charCount, const ArtisticTextRange &textRange)
0840 {
0841     QList<ArtisticTextRange> ranges;
0842     ranges.append(textRange);
0843     return replaceText(charIndex, charCount, ranges);
0844 }
0845 
0846 bool ArtisticTextShape::replaceText(int charIndex, int charCount, const QList<ArtisticTextRange> &textRanges)
0847 {
0848     CharIndex charPos = indexOfChar(charIndex);
0849     if (charPos.first < 0 || !charCount)
0850         return false;
0851 
0852     beginTextUpdate();
0853 
0854     removeText(charIndex, charCount);
0855     insertText(charIndex, textRanges);
0856 
0857     finishTextUpdate();
0858 
0859     return true;
0860 }
0861 
0862 qreal ArtisticTextShape::charAngleAt(int charIndex) const
0863 {
0864     if( isOnPath() ) {
0865         qreal t = m_charOffsets.value(qBound(0, charIndex, m_charOffsets.size()-1));
0866         return m_baseline.angleAtPercent( t );
0867     }
0868 
0869     return 0.0;
0870 }
0871 
0872 QPointF ArtisticTextShape::charPositionAt(int charIndex) const
0873 {
0874     return m_charPositions.value(qBound(0, charIndex, m_charPositions.size()-1));
0875 }
0876 
0877 QRectF ArtisticTextShape::charExtentsAt(int charIndex) const
0878 {
0879     CharIndex charPos = indexOfChar(charIndex);
0880     if (charIndex < 0 || isEmpty())
0881         charPos = CharIndex(0,0);
0882     else if(charPos.first < 0)
0883         charPos = CharIndex(m_ranges.count()-1, m_ranges.last().text().length()-1);
0884 
0885     if (charPos.first < m_ranges.size()) {
0886         const ArtisticTextRange &range = m_ranges.at(charPos.first);
0887         QFontMetrics metrics(range.font());
0888         int w = metrics.charWidth(range.text(), charPos.second);
0889         return QRectF( 0, 0, w, metrics.height() );
0890     }
0891 
0892     return QRectF();
0893 }
0894 
0895 void ArtisticTextShape::updateSizeAndPosition( bool global )
0896 {
0897     QTransform shapeTransform = absoluteTransformation(0);
0898 
0899     // determine baseline position in document coordinates
0900     QPointF oldBaselinePosition = shapeTransform.map(QPointF(0, baselineOffset()));
0901 
0902     createOutline();
0903 
0904     QRectF bbox = m_outline.boundingRect();
0905     if( bbox.isEmpty() )
0906         bbox = nullBoundBox();
0907 
0908     if( isOnPath() ) {
0909         // calculate the offset we have to apply to keep our position
0910         QPointF offset = m_outlineOrigin - bbox.topLeft();
0911         // cache topleft corner of baseline path
0912         m_outlineOrigin = bbox.topLeft();
0913         // the outline position is in document coordinates
0914         // so we adjust our position
0915         QTransform m;
0916         m.translate( -offset.x(), -offset.y() );
0917         global ? applyAbsoluteTransformation( m ) : applyTransformation( m );
0918     } else {
0919         // determine the new baseline position in document coordinates
0920         QPointF newBaselinePosition = shapeTransform.map(QPointF(0, -bbox.top()));
0921         // apply a transformation to compensate any translation of
0922         // our baseline position
0923         QPointF delta = oldBaselinePosition - newBaselinePosition;
0924         QTransform m;
0925         m.translate( delta.x(), delta.y() );
0926         applyAbsoluteTransformation(m);
0927     }
0928 
0929     setSize( bbox.size() );
0930 
0931     // map outline to shape coordinate system
0932     QTransform normalizeMatrix;
0933     normalizeMatrix.translate( -bbox.left(), -bbox.top() );
0934     m_outline = normalizeMatrix.map( m_outline );
0935     const int charCount = m_charPositions.count();
0936     for (int i = 0; i < charCount; ++i)
0937         m_charPositions[i] = normalizeMatrix.map(m_charPositions[i]);
0938 }
0939 
0940 void ArtisticTextShape::shapeChanged(ChangeType type, KoShape *shape)
0941 {
0942     if( m_path && shape == m_path ) {
0943         if( type == KoShape::Deleted ) {
0944             // baseline shape was deleted
0945             m_path = 0;
0946         } else if (type == KoShape::ParentChanged && !shape->parent()) {
0947             // baseline shape was probably removed from the document
0948             m_path->removeDependee(this);
0949             m_path = 0;
0950         } else {
0951             update();
0952             // use the paths outline converted to document coordinates as the baseline
0953             m_baseline = m_path->absoluteTransformation(0).map( m_path->outline() );
0954             updateSizeAndPosition( true );
0955             update();
0956         }
0957     }
0958 }
0959 
0960 CharIndex ArtisticTextShape::indexOfChar(int charIndex) const
0961 {
0962     if (isEmpty())
0963         return CharIndex(-1,-1);
0964 
0965     int rangeIndex = 0;
0966     int textLength = 0;
0967     foreach(const ArtisticTextRange &range, m_ranges) {
0968         const int rangeTextLength = range.text().length();
0969         if (static_cast<int>(charIndex) < textLength+rangeTextLength) {
0970             return CharIndex(rangeIndex, charIndex-textLength);
0971         }
0972         textLength += rangeTextLength;
0973         rangeIndex++;
0974     }
0975 
0976     return CharIndex(-1, -1);
0977 }
0978 
0979 void ArtisticTextShape::beginTextUpdate()
0980 {
0981     if (m_textUpdateCounter)
0982         return;
0983 
0984     m_textUpdateCounter++;
0985     update();
0986 }
0987 
0988 void ArtisticTextShape::finishTextUpdate()
0989 {
0990     if(!m_textUpdateCounter)
0991         return;
0992 
0993     updateSizeAndPosition();
0994     update();
0995     notifyChanged();
0996 
0997     m_textUpdateCounter--;
0998 }
0999 
1000 bool ArtisticTextShape::saveSvg(SvgSavingContext &context)
1001 {
1002     context.shapeWriter().startElement("text", false);
1003     context.shapeWriter().addAttribute("id", context.getID(this));
1004 
1005     SvgStyleWriter::saveSvgStyle(this, context);
1006 
1007     const QList<ArtisticTextRange> formattedText = text();
1008 
1009     // if we have only a single text range, save the font on the text element
1010     const bool hasSingleRange = formattedText.size() == 1;
1011     if (hasSingleRange) {
1012         saveSvgFont(formattedText.first().font(), context);
1013     }
1014 
1015     qreal anchorOffset = 0.0;
1016     if (textAnchor() == ArtisticTextShape::AnchorMiddle) {
1017         anchorOffset += 0.5 * this->size().width();
1018         context.shapeWriter().addAttribute("text-anchor", "middle");
1019     } else if (textAnchor() == ArtisticTextShape::AnchorEnd) {
1020         anchorOffset += this->size().width();
1021         context.shapeWriter().addAttribute("text-anchor", "end");
1022     }
1023 
1024     // check if we are set on a path
1025     if (layout() == ArtisticTextShape::Straight) {
1026         context.shapeWriter().addAttributePt("x", anchorOffset);
1027         context.shapeWriter().addAttributePt("y", baselineOffset());
1028         context.shapeWriter().addAttribute("transform", SvgUtil::transformToString(transformation()));
1029         foreach(const ArtisticTextRange &range, formattedText) {
1030             saveSvgTextRange(range, context, !hasSingleRange, baselineOffset());
1031         }
1032     } else {
1033         KoPathShape * baselineShape = KoPathShape::createShapeFromPainterPath(baseline());
1034 
1035         QString id = context.createUID("baseline");
1036         context.styleWriter().startElement("path");
1037         context.styleWriter().addAttribute("id", id);
1038         context.styleWriter().addAttribute("d", baselineShape->toString(baselineShape->absoluteTransformation(0) * context.userSpaceTransform()));
1039         context.styleWriter().endElement();
1040 
1041         context.shapeWriter().startElement("textPath");
1042         context.shapeWriter().addAttribute("xlink:href", QLatin1Char('#')+id);
1043         if (startOffset() > 0.0)
1044             context.shapeWriter().addAttribute("startOffset", QString("%1%").arg(startOffset() * 100.0));
1045         foreach(const ArtisticTextRange &range, formattedText) {
1046             saveSvgTextRange(range, context, !hasSingleRange, baselineOffset());
1047         }
1048         context.shapeWriter().endElement();
1049 
1050         delete baselineShape;
1051     }
1052 
1053     context.shapeWriter().endElement();
1054 
1055     return true;
1056 }
1057 
1058 void ArtisticTextShape::saveSvgFont(const QFont &font, SvgSavingContext &context)
1059 {
1060     context.shapeWriter().addAttribute("font-family", font.family());
1061     context.shapeWriter().addAttributePt("font-size", font.pointSizeF());
1062 
1063     if (font.bold())
1064         context.shapeWriter().addAttribute("font-weight", "bold");
1065     if (font.italic())
1066         context.shapeWriter().addAttribute("font-style", "italic");
1067 }
1068 
1069 void ArtisticTextShape::saveSvgTextRange(const ArtisticTextRange &range, SvgSavingContext &context, bool saveRangeFont, qreal baselineOffset)
1070 {
1071     context.shapeWriter().startElement("tspan", false);
1072     if (range.hasXOffsets()) {
1073         const char *attributeName = (range.xOffsetType() == ArtisticTextRange::AbsoluteOffset ? "x" : "dx");
1074         QString attributeValue;
1075         int charIndex = 0;
1076         while(range.hasXOffset(charIndex)) {
1077             if (charIndex)
1078                 attributeValue += QLatin1Char(',');
1079             attributeValue += QString("%1").arg(SvgUtil::toUserSpace(range.xOffset(charIndex++)));
1080         }
1081         context.shapeWriter().addAttribute(attributeName, attributeValue);
1082     }
1083     if (range.hasYOffsets()) {
1084         if (range.yOffsetType() != ArtisticTextRange::AbsoluteOffset)
1085             baselineOffset = 0;
1086         const char *attributeName = (range.yOffsetType() == ArtisticTextRange::AbsoluteOffset ? " y" : " dy");
1087         QString attributeValue;
1088         int charIndex = 0;
1089         while(range.hasYOffset(charIndex)) {
1090             if (charIndex)
1091                 attributeValue += QLatin1Char(',');
1092             attributeValue += QString("%1").arg(SvgUtil::toUserSpace(baselineOffset+range.yOffset(charIndex++)));
1093         }
1094         context.shapeWriter().addAttribute(attributeName, attributeValue);
1095     }
1096     if (range.hasRotations()) {
1097         QString attributeValue;
1098         int charIndex = 0;
1099         while(range.hasRotation(charIndex)) {
1100             if (charIndex)
1101                 attributeValue += ',';
1102             attributeValue += QString("%1").arg(range.rotation(charIndex++));
1103         }
1104         context.shapeWriter().addAttribute("rotate", attributeValue);
1105     }
1106     if (range.baselineShift() != ArtisticTextRange::None) {
1107         switch(range.baselineShift()) {
1108         case ArtisticTextRange::Sub:
1109             context.shapeWriter().addAttribute("baseline-shift", "sub");
1110             break;
1111         case ArtisticTextRange::Super:
1112             context.shapeWriter().addAttribute("baseline-shift", "super");
1113             break;
1114         case ArtisticTextRange::Percent:
1115             context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(range.baselineShiftValue()*100));
1116             break;
1117         case ArtisticTextRange::Length:
1118             context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(SvgUtil::toUserSpace(range.baselineShiftValue())));
1119             break;
1120         default:
1121             break;
1122         }
1123     }
1124     if (saveRangeFont)
1125         saveSvgFont(range.font(), context);
1126     context.shapeWriter().addTextNode(range.text());
1127     context.shapeWriter().endElement();
1128 }
1129 
1130 bool ArtisticTextShape::loadSvg(const KoXmlElement &textElement, SvgLoadingContext &context)
1131 {
1132     clear();
1133 
1134     QString anchor;
1135     if (!textElement.attribute("text-anchor").isEmpty())
1136         anchor = textElement.attribute("text-anchor");
1137 
1138     SvgStyles elementStyles = context.styleParser().collectStyles(textElement);
1139     context.styleParser().parseFont(elementStyles);
1140 
1141     ArtisticTextLoadingContext textContext;
1142     textContext.parseCharacterTransforms(textElement, context.currentGC());
1143 
1144     KoXmlElement parentElement = textElement;
1145     // first check if we have a "textPath" child element
1146     for (KoXmlNode n = textElement.firstChild(); !n.isNull(); n = n.nextSibling()) {
1147         KoXmlElement e = n.toElement();
1148         if (e.tagName() == "textPath") {
1149             parentElement = e;
1150             break;
1151         }
1152     }
1153 
1154     KoPathShape *path = 0;
1155     bool pathInDocument = false;
1156     double offset = 0.0;
1157 
1158     const bool hasTextPathElement = parentElement != textElement && parentElement.hasAttribute("xlink:href");
1159     if (hasTextPathElement) {
1160         // create the referenced path shape
1161         context.pushGraphicsContext(parentElement);
1162         context.styleParser().parseFont(context.styleParser().collectStyles(parentElement));
1163         textContext.pushCharacterTransforms();
1164         textContext.parseCharacterTransforms(parentElement, context.currentGC());
1165 
1166         QString href = parentElement.attribute("xlink:href").mid(1);
1167         if (context.hasDefinition(href)) {
1168             const KoXmlElement &p = context.definition(href);
1169             // must be a path element as per svg spec
1170             if (p.tagName() == "path") {
1171                 pathInDocument = false;
1172                 path = new KoPathShape();
1173                 path->clear();
1174 
1175                 KoPathShapeLoader loader(path);
1176                 loader.parseSvg(p.attribute("d"), true);
1177                 path->setPosition(path->normalize());
1178 
1179                 QPointF newPosition = QPointF(SvgUtil::fromUserSpace(path->position().x()),
1180                                               SvgUtil::fromUserSpace(path->position().y()));
1181                 QSizeF newSize = QSizeF(SvgUtil::fromUserSpace(path->size().width()),
1182                                         SvgUtil::fromUserSpace(path->size().height()));
1183 
1184                 path->setSize(newSize);
1185                 path->setPosition(newPosition);
1186                 path->applyAbsoluteTransformation(SvgUtil::parseTransform(p.attribute("transform")));
1187             }
1188         } else {
1189             path = dynamic_cast<KoPathShape*>(context.shapeById(href));
1190             if (path) {
1191                 pathInDocument = true;
1192             }
1193         }
1194         // parse the start offset
1195         if (! parentElement.attribute("startOffset").isEmpty()) {
1196             QString start = parentElement.attribute("startOffset");
1197             if (start.endsWith('%'))
1198                 offset = 0.01 * start.remove('%').toDouble();
1199             else {
1200                 const float pathLength = path ? path->outline().length() : 0.0;
1201                 if (pathLength > 0.0)
1202                     offset = start.toDouble() / pathLength;
1203             }
1204         }
1205     }
1206 
1207     if (parentElement.hasChildNodes()) {
1208         // parse child elements
1209         parseTextRanges(parentElement, context, textContext);
1210         if (!context.currentGC()->preserveWhitespace) {
1211             const QString text = plainText();
1212             if (text.endsWith(' '))
1213                 removeText(text.length()-1, 1);
1214         }
1215         setPosition(textContext.textPosition());
1216     } else {
1217         // a single text range
1218         appendText(createTextRange(textElement.text(), textContext, context.currentGC()));
1219         setPosition(textContext.textPosition());
1220     }
1221 
1222     if (hasTextPathElement) {
1223         if (path) {
1224             if (pathInDocument) {
1225                 putOnPath(path);
1226             } else {
1227                 putOnPath(path->absoluteTransformation(0).map(path->outline()));
1228                 delete path;
1229             }
1230 
1231             if (offset > 0.0)
1232                 setStartOffset(offset);
1233         }
1234         textContext.popCharacterTransforms();
1235         context.popGraphicsContext();
1236     }
1237 
1238     // adjust position by baseline offset
1239     if (! isOnPath())
1240         setPosition(position() - QPointF(0, baselineOffset()));
1241 
1242     if (anchor == "middle")
1243         setTextAnchor(ArtisticTextShape::AnchorMiddle);
1244     else if (anchor == "end")
1245         setTextAnchor(ArtisticTextShape::AnchorEnd);
1246 
1247     return true;
1248 }
1249 
1250 void ArtisticTextShape::parseTextRanges(const KoXmlElement &element, SvgLoadingContext &context, ArtisticTextLoadingContext &textContext)
1251 {
1252     for (KoXmlNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) {
1253         KoXmlElement e = n.toElement();
1254         if (e.isNull()) {
1255             ArtisticTextRange range = createTextRange(n.toText().data(), textContext, context.currentGC());
1256             appendText(range);
1257         }
1258         else if (e.tagName() == "tspan") {
1259             SvgGraphicsContext *gc = context.pushGraphicsContext(e);
1260             context.styleParser().parseFont(context.styleParser().collectStyles(e));
1261             textContext.pushCharacterTransforms();
1262             textContext.parseCharacterTransforms(e, gc);
1263             parseTextRanges(e, context, textContext);
1264             textContext.popCharacterTransforms();
1265             context.popGraphicsContext();
1266         }
1267         else if (e.tagName() == "tref") {
1268             if (e.attribute("xlink:href").isEmpty())
1269                 continue;
1270 
1271             QString href = e.attribute("xlink:href").mid(1);
1272             ArtisticTextShape *refText = dynamic_cast<ArtisticTextShape*>(context.shapeById(href));
1273             if (refText) {
1274                 foreach (const ArtisticTextRange &range, refText->text()) {
1275                     appendText(range);
1276                 }
1277             } else if (context.hasDefinition(href)) {
1278                 const KoXmlElement &p = context.definition(href);
1279                 SvgGraphicsContext *gc = context.currentGC();
1280                 appendText(ArtisticTextRange(textContext.simplifyText(p.text(), gc->preserveWhitespace), gc->font));
1281             }
1282         }
1283         else {
1284             continue;
1285         }
1286     }
1287 }
1288 
1289 ArtisticTextRange ArtisticTextShape::createTextRange(const QString &text, ArtisticTextLoadingContext &context, SvgGraphicsContext *gc)
1290 {
1291     ArtisticTextRange range(context.simplifyText(text, gc->preserveWhitespace), gc->font);
1292 
1293     const int textLength = range.text().length();
1294     switch(context.xOffsetType()) {
1295     case ArtisticTextLoadingContext::Absolute:
1296         range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::AbsoluteOffset);
1297         break;
1298     case ArtisticTextLoadingContext::Relative:
1299         range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::RelativeOffset);
1300         break;
1301     default:
1302         // no x-offsets
1303         break;
1304     }
1305     switch(context.yOffsetType()) {
1306     case ArtisticTextLoadingContext::Absolute:
1307         range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::AbsoluteOffset);
1308         break;
1309     case ArtisticTextLoadingContext::Relative:
1310         range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::RelativeOffset);
1311         break;
1312     default:
1313         // no y-offsets
1314         break;
1315     }
1316 
1317     range.setRotations(context.rotations(textLength));
1318     range.setLetterSpacing(gc->letterSpacing);
1319     range.setWordSpacing(gc->wordSpacing);
1320     if(gc->baselineShift == "sub") {
1321         range.setBaselineShift(ArtisticTextRange::Sub);
1322     } else if(gc->baselineShift == "super") {
1323         range.setBaselineShift(ArtisticTextRange::Super);
1324     } else if(gc->baselineShift.endsWith('%')) {
1325         range.setBaselineShift(ArtisticTextRange::Percent, SvgUtil::fromPercentage(gc->baselineShift));
1326     } else {
1327         qreal value = SvgUtil::parseUnitX(gc, gc->baselineShift);
1328         if (value != 0.0)
1329             range.setBaselineShift(ArtisticTextRange::Length, value);
1330     }
1331 
1332     //range.printDebug();
1333 
1334     return range;
1335 }