1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickqmlgenerator_p.h"
5#include "qquicknodeinfo_p.h"
6#include "utils_p.h"
7
8#include <private/qsgcurveprocessor_p.h>
9#include <private/qquickshape_p.h>
10#include <private/qquadpath_p.h>
11#include <private/qquickitem_p.h>
12#include <private/qquickimagebase_p_p.h>
13
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/qdir.h>
16
17QT_BEGIN_NAMESPACE
18
19Q_DECLARE_LOGGING_CATEGORY(lcQuickVectorImage)
20
21QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName)
22 : QQuickGenerator(fileName, flags)
23 , outputFileName(outFileName)
24{
25 m_result.open(openMode: QIODevice::ReadWrite);
26}
27
28QQuickQmlGenerator::~QQuickQmlGenerator()
29{
30 if (m_generationSucceeded && !outputFileName.isEmpty()) {
31 QFileInfo fileInfo(outputFileName);
32 QDir dir(fileInfo.absolutePath());
33 if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
34 qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath();
35 } else {
36 stream().flush(); // Add a final newline and flush the stream to m_result
37 QFile outFile(outputFileName);
38 outFile.open(flags: QIODevice::WriteOnly);
39 outFile.write(data: m_result.data());
40 outFile.close();
41 }
42 }
43
44 if (lcQuickVectorImage().isDebugEnabled())
45 qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(n: 300);
46}
47
48void QQuickQmlGenerator::setShapeTypeName(const QString &name)
49{
50 m_shapeTypeName = name.toLatin1();
51}
52
53QString QQuickQmlGenerator::shapeTypeName() const
54{
55 return QString::fromLatin1(ba: m_shapeTypeName);
56}
57
58void QQuickQmlGenerator::setCommentString(const QString commentString)
59{
60 m_commentString = commentString;
61}
62
63QString QQuickQmlGenerator::commentString() const
64{
65 return m_commentString;
66}
67
68void QQuickQmlGenerator::generateNodeBase(const NodeInfo &info)
69{
70 m_indentLevel++;
71 if (!info.nodeId.isEmpty())
72 stream() << "objectName: \"" << info.nodeId << "\"";
73 if (!info.isDefaultTransform) {
74 auto sx = info.transform.m11();
75 auto sy = info.transform.m22();
76 auto x = info.transform.m31();
77 auto y = info.transform.m32();
78 if (info.transform.type() == QTransform::TxTranslate) {
79 stream() << "transform: Translate { " << "x: " << x << "; y: " << y << " }";
80 } else if (info.transform.type() == QTransform::TxScale && !x && !y) {
81 stream() << "transform: Scale { xScale: " << sx << "; yScale: " << sy << " }";
82 } else {
83 stream() << "transform: Matrix4x4 { matrix: ";
84 generateTransform(xf: info.transform);
85 stream(flags: SameLine) << " }";
86 }
87 }
88 if (!info.isDefaultOpacity) {
89 stream() << "opacity: " << info.opacity;
90 }
91 m_indentLevel--;
92}
93
94bool QQuickQmlGenerator::generateDefsNode(const NodeInfo &info)
95{
96 Q_UNUSED(info)
97
98 return false;
99}
100
101void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info)
102{
103 if (!isNodeVisible(info))
104 return;
105
106 const QFileInfo outputFileInfo(outputFileName);
107 const QDir outputDir(outputFileInfo.absolutePath());
108
109 QString filePath;
110
111 if (!m_retainFilePaths || info.externalFileReference.isEmpty()) {
112 filePath = m_assetFileDirectory;
113 if (filePath.isEmpty())
114 filePath = outputDir.absolutePath();
115
116 if (!filePath.isEmpty() && !filePath.endsWith(c: u'/'))
117 filePath += u'/';
118
119 QDir fileDir(filePath);
120 if (!fileDir.exists()) {
121 if (!fileDir.mkpath(QStringLiteral(".")))
122 qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath;
123 }
124
125 filePath += QStringLiteral("%1%2.png").arg(a: m_assetFilePrefix.isEmpty()
126 ? QStringLiteral("svg_asset_")
127 : m_assetFilePrefix)
128 .arg(a: info.image.cacheKey());
129
130 if (!info.image.save(fileName: filePath))
131 qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath;
132 qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath;
133 } else {
134 filePath = info.externalFileReference;
135 }
136
137 const QFileInfo assetFileInfo(filePath);
138
139 stream() << "Image {";
140
141 generateNodeBase(info);
142 m_indentLevel++;
143 stream() << "x: " << info.rect.x();
144 stream() << "y: " << info.rect.y();
145 stream() << "width: " << info.rect.width();
146 stream() << "height: " << info.rect.height();
147 stream() << "source: \"" << outputDir.relativeFilePath(fileName: assetFileInfo.absoluteFilePath()) <<"\"";
148
149 m_indentLevel--;
150
151 stream() << "}";
152}
153
154void QQuickQmlGenerator::generatePath(const PathNodeInfo &info, const QRectF &overrideBoundingRect)
155{
156 if (!isNodeVisible(info))
157 return;
158
159 if (m_inShapeItem) {
160 if (!info.isDefaultTransform)
161 qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)";
162 optimizePaths(info, overrideBoundingRect);
163 } else {
164 m_inShapeItem = true;
165 stream() << shapeName() << " {";
166
167 generateNodeBase(info);
168
169 m_indentLevel++;
170 if (m_flags.testFlag(flag: QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
171 stream() << "preferredRendererType: Shape.CurveRenderer";
172 optimizePaths(info, overrideBoundingRect);
173 //qCDebug(lcQuickVectorGraphics) << *node->qpath();
174 m_indentLevel--;
175 stream() << "}";
176 m_inShapeItem = false;
177 }
178}
179
180void QQuickQmlGenerator::generateGradient(const QGradient *grad)
181{
182 if (grad->type() == QGradient::LinearGradient) {
183 auto *linGrad = static_cast<const QLinearGradient *>(grad);
184 stream() << "fillGradient: LinearGradient {";
185 m_indentLevel++;
186
187 QRectF gradRect(linGrad->start(), linGrad->finalStop());
188
189 stream() << "x1: " << gradRect.left();
190 stream() << "y1: " << gradRect.top();
191 stream() << "x2: " << gradRect.right();
192 stream() << "y2: " << gradRect.bottom();
193 for (auto &stop : linGrad->stops())
194 stream() << "GradientStop { position: " << stop.first << "; color: \"" << stop.second.name(format: QColor::HexArgb) << "\" }";
195 m_indentLevel--;
196 stream() << "}";
197 } else if (grad->type() == QGradient::RadialGradient) {
198 auto *radGrad = static_cast<const QRadialGradient*>(grad);
199 stream() << "fillGradient: RadialGradient {";
200 m_indentLevel++;
201
202 stream() << "centerX: " << radGrad->center().x();
203 stream() << "centerY: " << radGrad->center().y();
204 stream() << "centerRadius: " << radGrad->radius();
205 stream() << "focalX:" << radGrad->focalPoint().x();
206 stream() << "focalY:" << radGrad->focalPoint().y();
207 for (auto &stop : radGrad->stops())
208 stream() << "GradientStop { position: " << stop.first << "; color: \"" << stop.second.name(format: QColor::HexArgb) << "\" }";
209 m_indentLevel--;
210 stream() << "}";
211 }
212}
213
214void QQuickQmlGenerator::generateTransform(const QTransform &xf)
215{
216 if (xf.isAffine()) {
217 stream(flags: SameLine) << "PlanarTransform.fromAffineMatrix("
218 << xf.m11() << ", " << xf.m12() << ", "
219 << xf.m21() << ", " << xf.m22() << ", "
220 << xf.dx() << ", " << xf.dy() << ")";
221 } else {
222 QMatrix4x4 m(xf);
223 stream(flags: SameLine) << "Qt.matrix4x4(";
224 m_indentLevel += 3;
225 const auto *data = m.data();
226 for (int i = 0; i < 4; i++) {
227 stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12];
228 if (i < 3)
229 stream(flags: SameLine) << ", ";
230 }
231 stream(flags: SameLine) << ")";
232 m_indentLevel -= 3;
233 }
234}
235
236void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect)
237{
238 Q_UNUSED(pathSelector)
239 Q_ASSERT(painterPath || quadPath);
240
241 const bool noPen = info.strokeStyle.color == QColorConstants::Transparent;
242 if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen)
243 return;
244
245 const bool noFill = info.grad.type() == QGradient::NoGradient && info.fillColor == QColorConstants::Transparent;
246
247 if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill)
248 return;
249
250 auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule());
251 stream() << "ShapePath {";
252 m_indentLevel++;
253 if (!info.nodeId.isEmpty()) {
254 switch (pathSelector) {
255 case QQuickVectorImageGenerator::FillPath:
256 stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\"";
257 break;
258 case QQuickVectorImageGenerator::StrokePath:
259 stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\"";
260 break;
261 case QQuickVectorImageGenerator::FillAndStroke:
262 stream() << "objectName: \"svg_path:" << info.nodeId << "\"";
263 break;
264 }
265 }
266
267 if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) {
268 stream() << "strokeColor: \"transparent\"";
269 } else {
270 stream() << "strokeColor: \"" << info.strokeStyle.color.name(format: QColor::HexArgb) << "\"";
271 stream() << "strokeWidth: " << info.strokeStyle.width;
272 stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(strokeCapStyle: info.strokeStyle.lineCapStyle);
273 stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(strokeJoinStyle: info.strokeStyle.lineJoinStyle);
274 stream() << "miterLimit: " << info.strokeStyle.miterLimit;
275 if (info.strokeStyle.dashArray.length() != 0) {
276 stream() << "strokeStyle: " << "ShapePath.DashLine";
277 stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(list: info.strokeStyle.dashArray);
278 stream() << "dashOffset: " << info.strokeStyle.dashOffset;
279 }
280 }
281
282 QTransform fillTransform = info.fillTransform;
283 if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) {
284 stream() << "fillColor: \"transparent\"";
285 } else if (info.grad.type() != QGradient::NoGradient) {
286 generateGradient(grad: &info.grad);
287 if (info.grad.coordinateMode() == QGradient::ObjectMode) {
288 QTransform objectToUserSpace;
289 objectToUserSpace.translate(dx: boundingRect.x(), dy: boundingRect.y());
290 objectToUserSpace.scale(sx: boundingRect.width(), sy: boundingRect.height());
291 fillTransform *= objectToUserSpace;
292 }
293 } else {
294 stream() << "fillColor: \"" << info.fillColor.name(format: QColor::HexArgb) << "\"";
295 }
296
297 if (!fillTransform.isIdentity()) {
298 const QTransform &xf = fillTransform;
299 stream() << "fillTransform: ";
300 if (info.fillTransform.type() == QTransform::TxTranslate)
301 stream(flags: SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")";
302 else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy())
303 stream(flags: SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")";
304 else
305 generateTransform(xf);
306 }
307
308 if (fillRule == QQuickShapePath::WindingFill)
309 stream() << "fillRule: ShapePath.WindingFill";
310 else
311 stream() << "fillRule: ShapePath.OddEvenFill";
312
313 QString hintStr;
314 if (quadPath)
315 hintStr = QQuickVectorImageGenerator::Utils::pathHintString(qp: *quadPath);
316 if (!hintStr.isEmpty())
317 stream() << hintStr;
318
319
320 QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(path: *painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(path: *quadPath);
321 stream() << "PathSvg { path: \"" << svgPathString << "\" }";
322
323 m_indentLevel--;
324 stream() << "}";
325}
326
327void QQuickQmlGenerator::generateNode(const NodeInfo &info)
328{
329 if (!isNodeVisible(info))
330 return;
331
332 stream() << "// Missing Implementation for SVG Node: " << info.typeName;
333 stream() << "// Adding an empty Item and skipping";
334 stream() << "Item {";
335 generateNodeBase(info);
336 stream() << "}";
337}
338
339void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
340{
341 if (!isNodeVisible(info))
342 return;
343
344 static int counter = 0;
345 stream() << "Item {";
346 generateNodeBase(info);
347 m_indentLevel++;
348
349 if (!info.isTextArea)
350 stream() << "Item { id: textAlignItem_" << counter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}";
351
352 stream() << "Text {";
353
354 m_indentLevel++;
355
356 if (info.isTextArea) {
357 stream() << "x: " << info.position.x();
358 stream() << "y: " << info.position.y();
359 if (info.size.width() > 0)
360 stream() << "width: " << info.size.width();
361 if (info.size.height() > 0)
362 stream() << "height: " << info.size.height();
363 stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard
364 stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level
365 } else {
366 QString hAlign = QStringLiteral("left");
367 stream() << "anchors.baseline: textAlignItem_" << counter << ".top";
368 switch (info.alignment) {
369 case Qt::AlignHCenter:
370 hAlign = QStringLiteral("horizontalCenter");
371 break;
372 case Qt::AlignRight:
373 hAlign = QStringLiteral("right");
374 break;
375 default:
376 qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment;
377 Q_FALLTHROUGH();
378 case Qt::AlignLeft:
379 break;
380 }
381 stream() << "anchors." << hAlign << ": textAlignItem_" << counter << ".left";
382 }
383 counter++;
384
385 stream() << "color: \"" << info.fillColor.name(format: QColor::HexArgb) << "\"";
386 stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText");
387
388 QString s = info.text;
389 s.replace(c: QLatin1Char('"'), after: QLatin1String("\\\""));
390 stream() << "text: \"" << s << "\"";
391 stream() << "font.family: \"" << info.font.family() << "\"";
392 if (info.font.pixelSize() > 0)
393 stream() << "font.pixelSize:" << info.font.pixelSize();
394 else if (info.font.pointSize() > 0)
395 stream() << "font.pixelSize:" << info.font.pointSizeF();
396 if (info.font.underline())
397 stream() << "font.underline: true";
398 if (info.font.weight() != QFont::Normal)
399 stream() << "font.weight: " << int(info.font.weight());
400 if (info.font.italic())
401 stream() << "font.italic: true";
402 switch (info.font.hintingPreference()) {
403 case QFont::PreferFullHinting:
404 stream() << "font.hintingPreference: Font.PreferFullHinting";
405 break;
406 case QFont::PreferVerticalHinting:
407 stream() << "font.hintingPreference: Font.PreferVerticalHinting";
408 break;
409 case QFont::PreferNoHinting:
410 stream() << "font.hintingPreference: Font.PreferNoHinting";
411 break;
412 case QFont::PreferDefaultHinting:
413 stream() << "font.hintingPreference: Font.PreferDefaultHinting";
414 break;
415 };
416
417 if (info.strokeColor != QColorConstants::Transparent) {
418 stream() << "styleColor: \"" << info.strokeColor.name(format: QColor::HexArgb) << "\"";
419 stream() << "style: Text.Outline";
420 }
421
422 m_indentLevel--;
423 stream() << "}";
424
425 m_indentLevel--;
426 stream() << "}";
427}
428
429void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info)
430{
431 if (!isNodeVisible(info))
432 return;
433
434 if (info.stage == StructureNodeStage::Start) {
435 stream() << "Item {";
436 generateNodeBase(info);
437 m_indentLevel++;
438 stream() << "x: " << info.startPos.x();
439 stream() << "y: " << info.startPos.y();
440 } else {
441 m_indentLevel--;
442 stream() << "}";
443 }
444}
445
446void QQuickQmlGenerator::generatePathContainer(const StructureNodeInfo &info)
447{
448 Q_UNUSED(info);
449 stream() << shapeName() <<" {";
450 m_indentLevel++;
451 if (m_flags.testFlag(flag: QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
452 stream() << "preferredRendererType: Shape.CurveRenderer";
453 m_indentLevel--;
454
455 m_inShapeItem = true;
456}
457
458bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info)
459{
460 if (!isNodeVisible(info))
461 return false;
462
463 if (info.stage == StructureNodeStage::Start) {
464 if (!info.forceSeparatePaths && info.isPathContainer) {
465 generatePathContainer(info);
466 } else {
467 stream() << "Item {";
468 }
469
470 if (!info.viewBox.isEmpty()) {
471 m_indentLevel++;
472 stream() << "transform: [";
473 m_indentLevel++;
474 bool translate = !qFuzzyIsNull(d: info.viewBox.x()) || !qFuzzyIsNull(d: info.viewBox.y());
475 if (translate)
476 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
477 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
478 m_indentLevel--;
479 stream() << "]";
480 m_indentLevel--;
481 }
482
483 generateNodeBase(info);
484 m_indentLevel++;
485 } else {
486 m_indentLevel--;
487 stream() << "}";
488 m_inShapeItem = false;
489 }
490
491 return true;
492}
493
494bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
495{
496 const QStringList comments = m_commentString.split(sep: u'\n');
497
498 if (!isNodeVisible(info)) {
499 m_indentLevel = 0;
500
501 if (comments.isEmpty()) {
502 stream() << "// Generated from SVG";
503 } else {
504 for (const auto &comment : comments)
505 stream() << "// " << comment;
506 }
507
508 stream() << "import QtQuick";
509 stream() << "import QtQuick.Shapes" << Qt::endl;
510 stream() << "Item {";
511 m_indentLevel++;
512
513 double w = info.size.width();
514 double h = info.size.height();
515 if (w > 0)
516 stream() << "implicitWidth: " << w;
517 if (h > 0)
518 stream() << "implicitHeight: " << h;
519
520 m_indentLevel--;
521 stream() << "}";
522
523 return false;
524 }
525
526 if (info.stage == StructureNodeStage::Start) {
527 m_indentLevel = 0;
528
529 if (comments.isEmpty())
530 stream() << "// Generated from SVG";
531 else
532 for (const auto &comment : comments)
533 stream() << "// " << comment;
534
535 stream() << "import QtQuick";
536 stream() << "import QtQuick.Shapes" << Qt::endl;
537 stream() << "Item {";
538 m_indentLevel++;
539
540 double w = info.size.width();
541 double h = info.size.height();
542 if (w > 0)
543 stream() << "implicitWidth: " << w;
544 if (h > 0)
545 stream() << "implicitHeight: " << h;
546
547 if (!info.viewBox.isEmpty()) {
548 stream() << "transform: [";
549 m_indentLevel++;
550 bool translate = !qFuzzyIsNull(d: info.viewBox.x()) || !qFuzzyIsNull(d: info.viewBox.y());
551 if (translate)
552 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
553 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
554 m_indentLevel--;
555 stream() << "]";
556 }
557
558 generateNodeBase(info);
559
560 if (!info.forceSeparatePaths && info.isPathContainer) {
561 generatePathContainer(info);
562 m_indentLevel++;
563 }
564 } else {
565 if (m_inShapeItem) {
566 m_inShapeItem = false;
567 m_indentLevel--;
568 stream() << "}";
569 }
570
571 m_indentLevel--;
572 stream() << "}";
573 }
574
575 return true;
576}
577
578QStringView QQuickQmlGenerator::indent()
579{
580 static QString indentString;
581 int indentWidth = m_indentLevel * 4;
582 if (indentWidth > indentString.size())
583 indentString.fill(c: QLatin1Char(' '), size: indentWidth * 2);
584 return QStringView(indentString).first(n: indentWidth);
585}
586
587QTextStream &QQuickQmlGenerator::stream(int flags)
588{
589 if (m_stream.device() == nullptr)
590 m_stream.setDevice(&m_result);
591 else if (!(flags & StreamFlags::SameLine))
592 m_stream << Qt::endl << indent();
593 return m_stream;
594}
595
596const char *QQuickQmlGenerator::shapeName() const
597{
598 return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData();
599}
600
601QT_END_NAMESPACE
602

source code of qtdeclarative/src/quickvectorimage/generator/qquickqmlgenerator.cpp