Commit fa0cf6fb authored by Dmitry Kazakov's avatar Dmitry Kazakov

Implement basic loading of SVG text elements

The basic design is:

1) Each element of the text (text, tspan) is represented with
   KoSvgTextChunkShape. This chunk stores all the information
   about this portion of the text. The properties are stored in
   KoSvgTextProperties object, that will allow us to export them
   as a markup.

2) A subtree of test elements is stored in KoSvgTextShape, which
   is also a chunk.

3) The main text shape accesses the internals of its chunks with
   KoSvgTextChunkShapeLayoutInterface. It encapsulates all the
   details that should not be accessed in public.

4) A text chunk can be "a text" or "a node". In the latter case
   it is just an intermediate node that stores a set of children
   that actually have the text.

5) The layout of the text is performed by KoSvgTextShape
parent 91554d81
......@@ -4,6 +4,7 @@ include_directories(
${CMAKE_SOURCE_DIR}/libs/flake/commands
${CMAKE_SOURCE_DIR}/libs/flake/tools
${CMAKE_SOURCE_DIR}/libs/flake/svg
${CMAKE_SOURCE_DIR}/libs/flake/text
${CMAKE_BINARY_DIR}/libs/flake
)
......@@ -201,6 +202,10 @@ set(kritaflake_SRCS
svg/SvgLoadingContext.cpp
svg/SvgShapeFactory.cpp
svg/parsers/SvgTransformParser.cpp
text/KoSvgText.cpp
text/KoSvgTextProperties.cpp
text/KoSvgTextChunkShape.cpp
text/KoSvgTextShape.cpp
FlakeDebug.cpp
tests/MockShapes.cpp
......@@ -220,6 +225,7 @@ target_include_directories(kritaflake
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/commands>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/tools>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/svg>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/text>
)
target_link_libraries(kritaflake kritapigment kritawidgetutils kritaodf kritacommand KF5::WidgetsAddons Qt5::Svg)
......
......@@ -27,7 +27,7 @@
#include <klocalizedstring.h>
// static
KoShapeGroupCommand * KoShapeGroupCommand::createCommand(KoShapeGroup *container, const QList<KoShape *> &shapes, KUndo2Command *parent)
KoShapeGroupCommand * KoShapeGroupCommand::createCommand(KoShapeContainer *container, const QList<KoShape *> &shapes, KUndo2Command *parent)
{
QList<KoShape*> orderedShapes(shapes);
qSort(orderedShapes.begin(), orderedShapes.end(), KoShape::compareShapeZIndex);
......@@ -60,7 +60,7 @@ KoShapeGroupCommand::KoShapeGroupCommand(KoShapeContainer *container, const QLis
d->init(this);
}
KoShapeGroupCommand::KoShapeGroupCommand(KoShapeGroup *container, const QList<KoShape *> &shapes, KUndo2Command *parent)
KoShapeGroupCommand::KoShapeGroupCommand(KoShapeContainer *container, const QList<KoShape *> &shapes, KUndo2Command *parent)
: KUndo2Command(parent),
d(new KoShapeGroupCommandPrivate(container,shapes, QList<bool>(), QList<bool>(), true))
{
......
......@@ -45,7 +45,7 @@ public:
* @param parent the parent command if the resulting command is a compound undo command.
* @param shapes a list of all the shapes that should be grouped.
*/
static KoShapeGroupCommand *createCommand(KoShapeGroup *container, const QList<KoShape *> &shapes, KUndo2Command *parent = 0);
static KoShapeGroupCommand *createCommand(KoShapeContainer *container, const QList<KoShape *> &shapes, KUndo2Command *parent = 0);
/**
* Command to group a set of shapes into a predefined container.
......@@ -80,7 +80,7 @@ public:
* @param parent the parent command if the resulting command is a compound undo command.
* @param shapes a list of all the shapes that should be grouped.
*/
KoShapeGroupCommand(KoShapeGroup *container, const QList<KoShape *> &shapes, KUndo2Command *parent = 0);
KoShapeGroupCommand(KoShapeContainer *container, const QList<KoShape *> &shapes, KUndo2Command *parent = 0);
virtual ~KoShapeGroupCommand();
/// redo the command
virtual void redo();
......
......@@ -48,11 +48,10 @@ SvgGraphicsContext::SvgGraphicsContext()
clipRule = Qt::WindingFill;
preserveWhitespace = false;
letterSpacing = 0.0;
wordSpacing = 0.0;
pixelsPerInch = 72.0;
autoFillMarkers = false;
textProperties = KoSvgTextProperties::defaultProperties();
}
void SvgGraphicsContext::workaroundClearInheritedFillProperties()
......
......@@ -25,6 +25,7 @@
#include <KoShapeStroke.h>
#include <QFont>
#include <QTransform>
#include <text/KoSvgTextProperties.h>
class KRITAFLAKE_EXPORT SvgGraphicsContext
{
......@@ -56,6 +57,7 @@ public:
QTransform matrix; ///< the current transformation matrix
QFont font; ///< the current font
QStringList fontFamiliesList; ///< the full list of all the families to search glyphs in
QColor currentColor; ///< the current color
QString xmlBaseDir; ///< the current base directory (used for loading external content)
bool preserveWhitespace;///< preserve whitespace in element text
......@@ -64,10 +66,6 @@ public:
bool forcePercentage; ///< force parsing coordinates/length as percentages of currentBoundbox
QTransform viewboxTransform; ///< view box transformation
qreal letterSpacing; ///< additional spacing between characters of text elements
qreal wordSpacing; ///< additional spacing between words of text elements
QString baselineShift; ///< basline shift mode for text elements
bool display; ///< controls display of shape
bool visible; ///< controls visibility of the shape (inherited)
qreal pixelsPerInch; ///< controls the resolution of the image raster
......@@ -77,6 +75,8 @@ public:
QString markerEndId;
bool autoFillMarkers;
KoSvgTextProperties textProperties;
};
#endif // SVGGRAPHICCONTEXT_H
......@@ -94,15 +94,16 @@ SvgGraphicsContext *SvgLoadingContext::pushGraphicsContext(const KoXmlElement &e
SvgGraphicsContext *gc = new SvgGraphicsContext;
// copy data from current context
if (! d->gcStack.isEmpty() && inherit)
if (! d->gcStack.isEmpty() && inherit) {
*gc = *(d->gcStack.top());
}
gc->textProperties.resetNonInheritableToDefault(); // some of the text properties are not inherited
gc->filterId.clear(); // filters are not inherited
gc->clipPathId.clear(); // clip paths are not inherited
gc->clipMaskId.clear(); // clip masks are not inherited
gc->display = true; // display is not inherited
gc->opacity = 1.0; // opacity is not inherited
gc->baselineShift.clear(); // baseline-shift is not inherited
if (!element.isNull()) {
if (element.hasAttribute("transform")) {
......
......@@ -65,6 +65,9 @@
#include <KoVectorPatternBackground.h>
#include <KoMarker.h>
#include <text/KoSvgTextShape.h>
#include <text/KoSvgTextChunkShape.h>
#include "kis_debug.h"
#include "kis_global.h"
......@@ -662,13 +665,7 @@ void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginal
{
if (!shape) return;
SvgGraphicsContext *gc = m_context.currentGC();
KIS_ASSERT(gc);
if (!dynamic_cast<KoShapeGroup*>(shape)) {
applyFillStyle(shape);
applyStrokeStyle(shape);
}
applyCurrentBasicStyle(shape);
if (KoPathShape *pathShape = dynamic_cast<KoPathShape*>(shape)) {
applyMarkers(pathShape);
......@@ -678,6 +675,20 @@ void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginal
applyClipping(shape, shapeToOriginalUserCoordinates);
applyMaskClipping(shape, shapeToOriginalUserCoordinates);
}
void SvgParser::applyCurrentBasicStyle(KoShape *shape)
{
if (!shape) return;
SvgGraphicsContext *gc = m_context.currentGC();
KIS_ASSERT(gc);
if (!dynamic_cast<KoShapeGroup*>(shape)) {
applyFillStyle(shape);
applyStrokeStyle(shape);
}
if (!gc->display || !gc->visible) {
/**
* WARNING: here is a small inconsistency with the standard:
......@@ -694,6 +705,7 @@ void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginal
shape->setTransparency(1.0 - gc->opacity);
}
void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e, const QPointF &shapeToOriginalUserCoordinates)
{
applyStyle(obj, m_context.styleParser().collectStyles(e), shapeToOriginalUserCoordinates);
......@@ -1108,7 +1120,7 @@ KoShape* SvgParser::parseUse(const KoXmlElement &e)
return result;
}
void SvgParser::addToGroup(QList<KoShape*> shapes, KoShapeGroup *group)
void SvgParser::addToGroup(QList<KoShape*> shapes, KoShapeContainer *group)
{
m_shapes += shapes;
......@@ -1236,7 +1248,74 @@ KoShape* SvgParser::parseGroup(const KoXmlElement &b, const KoXmlElement &overri
return group;
}
QList<KoShape*> SvgParser::parseContainer(const KoXmlElement &e)
KoShape* SvgParser::parseTextNode(const KoXmlText &e)
{
QScopedPointer<KoSvgTextChunkShape> textChunk(new KoSvgTextChunkShape());
textChunk->setZIndex(m_context.nextZIndex());
if (!textChunk->loadSvgTextNode(e, m_context)) {
return 0;
}
applyCurrentBasicStyle(textChunk.data()); // apply style to this group after size is set
return textChunk.take();
}
KoXmlText getTheOnlyTextChild(const KoXmlElement &e)
{
KoXmlNode firstChild = e.firstChild();
return !firstChild.isNull() && firstChild == e.lastChild() && firstChild.isText() ?
firstChild.toText() : KoXmlText();
}
KoShape *SvgParser::parseTextElement(const KoXmlElement &e)
{
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(e.tagName() == "text" || e.tagName() == "tspan", 0);
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_isInsideTextSubtree || e.tagName() == "text", 0);
KoSvgTextShape *rootTextShape = e.tagName() == "text" ? new KoSvgTextShape() : 0;
if (rootTextShape) {
m_isInsideTextSubtree = true;
}
m_context.pushGraphicsContext(e);
uploadStyleToContext(e);
KoSvgTextChunkShape *textChunk = rootTextShape ? rootTextShape : new KoSvgTextChunkShape();
textChunk->setZIndex(m_context.nextZIndex());
textChunk->loadSvg(e, m_context);
KoXmlText onlyTextChild = getTheOnlyTextChild(e);
if (!onlyTextChild.isNull()) {
textChunk->loadSvgTextNode(onlyTextChild, m_context);
} else {
QList<KoShape*> childShapes = parseContainer(e, true);
addToGroup(childShapes, textChunk);
}
// groups should also have their own coordinate system!
textChunk->applyAbsoluteTransformation(m_context.currentGC()->matrix);
const QPointF extraOffset = extraShapeOffset(textChunk, m_context.currentGC()->matrix);
// handle id
applyId(e.attribute("id"), textChunk);
applyCurrentStyle(textChunk, extraOffset); // apply style to this group after size is set
m_context.popGraphicsContext();
textChunk->normalizeCharTransformations();
if (rootTextShape) {
m_isInsideTextSubtree = false;
rootTextShape->relayout();
}
return textChunk;
}
QList<KoShape*> SvgParser::parseContainer(const KoXmlElement &e, bool parseTextNodes)
{
QList<KoShape*> shapes;
......@@ -1245,8 +1324,17 @@ QList<KoShape*> SvgParser::parseContainer(const KoXmlElement &e)
for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
KoXmlElement b = n.toElement();
if (b.isNull())
if (b.isNull()) {
if (parseTextNodes && n.isText()) {
KoShape *shape = parseTextNode(n.toText());
if (shape) {
shapes += shape;
}
}
continue;
}
if (isSwitch) {
// if we are parsing a switch check the requiredFeatures, requiredExtensions
......@@ -1313,6 +1401,10 @@ QList<KoShape*> SvgParser::parseSingleElement(const KoXmlElement &b)
parseMarker(b);
} else if (b.tagName() == "style") {
m_context.addStyleSheet(b);
} else if (b.tagName() == "text" ||
b.tagName() == "tspan") {
shapes += parseTextElement(b);
} else if (b.tagName() == "rect" ||
b.tagName() == "ellipse" ||
b.tagName() == "circle" ||
......@@ -1320,8 +1412,7 @@ QList<KoShape*> SvgParser::parseSingleElement(const KoXmlElement &b)
b.tagName() == "polyline" ||
b.tagName() == "polygon" ||
b.tagName() == "path" ||
b.tagName() == "image" ||
b.tagName() == "text") {
b.tagName() == "image") {
KoShape *shape = createObjectDirect(b);
if (shape)
shapes.append(shape);
......@@ -1366,11 +1457,7 @@ KoShape * SvgParser::createPath(const KoXmlElement &element)
path->clear();
bool bFirst = true;
QString points = element.attribute("points").simplified();
points.replace(',', ' ');
points.remove('\r');
points.remove('\n');
QStringList pointList = points.split(' ', QString::SkipEmptyParts);
QStringList pointList = SvgUtil::simplifyList(element.attribute("points"));
for (QStringList::Iterator it = pointList.begin(); it != pointList.end(); ++it) {
QPointF point;
point.setX(SvgUtil::fromUserSpace((*it).toDouble()));
......
......@@ -41,6 +41,7 @@
class KoShape;
class KoShapeGroup;
class KoShapeContainer;
class KoDocumentResourceManager;
class KoVectorPatternBackground;
class KoMarker;
......@@ -73,8 +74,10 @@ protected:
/// Parses a group-like element element, saving all its topmost properties
KoShape* parseGroup(const KoXmlElement &e, const KoXmlElement &overrideChildrenFrom = KoXmlElement());
KoShape* parseTextNode(const KoXmlText &e);
KoShape* parseTextElement(const KoXmlElement &e);
/// Parses a container element, returning a list of child shapes
QList<KoShape*> parseContainer(const KoXmlElement &);
QList<KoShape*> parseContainer(const KoXmlElement &, bool parseTextNodes = false);
QList<KoShape*> parseSingleElement(const KoXmlElement &b);
/// Parses a use element, returning a list of child shapes
KoShape* parseUse(const KoXmlElement &);
......@@ -115,7 +118,7 @@ protected:
SvgClipPathHelper* findClipPath(const QString &id);
/// Adds list of shapes to the given group shape
void addToGroup(QList<KoShape*> shapes, KoShapeGroup * group);
void addToGroup(QList<KoShape*> shapes, KoShapeContainer *group);
/// creates a shape from the given shape id
KoShape * createShape(const QString &shapeID);
......@@ -127,6 +130,7 @@ protected:
void uploadStyleToContext(const KoXmlElement &e);
void applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates);
void applyCurrentBasicStyle(KoShape *shape);
/// Applies styles to the given shape
void applyStyle(KoShape *, const KoXmlElement &, const QPointF &shapeToOriginalUserCoordinates);
......@@ -166,6 +170,7 @@ private:
KoDocumentResourceManager *m_documentResourceManager;
QList<KoShape*> m_shapes;
QList<KoShape*> m_toplevelShapes;
bool m_isInsideTextSubtree = false;
};
#endif
This diff is collapsed.
......@@ -244,6 +244,7 @@ qreal SvgUtil::parseUnit(SvgGraphicsContext *gc, const QString &unit, bool horiz
else if (unit.right(2) == "in")
value = ptToPx(gc, INCH_TO_POINT(value));
else if (unit.right(2) == "em")
// NOTE: all the fonts should be created with 'pt' size, not px!
value = ptToPx(gc, value * gc->font.pointSize());
else if (unit.right(2) == "ex") {
QFontMetrics metrics(gc->font);
......@@ -422,6 +423,15 @@ QString SvgUtil::mapExtendedShapeTag(const QString &tagName, const KoXmlElement
return result;
}
QStringList SvgUtil::simplifyList(const QString &str)
{
QString attribute = str;
attribute.replace(',', ' ');
attribute.remove('\r');
attribute.remove('\n');
return attribute.simplified().split(' ', QString::SkipEmptyParts);
}
SvgUtil::PreserveAspectRatioParser::PreserveAspectRatioParser(const QString &str)
{
QRegExp rexp("(defer)?\\s*(none|(x(Min|Max|Mid)Y(Min|Max|Mid)))\\s*(meet|slice)?", Qt::CaseInsensitive);
......
......@@ -27,6 +27,7 @@ class QString;
class SvgGraphicsContext;
class QTransform;
class KoXmlElement;
class QStringList;
class KRITAFLAKE_EXPORT SvgUtil
{
......@@ -112,6 +113,8 @@ public:
static QString mapExtendedShapeTag(const QString &tagName, const KoXmlElement &element);
static QStringList simplifyList(const QString &str);
struct PreserveAspectRatioParser
{
PreserveAspectRatioParser(const QString &str);
......
......@@ -37,14 +37,14 @@ struct SvgTester
SvgTester (const QString &data)
: parser(&resourceManager)
{
QVERIFY(doc.setContent(data.toLatin1()));
QVERIFY(doc.setContent(data.toUtf8()));
root = doc.documentElement();
parser.setXmlBaseDir("./");
savedData = data;
//printf("%s", savedData.toLatin1().data());
//printf("%s", savedData.toUtf8().data());
}
......
This diff is collapsed.
......@@ -25,7 +25,24 @@ class TestSvgText : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testTextProperties();
void testDefaultTextProperties();
void testTextPropertiesDifference();
void testParseFontStyles();
void testParseTextStyles();
void testSimpleText();
void testComplexText();
void testTextAlignment();
void testTextBaselineShift();
void testTextSpacing();
void testTextDecorations();
void testRightToLeft();
void testQtBidi();
void testQtDxDy();
};
#endif // TESTSVGTEXT_H
/*
* Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "KoSvgText.h"
#include <SvgUtil.h>
#include <KoXmlReader.h>
#include <SvgLoadingContext.h>
#include <QDebug>
namespace {
struct TextPropertiesStaticRegistrar {
TextPropertiesStaticRegistrar() {
qRegisterMetaType<KoSvgText::AutoValue>("KoSvgText::AutoValue");
QMetaType::registerEqualsComparator<KoSvgText::AutoValue>();
QMetaType::registerDebugStreamOperator<KoSvgText::AutoValue>();
}
};
static TextPropertiesStaticRegistrar textPropertiesStaticRegistrar;
}
namespace KoSvgText {
AutoValue parseAutoValueX(const QString &value, const SvgLoadingContext &context, const QString &autoKeyword)
{
return value == autoKeyword ? AutoValue() : SvgUtil::parseUnitX(context.currentGC(), value);
}
AutoValue parseAutoValueY(const QString &value, const SvgLoadingContext &context, const QString &autoKeyword)
{
return value == autoKeyword ? AutoValue() : SvgUtil::parseUnitY(context.currentGC(), value);
}
AutoValue parseAutoValueXY(const QString &value, const SvgLoadingContext &context, const QString &autoKeyword)
{
return value == autoKeyword ? AutoValue() : SvgUtil::parseUnitXY(context.currentGC(), value);
}
AutoValue parseAutoValueAngular(const QString &value, const SvgLoadingContext &context, const QString &autoKeyword)
{
return value == autoKeyword ? AutoValue() : SvgUtil::parseUnitAngular(context.currentGC(), value);
}
WritingMode parseWritingMode(const QString &value) {
return (value == "tb-rl" || value == "tb") ? TopToBottom :
(value == "rl-tb" || value == "rl") ? RightToLeft :
LeftToRight;
}
Direction parseDirection(const QString &value) {
return value == "rtl" ? DirectionRightToLeft : DirectionLeftToRight;
}
UnicodeBidi parseUnicodeBidi(const QString &value)
{
return value == "embed" ? BidiEmbed :
value == "bidi-override" ? BidiOverride :
BidiNormal;
}
TextAnchor parseTextAnchor(const QString &value)
{
return value == "middle" ? AnchorMiddle :
value == "end" ? AnchorEnd :
AnchorStart;
}
DominantBaseline parseDominantBaseline(const QString &value)
{
return value == "use-script" ? DominantBaselineUseScript :
value == "no-change" ? DominantBaselineNoChange:
value == "reset-size" ? DominantBaselineResetSize:
value == "ideographic" ? DominantBaselineIdeographic :
value == "alphabetic" ? DominantBaselineAlphabetic :
value == "hanging" ? DominantBaselineHanging :
value == "mathematical" ? DominantBaselineMathematical :
value == "central" ? DominantBaselineCentral :
value == "middle" ? DominantBaselineMiddle :
value == "text-after-edge" ? DominantBaselineTextAfterEdge :
value == "text-before-edge" ? DominantBaselineTextBeforeEdge :
DominantBaselineAuto;
}
AlignmentBaseline parseAlignmentBaseline(const QString &value)
{
return value == "baseline" ? AlignmentBaselineDominant :
value == "ideographic" ? AlignmentBaselineIdeographic :
value == "alphabetic" ? AlignmentBaselineAlphabetic :
value == "hanging" ? AlignmentBaselineHanging :
value == "mathematical" ? AlignmentBaselineMathematical :
value == "central" ? AlignmentBaselineCentral :
value == "middle" ? AlignmentBaselineMiddle :
(value == "text-after-edge" || value == "after-edge") ? AlignmentBaselineTextAfterEdge :
(value == "text-before-edge" || value == "before-edge") ? AlignmentBaselineTextBeforeEdge :
AlignmentBaselineAuto;
}
BaselineShiftMode parseBaselineShiftMode(const QString &value)
{
return value == "baseline" ? ShiftNone :
value == "sub" ? ShiftSub :
value == "super" ? ShiftSuper :
ShiftPercentage;
}
QDebug operator<<(QDebug dbg, const KoSvgText::AutoValue &value)
{
dbg.nospace() << (value.isAuto ? "auto" : QString::number(value.customValue));
return dbg.space();
}
LengthAdjust parseLengthAdjust(const QString &value)
{
return value == "spacingAndGlyphs" ? LengthAdjustSpacingAndGlyphs : LengthAdjustSpacing;
}
void CharTransformation::mergeInParentTransformation(const CharTransformation &t)
{
if (!xPos && t.xPos) {
xPos = *t.xPos;
}
if (!yPos && t.yPos) {
yPos = *t.yPos;
}
if (!dxPos && t.dxPos) {
dxPos = *t.dxPos;
}
if (!dyPos && t.dyPos) {
dyPos = *t.dyPos;
}
if (!rotate && t.rotate) {
rotate = *t.rotate;
}
}
bool CharTransformation::isNull() const
{
return !xPos && !yPos && !dxPos && !dyPos && !rotate;
}
QPointF CharTransformation::adjustedTextPos(const QPointF &pos) const
{
QPointF result = pos;
if (xPos) {
result.rx() = *xPos;
}
if (yPos) {
result.ry() = *yPos;
}
if (dxPos) {
result.rx() += *dxPos;
}
if (dyPos) {
result.ry() += *dyPos;
}
return result;
}