Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextContentElement.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2024 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include "KoCssTextUtils.h"
10#include <kis_dom_utils.h>
11#include "SvgUtil.h"
12#include "KoXmlWriter.h"
13#include "SvgStyleWriter.h"
14#include <kis_global.h>
15
16#include "SvgGraphicContext.h"
17
18#include <QRegularExpression>
19
24
25namespace {
26void appendLazy(QVector<qreal> *list, boost::optional<qreal> value, int iteration, bool hasDefault = true, qreal defaultValue = 0.0)
27{
28 if (!value) return;
29 if (value && *value == defaultValue && hasDefault == true && list->isEmpty()) return;
30
31 while (list->size() < iteration) {
32 list->append(defaultValue);
33 }
34
35 list->append(*value);
36}
37
38void fillTransforms(QVector<qreal> *xPos, QVector<qreal> *yPos, QVector<qreal> *dxPos, QVector<qreal> *dyPos, QVector<qreal> *rotate,
39 QVector<KoSvgText::CharTransformation> localTransformations)
40{
41 for (int i = 0; i < localTransformations.size(); i++) {
42 const KoSvgText::CharTransformation &t = localTransformations[i];
43 appendLazy(xPos, t.xPos, i, false);
44 appendLazy(yPos, t.yPos, i, false);
45 appendLazy(dxPos, t.dxPos, i);
46 appendLazy(dyPos, t.dyPos, i);
47 appendLazy(rotate, t.rotate, i);
48 }
49}
50
51
52
53QVector<qreal> parseListAttributeX(const QString &value, SvgLoadingContext &context)
54{
55 QVector<qreal> result;
56
58 Q_FOREACH (const QString &str, list) {
59 result << SvgUtil::parseUnitX(context.currentGC(), context.resolvedProperties(), str);
60 }
61
62 return result;
63}
64
65QVector<qreal> parseListAttributeY(const QString &value, SvgLoadingContext &context)
66{
67 QVector<qreal> result;
68
70 Q_FOREACH (const QString &str, list) {
71 result << SvgUtil::parseUnitY(context.currentGC(), context.resolvedProperties(), str);
72 }
73
74 return result;
75}
76
77QVector<qreal> parseListAttributeAngular(const QString &value, SvgLoadingContext &context)
78{
79 QVector<qreal> result;
80
82 Q_FOREACH (const QString &str, list) {
83 result << SvgUtil::parseUnitAngular(context.currentGC(), str);
84 }
85
86 return result;
87}
88
89QString convertListAttribute(const QVector<qreal> &values) {
90 QStringList stringValues;
91
92 Q_FOREACH (qreal value, values) {
93 stringValues.append(KisDomUtils::toString(value));
94 }
95
96 return stringValues.join(',');
97}
98
99void writeTextListAttribute(const QString &attribute, const QVector<qreal> &values, KoXmlWriter &writer)
100{
101 const QString value = convertListAttribute(values);
102 if (!value.isEmpty()) {
103 writer.addAttribute(attribute.toLatin1().data(), value);
104 }
105}
106}
107
108#include <ksharedconfig.h>
109#include <kconfiggroup.h>
110
116Q_GUI_EXPORT int qt_defaultDpi();
117
118namespace {
119int forcedDpiForQtFontBugWorkaround() {
120 KConfigGroup cfg(KSharedConfig::openConfig(), "");
121 int value = cfg.readEntry("forcedDpiForQtFontBugWorkaround", qt_defaultDpi());
122
123 if (value < 0) {
125 }
126
127 return value;
128}
129
130
131KoSvgTextProperties adjustPropertiesForFontSizeWorkaround(const KoSvgTextProperties &properties)
132{
134 return properties;
135
136 KoSvgTextProperties result = properties;
137
138 const int forcedFontDPI = forcedDpiForQtFontBugWorkaround();
139
142 forcedFontDPI > 0) {
143
144 qreal fontSize = result.fontSize().value;
145 fontSize *= qreal(forcedFontDPI) / 72.0;
147 }
151 }
152
154
155 return result;
156}
157
158}
159
160const QString TEXT_STYLE_TYPE = "krita:style-type";
161const QString TEXT_STYLE_RES = "krita:style-resolution";
162
163bool KoSvgTextContentElement::loadSvg(const QDomElement &e, SvgLoadingContext &context, bool rootNode)
164{
165 SvgGraphicsContext *gc = context.currentGC();
167
168 KoSvgTextProperties props = rootNode? context.resolvedProperties(): gc->textProperties;
169
170
176 for (int i = 0; i < generic.size(); i++) {
177 auto id = generic[i];
178 if (properties.hasProperty(id)) {
179 props.setProperty(id, properties.property(id));
180 }
181 }
182 properties = props;
183
184 textLength = KoSvgText::parseAutoValueXY(e.attribute("textLength", ""), context, "");
185 lengthAdjust = KoSvgText::parseLengthAdjust(e.attribute("lengthAdjust", "spacing"));
186
187 QVector<qreal> xPos = parseListAttributeX(e.attribute("x", ""), context);
188 QVector<qreal> yPos = parseListAttributeY(e.attribute("y", ""), context);
189 QVector<qreal> dxPos = parseListAttributeX(e.attribute("dx", ""), context);
190 QVector<qreal> dyPos = parseListAttributeY(e.attribute("dy", ""), context);
191 QVector<qreal> rotate = parseListAttributeAngular(e.attribute("rotate", ""), context);
192
193 const int numLocalTransformations =
194 std::max({xPos.size(), yPos.size(),
195 dxPos.size(), dyPos.size(),
196 rotate.size()});
197
198 localTransformations.resize(numLocalTransformations);
199 for (int i = 0; i < numLocalTransformations; i++) {
200 if (i < xPos.size()) {
201 localTransformations[i].xPos = xPos[i];
202 }
203 if (i < yPos.size()) {
204 localTransformations[i].yPos = yPos[i];
205 }
206 if (i < dxPos.size() && dxPos[i] != 0.0) {
207 localTransformations[i].dxPos = dxPos[i];
208 }
209 if (i < dyPos.size() && dyPos[i] != 0.0) {
210 localTransformations[i].dyPos = dyPos[i];
211 }
212 if (i < rotate.size()) {
213 localTransformations[i].rotate = rotate[i];
214 }
215 }
216
217 if (e.tagName() == "textPath") {
218 // we'll read the value 'path' later.
219
220 textPathInfo.side = KoSvgText::parseTextPathSide(e.attribute("side", "left"));
221 textPathInfo.method = KoSvgText::parseTextPathMethod(e.attribute("method", "align"));
222 textPathInfo.spacing = KoSvgText::parseTextPathSpacing(e.attribute("spacing", "auto"));
223 // This depends on pathLength;
224 if (e.hasAttribute("startOffset")) {
225 QString offset = e.attribute("startOffset", "0");
226 if (offset.endsWith("%")) {
227 textPathInfo.startOffset = SvgUtil::parseNumber(offset.left(offset.size() - 1));
229 } else {
231 }
232 }
233 }
234
235 if (e.hasAttribute(TEXT_STYLE_TYPE.toLatin1().data())) {
237 if (e.hasAttribute(TEXT_STYLE_RES.toLatin1().data())) {
238 QString resolution = e.attribute(TEXT_STYLE_RES.toLatin1().data()).toLower();
239 if (resolution.endsWith("dpi")) {
240 resolution.chop(3);
241 }
243 }
244 }
245
246 return true;
247}
248
250{
251 SvgGraphicsContext *gc = context.currentGC();
253
254 // In theory, the XML spec requires XML parsers to normalize line endings to
255 // LF. However, QXmlInputSource + QXmlSimpleReader do not do this, so we can
256 // end up with CR in the text. The SVG spec explicitly calls out that all
257 // newlines in SVG are to be represented by a single LF (U+000A) character,
258 // so we can replace all CRLF and CR into LF here for simplicity.
259 static const QRegularExpression s_regexCrlf(R"==((?:\r\n|\r(?!\n)))==");
260 QString content = text.data();
261 content.replace(s_regexCrlf, QStringLiteral("\n"));
262
263 this->text = std::move(content);
264
265 return true;
266}
267
269 bool rootText,
270 bool saveText,
271 QMap<QString, QString> shapeSpecificAttributes)
272{
273 if (textPath) {
274 if (textPath) {
275 // we'll always save as an embedded shape as "path" is an svg 2.0
276 // feature.
277 QString id = SvgStyleWriter::embedShape(textPath.data(), context);
278 // inkscape can only read 'xlink:href'
279 if (!id.isEmpty()) {
280 context.shapeWriter().addAttribute("xlink:href", "#" + id);
281 }
282 }
283 if (textPathInfo.startOffset != 0) {
286 offset += "%";
287 }
288 context.shapeWriter().addAttribute("startOffset", offset);
289 }
291 context.shapeWriter().addAttribute("method", KoSvgText::writeTextPathMethod(textPathInfo.method));
292 }
294 context.shapeWriter().addAttribute("side", KoSvgText::writeTextPathSide(textPathInfo.side));
295 }
297 context.shapeWriter().addAttribute("spacing", KoSvgText::writeTextPathSpacing(textPathInfo.spacing));
298 }
299 }
300
301 if (!localTransformations.isEmpty()) {
302
303 QVector<qreal> xPos;
304 QVector<qreal> yPos;
305 QVector<qreal> dxPos;
306 QVector<qreal> dyPos;
307 QVector<qreal> rotate;
308
309 fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, localTransformations);
310
311 for (int i = 0; i < rotate.size(); i++) {
312 rotate[i] = kisRadiansToDegrees(rotate[i]);
313 }
314
315 writeTextListAttribute("x", xPos, context.shapeWriter());
316 writeTextListAttribute("y", yPos, context.shapeWriter());
317 writeTextListAttribute("dx", dxPos, context.shapeWriter());
318 writeTextListAttribute("dy", dyPos, context.shapeWriter());
319 writeTextListAttribute("rotate", rotate, context.shapeWriter());
320 }
321
322 if (!textLength.isAuto) {
323 context.shapeWriter().addAttribute("textLength", KisDomUtils::toString(textLength.customValue));
324
326 context.shapeWriter().addAttribute("lengthAdjust", "spacingAndGlyphs");
327 }
328 }
329
331
332 ownProperties = adjustPropertiesForFontSizeWorkaround(ownProperties);
333
334 // we write down stroke/fill if they are different from the parent's value
335 if (!rootText) {
336 if (ownProperties.hasProperty(KoSvgTextProperties::FillId)) {
338 false,
339 this->associatedOutline.boundingRect(),
340 associatedOutline.boundingRect().size(),
341 QTransform(),
342 context);
343 }
344
345 if (ownProperties.hasProperty(KoSvgTextProperties::StrokeId)) {
347 }
348 }
349
350 QMap<QString, QString> attributes = ownProperties.convertToSvgTextAttributes();
351 QStringList allowedAttributes = properties.supportedXmlAttributes();
352 QString styleString;
353
354 for (auto it = shapeSpecificAttributes.constBegin(); it != shapeSpecificAttributes.constEnd(); ++it) {
355 styleString.append(it.key().toLatin1().data()).append(": ").append(it.value()).append(";");
356 }
357 for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
358 if (allowedAttributes.contains(it.key())) {
359 context.shapeWriter().addAttribute(it.key().toLatin1().data(), it.value());
360 } else {
361 styleString.append(it.key().toLatin1().data()).append(": ").append(it.value()).append(";");
362 }
363 }
364 if (!styleString.isEmpty()) {
365 context.shapeWriter().addAttribute("style", styleString);
366 }
367
369 context.shapeWriter().addAttribute(TEXT_STYLE_TYPE.toLatin1().data(), properties.property(KoSvgTextProperties::KraTextStyleType).toString());
371 context.shapeWriter().addAttribute(TEXT_STYLE_RES.toLatin1().data(), QString::number(properties.property(KoSvgTextProperties::KraTextStyleResolution).toInt())+"dpi");
372 }
373 }
374
375 if (saveText) {
376 context.shapeWriter().addTextNode(text);
377 }
378 return true;
379}
380
381static QString transformText(QString text, KoSvgText::TextTransformInfo textTransformInfo, const QString &lang, QVector<QPair<int, int>> &positions)
382{
383 if (textTransformInfo.capitals == KoSvgText::TextTransformCapitalize) {
384 text = KoCssTextUtils::transformTextCapitalize(text, lang, positions);
385 } else if (textTransformInfo.capitals == KoSvgText::TextTransformUppercase) {
386 text = KoCssTextUtils::transformTextToUpperCase(text, lang, positions);
387 } else if (textTransformInfo.capitals == KoSvgText::TextTransformLowercase) {
388 text = KoCssTextUtils::transformTextToLowerCase(text, lang, positions);
389 } else {
390 positions.clear();
391 for (int i = 0; i < text.size(); i++) {
392 positions.append(QPair<int, int>(i, i));
393 }
394 }
395
396 if (textTransformInfo.fullWidth) {
398 }
399 if (textTransformInfo.fullSizeKana) {
401 }
402 return text;
403}
404
405int KoSvgTextContentElement::numChars(bool withControls, KoSvgTextProperties resolvedProps) const
406{
407 int result = 0;
408 if (withControls) {
411 KoSvgText::TextTransformInfo textTransformInfo =
413 QString lang = resolvedProps.property(KoSvgTextProperties::TextLanguage).toString().toUtf8();
414 QVector<QPair<int, int>> positions;
415
416 result = KoCssTextUtils::getBidiOpening(direction == KoSvgText::DirectionLeftToRight, bidi).size();
417 result += transformText(text, textTransformInfo, lang, positions).size();
418 result += KoCssTextUtils::getBidiClosing(bidi).size();
419 } else {
420 result = text.size();
421 }
422 return result;
423}
424
425void KoSvgTextContentElement::insertText(int start, QString insertText)
426{
427 if (start >= text.size()) {
428 text.append(insertText);
429 } else {
430 text.insert(start, insertText);
431 }
432}
433
434
435QString KoSvgTextContentElement::getTransformedString(QVector<QPair<int, int> > &positions, KoSvgTextProperties resolvedProps) const
436{
437 KoSvgText::TextTransformInfo textTransformInfo =
439 QString lang = resolvedProps.property(KoSvgTextProperties::TextLanguage).toString().toUtf8();
440 return transformText(text, textTransformInfo, lang, positions);
441}
442
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
Q_GUI_EXPORT int qt_defaultDpi()
const QString TEXT_STYLE_RES
static QString transformText(QString text, KoSvgText::TextTransformInfo textTransformInfo, const QString &lang, QVector< QPair< int, int > > &positions)
const QString TEXT_STYLE_TYPE
static QString transformTextFullSizeKana(const QString &text)
transformTextFullSizeKana This function will take 'small' Kana (Japanese phonetic script) and transfo...
static QString transformTextCapitalize(const QString &text, QString langCode, QVector< QPair< int, int > > &positions)
transformTextToUpperCase This function splits the text into graphemes, and then uses QLocale::toUpper...
static QString transformTextToUpperCase(const QString &text, const QString &langCode, QVector< QPair< int, int > > &positions)
transformTextToUpperCase convenience function that creates a QLocale and uses it's 'toUpper' function...
static QString transformTextToLowerCase(const QString &text, const QString &langCode, QVector< QPair< int, int > > &positions)
transformTextToUpperCase convenience function that creates a QLocale and uses it's 'toLower' function...
static QString getBidiClosing(KoSvgText::UnicodeBidi bidi)
getBidiClosing Returns the bidi closing string associated with the given Css unicode-bidi value.
static void removeText(QString &text, int &start, int length)
removeText Special removal of text that takes a text, start and length and will modify these values s...
static QString transformTextFullWidth(const QString &text)
transformTextFullWidth This function will transform 'narrow' or 'halfwidth' characters to their norma...
static QString getBidiOpening(bool ltr, KoSvgText::UnicodeBidi bidi)
getBidiOpening Get the bidi opening string associated with the given Css unicode-bidi value and direc...
@ Visiblity
Bool, CSS visibility.
@ UnicodeBidiId
KoSvgText::UnicodeBidi.
@ PaintOrder
QVector<KoShape::PaintOrder>
@ KraTextStyleResolution
Int, used to scale style presets to be pixel-relative.
@ KraTextVersionId
Int, used for handling incorrectly saved files.
@ Opacity
Double, SVG shape opacity.
@ KraTextStyleType
string, used to identify the style preset type (character or paragraph).
@ FontSizeAdjustId
KoSvgText::AutoValue.
@ StrokeId
KoSvgText::StrokeProperty.
@ TextTransformId
KoSvgText::TextTransformInfo Struct.
@ FillId
KoSvgText::BackgroundProperty.
@ DirectionId
KoSvgText::Direction.
@ TextLanguage
a language string.
QSharedPointer< KoShapeBackground > background() const
KoShapeStrokeModelSP stroke() const
QMap< QString, QString > convertToSvgTextAttributes() const
QVariant property(PropertyId id, const QVariant &defaultValue=QVariant()) const
static const KoSvgTextProperties & defaultProperties()
bool hasProperty(PropertyId id) const
void setFontSize(const KoSvgText::CssLengthPercentage length)
void setProperty(PropertyId id, const QVariant &value)
KoSvgTextProperties ownProperties(const KoSvgTextProperties &parentProperties, bool keepFontSize=false) const
static QStringList supportedXmlAttributes()
QVariant propertyOrDefault(PropertyId id) const
KoSvgText::CssLengthPercentage fontSize() const
void addAttribute(const char *attrName, const QString &value)
Definition KoXmlWriter.h:61
KoSvgTextProperties textProperties
Stores textProperties.
Contains data used for loading svg.
SvgGraphicsContext * currentGC() const
Returns the current graphics context.
KoSvgTextProperties resolvedProperties() const
These are the text properties, completely resolved, ensuring that everything is inherited and the siz...
Context for saving svg files.
QScopedPointer< KoXmlWriter > shapeWriter
static void saveSvgFill(QSharedPointer< KoShapeBackground > background, const bool fillRuleEvenOdd, const QRectF outlineRect, const QSizeF size, const QTransform absoluteTransform, SvgSavingContext &context)
Saves fill style of specified shape.
static QString embedShape(const KoShape *shape, SvgSavingContext &context)
static void saveSvgStroke(KoShapeStrokeModelSP, SvgSavingContext &context)
Saves stroke style of specified shape.
static qreal parseUnitX(SvgGraphicsContext *gc, const KoSvgTextProperties &resolved, const QString &unit)
parses a length attribute in x-direction
Definition SvgUtil.cpp:304
static qreal parseUnitAngular(SvgGraphicsContext *gc, const QString &unit)
parses angle, result in radians!
Definition SvgUtil.cpp:332
static const char * parseNumber(const char *ptr, qreal &number)
parses the number into parameter number
Definition SvgUtil.cpp:378
static QStringList simplifyList(const QString &str)
Definition SvgUtil.cpp:450
static qreal parseUnit(SvgGraphicsContext *gc, const KoSvgTextProperties &resolved, QStringView, bool horiz=false, bool vert=false, const QRectF &bbox=QRectF())
Parses a length attribute.
Definition SvgUtil.cpp:218
static qreal parseUnitY(SvgGraphicsContext *gc, const KoSvgTextProperties &resolved, const QString &unit)
parses a length attribute in y-direction
Definition SvgUtil.cpp:313
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
T kisRadiansToDegrees(T radians)
Definition kis_global.h:181
int toInt(const QString &str, bool *ok=nullptr)
QString toString(const QString &value)
QString writeTextPathSide(TextPathSide value)
TextPathSide parseTextPathSide(const QString &value)
TextPathSpacing parseTextPathSpacing(const QString &value)
@ LengthAdjustSpacingAndGlyphs
Stretches the glyphs as well.
Definition KoSvgText.h:252
LengthAdjust parseLengthAdjust(const QString &value)
Direction
Base direction used by Bidi algorithm.
Definition KoSvgText.h:48
@ DirectionLeftToRight
Definition KoSvgText.h:49
QString writeTextPathMethod(TextPathMethod value)
@ TextPathSideLeft
Definition KoSvgText.h:302
QVariant fromAutoValue(const KoSvgText::AutoValue &value)
Definition KoSvgText.h:493
AutoValue parseAutoValueXY(const QString &value, const SvgLoadingContext &context, const QString &autoKeyword)
QString writeTextPathSpacing(TextPathSpacing value)
TextPathMethod parseTextPathMethod(const QString &value)
@ TextTransformCapitalize
Definition KoSvgText.h:185
@ TextTransformUppercase
Convert all bicarmel text to upper-case, locale dependant.
Definition KoSvgText.h:187
@ TextTransformLowercase
Convert all bicarmel text to lower-case, locale dependant.
Definition KoSvgText.h:188
@ TextPathAlign
Only align position and rotation of glyphs to the path.
Definition KoSvgText.h:287
void removeText(int &start, int length)
removeText removes text,
KoSvgText::AutoValue textLength
the value 'textLength' attribute of the associated dom element
bool saveSvg(SvgSavingContext &context, bool rootText, bool saveText, QMap< QString, QString > shapeSpecificAttributes)
QVector< KoSvgText::CharTransformation > localTransformations
Local SVG char transforms.
QString getTransformedString(QVector< QPair< int, int > > &positions, KoSvgTextProperties resolvedProps=KoSvgTextProperties()) const
bool loadSvg(const QDomElement &element, SvgLoadingContext &context, bool rootNode=false)
loadSvg load SVG style data into the current content element.
KoSvgTextProperties properties
The textProperties. This includes.
QPainterPath associatedOutline
The associated outline. Currently only a bounding box.
QString text
Plain text of the current node. Use insertText and removeText to manipulate it.
QScopedPointer< KoShape > textPath
The textpath, if any. Defaults to null.
bool loadSvgTextNode(const QDomText &text, SvgLoadingContext &context)
KoSvgText::LengthAdjust lengthAdjust
the value 'lengthAdjust' attribute of the associated dom element
int numChars(bool withControls=false, KoSvgTextProperties resolvedProps=KoSvgTextProperties()) const
KoSvgText::TextOnPathInfo textPathInfo
Text path info for the text-on-path algorithm.
void insertText(int start, QString insertText)
insertText
boost::optional< qreal > yPos
Definition KoSvgText.h:606
boost::optional< qreal > dxPos
Definition KoSvgText.h:607
boost::optional< qreal > dyPos
Definition KoSvgText.h:608
boost::optional< qreal > rotate
Definition KoSvgText.h:609
boost::optional< qreal > xPos
Definition KoSvgText.h:605
TextPathMethod method
Definition KoSvgText.h:626
TextPathSpacing spacing
Definition KoSvgText.h:627
TextTransform capitals
Text transform upper/lower/capitalize.
Definition KoSvgText.h:637