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 }