diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/svgtoqml/CMakeLists.txt | 6 | ||||
-rw-r--r-- | tools/svgtoqml/main.cpp | 56 | ||||
-rw-r--r-- | tools/svgtoqml/qsvgloader.cpp | 1119 | ||||
-rw-r--r-- | tools/svgtoqml/qsvgloader_p.h | 32 |
4 files changed, 37 insertions, 1176 deletions
diff --git a/tools/svgtoqml/CMakeLists.txt b/tools/svgtoqml/CMakeLists.txt index 008a1641f5..9511212627 100644 --- a/tools/svgtoqml/CMakeLists.txt +++ b/tools/svgtoqml/CMakeLists.txt @@ -11,16 +11,12 @@ qt_internal_add_tool(${target_name} TOOLS_TARGET Quick SOURCES main.cpp - qsvgloader.cpp qsvgloader_p.h LIBRARIES Qt::Core Qt::Gui Qt::Qml Qt::Quick - Qt::QuickPrivate - Qt::Svg - Qt::SvgPrivate - Qt::QuickShapesPrivate + Qt::QuickVectorGraphicsGeneratorPrivate ) qt_internal_return_unless_building_tools() diff --git a/tools/svgtoqml/main.cpp b/tools/svgtoqml/main.cpp index 816f4808c0..91d55b73eb 100644 --- a/tools/svgtoqml/main.cpp +++ b/tools/svgtoqml/main.cpp @@ -5,18 +5,17 @@ #include <QQmlApplicationEngine> #include <QCommandLineParser> #include <QFile> -#include <private/qquickrectangle_p.h> -#include <private/qsvgtinydocument_p.h> #include <QQuickWindow> - -#include "qsvgloader_p.h" +#include <QQuickItem> +#include <QtQuickVectorGraphicsGenerator/private/qquickitemgenerator_p.h> +#include <QtQuickVectorGraphicsGenerator/private/qquickqmlgenerator_p.h> +#include <QtQuickVectorGraphicsGenerator/private/qquickvectorgraphicsglobal_p.h> #define ENABLE_GUI int main(int argc, char *argv[]) { #ifdef ENABLE_GUI - qputenv("QT_QUICKSHAPES_BACKEND", "curverenderer"); QGuiApplication app(argc, argv); #else QCoreApplication app(argc, argv); @@ -41,6 +40,16 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "typename")); parser.addOption(typeNameOption); + QCommandLineOption copyrightOption("copyright-statement", + QCoreApplication::translate("main", "Add <string> as a comment at the start of the generated file."), + QCoreApplication::translate("main", "string")); + parser.addOption(copyrightOption); + + QCommandLineOption outlineModeOption("outline-stroke-mode", + QCoreApplication::translate("main", "Stroke the outside of the filled shape instead of " + "the original path. Also sets optimize-paths.")); + parser.addOption(outlineModeOption); + #ifdef ENABLE_GUI QCommandLineOption guiOption(QStringList() << "v" << "view", QCoreApplication::translate("main", "Display the SVG in a window.")); @@ -54,22 +63,30 @@ int main(int argc, char *argv[]) const QString inFileName = args.at(0); - auto *doc = QSvgTinyDocument::load(inFileName); - if (!doc) { - fprintf(stderr, "%s is not a valid SVG\n", qPrintable(inFileName)); - return 2; - } - - const QString commentString = QLatin1String("Generated from SVG file %1").arg(inFileName); + QString commentString = QLatin1String("Generated from SVG file %1").arg(inFileName); const auto outFileName = args.size() > 1 ? args.at(1) : QString{}; const auto typeName = parser.value(typeNameOption); + auto copyrightString = parser.value(copyrightOption); - QSvgQmlWriter::GeneratorFlags flags; + if (!copyrightString.isEmpty()) { + copyrightString = copyrightString.replace("\\n", "\n"); + commentString = copyrightString + u"\n" + commentString; + } + + QQuickVectorGraphics::GeneratorFlags flags; if (parser.isSet(curveRendererOption)) - flags |= QSvgQmlWriter::CurveRenderer; + flags |= QQuickVectorGraphics::GeneratorFlag::CurveRenderer; if (parser.isSet(optimizeOption)) - flags |= QSvgQmlWriter::OptimizePaths; + flags |= QQuickVectorGraphics::GeneratorFlag::OptimizePaths; + if (parser.isSet(outlineModeOption)) + flags |= (QQuickVectorGraphics::GeneratorFlag::OutlineStrokeMode + | QQuickVectorGraphics::GeneratorFlag::OptimizePaths); + + QQuickQmlGenerator generator(inFileName, flags, outFileName); + generator.setShapeTypeName(typeName); + generator.setCommentString(commentString); + generator.generate(); #ifdef ENABLE_GUI if (parser.isSet(guiOption)) { @@ -82,16 +99,15 @@ int main(int argc, char *argv[]) QCoreApplication::exit(-1); if (obj) { auto *containerItem = obj->findChild<QQuickItem*>(QStringLiteral("svg_item")); - auto *contents = QSvgQmlWriter::loadSVG(doc, outFileName, flags, typeName, containerItem, commentString); - contents->setWidth(containerItem->implicitWidth()); // Workaround for runtime loader viewbox size logic. TODO: fix - contents->setHeight(containerItem->implicitHeight()); + QQuickItemGenerator generator(inFileName, flags, containerItem); + generator.generate(); } }); engine.load(url); return app.exec(); } +#else + return 0; #endif - QSvgQmlWriter::loadSVG(doc, outFileName, flags, typeName, nullptr, commentString); - return 0; } diff --git a/tools/svgtoqml/qsvgloader.cpp b/tools/svgtoqml/qsvgloader.cpp deleted file mode 100644 index fc72fb7454..0000000000 --- a/tools/svgtoqml/qsvgloader.cpp +++ /dev/null @@ -1,1119 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#include "qsvgloader_p.h" -#include <private/qsvgvisitor_p.h> - -#include <QString> -#include <QPainter> -#include <QMatrix4x4> -#include <QQuickItem> - -#include <private/qquickshape_p.h> -#include <private/qquicktext_p.h> -#include <private/qquicktranslate_p.h> -#include <private/qquickitem_p.h> - -#include <private/qquickimagebase_p_p.h> -#include <private/qquickimage_p.h> -#include <private/qsgcurveprocessor_p.h> - -#include <private/qquadpath_p.h> - -QT_BEGIN_NAMESPACE - -class RaiiStream : public QTextStream -{ -public: - RaiiStream() = default; - explicit RaiiStream(QTextStream *stream): QTextStream(&output, QIODevice::ReadWrite), m_stream(stream) {} - ~RaiiStream() { - flush(); - if (m_stream && !output.isEmpty()) - *m_stream << output << Qt::endl; - } - - RaiiStream(RaiiStream &other) = delete; - RaiiStream &operator=(const RaiiStream &other) = delete; - RaiiStream(RaiiStream &&other) : m_stream(std::exchange(other.m_stream, nullptr)), output(std::move(other.output)) {} - RaiiStream &operator=(RaiiStream &&other) { - std::swap(m_stream, other.m_stream); - std::swap(output, other.output); - return *this; - } - -private: - QTextStream *m_stream = nullptr; - QByteArray output; -}; - -class SvgLoaderVisitor : public QSvgVisitor -{ -public: - SvgLoaderVisitor(); - ~SvgLoaderVisitor(); - void setShapeTypeName(const QString &name) { m_shapeTypeName = name.toLatin1(); } - void setFlags(QSvgQmlWriter::GeneratorFlags flags) { m_flags = flags; } - QQuickItem *loadQML(QTextStream *stream, const QSvgTinyDocument *doc, QQuickItem *svgItem); - -protected: - void visitNode(const QSvgNode *) override; - - bool visitStructureNodeStart(const QSvgStructureNode *node) override; - void visitStructureNodeEnd(const QSvgStructureNode *node) override; - bool visitDefsNodeStart(const QSvgDefs *node) override; - - void visitTextNode(const QSvgText *node) override; - void visitPathNode(const QSvgPath *node) override; - void visitRectNode(const QSvgRect *node) override; - void visitEllipseNode(const QSvgEllipse *node) override; - - void visitLineNode(const QSvgLine *node) override; - void visitPolygonNode(const QSvgPolygon *node) override; - void visitPolylineNode(const QSvgPolyline *node) override; - - void visitImageNode(const QSvgImage *node) override; - -private: - QString indent() { return QString().fill(' ', m_indentLevel * 4);} - RaiiStream stream() { - RaiiStream strm(m_stream); - strm << indent(); - return strm; - } - - const char *shapeName() const { return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData(); } - - QString currentFillColor() const; - const QGradient *currentFillGradient() const; - QString currentStrokeColor() const; - float currentStrokeWidth() const; - - void handleBaseNodeSetup(const QSvgNode *node); - void handleBaseNode(const QSvgNode *node); - void handleBaseNodeEnd(const QSvgNode *node); - void handlePathNode(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle = Qt::SquareCap); - void outputShapePath(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle capStyle); - void outputGradient(const QGradient *grad, QQuickShapePath *shapePath, const QRectF &boundingRect); - - enum PathSelector { FillPath = 0x1, StrokePath = 0x2, FillAndStroke = 0x3 }; - void outputShapePath(const QSvgNode *node, const QPainterPath *path, const QQuadPath *quadPath, Qt::PenCapStyle capStyle, - PathSelector pathSelector, const QRectF &boundingRect); - - QQuickItem *currentItem() { return m_items.top(); } - void addCurrentItem(QQuickItem *item, const QSvgNode *node = nullptr) { - item->setParentItem(currentItem()); - m_items.push(item); - if (node) { - if (!node->nodeId().isEmpty()) - item->setObjectName(node->nodeId()); - else - item->setObjectName(node->typeName()); - } - } - - int m_indentLevel = 0; - QTextStream *m_stream = nullptr; - QPainter m_dummyPainter; - QImage m_dummyImage; - QSvgExtraStates m_svgState; - bool m_inShapeItem = false; - QQuickShape *m_parentShapeItem = nullptr; - QByteArray m_shapeTypeName; - - QStack<QQuickItem *> m_items; - - QQuickItem *m_loadedItem = nullptr; - bool m_generateQML = true; - bool m_generateItems = false; - QSvgQmlWriter::GeneratorFlags m_flags; -}; - -SvgLoaderVisitor::SvgLoaderVisitor() -{ - m_dummyImage = QImage(1, 1, QImage::Format_RGB32); - m_dummyPainter.begin(&m_dummyImage); - QPen noPen(Qt::NoPen); - noPen.setBrush(Qt::NoBrush); - m_dummyPainter.setPen(noPen); - m_dummyPainter.setBrush(Qt::black); -} - -SvgLoaderVisitor::~SvgLoaderVisitor() -{ - m_dummyPainter.end(); -} - -void SvgLoaderVisitor::handlePathNode(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle capStyle) -{ - handleBaseNodeSetup(node); - - QPainterPath pathCopy = path; - auto fillStyle = node->style().fill; - if (fillStyle) - pathCopy.setFillRule(fillStyle->fillRule()); - if (m_inShapeItem) { - if (!node->style().transform.isDefault()) - qWarning() << "Skipped transform for node" << node->nodeId() << "type" << node->typeName() << "(this is not supposed to happen)"; - outputShapePath(node, pathCopy, capStyle); - } else { - if (m_generateItems) { - auto *shapeItem = new QQuickShape; - shapeItem->setPreferredRendererType(QQuickShape::CurveRenderer); - shapeItem->setContainsMode(QQuickShape::ContainsMode::FillContains); // TODO: configurable? - addCurrentItem(shapeItem, node); - m_parentShapeItem = shapeItem; - } - m_inShapeItem = true; - stream() << shapeName() << " {"; - handleBaseNode(node); - m_indentLevel++; - if (m_flags & QSvgQmlWriter::CurveRenderer) - stream() << "preferredRendererType: Shape.CurveRenderer"; - outputShapePath(node, pathCopy, capStyle); - //qDebug() << *node->qpath(); - m_indentLevel--; - stream() << "}"; - if (m_generateItems) - m_items.pop(); - m_inShapeItem = false; - m_parentShapeItem = nullptr; - } - handleBaseNodeEnd(node); -} - -void SvgLoaderVisitor::visitPathNode(const QSvgPath *node) -{ - stream() << "// PATH visit " << node->nodeId() << " count: " << node->path().elementCount(); - - handlePathNode(node, node->path()); -} - -void SvgLoaderVisitor::visitLineNode(const QSvgLine *node) -{ - // TODO: proper end caps (should be flat by default?) - QPainterPath p; - p.moveTo(node->line().p1()); - p.lineTo(node->line().p2()); - handlePathNode(node, p, Qt::FlatCap); -} - -static QPainterPath polygonToPath(const QPolygonF &poly, bool closed) -{ - QPainterPath path; - if (poly.isEmpty()) - return path; - bool first = true; - for (const auto &p : poly) { - if (first) - path.moveTo(p); - else - path.lineTo(p); - first = false; - } - if (closed) - path.closeSubpath(); - return path; -} - -void SvgLoaderVisitor::visitPolygonNode(const QSvgPolygon *node) -{ - stream() << "// POLYGON visit " << node->nodeId() << " count: " << node->polygon().count(); - QPainterPath p = polygonToPath(node->polygon(), true); - handlePathNode(node, p); -} - -void SvgLoaderVisitor::visitPolylineNode(const QSvgPolyline *node) -{ - stream() << "// POLYLINE visit " << node->nodeId() << " count: " << node->polygon().count(); - QPainterPath p = polygonToPath(node->polygon(), false); - handlePathNode(node, p, Qt::FlatCap); -} - -void SvgLoaderVisitor::visitImageNode(const QSvgImage *node) -{ - // TODO: this requires proper asset management. - stream() << "// IMAGE visit " << node->nodeId() << " type: " << node->typeName() << " " << node->type(); - - handleBaseNodeSetup(node); - const auto &img = node->image(); - QRectF rect = node->rect(); - - if (m_generateItems) { - auto *imageItem = new QQuickImage; - addCurrentItem(imageItem, node); - auto *imagePriv = static_cast<QQuickImageBasePrivate*>(QQuickItemPrivate::get(imageItem)); - imagePriv->pix.setImage(img); - - imageItem->setX(rect.x()); - imageItem->setY(rect.y()); - imageItem->setWidth(rect.width()); - imageItem->setHeight(rect.height()); - } - - QString fn = img.hasAlphaChannel() ? QStringLiteral("svg_asset_%1.png").arg(img.cacheKey()) : QStringLiteral("svg_asset_%1.jpg").arg(img.cacheKey()); - if (m_generateQML) { - // For now we just create a copy of the image in the current directory - img.save(fn); - qDebug() << "Saving copy of IMAGE" << fn; - } - stream() << "Image {"; - handleBaseNode(node); - m_indentLevel++; - stream() << "x: " << rect.x(); - stream() << "y: " << rect.y(); - stream() << "width: " << rect.width(); - stream() << "height: " << rect.height(); - stream() << "source: \"" << fn <<"\""; - - m_indentLevel--; - stream() << "}"; - handleBaseNodeEnd(node); - if (m_generateItems) - m_items.pop(); -} - -void SvgLoaderVisitor::visitTextNode(const QSvgText *node) -{ - // TODO: font/size - // TODO: fallback to path for gradient fill - stream() << "// TEXT visit " << node->nodeId() << " type: " << node->typeName() << " " << node->type(); - QPointF pos = node->position(); - static int counter = 0; - - const bool isTextArea = node->type() == QSvgNode::Textarea; - QQuickItem *alignItem = nullptr; - QQuickText *textItem = nullptr; - if (!isTextArea) { - stream() << "Item { id: textAlignItem_" << counter << "; x: " << pos.x() << "; y: " << pos.y() << "}"; - if (m_generateItems) { - alignItem = new QQuickItem(currentItem()); - alignItem->setX(pos.x()); - alignItem->setY(pos.y()); - } - } - stream() << "Text {"; - - if (m_generateItems) { - textItem = new QQuickText; - addCurrentItem(textItem, node); - } - handleBaseNodeSetup(node); - m_indentLevel++; - - if (isTextArea) { - stream() << "x: " << pos.x(); - stream() << "y: " << pos.y(); - stream() << "width: " << node->size().width(); - stream() << "height: " << node->size().height(); - stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard - stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level - if (textItem) { - textItem->setX(pos.x()); - textItem->setY(pos.y()); - textItem->setWidth(node->size().width()); - textItem->setHeight(node->size().height()); - textItem->setWrapMode(QQuickText::Wrap); - textItem->setClip(true); - } - } else { - auto *anchors = m_generateItems ? QQuickItemPrivate::get(textItem)->anchors() : nullptr; - auto *alignPrivate = m_generateItems ? QQuickItemPrivate::get(alignItem) : nullptr; - if (m_generateItems) - anchors->setBaseline(alignPrivate->top()); - QString hAlign = QStringLiteral("left"); - stream() << "anchors.baseline: textAlignItem_" << counter << ".top"; - switch (m_svgState.textAnchor) { - case Qt::AlignHCenter: - hAlign = "horizontalCenter"; - if (m_generateItems) - anchors->setHorizontalCenter(alignPrivate->left()); - break; - case Qt::AlignRight: - hAlign = "right"; - if (m_generateItems) - anchors->setRight(alignPrivate->left()); - break; - default: - qDebug() << "Unexpected text alignment" << m_svgState.textAnchor; - Q_FALLTHROUGH(); - case Qt::AlignLeft: - if (m_generateItems) - anchors->setLeft(alignPrivate->left()); - break; - } - stream() << "anchors." << hAlign << ": textAlignItem_" << counter << ".left"; - } - counter++; - QString text; - for (const auto *tspan : node->tspans()) { - if (!tspan) { - text += "<br>"; - continue; - } - - if (!tspan->style().font.isDefault()) // TODO: switch to rich text when we have more complex spans with fonts? - qDebug() << "Not implemented Tspan with font:" << tspan->style().font->qfont(); - QString spanColor; - if (!tspan->style().fill.isDefault()) { - auto &b = tspan->style().fill->qbrush(); - qDebug() << "tspan FILL:" << b; - if (b.style() != Qt::NoBrush) - spanColor = b.color().name(); - } - bool fontTag = !spanColor.isEmpty(); - if (fontTag) - text += QStringLiteral("<font color=\"%1\">").arg(spanColor); // TODO: size="1-7" ??? - text += tspan->text().toHtmlEscaped(); - if (fontTag) - text += QStringLiteral("</font>"); - } - stream() << "color: \"" << currentFillColor() << "\""; - stream() << "textFormat: Text.StyledText"; - - QFont font = m_dummyPainter.font(); - - stream() << "text: \"" << text << "\""; // TODO: how about adding template<T> TestVisitor::streamProperty(const QString &name, const T &value) - stream() << "font.family: \"" << font.family() << "\""; - if (font.pixelSize() > 0) - stream() << "font.pixelSize:" << font.pixelSize(); - else if (font.pointSize() > 0) - stream() << "font.pixelSize:" << font.pointSizeF(); - if (font.underline()) - stream() << "font.underline: true"; - if (font.weight() != QFont::Normal) - stream() << "font.weight: " << int(font.weight()); - - if (font.pixelSize() <= 0 && font.pointSize() > 0) - font.setPixelSize(font.pointSize()); // ### TODO: this makes no sense ### - - if (m_generateItems) { - textItem->setColor(QColor::fromString(currentFillColor())); - textItem->setTextFormat(QQuickText::StyledText); - textItem->setText(text); - textItem->setFont(font); - } - - m_indentLevel--; - stream() << "}"; - if (m_generateItems) - m_items.pop(); - handleBaseNodeEnd(node); -} - -//#define EXTRA_DEBUG -#ifdef EXTRA_DEBUG -static int nodeSetupLevel = 0; -#endif - -void SvgLoaderVisitor::handleBaseNodeSetup(const QSvgNode *node) -{ -#ifdef EXTRA_DEBUG - qDebug() << QByteArray().fill(' ', m_indentLevel * 2).constData() << "before SETUP" << node << "fill" << currentFillColor() - << "stroke" << currentStrokeColor() << currentStrokeWidth() << node->nodeId() << " type: " << node->typeName() << " " << node->type() << "level" << nodeSetupLevel; -#endif - node->applyStyle(&m_dummyPainter, m_svgState); - -#ifdef EXTRA_DEBUG - nodeSetupLevel++; - qDebug() << QByteArray().fill(' ', m_indentLevel * 2).constData() << "after SETUP" << node << "fill" << currentFillColor() - << "stroke" << currentStrokeColor() << currentStrokeWidth() << node->nodeId(); -#endif -} - -void SvgLoaderVisitor::handleBaseNode(const QSvgNode *node) -{ - m_indentLevel++; - if (!node->nodeId().isEmpty()) - stream() << "objectName: \"" << node->nodeId() << "\""; // or maybe "objectName: \"svg_node:" << node->nodeId() - if (!node->style().transform.isDefault()) { - QTransform tr = node->style().transform->qtransform(); - auto sx = tr.m11(); - auto sy = tr.m22(); - auto x = tr.m31(); - auto y = tr.m32(); - if (tr.type() == QTransform::TxTranslate) { - stream() << "// Translate " << tr.m31() << ", " << tr.m32(); - stream() << "transform: Translate { " << "x: " << x << "; y: " << y << " }"; - } else if (tr.type() == QTransform::TxScale && !x && !y) { - stream() << "// Scale " << tr.m11() << ", " << tr.m22(); - stream() << "transform: Scale { xScale: " << sx << "; yScale: " << sy << " }"; - } else { - stream() << "// Complex xform " << tr.type(); - const QMatrix4x4 m(tr); - auto xform = new QQuickMatrix4x4; - xform->setMatrix(m); - { - stream() << "transform: [ Matrix4x4 { matrix: Qt.matrix4x4 ("; - m_indentLevel += 3; - const auto *data = m.data(); - for (int i = 0; i < 4; i++) { - stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12] << ", "; - } - stream() << ") } ]"; - m_indentLevel -= 3; - } - } - if (m_generateItems) { - auto xformProp = currentItem()->transform(); - if (tr.type() == QTransform::TxTranslate) { - auto *translate = new QQuickTranslate; - translate->setX(x); - translate->setY(y); - xformProp.append(&xformProp, translate); - } else if (tr.type() == QTransform::TxScale && !x && !y) { - auto scale = new QQuickScale; - scale->setParent(currentItem()); - scale->setXScale(sx); - scale->setYScale(sy); - xformProp.append(&xformProp, scale); - } else { - const QMatrix4x4 m(tr); - auto xform = new QQuickMatrix4x4; - xform->setMatrix(m); - xformProp.append(&xformProp, xform); - } - } - } - if (!node->style().opacity.isDefault()) { - stream() << "opacity: " << node->style().opacity->opacity(); - if (m_generateItems) - currentItem()->setOpacity(node->style().opacity->opacity()); - } - m_indentLevel--; -} - -void SvgLoaderVisitor::handleBaseNodeEnd(const QSvgNode *node) -{ - node->revertStyle(&m_dummyPainter, m_svgState); -#ifdef EXTRA_DEBUG - nodeSetupLevel--; - qDebug() << QByteArray().fill(' ', m_indentLevel * 2).constData() << "AFTER" << node << "fill" << currentFillColor() - << "stroke" << currentStrokeColor() << currentStrokeWidth() << node->nodeId() << "level" << nodeSetupLevel; -#endif -} - -QString SvgLoaderVisitor::currentFillColor() const -{ - if (m_dummyPainter.brush().style() != Qt::NoBrush) { - QColor c(m_dummyPainter.brush().color()); - c.setAlphaF(m_svgState.fillOpacity); - //qDebug() << "FILL" << c << m_svgState.fillOpacity << c.name(); - return c.name(QColor::HexArgb); - } else { - return QStringLiteral("transparent"); - } -} - -const QGradient *SvgLoaderVisitor::currentFillGradient() const -{ - - if (m_dummyPainter.brush().style() == Qt::LinearGradientPattern || m_dummyPainter.brush().style() == Qt::RadialGradientPattern || m_dummyPainter.brush().style() == Qt::ConicalGradientPattern ) - return m_dummyPainter.brush().gradient(); - return nullptr; -} - -QString SvgLoaderVisitor::currentStrokeColor() const -{ - if (m_dummyPainter.pen().style() != Qt::NoPen) - return m_dummyPainter.pen().color().name(); - else if (m_dummyPainter.pen().brush().style() == Qt::SolidPattern) - return m_dummyPainter.pen().brush().color().name(); - return {}; -} - -float SvgLoaderVisitor::currentStrokeWidth() const -{ - float penWidth = m_dummyPainter.pen().widthF(); - return penWidth ? penWidth : 1; -} - -// Find the square that gives the same gradient in QGradient::LogicalMode as -// objModeRect does in QGradient::ObjectMode - -// When the object's bounding box is not square, the stripes that are conceptually -// perpendicular to the gradient vector within object bounding box space shall render -// non-perpendicular relative to the gradient vector in user space due to application -// of the non-uniform scaling transformation from bounding box space to user space. -static QRectF mapToQtLogicalMode(const QRectF &objModeRect, const QRectF &boundingRect) -{ - - QRect pixelRect(objModeRect.x() * boundingRect.width() + boundingRect.left(), - objModeRect.y() * boundingRect.height() + boundingRect.top(), - objModeRect.width() * boundingRect.width(), - objModeRect.height() * boundingRect.height()); - - if (pixelRect.isEmpty()) // pure horizontal/vertical gradient - return pixelRect; - - double w = boundingRect.width(); - double h = boundingRect.height(); - double objModeSlope = objModeRect.height() / objModeRect.width(); - double a = objModeSlope * w / h; - - // do calculation with origin == pixelRect.topLeft - double x2 = pixelRect.width(); - double y2 = pixelRect.height(); - double x = (x2 + a * y2) / (1 + a * a); - double y = y2 - (x - x2)/a; - - return QRectF(pixelRect.topLeft(), QSizeF(x,y)); -} - -static QString toSvgString(const QPainterPath &path) -{ - QString svgPathString; - QTextStream strm(&svgPathString); - - for (int i = 0; i < path.elementCount(); ++i) { - QPainterPath::Element element = path.elementAt(i); - if (element.isMoveTo()) { - strm << "M " << element.x << " " << element.y << " "; - } else if (element.isLineTo()) { - strm << "L " << element.x << " " << element.y << " "; - } else if (element.isCurveTo()) { - QPointF c1(element.x, element.y); - ++i; - element = path.elementAt(i); - - QPointF c2(element.x, element.y); - ++i; - element = path.elementAt(i); - QPointF ep(element.x, element.y); - - strm << "C " - << c1.x() << " " - << c1.y() << " " - << c2.x() << " " - << c2.y() << " " - << ep.x() << " " - << ep.y() << " "; - } - } - - return svgPathString; -} - -static QString toSvgString(const QQuadPath &path) -{ - QString svgPathString; - QTextStream strm(&svgPathString); - path.iterateElements([&](const QQuadPath::Element &e){ - if (e.isSubpathStart()) - strm << "M " << e.startPoint().x() << " " << e.startPoint().y() << " "; - - if (e.isLine()) { - strm << "L " << e.endPoint().x() << " " << e.endPoint().y() << " "; - } else { - strm << "Q " << e.controlPoint().x() << " " << e.controlPoint().y() << " " - << e.endPoint().x() << " " << e.endPoint().y() << " "; - } - }); - - return svgPathString; -} - -void SvgLoaderVisitor::outputGradient(const QGradient *grad, QQuickShapePath *shapePath, const QRectF &boundingRect) -{ - auto setStops = [](QQuickShapeGradient *quickGrad, const QGradientStops &stops) { - auto stopsProp = quickGrad->stops(); - for (auto &stop : stops) { - auto *stopObj = new QQuickGradientStop(quickGrad); - stopObj->setPosition(stop.first); - stopObj->setColor(stop.second); - stopsProp.append(&stopsProp, stopObj); - } - }; - - if (grad->type() == QGradient::LinearGradient) { - auto *linGrad = static_cast<const QLinearGradient *>(grad); - stream() << "fillGradient: LinearGradient {"; - m_indentLevel++; - - QRectF gradRect(linGrad->start(), linGrad->finalStop()); - QRectF logRect = linGrad->coordinateMode() == QGradient::LogicalMode ? gradRect : mapToQtLogicalMode(gradRect, boundingRect); - - stream() << "x1: " << logRect.left(); - stream() << "y1: " << logRect.top(); - stream() << "x2: " << logRect.right(); - stream() << "y2: " << logRect.bottom(); - for (auto &stop : linGrad->stops()) { - stream() << "GradientStop { position: " << stop.first << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }"; - } - m_indentLevel--; - stream() << "}"; - - if (shapePath) { - auto *quickGrad = new QQuickShapeLinearGradient(shapePath); - - quickGrad->setX1(logRect.left()); - quickGrad->setY1(logRect.top()); - quickGrad->setX2(logRect.right()); - quickGrad->setY2(logRect.bottom()); - setStops(quickGrad, linGrad->stops()); - - shapePath->setFillGradient(quickGrad); - } - } else if (grad->type() == QGradient::RadialGradient) { - auto *radGrad = static_cast<const QRadialGradient*>(grad); - stream() << "fillGradient: RadialGradient {"; - m_indentLevel++; - - stream() << "centerX: " << radGrad->center().x(); - stream() << "centerY: " << radGrad->center().y(); - stream() << "centerRadius: " << radGrad->radius(); - stream() << "focalX: centerX; focalY: centerY"; - for (auto &stop : radGrad->stops()) { - stream() << "GradientStop { position: " << stop.first << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }"; - } - m_indentLevel--; - stream() << "}"; - - if (shapePath) { - auto *quickGrad = new QQuickShapeRadialGradient(shapePath); - quickGrad->setCenterX(radGrad->center().x()); - quickGrad->setCenterY(radGrad->center().y()); - quickGrad->setCenterRadius(radGrad->radius()); - quickGrad->setFocalX(radGrad->center().x()); - quickGrad->setFocalY(radGrad->center().y()); - setStops(quickGrad, radGrad->stops()); - - shapePath->setFillGradient(quickGrad); - } - } -} - -void SvgLoaderVisitor::outputShapePath(const QSvgNode *node, const QPainterPath &path, Qt::PenCapStyle capStyle) -{ - const bool optimize = m_flags.testFlag(QSvgQmlWriter::OptimizePaths); - QRectF boundingRect = path.boundingRect(); - if (optimize) { - const bool outlineMode = m_flags.testFlag(QSvgQmlWriter::OutlineStrokeMode); - QQuadPath strokePath = QQuadPath::fromPainterPath(path); - bool fillPathNeededClose; - QQuadPath fillPath = strokePath.subPathsClosed(&fillPathNeededClose); - const bool intersectionsFound = QSGCurveProcessor::solveIntersections(fillPath, false); - fillPath.addCurvatureData(); - QSGCurveProcessor::solveOverlaps(fillPath); - - const bool compatibleStrokeAndFill = !fillPathNeededClose && !intersectionsFound; - - if (compatibleStrokeAndFill || outlineMode) { - outputShapePath(node, nullptr, &fillPath, capStyle, FillAndStroke, boundingRect); - } else { - outputShapePath(node, nullptr, &fillPath, capStyle, FillPath, boundingRect); - outputShapePath(node, nullptr, &strokePath, capStyle, StrokePath, boundingRect); - } - } else { - outputShapePath(node, &path, nullptr, capStyle, FillAndStroke, boundingRect); - } -} - -static QString pathHintString(const QQuadPath &qp) -{ - QString res; - QTextStream str(&res); - auto flags = qp.pathHints(); - if (!flags) - return res; - str << "pathHints:"; - bool first = true; - -#define CHECK_PATH_HINT(flagName) \ - if (flags.testFlag(QQuadPath::flagName)) { \ - if (!first) \ - str << " |"; \ - first = false; \ - str << " ShapePath." #flagName; \ - } - - CHECK_PATH_HINT(PathLinear) - CHECK_PATH_HINT(PathQuadratic) - CHECK_PATH_HINT(PathConvex) - CHECK_PATH_HINT(PathFillOnRight) - CHECK_PATH_HINT(PathSolid) - CHECK_PATH_HINT(PathNonIntersecting) - CHECK_PATH_HINT(PathNonOverlappingControlPointTriangles) - - return res; -} - -void SvgLoaderVisitor::outputShapePath(const QSvgNode *node, const QPainterPath *painterPath, const QQuadPath *quadPath, Qt::PenCapStyle capStyle, PathSelector pathSelector, const QRectF &boundingRect) -{ - Q_UNUSED(pathSelector) - Q_ASSERT(painterPath || quadPath); - - QString penName = currentStrokeColor(); - const bool noPen = penName.isEmpty() || penName == u"transparent"; - if (pathSelector == StrokePath && noPen) - return; - - const bool noFill = !currentFillGradient() && currentFillColor() == u"transparent"; - if (pathSelector == FillPath && noFill) - return; - - auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule()); - stream() << "ShapePath {"; - m_indentLevel++; - auto *shapePath = m_generateItems ? new QQuickShapePath : nullptr; - if (!node->nodeId().isEmpty()) { - switch (pathSelector) { - case FillPath: - stream() << "objectName: \"svg_fill_path:" << node->nodeId() << "\""; - break; - case StrokePath: - stream() << "objectName: \"svg_stroke_path:" << node->nodeId() << "\""; - break; - case FillAndStroke: - stream() << "objectName: \"svg_path:" << node->nodeId() << "\""; - break; - } - if (shapePath) - shapePath->setObjectName(QStringLiteral("svg_path:") + node->nodeId()); - } - stream() << "// boundingRect: " << boundingRect.x() << ", " << boundingRect.y() << " " << boundingRect.width() << "x" << boundingRect.height(); - - if (noPen || !(pathSelector & StrokePath)) { - stream() << "strokeColor: \"transparent\""; - if (shapePath) - shapePath->setStrokeColor(Qt::transparent); - } else { - stream() << "strokeColor: \"" << penName << "\""; - stream() << "strokeWidth: " << currentStrokeWidth(); - if (shapePath) { - shapePath->setStrokeColor(QColor::fromString(penName)); - shapePath->setStrokeWidth(currentStrokeWidth()); - } - } - if (capStyle == Qt::FlatCap) - stream() << "capStyle: ShapePath.FlatCap"; //### TODO Add the rest of the styles, as well as join styles etc. - - if (shapePath) - shapePath->setCapStyle(QQuickShapePath::CapStyle(capStyle)); - - if (!(pathSelector & FillPath)) { - stream() << "fillColor: \"transparent\""; - if (shapePath) - shapePath->setFillColor(Qt::transparent); - } else if (auto *grad = currentFillGradient()) { - outputGradient(grad, shapePath, boundingRect); - } else { - stream() << "fillColor: \"" << currentFillColor() << "\""; - if (shapePath) - shapePath->setFillColor(QColor::fromString(currentFillColor())); - } - if (fillRule == QQuickShapePath::WindingFill) - stream() << "fillRule: ShapePath.WindingFill"; - else - stream() << "fillRule: ShapePath.OddEvenFill"; - if (shapePath) - shapePath->setFillRule(fillRule); - - if (quadPath) { - QString hintStr = pathHintString(*quadPath); - if (!hintStr.isEmpty()) - stream() << hintStr; - } - - QString svgPathString = painterPath ? toSvgString(*painterPath) : toSvgString(*quadPath); - stream() << "PathSvg { path: \"" << svgPathString << "\" }"; - - if (shapePath) { - auto *pathSvg = new QQuickPathSvg; - pathSvg->setPath(svgPathString); - pathSvg->setParent(shapePath); - - auto pathElementProp = shapePath->pathElements(); - pathElementProp.append(&pathElementProp, pathSvg); - - shapePath->setParent(currentItem()); - auto shapeDataProp = m_parentShapeItem->data(); - shapeDataProp.append(&shapeDataProp, shapePath); - } - m_indentLevel--; - stream() << "}"; -} - -static bool isPathContainer(const QSvgStructureNode *node) -{ - bool foundPath = false; - for (const auto *child : node->renderers()) { - switch (child->type()) { - // nodes that shouldn't go inside Shape{} - case QSvgNode::Switch: - case QSvgNode::Doc: - case QSvgNode::Group: - case QSvgNode::Animation: - case QSvgNode::Use: - case QSvgNode::Video: - //qDebug() << "NOT path container because" << node->typeName() ; - return false; - - // nodes that could go inside Shape{} - case QSvgNode::Defs: - case QSvgNode::Image: - case QSvgNode::Textarea: - case QSvgNode::Text: - case QSvgNode::Tspan: - break; - - // nodes that are done as pure ShapePath{} - case QSvgNode::Rect: - case QSvgNode::Circle: - case QSvgNode::Ellipse: - case QSvgNode::Line: - case QSvgNode::Path: - case QSvgNode::Polygon: - case QSvgNode::Polyline: - if (!child->style().transform.isDefault()) { - //qDebug() << "NOT path container because local transform"; - return false; - } - foundPath = true; - break; - default: - qDebug() << "Unhandled type in switch" << child->type(); - break; - } - } - //qDebug() << "Container" << node->nodeId() << node->typeName() << "is" << foundPath; - return foundPath; -} - -namespace { -class ViewBoxItem : public QQuickItem -{ -public: - ViewBoxItem(const QRectF viewBox, QQuickItem *parent = nullptr) : QQuickItem(parent), m_viewBox(viewBox) { setXForm(); } - -protected: - void geometryChange(const QRectF &/*newGeometry*/, const QRectF &/*oldGeometry*/) override - { - setXForm(); - } - -private: - void setXForm() - { - auto xformProp = transform(); - xformProp.clear(&xformProp); - bool translate = !qFuzzyIsNull(m_viewBox.x()) || !qFuzzyIsNull(m_viewBox.y()); - if (translate) { - auto *tr = new QQuickTranslate(this); - tr->setX(-m_viewBox.x()); - tr->setY(-m_viewBox.y()); - xformProp.append(&xformProp, tr); - } - if (!m_viewBox.isEmpty() && width() && height()) { - auto *scale = new QQuickScale(this); - qreal sx = width() / m_viewBox.width(); - qreal sy = height() / m_viewBox.height(); - - scale->setXScale(sx); - scale->setYScale(sy); - xformProp.append(&xformProp, scale); - } - } - QRectF m_viewBox; -}; -} - -bool SvgLoaderVisitor::visitDefsNodeStart(const QSvgDefs *node) -{ - stream() << "// skipping DEFS \"" << node->nodeId() << "\""; - return false; // skip to end; TODO: implement defs -} - -bool SvgLoaderVisitor::visitStructureNodeStart(const QSvgStructureNode *node) -{ - constexpr bool forceSeparatePaths = false; - handleBaseNodeSetup(node); - stream() << "// START " << node->nodeId() << " type: " << node->typeName() << " " << node->type(); - - bool isTopLevel = node->type() == QSvgNode::Doc; - bool hasViewBox = false; - QRectF viewBox; - - if (isTopLevel) { - auto *doc = static_cast<const QSvgTinyDocument *>(node); - viewBox = doc->viewBox(); - hasViewBox = !viewBox.isEmpty(); - } - - if (!forceSeparatePaths && !isTopLevel && isPathContainer(node)) { - stream() << shapeName() <<" { //combined path container"; - m_indentLevel++; - if (m_flags & QSvgQmlWriter::CurveRenderer) - stream() << "preferredRendererType: Shape.CurveRenderer"; - m_indentLevel--; - - m_inShapeItem = true; - if (m_generateItems) { - auto *shapeItem = new QQuickShape; - shapeItem->setPreferredRendererType(QQuickShape::CurveRenderer); // TODO: settable - m_parentShapeItem = shapeItem; - addCurrentItem(shapeItem, node); - } - } else { - stream() << "Item { // structure node"; - if (m_generateItems) { - auto *item = hasViewBox ? new ViewBoxItem(viewBox) : new QQuickItem; - addCurrentItem(item, node ); - if (isTopLevel) - m_loadedItem = item; - } - } - if (hasViewBox) { - m_indentLevel++; - stream() << "transform: ["; - m_indentLevel++; - bool translate = !qFuzzyIsNull(viewBox.x()) || !qFuzzyIsNull(viewBox.y()); - if (translate) - stream() << "Translate { x: " << -viewBox.x() << "; y: " << -viewBox.y() << " },"; - stream() << "Scale { xScale: width / " << viewBox.width() << "; yScale: height / " << viewBox.height() << " }"; - m_indentLevel--; - stream() << "]";; - m_indentLevel--; - } - handleBaseNode(node); - m_indentLevel++; - return true; -} - -void SvgLoaderVisitor::visitStructureNodeEnd(const QSvgStructureNode *node) -{ - m_indentLevel--; - stream() << "} // END " << node->nodeId() << " type: " << node->typeName() << " " << node->type(); - handleBaseNodeEnd(node); -// qDebug() << "REVERT" << node->nodeId() << node->type() << (m_dummyPainter.pen().style() != Qt::NoPen) << m_dummyPainter.pen().color().name() -// << (m_dummyPainter.pen().brush().style() != Qt::NoBrush) << m_dummyPainter.pen().brush().color().name(); - m_inShapeItem = false; - m_parentShapeItem = nullptr; - if (m_generateItems) - m_items.pop(); -} - -void SvgLoaderVisitor::visitRectNode(const QSvgRect *node) -{ - QRectF rect = node->rect(); - QPointF rads = node->radius(); - // This is using Qt::RelativeSize semantics: percentage of half rect size - qreal x1 = rect.left(); - qreal x2 = rect.right(); - qreal y1 = rect.top(); - qreal y2 = rect.bottom(); - - qreal rx = rads.x() * rect.width() / 200; - qreal ry = rads.y() * rect.height() / 200; - QPainterPath p; - - p.moveTo(x1 + rx, y1); - p.lineTo(x2 - rx, y1); - // qDebug() << "Line1" << x2 - rx << y1; - p.arcTo(x2 - rx * 2, y1, rx * 2, ry * 2, 90, -90); // ARC to x2, y1 + ry - // qDebug() << "p1" << p; - - p.lineTo(x2, y2 - ry); - p.arcTo(x2 - rx * 2, y2 - ry * 2, rx * 2, ry * 2, 0, -90); // ARC to x2 - rx, y2 - - p.lineTo(x1 + rx, y2); - p.arcTo(x1, y2 - ry * 2, rx * 2, ry * 2, 270, -90); // ARC to x1, y2 - ry - - p.lineTo(x1, y1 + ry); - p.arcTo(x1, y1, rx * 2, ry * 2, 180, -90); // ARC to x1 + rx, y1 - - - stream() << "// Path from rect " << node->nodeId() << " r " << rect.x() << ", " << rect.y() - << " " << rect.width() << "x" << rect.height() << " R: " << rads.x() << ", " << rads.y(); - - handlePathNode(node, p); - - return; - -} - -void SvgLoaderVisitor::visitEllipseNode(const QSvgEllipse *node) -{ - QRectF rect = node->rect(); - stream() << "// Ellipse" << node->nodeId() << " rect: " << rect.x() << ", " << rect.y() - << " " << rect.width() << "x" << rect.height(); - QPainterPath p; - p.addEllipse(rect); - - handlePathNode(node, p); -} - -QQuickItem *SvgLoaderVisitor::loadQML(QTextStream *outStream, const QSvgTinyDocument *doc, QQuickItem *parentItem) -{ - Q_ASSERT(outStream); - m_stream = outStream; - m_indentLevel = 0; - - stream() << "import QtQuick"; - stream() << "import QtQuick.Shapes" << Qt::endl; - - QRectF viewBox = doc->viewBox(); - - m_generateItems = parentItem != nullptr; - m_items.push(parentItem); - - stream() << "Item {"; - m_indentLevel++; - stream() << "// viewBox " << viewBox.x() << ", " << viewBox.y() - << " " << viewBox.width() << "x" << viewBox.height(); - stream() << "// size " << doc->width() << "x" << doc->height(); - double w = doc->width(); - double h = doc->height(); - if (w > 0) - stream() << "implicitWidth: " << w; - if (h > 0) - stream() << "implicitHeight: " << h; - - if (parentItem) { - m_generateItems = true; - parentItem->setImplicitWidth(w); - parentItem->setImplicitHeight(h); - } - traverse(doc); - m_indentLevel--; - stream() << "}"; - return m_loadedItem; -} - -void SvgLoaderVisitor::visitNode(const QSvgNode *node) -{ - handleBaseNodeSetup(node); - stream() << "//### SVG NODE NOT IMPLEMENTED: " << node->nodeId() << " type: " << node->typeName() << " " << node->type(); - stream() << "Item {"; - handleBaseNode(node); - stream() << "}"; - handleBaseNodeEnd(node); -} - -QQuickItem *QSvgQmlWriter::loadSVG(const QSvgTinyDocument *doc, const QString &outFileName, GeneratorFlags flags, const QString &typeName, QQuickItem *parentItem, const QString &commentString) -{ - SvgLoaderVisitor visitor; - if (!typeName.isEmpty()) - visitor.setShapeTypeName(typeName); - visitor.setFlags(flags); - QByteArray result; - QTextStream str(&result); - if (commentString.isEmpty()) - str << "// Generated from SVG" << Qt::endl; - else - str << "// " << commentString << Qt::endl; - auto *loadedItem = visitor.loadQML(&str, doc, parentItem); - if (!outFileName.isEmpty()) { - QFile outFile(outFileName); - outFile.open(QIODevice::WriteOnly); - outFile.write(result); - outFile.close(); - } -#if 0 - result.truncate(300); - qDebug().noquote() << result; -#endif - return loadedItem; -} - -QT_END_NAMESPACE diff --git a/tools/svgtoqml/qsvgloader_p.h b/tools/svgtoqml/qsvgloader_p.h deleted file mode 100644 index 1804a7d358..0000000000 --- a/tools/svgtoqml/qsvgloader_p.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2023 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only - -#ifndef QSVGQMLWRITER_P_H -#define QSVGQMLWRITER_P_H - -#include <QtCore/qtconfigmacros.h> -#include <QtCore/qflags.h> - -QT_BEGIN_NAMESPACE - -class QTextStream; -class QSvgTinyDocument; -class QString; -class QQuickItem; - -class QSvgQmlWriter -{ -public: - enum GeneratorFlag { - OptimizePaths = 0x01, - CurveRenderer = 0x02, - OutlineStrokeMode = 0x04 - }; - Q_DECLARE_FLAGS(GeneratorFlags, GeneratorFlag); - static QQuickItem *loadSVG(const QSvgTinyDocument *doc, const QString &outFileName, GeneratorFlags flags, const QString &typeName, QQuickItem *parentItem, const QString &commentString); -}; -Q_DECLARE_OPERATORS_FOR_FLAGS(QSvgQmlWriter::GeneratorFlags); - -QT_END_NAMESPACE - -#endif // QSVGQMLWRITER_P_H |