diff --git a/plugins/flake/artistictextshape/ArtisticTextShape.cpp b/plugins/flake/artistictextshape/ArtisticTextShape.cpp index 0cd1f7338b..b4e694f972 100644 --- a/plugins/flake/artistictextshape/ArtisticTextShape.cpp +++ b/plugins/flake/artistictextshape/ArtisticTextShape.cpp @@ -1,1375 +1,1409 @@ /* This file is part of the KDE project * Copyright (C) 2007-2009,2011 Jan Hambrecht * Copyright (C) 2008 Rob Buis * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ArtisticTextShape.h" #include "ArtisticTextLoadingContext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include + ArtisticTextShape::ArtisticTextShape() : m_path(0) , m_startOffset(0.0) , m_textAnchor(AnchorStart) , m_textUpdateCounter(0) , m_defaultFont("ComicSans", 20) { setShapeId(ArtisticTextShapeID); - cacheGlyphOutlines(); updateSizeAndPosition(); } ArtisticTextShape::~ArtisticTextShape() { if (m_path) { m_path->removeDependee(this); } } void ArtisticTextShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { applyConversion(painter, converter); if (background()) { background()->paint(painter, converter, paintContext, outline()); } } void ArtisticTextShape::saveOdf(KoShapeSavingContext &context) const { SvgWriter svgWriter(QList() << const_cast(this), size()); QByteArray fileContent; QBuffer fileContentDevice(&fileContent); if (!fileContentDevice.open(QIODevice::WriteOnly)) { return; } if (!svgWriter.save(fileContentDevice)) { qWarning() << "Could not write svg content"; return; } const QString fileName = context.embeddedSaver().getFilename("SvgImages/Image"); const QString mimeType = "image/svg+xml"; context.xmlWriter().startElement("draw:frame"); context.embeddedSaver().embedFile(context.xmlWriter(), "draw:image", fileName, mimeType.toLatin1(), fileContent); context.xmlWriter().endElement(); // draw:frame } bool ArtisticTextShape::loadOdf(const KoXmlElement &/*element*/, KoShapeLoadingContext &/*context*/) { return false; } QSizeF ArtisticTextShape::size() const { if (m_ranges.isEmpty()) { return nullBoundBox().size(); } else { return outline().boundingRect().size(); } } void ArtisticTextShape::setSize(const QSizeF &newSize) { QSizeF oldSize = size(); if (!oldSize.isNull()) { qreal zoomX = newSize.width() / oldSize.width(); qreal zoomY = newSize.height() / oldSize.height(); QTransform matrix(zoomX, 0, 0, zoomY, 0, 0); update(); applyTransformation(matrix); update(); } KoShape::setSize(newSize); } QPainterPath ArtisticTextShape::outline() const { return m_outline; } QRectF ArtisticTextShape::nullBoundBox() const { QFontMetrics metrics(defaultFont()); QPointF tl(0.0, -metrics.ascent()); QPointF br(metrics.averageCharWidth(), metrics.descent()); return QRectF(tl, br); } QFont ArtisticTextShape::defaultFont() const { return m_defaultFont; } qreal baselineShiftForFontSize(const ArtisticTextRange &range, qreal fontSize) { switch (range.baselineShift()) { case ArtisticTextRange::Sub: return fontSize / 3.; // taken from wikipedia case ArtisticTextRange::Super: return -fontSize / 3.; // taken from wikipedia case ArtisticTextRange::Percent: return range.baselineShiftValue() * fontSize; case ArtisticTextRange::Length: return range.baselineShiftValue(); default: return 0.0; } } QVector ArtisticTextShape::calculateAbstractCharacterPositions() { const int totalTextLength = plainText().length(); QVector charPositions; // one more than the number of characters for position after the last character charPositions.resize(totalTextLength + 1); // the character index within the text shape int globalCharIndex = 0; QPointF charPos(0, 0); QPointF advance(0, 0); const bool attachedToPath = isOnPath(); Q_FOREACH (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); const QString textRange = range.text(); const qreal letterSpacing = range.letterSpacing(); const int localTextLength = textRange.length(); const bool absoluteXOffset = range.xOffsetType() == ArtisticTextRange::AbsoluteOffset; const bool absoluteYOffset = range.yOffsetType() == ArtisticTextRange::AbsoluteOffset; // set baseline shift const qreal baselineShift = baselineShiftForFontSize(range, defaultFont().pointSizeF()); for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { // apply offset to character if (range.hasXOffset(localCharIndex)) { if (absoluteXOffset) { charPos.rx() = range.xOffset(localCharIndex); } else { charPos.rx() += range.xOffset(localCharIndex); } } else { charPos.rx() += advance.x(); } if (range.hasYOffset(localCharIndex)) { if (absoluteYOffset) { // when attached to a path, absolute y-offsets are ignored if (!attachedToPath) { charPos.ry() = range.yOffset(localCharIndex); } } else { charPos.ry() += range.yOffset(localCharIndex); } } else { charPos.ry() += advance.y(); } // apply baseline shift charPos.ry() += baselineShift; // save character position of current character charPositions[globalCharIndex] = charPos; // advance character position advance = QPointF(metrics.width(textRange[localCharIndex]) + letterSpacing, 0.0); charPos.ry() -= baselineShift; } } charPositions[globalCharIndex] = charPos + advance; return charPositions; } void ArtisticTextShape::createOutline() { // reset relevant data m_outline = QPainterPath(); - m_charPositions.clear(); m_charOffsets.clear(); - - // calculate character positions in baseline coordinates - m_charPositions = calculateAbstractCharacterPositions(); + cacheGlyphOutlines(); // the character index within the text shape int globalCharIndex = 0; if (isOnPath()) { // one more than the number of characters for offset after the last character m_charOffsets.insert(0, m_charPositions.size(), -1); // the current character position qreal startCharOffset = m_startOffset * m_baseline.length(); // calculate total text width qreal totalTextWidth = 0.0; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); totalTextWidth += metrics.width(range.text()); } // adjust starting character position to anchor point if (m_textAnchor == AnchorMiddle) { startCharOffset -= 0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { startCharOffset -= totalTextWidth; } QPointF pathPoint; qreal rotation = 0.0; qreal charOffset; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); const QString localText = range.text(); const int localTextLength = localText.length(); for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { QPointF charPos = m_charPositions[globalCharIndex]; // apply advance along baseline charOffset = startCharOffset + charPos.x(); const qreal charMidPoint = charOffset + 0.5 * metrics.width(localText[localCharIndex]); // get the normalized position of the middle of the character const qreal midT = m_baseline.percentAtLength(charMidPoint); // is the character midpoint beyond the baseline ends? if (midT <= 0.0 || midT >= 1.0) { if (midT >= 1.0) { pathPoint = m_baseline.pointAtPercent(1.0); for (int i = globalCharIndex; i < m_charPositions.size(); ++i) { m_charPositions[i] = pathPoint; m_charOffsets[i] = 1.0; } break; } else { m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(0.0); m_charOffsets[globalCharIndex] = 0.0; continue; } } // get the percent value of the actual char position qreal t = m_baseline.percentAtLength(charOffset); // get the path point of the given path position pathPoint = m_baseline.pointAtPercent(t); // save character offset as fraction of baseline length m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(charOffset); // save character position as point m_charPositions[globalCharIndex] = pathPoint; // get the angle at the given path position const qreal angle = m_baseline.angleAtPercent(midT); if (range.hasRotation(localCharIndex)) { rotation = range.rotation(localCharIndex); } QTransform m; m.translate(pathPoint.x(), pathPoint.y()); m.rotate(360. - angle + rotation); m.translate(0.0, charPos.y()); m_outline.addPath(m.map(m_charOutlines[globalCharIndex])); } } // save offset and position after last character m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(startCharOffset + m_charPositions[globalCharIndex].x()); m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(m_charOffsets[globalCharIndex]); } else { qreal rotation = 0.0; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const QString textRange = range.text(); const int localTextLength = textRange.length(); - for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { + for (int localCharIndex = 0; localCharIndex < m_charOutlines.size(); ++localCharIndex, ++globalCharIndex) { const QPointF &charPos = m_charPositions[globalCharIndex]; if (range.hasRotation(localCharIndex)) { rotation = range.rotation(localCharIndex); } QTransform m; m.translate(charPos.x(), charPos.y()); m.rotate(rotation); m_outline.addPath(m.map(m_charOutlines[globalCharIndex])); } } } } void ArtisticTextShape::setPlainText(const QString &newText) { if (plainText() == newText) { return; } beginTextUpdate(); if (newText.isEmpty()) { // remove all text ranges m_ranges.clear(); } else if (isEmpty()) { // create new text range m_ranges.append(ArtisticTextRange(newText, defaultFont())); } else { // set text to first range m_ranges.first().setText(newText); // remove all ranges except the first while (m_ranges.count() > 1) { m_ranges.pop_back(); } } finishTextUpdate(); } QString ArtisticTextShape::plainText() const { QString allText; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { allText += range.text(); } return allText; } +int ArtisticTextShape::glyphsIndexSize() const +{ + return m_charOutlines.size(); +} + QList ArtisticTextShape::text() const { return m_ranges; } bool ArtisticTextShape::isEmpty() const { return m_ranges.isEmpty(); } void ArtisticTextShape::clear() { beginTextUpdate(); m_ranges.clear(); finishTextUpdate(); } void ArtisticTextShape::setFont(const QFont &newFont) { // no text if (isEmpty()) { return; } const int rangeCount = m_ranges.count(); // only one text range with the same font if (rangeCount == 1 && m_ranges.first().font() == newFont) { return; } beginTextUpdate(); // set font on ranges for (int i = 0; i < rangeCount; ++i) { m_ranges[i].setFont(newFont); } m_defaultFont = newFont; finishTextUpdate(); } void ArtisticTextShape::setFont(int charIndex, int charCount, const QFont &font) { if (isEmpty() || charCount <= 0) { return; } - if (charIndex == 0 && charCount == plainText().length()) { + if (charIndex == 0 && charCount == glyphsIndexSize()) { setFont(font); return; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return; } beginTextUpdate(); int remainingCharCount = charCount; while (remainingCharCount > 0) { ArtisticTextRange &currRange = m_ranges[charPos.first]; // does this range have a different font ? if (currRange.font() != font) { if (charPos.second == 0 && currRange.text().length() < remainingCharCount) { // set font on all characters of this range currRange.setFont(font); remainingCharCount -= currRange.text().length(); } else { ArtisticTextRange changedRange = currRange.extract(charPos.second, remainingCharCount); changedRange.setFont(font); if (charPos.second == 0) { m_ranges.insert(charPos.first, changedRange); } else if (charPos.second >= currRange.text().length()) { m_ranges.insert(charPos.first + 1, changedRange); } else { ArtisticTextRange remainingRange = currRange.extract(charPos.second); m_ranges.insert(charPos.first + 1, changedRange); m_ranges.insert(charPos.first + 2, remainingRange); } charPos.first++; remainingCharCount -= changedRange.text().length(); } } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } finishTextUpdate(); } QFont ArtisticTextShape::fontAt(int charIndex) const { if (isEmpty()) { return defaultFont(); } if (charIndex < 0) { return m_ranges.first().font(); } const int rangeIndex = indexOfChar(charIndex).first; if (rangeIndex < 0) { return m_ranges.last().font(); } return m_ranges[rangeIndex].font(); } void ArtisticTextShape::setStartOffset(qreal offset) { if (m_startOffset == offset) { return; } update(); m_startOffset = qBound(0.0, offset, 1.0); updateSizeAndPosition(); update(); notifyChanged(); } qreal ArtisticTextShape::startOffset() const { return m_startOffset; } qreal ArtisticTextShape::baselineOffset() const { return m_charPositions.value(0).y(); } void ArtisticTextShape::setTextAnchor(TextAnchor anchor) { if (anchor == m_textAnchor) { return; } qreal totalTextWidth = 0.0; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); totalTextWidth += metrics.width(range.text()); } qreal oldOffset = 0.0; if (m_textAnchor == AnchorMiddle) { oldOffset = -0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { oldOffset = -totalTextWidth; } m_textAnchor = anchor; qreal newOffset = 0.0; if (m_textAnchor == AnchorMiddle) { newOffset = -0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { newOffset = -totalTextWidth; } update(); updateSizeAndPosition(); if (! isOnPath()) { QTransform m; m.translate(newOffset - oldOffset, 0.0); setTransformation(transformation() * m); } update(); notifyChanged(); } ArtisticTextShape::TextAnchor ArtisticTextShape::textAnchor() const { return m_textAnchor; } bool ArtisticTextShape::putOnPath(KoPathShape *path) { if (! path) { return false; } if (path->outline().isEmpty()) { return false; } if (! path->addDependee(this)) { return false; } update(); m_path = path; // use the paths outline converted to document coordinates as the baseline m_baseline = m_path->absoluteTransformation(0).map(m_path->outline()); // reset transformation setTransformation(QTransform()); updateSizeAndPosition(); // move to correct position setAbsolutePosition(m_outlineOrigin, KoFlake::TopLeftCorner); update(); return true; } bool ArtisticTextShape::putOnPath(const QPainterPath &path) { if (path.isEmpty()) { return false; } update(); if (m_path) { m_path->removeDependee(this); } m_path = 0; m_baseline = path; // reset transformation setTransformation(QTransform()); updateSizeAndPosition(); // move to correct position setAbsolutePosition(m_outlineOrigin, KoFlake::TopLeftCorner); update(); return true; } void ArtisticTextShape::removeFromPath() { update(); if (m_path) { m_path->removeDependee(this); } m_path = 0; m_baseline = QPainterPath(); updateSizeAndPosition(); update(); } bool ArtisticTextShape::isOnPath() const { return (m_path != 0 || ! m_baseline.isEmpty()); } ArtisticTextShape::LayoutMode ArtisticTextShape::layout() const { if (m_path) { return OnPathShape; } else if (! m_baseline.isEmpty()) { return OnPath; } else { return Straight; } } QPainterPath ArtisticTextShape::baseline() const { return m_baseline; } KoPathShape *ArtisticTextShape::baselineShape() const { return m_path; } QList ArtisticTextShape::removeText(int charIndex, int charCount) { QList extractedRanges; if (!charCount) { return extractedRanges; } - if (charIndex == 0 && charCount >= plainText().length()) { + if (charIndex == 0 && charCount >= glyphsIndexSize()) { beginTextUpdate(); extractedRanges = m_ranges; m_ranges.clear(); finishTextUpdate(); return extractedRanges; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return extractedRanges; } beginTextUpdate(); int extractedTextLength = 0; while (extractedTextLength < charCount) { ArtisticTextRange r = m_ranges[charPos.first].extract(charPos.second, charCount - extractedTextLength); extractedTextLength += r.text().length(); extractedRanges.append(r); if (extractedTextLength == charCount) { break; } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } // now remove all empty ranges const int rangeCount = m_ranges.count(); for (int i = charPos.first; i < rangeCount; ++i) { if (m_ranges[charPos.first].text().isEmpty()) { m_ranges.removeAt(charPos.first); } } finishTextUpdate(); return extractedRanges; } QList ArtisticTextShape::copyText(int charIndex, int charCount) { QList extractedRanges; if (!charCount) { return extractedRanges; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return extractedRanges; } int extractedTextLength = 0; while (extractedTextLength < charCount) { ArtisticTextRange copy = m_ranges[charPos.first]; ArtisticTextRange r = copy.extract(charPos.second, charCount - extractedTextLength); extractedTextLength += r.text().length(); extractedRanges.append(r); if (extractedTextLength == charCount) { break; } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } return extractedRanges; } void ArtisticTextShape::insertText(int charIndex, const QString &str) { if (isEmpty()) { appendText(str); return; } CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0) { // insert before first character charPos = CharIndex(0, 0); - } else if (charIndex >= plainText().length()) { + } else if (charIndex >= glyphsIndexSize()) { // insert after last character charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length()); } // check range index, just in case if (charPos.first < 0) { return; } beginTextUpdate(); m_ranges[charPos.first].insertText(charPos.second, str); finishTextUpdate(); } void ArtisticTextShape::insertText(int charIndex, const ArtisticTextRange &textRange) { QList ranges; ranges.append(textRange); insertText(charIndex, ranges); } void ArtisticTextShape::insertText(int charIndex, const QList &textRanges) { if (isEmpty()) { beginTextUpdate(); m_ranges = textRanges; finishTextUpdate(); return; } CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0) { // insert before first character charPos = CharIndex(0, 0); - } else if (charIndex >= plainText().length()) { + } else if (charIndex >= glyphsIndexSize()) { // insert after last character charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length()); } // check range index, just in case if (charPos.first < 0) { return; } beginTextUpdate(); ArtisticTextRange &hitRange = m_ranges[charPos.first]; if (charPos.second == 0) { // insert ranges before the hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first, range); charPos.first++; } } else if (charPos.second == hitRange.text().length()) { // insert ranges after the hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first + 1, range); charPos.first++; } } else { // insert ranges inside hit range ArtisticTextRange right = hitRange.extract(charPos.second, hitRange.text().length()); m_ranges.insert(charPos.first + 1, right); // now insert after the left part of hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first + 1, range); charPos.first++; } } // TODO: merge ranges with same style finishTextUpdate(); } void ArtisticTextShape::appendText(const QString &text) { beginTextUpdate(); if (isEmpty()) { m_ranges.append(ArtisticTextRange(text, defaultFont())); } else { m_ranges.last().appendText(text); } finishTextUpdate(); } void ArtisticTextShape::appendText(const ArtisticTextRange &text) { beginTextUpdate(); m_ranges.append(text); // TODO: merge ranges with same style finishTextUpdate(); } bool ArtisticTextShape::replaceText(int charIndex, int charCount, const ArtisticTextRange &textRange) { QList ranges; ranges.append(textRange); return replaceText(charIndex, charCount, ranges); } bool ArtisticTextShape::replaceText(int charIndex, int charCount, const QList &textRanges) { CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || !charCount) { return false; } beginTextUpdate(); removeText(charIndex, charCount); insertText(charIndex, textRanges); finishTextUpdate(); return true; } qreal ArtisticTextShape::charAngleAt(int charIndex) const { if (isOnPath()) { qreal t = m_charOffsets.value(qBound(0, charIndex, m_charOffsets.size() - 1)); return m_baseline.angleAtPercent(t); } return 0.0; } QPointF ArtisticTextShape::charPositionAt(int charIndex) const { return m_charPositions.value(qBound(0, charIndex, m_charPositions.size() - 1)); } QRectF ArtisticTextShape::charExtentsAt(int charIndex) const { CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0 || isEmpty()) { charPos = CharIndex(0, 0); } else if (charPos.first < 0) { - charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length() - 1); + charPos = CharIndex(m_ranges.count() - 1, glyphsIndexSize() - 1); } if (charPos.first < m_ranges.size()) { const ArtisticTextRange &range = m_ranges.at(charPos.first); QFontMetrics metrics(range.font()); int w = metrics.charWidth(range.text(), charPos.second); return QRectF(0, 0, w, metrics.height()); } return QRectF(); } void ArtisticTextShape::updateSizeAndPosition(bool global) { QTransform shapeTransform = absoluteTransformation(0); // determine baseline position in document coordinates QPointF oldBaselinePosition = shapeTransform.map(QPointF(0, baselineOffset())); createOutline(); QRectF bbox = m_outline.boundingRect(); if (bbox.isEmpty()) { bbox = nullBoundBox(); } if (isOnPath()) { // calculate the offset we have to apply to keep our position QPointF offset = m_outlineOrigin - bbox.topLeft(); // cache topleft corner of baseline path m_outlineOrigin = bbox.topLeft(); // the outline position is in document coordinates // so we adjust our position QTransform m; m.translate(-offset.x(), -offset.y()); global ? applyAbsoluteTransformation(m) : applyTransformation(m); } else { // determine the new baseline position in document coordinates QPointF newBaselinePosition = shapeTransform.map(QPointF(0, -bbox.top())); // apply a transformation to compensate any translation of // our baseline position QPointF delta = oldBaselinePosition - newBaselinePosition; QTransform m; m.translate(delta.x(), delta.y()); applyAbsoluteTransformation(m); } setSize(bbox.size()); // map outline to shape coordinate system QTransform normalizeMatrix; normalizeMatrix.translate(-bbox.left(), -bbox.top()); m_outline = normalizeMatrix.map(m_outline); const int charCount = m_charPositions.count(); for (int i = 0; i < charCount; ++i) { m_charPositions[i] = normalizeMatrix.map(m_charPositions[i]); } } void ArtisticTextShape::cacheGlyphOutlines() { m_charOutlines.clear(); - + m_charPositions.clear(); + m_charPositions.resize(1); + QPointF charPos(0, 0); + int globalCharIndex = 0; + int glyph_count = 0; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const QString rangeText = range.text(); const QFont rangeFont(range.font(), &m_paintDevice); - const int textLength = rangeText.length(); - for (int charIdx = 0; charIdx < textLength; ++charIdx) { - QPainterPath charOutline; - charOutline.addText(QPointF(), rangeFont, rangeText[charIdx]); - m_charOutlines.append(charOutline); + QFontMetrics metrics(rangeFont); + QTextLayout layout; + layout.setText(rangeText); + layout.beginLayout(); + int leading = metrics.leading(); + qreal height = QPointF().y(); + while (1) { + QTextLine line = layout.createLine(); + if (!line.isValid()) + break; + height += leading; + line.setPosition(QPointF(QPointF().x(), height)); + height += line.height(); + } + layout.endLayout(); + QList runs = layout.glyphRuns(); + + + foreach (QGlyphRun run, runs) { + QRawFont rawFont(run.rawFont()); + QVector indexes =run.glyphIndexes(); + QVector positions = run.positions(); + glyph_count += positions.size(); + m_charPositions.resize(glyph_count + 1); + for (int i =0; i< indexes.size(); i++, ++globalCharIndex){ + QPainterPath path; + path = rawFont.pathForGlyph(indexes.at(i)); + m_charOutlines.append(path); + qreal yposiotn = positions[i].ry() - positions[0].ry(); + charPos = QPointF(positions.at(i).x() , yposiotn); + m_charPositions[globalCharIndex] = charPos; + } } } + m_charPositions[globalCharIndex] = charPos; + } void ArtisticTextShape::shapeChanged(ChangeType type, KoShape *shape) { if (m_path && shape == m_path) { if (type == KoShape::Deleted) { // baseline shape was deleted m_path = 0; } else if (type == KoShape::ParentChanged && !shape->parent()) { // baseline shape was probably removed from the document m_path->removeDependee(this); m_path = 0; } else { update(); // use the paths outline converted to document coordinates as the baseline m_baseline = m_path->absoluteTransformation(0).map(m_path->outline()); updateSizeAndPosition(true); update(); } } } CharIndex ArtisticTextShape::indexOfChar(int charIndex) const { if (isEmpty()) { return CharIndex(-1, -1); } int rangeIndex = 0; int textLength = 0; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const int rangeTextLength = range.text().length(); if (static_cast(charIndex) < textLength + rangeTextLength) { return CharIndex(rangeIndex, charIndex - textLength); } textLength += rangeTextLength; rangeIndex++; } return CharIndex(-1, -1); } void ArtisticTextShape::beginTextUpdate() { if (m_textUpdateCounter) { return; } m_textUpdateCounter++; update(); } void ArtisticTextShape::finishTextUpdate() { if (!m_textUpdateCounter) { return; } - cacheGlyphOutlines(); updateSizeAndPosition(); update(); notifyChanged(); m_textUpdateCounter--; } bool ArtisticTextShape::saveSvg(SvgSavingContext &context) { context.shapeWriter().startElement("text", false); context.shapeWriter().addAttribute("id", context.getID(this)); SvgStyleWriter::saveSvgStyle(this, context); const QList formattedText = text(); // if we have only a single text range, save the font on the text element const bool hasSingleRange = formattedText.size() == 1; if (hasSingleRange) { saveSvgFont(formattedText.first().font(), context); } qreal anchorOffset = 0.0; if (textAnchor() == ArtisticTextShape::AnchorMiddle) { anchorOffset += 0.5 * this->size().width(); context.shapeWriter().addAttribute("text-anchor", "middle"); } else if (textAnchor() == ArtisticTextShape::AnchorEnd) { anchorOffset += this->size().width(); context.shapeWriter().addAttribute("text-anchor", "end"); } // check if we are set on a path if (layout() == ArtisticTextShape::Straight) { context.shapeWriter().addAttributePt("x", anchorOffset); context.shapeWriter().addAttributePt("y", baselineOffset()); context.shapeWriter().addAttribute("transform", SvgUtil::transformToString(transformation())); Q_FOREACH (const ArtisticTextRange &range, formattedText) { saveSvgTextRange(range, context, !hasSingleRange, baselineOffset()); } } else { KoPathShape *baselineShape = KoPathShape::createShapeFromPainterPath(baseline()); QString id = context.createUID("baseline"); context.styleWriter().startElement("path"); context.styleWriter().addAttribute("id", id); context.styleWriter().addAttribute("d", baselineShape->toString(baselineShape->absoluteTransformation(0) * context.userSpaceTransform())); context.styleWriter().endElement(); context.shapeWriter().startElement("textPath"); context.shapeWriter().addAttribute("xlink:href", QLatin1Char('#') + id); if (startOffset() > 0.0) { context.shapeWriter().addAttribute("startOffset", QString("%1%").arg(startOffset() * 100.0)); } Q_FOREACH (const ArtisticTextRange &range, formattedText) { saveSvgTextRange(range, context, !hasSingleRange, baselineOffset()); } context.shapeWriter().endElement(); delete baselineShape; } context.shapeWriter().endElement(); return true; } void ArtisticTextShape::saveSvgFont(const QFont &font, SvgSavingContext &context) { context.shapeWriter().addAttribute("font-family", font.family()); context.shapeWriter().addAttributePt("font-size", font.pointSizeF()); if (font.bold()) { context.shapeWriter().addAttribute("font-weight", "bold"); } if (font.italic()) { context.shapeWriter().addAttribute("font-style", "italic"); } } void ArtisticTextShape::saveSvgTextRange(const ArtisticTextRange &range, SvgSavingContext &context, bool saveRangeFont, qreal baselineOffset) { context.shapeWriter().startElement("tspan", false); if (range.hasXOffsets()) { const char *attributeName = (range.xOffsetType() == ArtisticTextRange::AbsoluteOffset ? "x" : "dx"); QString attributeValue; int charIndex = 0; while (range.hasXOffset(charIndex)) { if (charIndex) { attributeValue += QLatin1Char(','); } attributeValue += QString("%1").arg(SvgUtil::toUserSpace(range.xOffset(charIndex++))); } context.shapeWriter().addAttribute(attributeName, attributeValue); } if (range.hasYOffsets()) { if (range.yOffsetType() != ArtisticTextRange::AbsoluteOffset) { baselineOffset = 0; } const char *attributeName = (range.yOffsetType() == ArtisticTextRange::AbsoluteOffset ? " y" : " dy"); QString attributeValue; int charIndex = 0; while (range.hasYOffset(charIndex)) { if (charIndex) { attributeValue += QLatin1Char(','); } attributeValue += QString("%1").arg(SvgUtil::toUserSpace(baselineOffset + range.yOffset(charIndex++))); } context.shapeWriter().addAttribute(attributeName, attributeValue); } if (range.hasRotations()) { QString attributeValue; int charIndex = 0; while (range.hasRotation(charIndex)) { if (charIndex) { attributeValue += ','; } attributeValue += QString("%1").arg(range.rotation(charIndex++)); } context.shapeWriter().addAttribute("rotate", attributeValue); } if (range.baselineShift() != ArtisticTextRange::None) { switch (range.baselineShift()) { case ArtisticTextRange::Sub: context.shapeWriter().addAttribute("baseline-shift", "sub"); break; case ArtisticTextRange::Super: context.shapeWriter().addAttribute("baseline-shift", "super"); break; case ArtisticTextRange::Percent: context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(range.baselineShiftValue() * 100)); break; case ArtisticTextRange::Length: context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(SvgUtil::toUserSpace(range.baselineShiftValue()))); break; default: break; } } if (saveRangeFont) { saveSvgFont(range.font(), context); } context.shapeWriter().addTextNode(range.text()); context.shapeWriter().endElement(); } bool ArtisticTextShape::loadSvg(const KoXmlElement &textElement, SvgLoadingContext &context) { clear(); QString anchor; if (!textElement.attribute("text-anchor").isEmpty()) { anchor = textElement.attribute("text-anchor"); } SvgStyles elementStyles = context.styleParser().collectStyles(textElement); context.styleParser().parseFont(elementStyles); ArtisticTextLoadingContext textContext; textContext.parseCharacterTransforms(textElement, context.currentGC()); KoXmlElement parentElement = textElement; // first check if we have a "textPath" child element for (KoXmlNode n = textElement.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement e = n.toElement(); if (e.tagName() == "textPath") { parentElement = e; break; } } KoPathShape *path = 0; bool pathInDocument = false; double offset = 0.0; const bool hasTextPathElement = parentElement != textElement && parentElement.hasAttribute("xlink:href"); if (hasTextPathElement) { // create the referenced path shape context.pushGraphicsContext(parentElement); context.styleParser().parseFont(context.styleParser().collectStyles(parentElement)); textContext.pushCharacterTransforms(); textContext.parseCharacterTransforms(parentElement, context.currentGC()); QString href = parentElement.attribute("xlink:href").mid(1); if (context.hasDefinition(href)) { const KoXmlElement &p = context.definition(href); // must be a path element as per svg spec if (p.tagName() == "path") { pathInDocument = false; path = new KoPathShape(); path->clear(); KoPathShapeLoader loader(path); loader.parseSvg(p.attribute("d"), true); path->setPosition(path->normalize()); QPointF newPosition = QPointF(SvgUtil::fromUserSpace(path->position().x()), SvgUtil::fromUserSpace(path->position().y())); QSizeF newSize = QSizeF(SvgUtil::fromUserSpace(path->size().width()), SvgUtil::fromUserSpace(path->size().height())); path->setSize(newSize); path->setPosition(newPosition); path->applyAbsoluteTransformation(SvgUtil::parseTransform(p.attribute("transform"))); } } else { path = dynamic_cast(context.shapeById(href)); if (path) { pathInDocument = true; } } // parse the start offset if (! parentElement.attribute("startOffset").isEmpty()) { QString start = parentElement.attribute("startOffset"); if (start.endsWith('%')) { offset = 0.01 * start.remove('%').toDouble(); } else { const float pathLength = path ? path->outline().length() : 0.0; if (pathLength > 0.0) { offset = start.toDouble() / pathLength; } } } } if (parentElement.hasChildNodes()) { // parse child elements parseTextRanges(parentElement, context, textContext); if (!context.currentGC()->preserveWhitespace) { const QString text = plainText(); if (text.endsWith(' ')) { removeText(text.length() - 1, 1); } } setPosition(textContext.textPosition()); } else { // a single text range appendText(createTextRange(textElement.text(), textContext, context.currentGC())); setPosition(textContext.textPosition()); } if (hasTextPathElement) { if (path) { if (pathInDocument) { putOnPath(path); } else { putOnPath(path->absoluteTransformation(0).map(path->outline())); delete path; } if (offset > 0.0) { setStartOffset(offset); } } textContext.popCharacterTransforms(); context.popGraphicsContext(); } // adjust position by baseline offset if (! isOnPath()) { setPosition(position() - QPointF(0, baselineOffset())); } if (anchor == "middle") { setTextAnchor(ArtisticTextShape::AnchorMiddle); } else if (anchor == "end") { setTextAnchor(ArtisticTextShape::AnchorEnd); } return true; } void ArtisticTextShape::parseTextRanges(const KoXmlElement &element, SvgLoadingContext &context, ArtisticTextLoadingContext &textContext) { for (KoXmlNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement e = n.toElement(); if (e.isNull()) { ArtisticTextRange range = createTextRange(n.toText().data(), textContext, context.currentGC()); appendText(range); } else if (e.tagName() == "tspan") { SvgGraphicsContext *gc = context.pushGraphicsContext(e); context.styleParser().parseFont(context.styleParser().collectStyles(e)); textContext.pushCharacterTransforms(); textContext.parseCharacterTransforms(e, gc); parseTextRanges(e, context, textContext); textContext.popCharacterTransforms(); context.popGraphicsContext(); } else if (e.tagName() == "tref") { if (e.attribute("xlink:href").isEmpty()) { continue; } QString href = e.attribute("xlink:href").mid(1); ArtisticTextShape *refText = dynamic_cast(context.shapeById(href)); if (refText) { foreach (const ArtisticTextRange &range, refText->text()) { appendText(range); } } else if (context.hasDefinition(href)) { const KoXmlElement &p = context.definition(href); SvgGraphicsContext *gc = context.currentGC(); appendText(ArtisticTextRange(textContext.simplifyText(p.text(), gc->preserveWhitespace), gc->font)); } } else { continue; } } } ArtisticTextRange ArtisticTextShape::createTextRange(const QString &text, ArtisticTextLoadingContext &context, SvgGraphicsContext *gc) { ArtisticTextRange range(context.simplifyText(text, gc->preserveWhitespace), gc->font); const int textLength = range.text().length(); switch (context.xOffsetType()) { case ArtisticTextLoadingContext::Absolute: range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::AbsoluteOffset); break; case ArtisticTextLoadingContext::Relative: range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::RelativeOffset); break; default: // no x-offsets break; } switch (context.yOffsetType()) { case ArtisticTextLoadingContext::Absolute: range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::AbsoluteOffset); break; case ArtisticTextLoadingContext::Relative: range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::RelativeOffset); break; default: // no y-offsets break; } range.setRotations(context.rotations(textLength)); range.setLetterSpacing(gc->letterSpacing); range.setWordSpacing(gc->wordSpacing); if (gc->baselineShift == "sub") { range.setBaselineShift(ArtisticTextRange::Sub); } else if (gc->baselineShift == "super") { range.setBaselineShift(ArtisticTextRange::Super); } else if (gc->baselineShift.endsWith('%')) { range.setBaselineShift(ArtisticTextRange::Percent, SvgUtil::fromPercentage(gc->baselineShift)); } else { qreal value = SvgUtil::parseUnitX(gc, gc->baselineShift); if (value != 0.0) { range.setBaselineShift(ArtisticTextRange::Length, value); } } //range.printDebug(); return range; } diff --git a/plugins/flake/artistictextshape/ArtisticTextShape.h b/plugins/flake/artistictextshape/ArtisticTextShape.h index 65456c3dde..089117b1d7 100644 --- a/plugins/flake/artistictextshape/ArtisticTextShape.h +++ b/plugins/flake/artistictextshape/ArtisticTextShape.h @@ -1,236 +1,242 @@ /* This file is part of the KDE project * Copyright (C) 2007-2008,2011 Jan Hambrecht * Copyright (C) 2008 Rob Buis * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef ARTISTICTEXTSHAPE_H #define ARTISTICTEXTSHAPE_H #include "ArtisticTextRange.h" #include #include #include #include #include #include +#include class QPainter; class KoPathShape; class ArtisticTextLoadingContext; class SvgGraphicsContext; #define ArtisticTextShapeID "ArtisticText" /// Character position within text shape (range index, range character index) typedef QPair CharIndex; class ArtisticTextShape : public KoShape, public SvgShape { public: enum TextAnchor { AnchorStart, AnchorMiddle, AnchorEnd }; enum LayoutMode { Straight, ///< baseline is a straight line OnPath, ///< baseline is a QPainterPath OnPathShape ///< baseline is the outline of a path shape }; ArtisticTextShape(); virtual ~ArtisticTextShape(); /// reimplemented void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); /// reimplemented virtual void saveOdf(KoShapeSavingContext &context) const; /// reimplemented virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context); /// reimplemented virtual QSizeF size() const; /// reimplemented virtual void setSize(const QSizeF &size); /// reimplemented virtual QPainterPath outline() const; /// reimplemented from SvgShape virtual bool saveSvg(SvgSavingContext &context); /// reimplemented from SvgShape virtual bool loadSvg(const KoXmlElement &element, SvgLoadingContext &context); /// Sets the plain text to display void setPlainText(const QString &newText); /// Returns the plain text content QString plainText() const; + /// Returns the glyphs index size + int glyphsIndexSize() const; + + /// Returns the QTextLayout opject + QTextLayout textLayout() const; + /// Returns formatted text QList text() const; /// Returns if text shape is empty, i.e. no text bool isEmpty() const; /// Clears the text shape void clear(); /** * Sets the font used for drawing * Note that it is expected that the font has its point size set * in postscript points. */ void setFont(const QFont &font); /** * Sets the font for the specified range of characters * @param charIndex the index of the first character of the range * @param charCount the number of characters of the range * @param font the new font to set */ void setFont(int charIndex, int charCount, const QFont &font); /** * Returns the font at the specified character position * If the text shape is empty it will return the default font. * If the character index is smaller than zero it will return the font * of the first character. If the character index is greater than the * last character index it will return the font of the last character. */ QFont fontAt(int charIndex) const; /// Returns the default font QFont defaultFont() const; /// Attaches this text shape to the given path shape bool putOnPath(KoPathShape *path); /// Puts the text on the given path, the path is expected to be in document coordinates bool putOnPath(const QPainterPath &path); /// Detaches this text shape from an already attached path shape void removeFromPath(); /// Returns if shape is attached to a path shape bool isOnPath() const; /// Sets the offset for for text on path void setStartOffset(qreal offset); /// Returns the start offset for text on path qreal startOffset() const; /** * Returns the y-offset from the top-left corner to the baseline. * This is usable for being able to exactly position the texts baseline. * Note: The value makes only sense for text not attached to a path. */ qreal baselineOffset() const; /// Sets the text anchor void setTextAnchor(TextAnchor anchor); /// Returns the actual text anchor TextAnchor textAnchor() const; /// Returns the current layout mode LayoutMode layout() const; /// Returns the baseline path QPainterPath baseline() const; /// Returns a pointer to the shape used as baseline KoPathShape *baselineShape() const; /// Removes a range of text starting from the given character QList removeText(int charIndex, int charCount); /// Copies a range of text starting from the given character QList copyText(int charIndex, int charCount); /// Adds a range of text at the given index void insertText(int charIndex, const QString &plainText); /// Adds range of text at the given index void insertText(int charIndex, const ArtisticTextRange &textRange); /// Adds ranges of text at the given index void insertText(int charIndex, const QList &textRanges); /// Appends plain text to the last text range void appendText(const QString &plainText); /// Appends a single formatted range of text void appendText(const ArtisticTextRange &text); /// Replaces a range of text with the specified text range bool replaceText(int charIndex, int charCount, const ArtisticTextRange &textRange); /// Replaces a range of text with the specified text ranges bool replaceText(int charIndex, int charCount, const QList &textRanges); /// Gets the angle of the char with the given index qreal charAngleAt(int charIndex) const; /// Gets the position of the char with the given index in shape coordinates QPointF charPositionAt(int charIndex) const; /// Gets the extents of the char with the given index QRectF charExtentsAt(int charIndex) const; /// Returns index of range and index within range of specified character CharIndex indexOfChar(int charIndex) const; /// reimplemented from KoShape virtual void shapeChanged(ChangeType type, KoShape *shape); - + QList m_charOutlines; private: void updateSizeAndPosition(bool global = false); void cacheGlyphOutlines(); bool pathHasChanged() const; void createOutline(); void beginTextUpdate(); void finishTextUpdate(); /// Calculates abstract character positions in baseline coordinates QVector calculateAbstractCharacterPositions(); /// Returns the bounding box for an empty text shape QRectF nullBoundBox() const; /// Saves svg font void saveSvgFont(const QFont &font, SvgSavingContext &context); /// Saves svg text range void saveSvgTextRange(const ArtisticTextRange &range, SvgSavingContext &context, bool saveFont, qreal baselineOffset); /// Parse nested text ranges void parseTextRanges(const KoXmlElement &element, SvgLoadingContext &context, ArtisticTextLoadingContext &textContext); /// Creates text range ArtisticTextRange createTextRange(const QString &text, ArtisticTextLoadingContext &context, SvgGraphicsContext *gc); QList m_ranges; KoPostscriptPaintDevice m_paintDevice; KoPathShape *m_path; ///< the path shape we are attached to - QList m_charOutlines; ///< cached character oulines qreal m_startOffset; ///< the offset from the attached path start point QPointF m_outlineOrigin; ///< the top-left corner of the non-normalized text outline QPainterPath m_outline; ///< the actual text outline QPainterPath m_baseline; ///< the baseline path the text is put on TextAnchor m_textAnchor; ///< the actual text anchor QVector m_charOffsets; ///< char positions [0..1] on baseline path QVector m_charPositions; ///< char positions in shape coordinates int m_textUpdateCounter; QFont m_defaultFont; }; #endif // ARTISTICTEXTSHAPE_H