diff options
Diffstat (limited to 'src/quickvectorimage/generator')
-rw-r--r-- | src/quickvectorimage/generator/qquickgenerator.cpp | 81 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquickgenerator_p.h | 79 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquickitemgenerator.cpp | 393 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquickitemgenerator_p.h | 56 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquicknodeinfo_p.h | 119 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquickqmlgenerator.cpp | 561 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qquickqmlgenerator_p.h | 103 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qsvgvisitorimpl.cpp | 1094 | ||||
-rw-r--r-- | src/quickvectorimage/generator/qsvgvisitorimpl_p.h | 69 | ||||
-rw-r--r-- | src/quickvectorimage/generator/utils_p.h | 264 |
10 files changed, 2819 insertions, 0 deletions
diff --git a/src/quickvectorimage/generator/qquickgenerator.cpp b/src/quickvectorimage/generator/qquickgenerator.cpp new file mode 100644 index 0000000000..e31b6ae99f --- /dev/null +++ b/src/quickvectorimage/generator/qquickgenerator.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickgenerator_p.h" +#include "qsvgvisitorimpl_p.h" +#include "qquicknodeinfo_p.h" + +#include <private/qsgcurveprocessor_p.h> +#include <private/qquickshape_p.h> +#include <private/qquadpath_p.h> +#include <private/qquickitem_p.h> +#include <private/qquickimagebase_p_p.h> + +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQuickVectorImage, "qt.quick.vectorimage", QtWarningMsg) + +QQuickGenerator::QQuickGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags) + : m_flags(flags) + , m_fileName(fileName) + , m_loader(nullptr) +{ +} + +QQuickGenerator::~QQuickGenerator() +{ + delete m_loader; +} + +void QQuickGenerator::setGeneratorFlags(QQuickVectorImageGenerator::GeneratorFlags flags) +{ + m_flags = flags; +} + +QQuickVectorImageGenerator::GeneratorFlags QQuickGenerator::generatorFlags() +{ + return m_flags; +} + +bool QQuickGenerator::generate() +{ + m_loader = new QSvgVisitorImpl(m_fileName, this); + m_generationSucceeded = m_loader->traverse(); + return m_generationSucceeded; +} + +void QQuickGenerator::optimizePaths(const PathNodeInfo &info) +{ + QPainterPath pathCopy = info.painterPath; + pathCopy.setFillRule(info.fillRule); + + if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::OptimizePaths)) { + QQuadPath strokePath = QQuadPath::fromPainterPath(pathCopy); + 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 || m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::OutlineStrokeMode)) { + outputShapePath(info, nullptr, &fillPath, QQuickVectorImageGenerator::FillAndStroke, pathCopy.boundingRect()); + } else { + outputShapePath(info, nullptr, &fillPath, QQuickVectorImageGenerator::FillPath, pathCopy.boundingRect()); + outputShapePath(info, nullptr, &strokePath, QQuickVectorImageGenerator::StrokePath, pathCopy.boundingRect()); + } + } else { + outputShapePath(info, &pathCopy, nullptr, QQuickVectorImageGenerator::FillAndStroke, pathCopy.boundingRect()); + } +} + +bool QQuickGenerator::isNodeVisible(const NodeInfo &info) +{ + if (!info.isVisible || !info.isDisplayed) + return false; + + return true; +} + +QT_END_NAMESPACE diff --git a/src/quickvectorimage/generator/qquickgenerator_p.h b/src/quickvectorimage/generator/qquickgenerator_p.h new file mode 100644 index 0000000000..d8ef5c2819 --- /dev/null +++ b/src/quickvectorimage/generator/qquickgenerator_p.h @@ -0,0 +1,79 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKGENERATOR_P_H +#define QQUICKGENERATOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qquickvectorimageglobal_p.h> +#include <QtCore/qstring.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +QT_DECLARE_EXPORTED_QT_LOGGING_CATEGORY(lcQuickVectorImage, Q_QUICKVECTORIMAGEGENERATOR_EXPORT) + +class QSvgVisitorImpl; +class QPainterPath; +class QGradient; +class QQuickShapePath; +class QQuadPath; +class QQuickItem; +class QQuickShape; +class QRectF; + +struct NodeInfo; +struct ImageNodeInfo; +struct PathNodeInfo; +struct TextNodeInfo; +struct UseNodeInfo; +struct StructureNodeInfo; + +class Q_QUICKVECTORIMAGEGENERATOR_EXPORT QQuickGenerator +{ +public: + QQuickGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags); + virtual ~QQuickGenerator(); + + void setGeneratorFlags(QQuickVectorImageGenerator::GeneratorFlags flags); + QQuickVectorImageGenerator::GeneratorFlags generatorFlags(); + + bool generate(); + +protected: + virtual void generateNodeBase(const NodeInfo &info) = 0; + virtual bool generateDefsNode(const NodeInfo &info) = 0; + virtual void generateImageNode(const ImageNodeInfo &info) = 0; + virtual void generatePath(const PathNodeInfo &info) = 0; + virtual void generateNode(const NodeInfo &info) = 0; + virtual void generateTextNode(const TextNodeInfo &info) = 0; + virtual void generateUseNode(const UseNodeInfo &info) = 0; + virtual bool generateStructureNode(const StructureNodeInfo &info) = 0; + virtual bool generateRootNode(const StructureNodeInfo &info) = 0; + virtual void outputShapePath(const PathNodeInfo &info, const QPainterPath *path, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) = 0; + void optimizePaths(const PathNodeInfo &info); + bool isNodeVisible(const NodeInfo &info); + +protected: + bool m_generationSucceeded = false; + QQuickVectorImageGenerator::GeneratorFlags m_flags; + +private: + QString m_fileName; + QSvgVisitorImpl *m_loader; + friend class QSvgVisitorImpl; +}; + +QT_END_NAMESPACE + +#endif // QQUICKGENERATOR_P_H diff --git a/src/quickvectorimage/generator/qquickitemgenerator.cpp b/src/quickvectorimage/generator/qquickitemgenerator.cpp new file mode 100644 index 0000000000..ad85bd3685 --- /dev/null +++ b/src/quickvectorimage/generator/qquickitemgenerator.cpp @@ -0,0 +1,393 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickitemgenerator_p.h" +#include "utils_p.h" +#include "qquicknodeinfo_p.h" + +#include <private/qsgcurveprocessor_p.h> +#include <private/qquickshape_p.h> +#include <private/qquadpath_p.h> +#include <private/qquickitem_p.h> +#include <private/qquickimagebase_p_p.h> + +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +QQuickItemGenerator::QQuickItemGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, QQuickItem *parentItem) + :QQuickGenerator(fileName, flags) +{ + Q_ASSERT(parentItem); + m_items.push(parentItem); + m_parentItem = parentItem; +} + +QQuickItemGenerator::~QQuickItemGenerator() +{ +} + +void QQuickItemGenerator::generateNodeBase(const NodeInfo &info) +{ + if (!info.isDefaultTransform) { + auto sx = info.transform.m11(); + auto sy = info.transform.m22(); + auto x = info.transform.m31(); + auto y = info.transform.m32(); + + auto xformProp = currentItem()->transform(); + if (info.transform.type() == QTransform::TxTranslate) { + auto *translate = new QQuickTranslate; + translate->setX(x); + translate->setY(y); + xformProp.append(&xformProp, translate); + } else if (info.transform.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(info.transform); + auto xform = new QQuickMatrix4x4; + xform->setMatrix(m); + xformProp.append(&xformProp, xform); + } + } + if (!info.isDefaultOpacity) { + currentItem()->setOpacity(info.opacity); + } +} + +bool QQuickItemGenerator::generateDefsNode(const NodeInfo &info) +{ + Q_UNUSED(info) + + return false; +} + +void QQuickItemGenerator::generateImageNode(const ImageNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + auto *imageItem = new QQuickImage; + auto *imagePriv = static_cast<QQuickImageBasePrivate*>(QQuickItemPrivate::get(imageItem)); + imagePriv->currentPix->setImage(info.image); + + imageItem->setX(info.rect.x()); + imageItem->setY(info.rect.y()); + imageItem->setWidth(info.rect.width()); + imageItem->setHeight(info.rect.height()); + + generateNodeBase(info); + + addCurrentItem(imageItem, info); + m_items.pop(); +} + +void QQuickItemGenerator::generatePath(const PathNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + if (m_inShapeItem) { + if (!info.isDefaultTransform) + qCWarning(lcQuickVectorImage) << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)"; + optimizePaths(info); + } else { + auto *shapeItem = new QQuickShape; + if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) + shapeItem->setPreferredRendererType(QQuickShape::CurveRenderer); + shapeItem->setContainsMode(QQuickShape::ContainsMode::FillContains); // TODO: configurable? + addCurrentItem(shapeItem, info); + m_parentShapeItem = shapeItem; + m_inShapeItem = true; + + generateNodeBase(info); + + optimizePaths(info); + //qCDebug(lcQuickVectorGraphics) << *node->qpath(); + m_items.pop(); + m_inShapeItem = false; + m_parentShapeItem = nullptr; + } +} + +void QQuickItemGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) +{ + Q_UNUSED(pathSelector) + Q_ASSERT(painterPath || quadPath); + + const bool noPen = info.strokeStyle.color == QColorConstants::Transparent; + if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen) + return; + + const bool noFill = info.grad.type() == QGradient::NoGradient && info.fillColor == QColorConstants::Transparent; + + if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill) + return; + + QQuickShapePath::FillRule fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule()); + + QQuickShapePath *shapePath = new QQuickShapePath; + Q_ASSERT(shapePath); + + if (!info.nodeId.isEmpty()) + shapePath->setObjectName(QStringLiteral("svg_path:") + info.nodeId); + + if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) { + shapePath->setStrokeColor(Qt::transparent); + } else { + shapePath->setStrokeColor(info.strokeStyle.color); + shapePath->setStrokeWidth(info.strokeStyle.width); + shapePath->setCapStyle(QQuickShapePath::CapStyle(info.strokeStyle.lineCapStyle)); + shapePath->setJoinStyle(QQuickShapePath::JoinStyle(info.strokeStyle.lineJoinStyle)); + shapePath->setMiterLimit(info.strokeStyle.miterLimit); + if (info.strokeStyle.dashArray.length() != 0) { + shapePath->setStrokeStyle(QQuickShapePath::DashLine); + shapePath->setDashPattern(info.strokeStyle.dashArray.toVector()); + shapePath->setDashOffset(info.strokeStyle.dashOffset); + } + } + + if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) + shapePath->setFillColor(Qt::transparent); + else if (info.grad.type() != QGradient::NoGradient) + generateGradient(&info.grad, shapePath, boundingRect); + else + shapePath->setFillColor(info.fillColor); + + shapePath->setFillRule(fillRule); + if (!info.fillTransform.isIdentity()) + shapePath->setFillTransform(info.fillTransform); + + QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(*painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(*quadPath); + + 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); +} + +void QQuickItemGenerator::generateGradient(const QGradient *grad, QQuickShapePath *shapePath, const QRectF &boundingRect) +{ + if (!shapePath) + return; + + 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); + + QRectF gradRect(linGrad->start(), linGrad->finalStop()); + QRectF logRect = linGrad->coordinateMode() == QGradient::LogicalMode ? gradRect : QQuickVectorImageGenerator::Utils::mapToQtLogicalMode(gradRect, boundingRect); + + 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); + auto *quickGrad = new QQuickShapeRadialGradient(shapePath); + quickGrad->setCenterX(radGrad->center().x()); + quickGrad->setCenterY(radGrad->center().y()); + quickGrad->setCenterRadius(radGrad->radius()); + quickGrad->setFocalX(radGrad->focalPoint().x()); + quickGrad->setFocalY(radGrad->focalPoint().y()); + setStops(quickGrad, radGrad->stops()); + + shapePath->setFillGradient(quickGrad); + } +} + +void QQuickItemGenerator::generateNode(const NodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + qCWarning(lcQuickVectorImage) << "SVG NODE NOT IMPLEMENTED: " + << info.nodeId + << " type: " << info.typeName; +} + +void QQuickItemGenerator::generateTextNode(const TextNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + QQuickItem *alignItem = nullptr; + QQuickText *textItem = nullptr; + + QQuickItem *containerItem = new QQuickItem(currentItem()); + addCurrentItem(containerItem, info); + + generateNodeBase(info); + + if (!info.isTextArea) { + alignItem = new QQuickItem(currentItem()); + alignItem->setX(info.position.x()); + alignItem->setY(info.position.y()); + } + + textItem = new QQuickText(containerItem); + addCurrentItem(textItem, info); + + if (info.isTextArea) { + textItem->setX(info.position.x()); + textItem->setY(info.position.y()); + if (info.size.width() > 0) + textItem->setWidth(info.size.width()); + if (info.size.height() > 0) + textItem->setHeight(info.size.height()); + textItem->setWrapMode(QQuickText::Wrap); + textItem->setClip(true); + } else { + auto *anchors = QQuickItemPrivate::get(textItem)->anchors(); + auto *alignPrivate = QQuickItemPrivate::get(alignItem); + anchors->setBaseline(alignPrivate->top()); + + switch (info.alignment) { + case Qt::AlignHCenter: + anchors->setHorizontalCenter(alignPrivate->left()); + break; + case Qt::AlignRight: + anchors->setRight(alignPrivate->left()); + break; + default: + qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment; + Q_FALLTHROUGH(); + case Qt::AlignLeft: + anchors->setLeft(alignPrivate->left()); + break; + } + } + + textItem->setColor(info.fillColor); + textItem->setTextFormat(info.needsRichText ? QQuickText::RichText : QQuickText::StyledText); + textItem->setText(info.text); + textItem->setFont(info.font); + + if (info.strokeColor != QColorConstants::Transparent) { + textItem->setStyleColor(info.strokeColor); + textItem->setStyle(QQuickText::Outline); + } + + m_items.pop(); m_items.pop(); +} + +void QQuickItemGenerator::generateUseNode(const UseNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + if (info.stage == StructureNodeStage::Start) { + QQuickItem *item = new QQuickItem(); + item->setPosition(info.startPos); + addCurrentItem(item, info); + generateNodeBase(info); + } else { + m_items.pop(); + } + +} + +bool QQuickItemGenerator::generateStructureNode(const StructureNodeInfo &info) +{ + if (!isNodeVisible(info)) + return false; + + if (info.stage == StructureNodeStage::Start) { + if (!info.forceSeparatePaths && info.isPathContainer) { + m_inShapeItem = true; + auto *shapeItem = new QQuickShape; + if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) + shapeItem->setPreferredRendererType(QQuickShape::CurveRenderer); + m_parentShapeItem = shapeItem; + addCurrentItem(shapeItem, info); + } else { + QQuickItem *item = !info.viewBox.isEmpty() ? new QQuickVectorImageGenerator::Utils::ViewBoxItem(info.viewBox) : new QQuickItem; + addCurrentItem(item, info); + } + + generateNodeBase(info); + } else { + m_inShapeItem = false; + m_parentShapeItem = nullptr; + m_items.pop(); + } + + return true; +} + +bool QQuickItemGenerator::generateRootNode(const StructureNodeInfo &info) +{ + if (!isNodeVisible(info)) { + QQuickItem *item = new QQuickItem(); + item->setParentItem(m_parentItem); + + if (info.size.width() > 0) + m_parentItem->setImplicitWidth(info.size.width()); + + if (info.size.height() > 0) + m_parentItem->setImplicitHeight(info.size.height()); + + item->setWidth(m_parentItem->implicitWidth()); + item->setHeight(m_parentItem->implicitHeight()); + + return false; + } + + if (info.stage == StructureNodeStage::Start) { + QQuickItem *item = !info.viewBox.isEmpty() ? new QQuickVectorImageGenerator::Utils::ViewBoxItem(info.viewBox) : new QQuickItem; + addCurrentItem(item, info); + if (info.size.width() > 0) + m_parentItem->setImplicitWidth(info.size.width()); + + if (info.size.height() > 0) + m_parentItem->setImplicitHeight(info.size.height()); + + item->setWidth(m_parentItem->implicitWidth()); + item->setHeight(m_parentItem->implicitHeight()); + generateNodeBase(info); + } else { + m_inShapeItem = false; + m_parentShapeItem = nullptr; + m_items.pop(); + } + + return true; +} + +QQuickItem *QQuickItemGenerator::currentItem() +{ + return m_items.top(); +} + +void QQuickItemGenerator::addCurrentItem(QQuickItem *item, const NodeInfo &info) +{ + item->setParentItem(currentItem()); + m_items.push(item); + QStringView name = !info.nodeId.isEmpty() ? info.nodeId : info.typeName; + item->setObjectName(name); +} + +QT_END_NAMESPACE diff --git a/src/quickvectorimage/generator/qquickitemgenerator_p.h b/src/quickvectorimage/generator/qquickitemgenerator_p.h new file mode 100644 index 0000000000..e96812ec30 --- /dev/null +++ b/src/quickvectorimage/generator/qquickitemgenerator_p.h @@ -0,0 +1,56 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKITEMGENERATOR_P_H +#define QQUICKITEMGENERATOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickgenerator_p.h" +#include <QStack> + +QT_BEGIN_NAMESPACE + +class Q_QUICKVECTORIMAGEGENERATOR_EXPORT QQuickItemGenerator : public QQuickGenerator +{ +public: + QQuickItemGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, QQuickItem *parentItem); + ~QQuickItemGenerator(); + +protected: + void generateNodeBase(const NodeInfo &info) override; + bool generateDefsNode(const NodeInfo &info) override; + void generateImageNode(const ImageNodeInfo &info) override; + void generatePath(const PathNodeInfo &info) override; + void generateNode(const NodeInfo &info) override; + void generateTextNode(const TextNodeInfo &info) override; + void generateUseNode(const UseNodeInfo &info) override; + bool generateStructureNode(const StructureNodeInfo &info) override; + bool generateRootNode(const StructureNodeInfo &info) override; + void outputShapePath(const PathNodeInfo &info, const QPainterPath *path, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) override; + +private: + void generateGradient(const QGradient *grad, QQuickShapePath *shapePath, const QRectF &boundingRect); + QQuickItem *currentItem(); + void addCurrentItem(QQuickItem *item, const NodeInfo &info); + + bool m_inShapeItem = false; + QQuickShape *m_parentShapeItem = nullptr; + + QStack<QQuickItem *> m_items; + + QQuickItem *m_parentItem = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QQUICKITEMGENERATOR_P_H diff --git a/src/quickvectorimage/generator/qquicknodeinfo_p.h b/src/quickvectorimage/generator/qquicknodeinfo_p.h new file mode 100644 index 0000000000..b36f4a59ab --- /dev/null +++ b/src/quickvectorimage/generator/qquicknodeinfo_p.h @@ -0,0 +1,119 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKNODEINFO_P_H +#define QQUICKNODEINFO_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QString> +#include <QPainter> +#include <QPainterPath> +#include <QMatrix4x4> +#include <QQuickItem> +#include <private/qquicktext_p.h> +#include <private/qquicktranslate_p.h> +#include <private/qquickimage_p.h> + +QT_BEGIN_NAMESPACE + +struct NodeInfo +{ + QString nodeId; + QString typeName; + QTransform transform; + qreal opacity; + bool isDefaultTransform; + bool isDefaultOpacity; + bool isVisible; + bool isDisplayed; // TODO: Map to display enum in QtSvg +}; + +struct ImageNodeInfo : NodeInfo +{ + QImage image; + QRectF rect; + QString externalFileReference; +}; + +struct StrokeStyle +{ + Qt::PenCapStyle lineCapStyle = Qt::SquareCap; + Qt::PenJoinStyle lineJoinStyle = Qt::MiterJoin; + qreal miterLimit = 4; + qreal dashOffset = 0; + QList<qreal> dashArray; + QColor color = QColorConstants::Transparent; + qreal width = 1.0; + + static StrokeStyle fromPen(const QPen &p) + { + StrokeStyle style; + style.lineCapStyle = p.capStyle(); + style.lineJoinStyle = p.joinStyle() == Qt::SvgMiterJoin ? Qt::MiterJoin : p.joinStyle(); //TODO support SvgMiterJoin + style.miterLimit = p.miterLimit(); + style.dashOffset = p.dashOffset(); + style.dashArray = p.dashPattern(); + style.width = p.widthF(); + + return style; + } +}; + +struct PathNodeInfo : NodeInfo +{ + QPainterPath painterPath; + Qt::FillRule fillRule = Qt::FillRule::WindingFill; + QColor fillColor; + StrokeStyle strokeStyle; + QGradient grad; + QTransform fillTransform; +}; + +struct TextNodeInfo : NodeInfo +{ + bool isTextArea; + bool needsRichText; + QPointF position; + QSizeF size; + QString text; + QFont font; + Qt::Alignment alignment; + QColor fillColor; + QColor strokeColor; +}; + +enum class StructureNodeStage +{ + Start, + End +}; + +struct UseNodeInfo : NodeInfo +{ + QPointF startPos; + StructureNodeStage stage; +}; + +struct StructureNodeInfo : NodeInfo +{ + StructureNodeStage stage; + bool forceSeparatePaths; + QRectF viewBox; + QSize size; + bool isPathContainer; +}; + + +QT_END_NAMESPACE + +#endif //QQUICKNODEINFO_P_H diff --git a/src/quickvectorimage/generator/qquickqmlgenerator.cpp b/src/quickvectorimage/generator/qquickqmlgenerator.cpp new file mode 100644 index 0000000000..bd3e781c32 --- /dev/null +++ b/src/quickvectorimage/generator/qquickqmlgenerator.cpp @@ -0,0 +1,561 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquickqmlgenerator_p.h" +#include "qquicknodeinfo_p.h" +#include "utils_p.h" + +#include <private/qsgcurveprocessor_p.h> +#include <private/qquickshape_p.h> +#include <private/qquadpath_p.h> +#include <private/qquickitem_p.h> +#include <private/qquickimagebase_p_p.h> + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdir.h> + +QT_BEGIN_NAMESPACE + +QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName) + : QQuickGenerator(fileName, flags) + , outputFileName(outFileName) +{ + m_result.open(QIODevice::ReadWrite); +} + +QQuickQmlGenerator::~QQuickQmlGenerator() +{ + if (m_generationSucceeded && !outputFileName.isEmpty()) { + QFileInfo fileInfo(outputFileName); + QDir dir(fileInfo.absolutePath()); + if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { + qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath(); + } else { + stream().flush(); // Add a final newline and flush the stream to m_result + QFile outFile(outputFileName); + outFile.open(QIODevice::WriteOnly); + outFile.write(m_result.data()); + outFile.close(); + } + } + + if (lcQuickVectorImage().isDebugEnabled()) + qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(300); +} + +void QQuickQmlGenerator::setShapeTypeName(const QString &name) +{ + m_shapeTypeName = name.toLatin1(); +} + +QString QQuickQmlGenerator::shapeTypeName() const +{ + return QString::fromLatin1(m_shapeTypeName); +} + +void QQuickQmlGenerator::setCommentString(const QString commentString) +{ + m_commentString = commentString; +} + +QString QQuickQmlGenerator::commentString() const +{ + return m_commentString; +} + +void QQuickQmlGenerator::generateNodeBase(const NodeInfo &info) +{ + m_indentLevel++; + if (!info.nodeId.isEmpty()) + stream() << "objectName: \"" << info.nodeId << "\""; + if (!info.isDefaultTransform) { + auto sx = info.transform.m11(); + auto sy = info.transform.m22(); + auto x = info.transform.m31(); + auto y = info.transform.m32(); + if (info.transform.type() == QTransform::TxTranslate) { + stream() << "transform: Translate { " << "x: " << x << "; y: " << y << " }"; + } else if (info.transform.type() == QTransform::TxScale && !x && !y) { + stream() << "transform: Scale { xScale: " << sx << "; yScale: " << sy << " }"; + } else { + stream() << "transform: Matrix4x4 { matrix: "; + generateTransform(info.transform); + stream(SameLine) << " }"; + } + } + if (!info.isDefaultOpacity) { + stream() << "opacity: " << info.opacity; + } + m_indentLevel--; +} + +bool QQuickQmlGenerator::generateDefsNode(const NodeInfo &info) +{ + Q_UNUSED(info) + + return false; +} + +void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + const QFileInfo outputFileInfo(outputFileName); + const QDir outputDir(outputFileInfo.absolutePath()); + + QString filePath; + + if (!m_retainFilePaths || info.externalFileReference.isEmpty()) { + filePath = m_assetFileDirectory; + if (filePath.isEmpty()) + filePath = outputDir.absolutePath(); + + if (!filePath.isEmpty() && !filePath.endsWith(u'/')) + filePath += u'/'; + + QDir fileDir(filePath); + if (!fileDir.exists()) { + if (!fileDir.mkpath(QStringLiteral("."))) + qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath; + } + + filePath += QStringLiteral("%1%2.png").arg(m_assetFilePrefix.isEmpty() + ? QStringLiteral("svg_asset_") + : m_assetFilePrefix) + .arg(info.image.cacheKey()); + + if (!info.image.save(filePath)) + qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath; + qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath; + } else { + filePath = info.externalFileReference; + } + + const QFileInfo assetFileInfo(filePath); + + // TODO: this requires proper asset management. + stream() << "Image {"; + m_indentLevel++; + + generateNodeBase(info); + stream() << "x: " << info.rect.x(); + stream() << "y: " << info.rect.y(); + stream() << "width: " << info.rect.width(); + stream() << "height: " << info.rect.height(); + stream() << "source: \"" << outputDir.relativeFilePath(assetFileInfo.absoluteFilePath()) <<"\""; + + m_indentLevel--; + + stream() << "}"; +} + +void QQuickQmlGenerator::generatePath(const PathNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + if (m_inShapeItem) { + if (!info.isDefaultTransform) + qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)"; + optimizePaths(info); + } else { + m_inShapeItem = true; + stream() << shapeName() << " {"; + + // Check ?? + generateNodeBase(info); + + m_indentLevel++; + if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) + stream() << "preferredRendererType: Shape.CurveRenderer"; + optimizePaths(info); + //qCDebug(lcQuickVectorGraphics) << *node->qpath(); + m_indentLevel--; + stream() << "}"; + m_inShapeItem = false; + } +} + +void QQuickQmlGenerator::generateGradient(const QGradient *grad, const QRectF &boundingRect) +{ + 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 : QQuickVectorImageGenerator::Utils::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() << "}"; + } 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:" << radGrad->focalPoint().x(); + stream() << "focalY:" << radGrad->focalPoint().y(); + for (auto &stop : radGrad->stops()) + stream() << "GradientStop { position: " << stop.first << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }"; + m_indentLevel--; + stream() << "}"; + } +} + +void QQuickQmlGenerator::generateTransform(const QTransform &xf) +{ + if (xf.isAffine()) { + stream(SameLine) << "PlanarTransform.fromAffineMatrix(" + << xf.m11() << ", " << xf.m12() << ", " + << xf.m21() << ", " << xf.m22() << ", " + << xf.dx() << ", " << xf.dy() << ")"; + } else { + QMatrix4x4 m(xf); + stream(SameLine) << "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]; + if (i < 3) + stream(SameLine) << ", "; + } + stream(SameLine) << ")"; + m_indentLevel -= 3; + } +} + +void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) +{ + Q_UNUSED(pathSelector) + Q_ASSERT(painterPath || quadPath); + + const bool noPen = info.strokeStyle.color == QColorConstants::Transparent; + if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen) + return; + + const bool noFill = info.grad.type() == QGradient::NoGradient && info.fillColor == QColorConstants::Transparent; + + if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill) + return; + + auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule()); + stream() << "ShapePath {"; + m_indentLevel++; + if (!info.nodeId.isEmpty()) { + switch (pathSelector) { + case QQuickVectorImageGenerator::FillPath: + stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\""; + break; + case QQuickVectorImageGenerator::StrokePath: + stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\""; + break; + case QQuickVectorImageGenerator::FillAndStroke: + stream() << "objectName: \"svg_path:" << info.nodeId << "\""; + break; + } + } + + if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) { + stream() << "strokeColor: \"transparent\""; + } else { + stream() << "strokeColor: \"" << info.strokeStyle.color.name(QColor::HexArgb) << "\""; + stream() << "strokeWidth: " << info.strokeStyle.width; + stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(info.strokeStyle.lineCapStyle); + stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(info.strokeStyle.lineJoinStyle); + stream() << "miterLimit: " << info.strokeStyle.miterLimit; + if (info.strokeStyle.dashArray.length() != 0) { + stream() << "strokeStyle: " << "ShapePath.DashLine"; + stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(info.strokeStyle.dashArray); + stream() << "dashOffset: " << info.strokeStyle.dashOffset; + } + } + + if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) { + stream() << "fillColor: \"transparent\""; + } else if (info.grad.type() != QGradient::NoGradient) { + generateGradient(&info.grad, boundingRect); + } else { + stream() << "fillColor: \"" << info.fillColor.name(QColor::HexArgb) << "\""; + } + + if (!info.fillTransform.isIdentity()) { + const QTransform &xf = info.fillTransform; + stream() << "fillTransform: "; + if (info.fillTransform.type() == QTransform::TxTranslate) + stream(SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")"; + else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy()) + stream(SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")"; + else + generateTransform(xf); + } + + if (fillRule == QQuickShapePath::WindingFill) + stream() << "fillRule: ShapePath.WindingFill"; + else + stream() << "fillRule: ShapePath.OddEvenFill"; + + QString hintStr; + if (quadPath) + hintStr = QQuickVectorImageGenerator::Utils::pathHintString(*quadPath); + if (!hintStr.isEmpty()) + stream() << hintStr; + + + QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(*painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(*quadPath); + stream() << "PathSvg { path: \"" << svgPathString << "\" }"; + + m_indentLevel--; + stream() << "}"; +} + +void QQuickQmlGenerator::generateNode(const NodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + stream() << "// Missing Implementation for SVG Node: " << info.typeName; + stream() << "// Adding an empty Item and skipping"; + stream() << "Item {"; + generateNodeBase(info); + stream() << "}"; +} + +void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + static int counter = 0; + stream() << "Item {"; + generateNodeBase(info); + m_indentLevel++; + + if (!info.isTextArea) + stream() << "Item { id: textAlignItem_" << counter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}"; + + stream() << "Text {"; + + m_indentLevel++; + + if (info.isTextArea) { + stream() << "x: " << info.position.x(); + stream() << "y: " << info.position.y(); + if (info.size.width() > 0) + stream() << "width: " << info.size.width(); + if (info.size.height() > 0) + stream() << "height: " << info.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 + } else { + QString hAlign = QStringLiteral("left"); + stream() << "anchors.baseline: textAlignItem_" << counter << ".top"; + switch (info.alignment) { + case Qt::AlignHCenter: + hAlign = QStringLiteral("horizontalCenter"); + break; + case Qt::AlignRight: + hAlign = QStringLiteral("right"); + break; + default: + qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment; + Q_FALLTHROUGH(); + case Qt::AlignLeft: + break; + } + stream() << "anchors." << hAlign << ": textAlignItem_" << counter << ".left"; + } + counter++; + + stream() << "color: \"" << info.fillColor.name(QColor::HexArgb) << "\""; + stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText"); + + QString s = info.text; + s.replace(QLatin1Char('"'), QLatin1String("\\\"")); + stream() << "text: \"" << s << "\""; + stream() << "font.family: \"" << info.font.family() << "\""; + if (info.font.pixelSize() > 0) + stream() << "font.pixelSize:" << info.font.pixelSize(); + else if (info.font.pointSize() > 0) + stream() << "font.pixelSize:" << info.font.pointSizeF(); + if (info.font.underline()) + stream() << "font.underline: true"; + if (info.font.weight() != QFont::Normal) + stream() << "font.weight: " << int(info.font.weight()); + if (info.font.italic()) + stream() << "font.italic: true"; + + if (info.strokeColor != QColorConstants::Transparent) { + stream() << "styleColor: \"" << info.strokeColor.name(QColor::HexArgb) << "\""; + stream() << "style: Text.Outline"; + } + + m_indentLevel--; + stream() << "}"; + + m_indentLevel--; + stream() << "}"; +} + +void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info) +{ + if (!isNodeVisible(info)) + return; + + if (info.stage == StructureNodeStage::Start) { + stream() << "Item {"; + generateNodeBase(info); + m_indentLevel++; + stream() << "x: " << info.startPos.x(); + stream() << "y: " << info.startPos.y(); + } else { + m_indentLevel--; + stream() << "}"; + } +} + +bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info) +{ + if (!isNodeVisible(info)) + return false; + + if (info.stage == StructureNodeStage::Start) { + if (!info.forceSeparatePaths && info.isPathContainer) { + stream() << shapeName() <<" {"; + m_indentLevel++; + if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer)) + stream() << "preferredRendererType: Shape.CurveRenderer"; + m_indentLevel--; + + m_inShapeItem = true; + } else { + stream() << "Item {"; + } + + if (!info.viewBox.isEmpty()) { + m_indentLevel++; + stream() << "transform: ["; + m_indentLevel++; + bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y()); + if (translate) + stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },"; + stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }"; + m_indentLevel--; + stream() << "]"; + m_indentLevel--; + } + + generateNodeBase(info); + m_indentLevel++; + } else { + m_indentLevel--; + stream() << "}"; + m_inShapeItem = false; + } + + return true; +} + +bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info) +{ + m_indentLevel = 0; + const QStringList comments = m_commentString.split(u'\n'); + + if (!isNodeVisible(info)) { + if (comments.isEmpty()) { + stream() << "// Generated from SVG"; + } else { + for (const auto &comment : comments) + stream() << "// " << comment; + } + + stream() << "import QtQuick"; + stream() << "import QtQuick.Shapes" << Qt::endl; + stream() << "Item {"; + m_indentLevel++; + + double w = info.size.width(); + double h = info.size.height(); + if (w > 0) + stream() << "implicitWidth: " << w; + if (h > 0) + stream() << "implicitHeight: " << h; + + m_indentLevel--; + stream() << "}"; + + return false; + } + + if (info.stage == StructureNodeStage::Start) { + if (comments.isEmpty()) + stream() << "// Generated from SVG"; + else + for (const auto &comment : comments) + stream() << "// " << comment; + + stream() << "import QtQuick"; + stream() << "import QtQuick.Shapes" << Qt::endl; + stream() << "Item {"; + m_indentLevel++; + + double w = info.size.width(); + double h = info.size.height(); + if (w > 0) + stream() << "implicitWidth: " << w; + if (h > 0) + stream() << "implicitHeight: " << h; + + if (!info.viewBox.isEmpty()) { + stream() << "transform: ["; + m_indentLevel++; + bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y()); + if (translate) + stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },"; + stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }"; + m_indentLevel--; + stream() << "]";; + } + + generateNodeBase(info); + } else { + stream() << "}"; + m_inShapeItem = false; + } + + return true; +} + +QStringView QQuickQmlGenerator::indent() +{ + static QString indentString; + int indentWidth = m_indentLevel * 4; + if (indentWidth > indentString.size()) + indentString.fill(QLatin1Char(' '), indentWidth * 2); + return QStringView(indentString).first(indentWidth); +} + +QTextStream &QQuickQmlGenerator::stream(int flags) +{ + if (m_stream.device() == nullptr) + m_stream.setDevice(&m_result); + else if (!(flags & StreamFlags::SameLine)) + m_stream << Qt::endl << indent(); + return m_stream; +} + +const char *QQuickQmlGenerator::shapeName() const +{ + return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData(); +} + +QT_END_NAMESPACE diff --git a/src/quickvectorimage/generator/qquickqmlgenerator_p.h b/src/quickvectorimage/generator/qquickqmlgenerator_p.h new file mode 100644 index 0000000000..dae34757b0 --- /dev/null +++ b/src/quickvectorimage/generator/qquickqmlgenerator_p.h @@ -0,0 +1,103 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQUICKQMLGENERATOR_P_H +#define QQUICKQMLGENERATOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickgenerator_p.h" + +#include <QtCore/qtextstream.h> +#include <QtCore/qbuffer.h> + +QT_BEGIN_NAMESPACE + +class Q_QUICKVECTORIMAGEGENERATOR_EXPORT QQuickQmlGenerator : public QQuickGenerator +{ +public: + QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName); + ~QQuickQmlGenerator(); + + void setShapeTypeName(const QString &name); + QString shapeTypeName() const; + + void setCommentString(const QString commentString); + QString commentString() const; + + void setRetainFilePaths(bool retainFilePaths) + { + m_retainFilePaths = retainFilePaths; + } + + bool retainFilePaths() const + { + return m_retainFilePaths; + } + + void setAssetFileDirectory(const QString &assetFileDirectory) + { + m_assetFileDirectory = assetFileDirectory; + } + + QString assetFileDirectory() const + { + return m_assetFileDirectory; + } + + void setAssetFilePrefix(const QString &assetFilePrefix) + { + m_assetFilePrefix = assetFilePrefix; + } + + QString assetFilePrefix() const + { + return m_assetFilePrefix; + } + +protected: + void generateNodeBase(const NodeInfo &info) override; + bool generateDefsNode(const NodeInfo &info) override; + void generateImageNode(const ImageNodeInfo &info) override; + void generatePath(const PathNodeInfo &info) override; + void generateNode(const NodeInfo &info) override; + void generateTextNode(const TextNodeInfo &info) override; + void generateUseNode(const UseNodeInfo &info) override; + bool generateStructureNode(const StructureNodeInfo &info) override; + bool generateRootNode(const StructureNodeInfo &info) override; + void outputShapePath(const PathNodeInfo &info, const QPainterPath *path, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect) override; + +private: + void generateGradient(const QGradient *grad, const QRectF &boundingRect); + void generateTransform(const QTransform &xf); + + QStringView indent(); + enum StreamFlags { NoFlags = 0x0, SameLine = 0x1 }; + QTextStream &stream(int flags = NoFlags); + const char *shapeName() const; + +private: + int m_indentLevel = 0; + QBuffer m_result; + QTextStream m_stream; + QString outputFileName; + bool m_inShapeItem = false; + QByteArray m_shapeTypeName; + QString m_commentString; + bool m_retainFilePaths = false; + QString m_assetFileDirectory; + QString m_assetFilePrefix; +}; + +QT_END_NAMESPACE + +#endif // QQUICKQMLGENERATOR_P_H diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl.cpp b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp new file mode 100644 index 0000000000..0717aa21e9 --- /dev/null +++ b/src/quickvectorimage/generator/qsvgvisitorimpl.cpp @@ -0,0 +1,1094 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsvgvisitorimpl_p.h" +#include "qquickgenerator_p.h" +#include "qquicknodeinfo_p.h" + +#include <private/qsvgvisitor_p.h> + +#include <QString> +#include <QPainter> +#include <QTextDocument> +#include <QTextLayout> +#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> + +#include <QtCore/private/qstringiterator_p.h> + +#include "utils_p.h" +#include <QtCore/qloggingcategory.h> + +#include <QtSvg/private/qsvgstyle_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +class QSvgStyleResolver +{ +public: + QSvgStyleResolver() + { + m_dummyImage = QImage(1, 1, QImage::Format_RGB32); + m_dummyPainter.begin(&m_dummyImage); + QPen defaultPen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin); + defaultPen.setMiterLimit(4); + m_dummyPainter.setPen(defaultPen); + m_dummyPainter.setBrush(Qt::black); + } + + ~QSvgStyleResolver() + { + m_dummyPainter.end(); + } + + QPainter& painter() { return m_dummyPainter; } + QSvgExtraStates& states() { return m_svgState; } + + QColor currentFillColor() const + { + if (m_dummyPainter.brush().style() == Qt::NoBrush || + m_dummyPainter.brush().color() == QColorConstants::Transparent) { + return QColor(QColorConstants::Transparent); + } + + QColor fillColor; + fillColor = m_dummyPainter.brush().color(); + fillColor.setAlphaF(m_svgState.fillOpacity); + + return fillColor; + } + + qreal currentFillOpacity() const + { + return m_svgState.fillOpacity; + } + + const QGradient *currentStrokeGradient() const + { + QBrush brush = m_dummyPainter.pen().brush(); + if (brush.style() == Qt::LinearGradientPattern + || brush.style() == Qt::RadialGradientPattern + || brush.style() == Qt::ConicalGradientPattern) { + return brush.gradient(); + } + return nullptr; + } + + const QGradient *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; + } + + QTransform currentFillTransform() const + { + return m_dummyPainter.brush().transform(); + } + + QColor currentStrokeColor() const + { + if (m_dummyPainter.pen().brush().style() == Qt::NoBrush || + m_dummyPainter.pen().brush().color() == QColorConstants::Transparent) { + return QColor(QColorConstants::Transparent); + } + + QColor strokeColor; + strokeColor = m_dummyPainter.pen().brush().color(); + strokeColor.setAlphaF(m_svgState.strokeOpacity); + + return strokeColor; + } + + static QGradient applyOpacityToGradient(const QGradient &gradient, float opacity) + { + QGradient grad = gradient; + QGradientStops stops; + for (auto &stop : grad.stops()) { + stop.second.setAlphaF(stop.second.alphaF() * opacity); + stops.append(stop); + } + + grad.setStops(stops); + + return grad; + } + + float currentStrokeWidth() const + { + float penWidth = m_dummyPainter.pen().widthF(); + return penWidth ? penWidth : 1; + } + + QPen currentStroke() const + { + return m_dummyPainter.pen(); + } + +protected: + QPainter m_dummyPainter; + QImage m_dummyImage; + QSvgExtraStates m_svgState; +}; + +Q_GLOBAL_STATIC(QSvgStyleResolver, styleResolver) + +namespace { +inline 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: + //qCDebug(lcQuickVectorGraphics) << "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()) { + //qCDebug(lcQuickVectorGraphics) << "NOT path container because local transform"; + return false; + } + foundPath = true; + break; + default: + qCDebug(lcQuickVectorImage) << "Unhandled type in switch" << child->type(); + break; + } + } + //qCDebug(lcQuickVectorGraphics) << "Container" << node->nodeId() << node->typeName() << "is" << foundPath; + return foundPath; +} + +static QString capStyleName(Qt::PenCapStyle style) +{ + QString styleName; + + switch (style) { + case Qt::SquareCap: + styleName = QStringLiteral("squarecap"); + break; + case Qt::FlatCap: + styleName = QStringLiteral("flatcap"); + break; + case Qt::RoundCap: + styleName = QStringLiteral("roundcap"); + break; + default: + break; + } + + return styleName; +} + +static QString joinStyleName(Qt::PenJoinStyle style) +{ + QString styleName; + + switch (style) { + case Qt::MiterJoin: + styleName = QStringLiteral("miterjoin"); + break; + case Qt::BevelJoin: + styleName = QStringLiteral("beveljoin"); + break; + case Qt::RoundJoin: + styleName = QStringLiteral("roundjoin"); + break; + case Qt::SvgMiterJoin: + styleName = QStringLiteral("svgmiterjoin"); + break; + default: + break; + } + + return styleName; +} + +static QString dashArrayString(QList<qreal> dashArray) +{ + if (dashArray.isEmpty()) + return QString(); + + QString dashArrayString; + QTextStream stream(&dashArrayString); + + for (int i = 0; i < dashArray.length() - 1; i++) { + qreal value = dashArray[i]; + stream << value << ", "; + } + + stream << dashArray.last(); + + return dashArrayString; +} +}; + +QSvgVisitorImpl::QSvgVisitorImpl(const QString svgFileName, QQuickGenerator *generator) + : m_svgFileName(svgFileName) + , m_generator(generator) +{ +} + +bool QSvgVisitorImpl::traverse() +{ + if (!m_generator) { + qCDebug(lcQuickVectorImage) << "No valid QQuickGenerator is set. Genration will stop"; + return false; + } + + auto *doc = QSvgTinyDocument::load(m_svgFileName); + if (!doc) { + qCDebug(lcQuickVectorImage) << "Not a valid Svg File : " << m_svgFileName; + return false; + } + + QSvgVisitor::traverse(doc); + return true; +} + +void QSvgVisitorImpl::visitNode(const QSvgNode *node) +{ + handleBaseNodeSetup(node); + + NodeInfo info; + fillCommonNodeInfo(node, info); + + m_generator->generateNode(info); + + handleBaseNodeEnd(node); +} + +void QSvgVisitorImpl::visitImageNode(const QSvgImage *node) +{ + // TODO: this requires proper asset management. + handleBaseNodeSetup(node); + + ImageNodeInfo info; + fillCommonNodeInfo(node, info); + info.image = node->image(); + info.rect = node->rect(); + info.externalFileReference = node->filename(); + + m_generator->generateImageNode(info); + + handleBaseNodeEnd(node); +} + +void QSvgVisitorImpl::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); + // qCDebug(lcQuickVectorGraphics) << "Line1" << x2 - rx << y1; + p.arcTo(x2 - rx * 2, y1, rx * 2, ry * 2, 90, -90); // ARC to x2, y1 + ry + // qCDebug(lcQuickVectorGraphics) << "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 + + handlePathNode(node, p); +} + +void QSvgVisitorImpl::visitEllipseNode(const QSvgEllipse *node) +{ + QRectF rect = node->rect(); + + QPainterPath p; + p.addEllipse(rect); + + handlePathNode(node, p); +} + +void QSvgVisitorImpl::visitPathNode(const QSvgPath *node) +{ + handlePathNode(node, node->path()); +} + +void QSvgVisitorImpl::visitLineNode(const QSvgLine *node) +{ + QPainterPath p; + p.moveTo(node->line().p1()); + p.lineTo(node->line().p2()); + handlePathNode(node, p); +} + +void QSvgVisitorImpl::visitPolygonNode(const QSvgPolygon *node) +{ + QPainterPath p = QQuickVectorImageGenerator::Utils::polygonToPath(node->polygon(), true); + handlePathNode(node, p); +} + +void QSvgVisitorImpl::visitPolylineNode(const QSvgPolyline *node) +{ + QPainterPath p = QQuickVectorImageGenerator::Utils::polygonToPath(node->polygon(), false); + handlePathNode(node, p); +} + +QString QSvgVisitorImpl::gradientCssDescription(const QGradient *gradient) +{ + QString cssDescription; + if (gradient->type() == QGradient::LinearGradient) { + const QLinearGradient *linearGradient = static_cast<const QLinearGradient *>(gradient); + + cssDescription += " -qt-foreground: qlineargradient("_L1; + cssDescription += "x1:"_L1 + QString::number(linearGradient->start().x()) + u','; + cssDescription += "y1:"_L1 + QString::number(linearGradient->start().y()) + u','; + cssDescription += "x2:"_L1 + QString::number(linearGradient->finalStop().x()) + u','; + cssDescription += "y2:"_L1 + QString::number(linearGradient->finalStop().y()) + u','; + } else if (gradient->type() == QGradient::RadialGradient) { + const QRadialGradient *radialGradient = static_cast<const QRadialGradient *>(gradient); + + cssDescription += " -qt-foreground: qradialgradient("_L1; + cssDescription += "cx:"_L1 + QString::number(radialGradient->center().x()) + u','; + cssDescription += "cy:"_L1 + QString::number(radialGradient->center().y()) + u','; + cssDescription += "fx:"_L1 + QString::number(radialGradient->focalPoint().x()) + u','; + cssDescription += "fy:"_L1 + QString::number(radialGradient->focalPoint().y()) + u','; + cssDescription += "radius:"_L1 + QString::number(radialGradient->radius()) + u','; + } else { + const QConicalGradient *conicalGradient = static_cast<const QConicalGradient *>(gradient); + + cssDescription += " -qt-foreground: qconicalgradient("_L1; + cssDescription += "cx:"_L1 + QString::number(conicalGradient->center().x()) + u','; + cssDescription += "cy:"_L1 + QString::number(conicalGradient->center().y()) + u','; + cssDescription += "angle:"_L1 + QString::number(conicalGradient->angle()) + u','; + } + + const QStringList coordinateModes = { "logical"_L1, "stretchtodevice"_L1, "objectbounding"_L1, "object"_L1 }; + cssDescription += "coordinatemode:"_L1; + cssDescription += coordinateModes.at(int(gradient->coordinateMode())); + cssDescription += u','; + + const QStringList spreads = { "pad"_L1, "reflect"_L1, "repeat"_L1 }; + cssDescription += "spread:"_L1; + cssDescription += spreads.at(int(gradient->spread())); + + for (const QGradientStop &stop : gradient->stops()) { + cssDescription += ",stop:"_L1; + cssDescription += QString::number(stop.first); + cssDescription += u' '; + cssDescription += stop.second.name(QColor::HexArgb); + } + + cssDescription += ");"_L1; + + return cssDescription; +} + +QString QSvgVisitorImpl::colorCssDescription(QColor color) +{ + QString cssDescription; + cssDescription += QStringLiteral("rgba("); + cssDescription += QString::number(color.red()) + QStringLiteral(","); + cssDescription += QString::number(color.green()) + QStringLiteral(","); + cssDescription += QString::number(color.blue()) + QStringLiteral(","); + cssDescription += QString::number(color.alphaF()) + QStringLiteral(")"); + + return cssDescription; +} + +namespace { + + // Simple class for representing the SVG font as a font engine + // We use the Proxy font engine type, which is currently unused and does not map to + // any specific font engine + // (The QSvgFont object must outlive the engine.) + class QSvgFontEngine : public QFontEngine + { + public: + QSvgFontEngine(const QSvgFont *font, qreal size); + + QFontEngine *cloneWithSize(qreal size) const override; + + glyph_t glyphIndex(uint ucs4) const override; + int stringToCMap(const QChar *str, + int len, + QGlyphLayout *glyphs, + int *nglyphs, + ShaperFlags flags) const override; + + void addGlyphsToPath(glyph_t *glyphs, + QFixedPoint *positions, + int nGlyphs, + QPainterPath *path, + QTextItem::RenderFlags flags) override; + + glyph_metrics_t boundingBox(glyph_t glyph) override; + + void recalcAdvances(QGlyphLayout *, ShaperFlags) const override; + QFixed ascent() const override; + QFixed capHeight() const override; + QFixed descent() const override; + QFixed leading() const override; + qreal maxCharWidth() const override; + qreal minLeftBearing() const override; + qreal minRightBearing() const override; + + QFixed emSquareSize() const override; + + private: + const QSvgFont *m_font; + }; + + QSvgFontEngine::QSvgFontEngine(const QSvgFont *font, qreal size) + : QFontEngine(Proxy) + , m_font(font) + { + fontDef.pixelSize = size; + fontDef.families = QStringList(m_font->m_familyName); + } + + QFixed QSvgFontEngine::emSquareSize() const + { + return QFixed::fromReal(m_font->m_unitsPerEm); + } + + glyph_t QSvgFontEngine::glyphIndex(uint ucs4) const + { + if (ucs4 < USHRT_MAX && m_font->m_glyphs.contains(QChar(ushort(ucs4)))) + return glyph_t(ucs4); + + return 0; + } + + int QSvgFontEngine::stringToCMap(const QChar *str, + int len, + QGlyphLayout *glyphs, + int *nglyphs, + ShaperFlags flags) const + { + Q_ASSERT(glyphs->numGlyphs >= *nglyphs); + if (*nglyphs < len) { + *nglyphs = len; + return -1; + } + + int ucs4Length = 0; + QStringIterator it(str, str + len); + while (it.hasNext()) { + char32_t ucs4 = it.next(); + glyph_t index = glyphIndex(ucs4); + glyphs->glyphs[ucs4Length++] = index; + } + + *nglyphs = ucs4Length; + glyphs->numGlyphs = ucs4Length; + + if (!(flags & GlyphIndicesOnly)) + recalcAdvances(glyphs, flags); + + return *nglyphs; + } + + void QSvgFontEngine::addGlyphsToPath(glyph_t *glyphs, + QFixedPoint *positions, + int nGlyphs, + QPainterPath *path, + QTextItem::RenderFlags flags) + { + Q_UNUSED(flags); + const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; + for (int i = 0; i < nGlyphs; ++i) { + glyph_t index = glyphs[i]; + if (index > 0) { + QPointF position = positions[i].toPointF(); + QPainterPath glyphPath = m_font->m_glyphs.value(QChar(ushort(index))).m_path; + + QTransform xform; + xform.translate(position.x(), position.y()); + xform.scale(scale, -scale); + glyphPath = xform.map(glyphPath); + path->addPath(glyphPath); + } + } + } + + glyph_metrics_t QSvgFontEngine::boundingBox(glyph_t glyph) + { + glyph_metrics_t ret; + ret.x = 0; // left bearing + ret.y = -ascent(); + const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; + const QSvgGlyph &svgGlyph = m_font->m_glyphs.value(QChar(ushort(glyph))); + ret.width = QFixed::fromReal(svgGlyph.m_horizAdvX * scale); + ret.height = ascent() + descent(); + return ret; + } + + QFontEngine *QSvgFontEngine::cloneWithSize(qreal size) const + { + QSvgFontEngine *otherEngine = new QSvgFontEngine(m_font, size); + return otherEngine; + } + + void QSvgFontEngine::recalcAdvances(QGlyphLayout *glyphLayout, ShaperFlags) const + { + const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; + for (int i = 0; i < glyphLayout->numGlyphs; i++) { + glyph_t glyph = glyphLayout->glyphs[i]; + const QSvgGlyph &svgGlyph = m_font->m_glyphs.value(QChar(ushort(glyph))); + glyphLayout->advances[i] = QFixed::fromReal(svgGlyph.m_horizAdvX * scale); + } + } + + QFixed QSvgFontEngine::ascent() const + { + return QFixed::fromReal(fontDef.pixelSize); + } + + QFixed QSvgFontEngine::capHeight() const + { + return ascent(); + } + QFixed QSvgFontEngine::descent() const + { + return QFixed{}; + } + + QFixed QSvgFontEngine::leading() const + { + return QFixed{}; + } + + qreal QSvgFontEngine::maxCharWidth() const + { + const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; + return m_font->m_horizAdvX * scale; + } + + qreal QSvgFontEngine::minLeftBearing() const + { + return 0.0; + } + + qreal QSvgFontEngine::minRightBearing() const + { + return 0.0; + } +} + +void QSvgVisitorImpl::visitTextNode(const QSvgText *node) +{ + handleBaseNodeSetup(node); + const bool isTextArea = node->type() == QSvgNode::Textarea; + + QString text; + const QSvgFont *svgFont = styleResolver->states().svgFont; + bool needsRichText = false; + bool preserveWhiteSpace = node->whitespaceMode() == QSvgText::Preserve; + const QGradient *mainGradient = styleResolver->currentFillGradient(); + + QFontEngine *fontEngine = nullptr; + if (svgFont != nullptr) { + fontEngine = new QSvgFontEngine(svgFont, styleResolver->painter().font().pointSize()); + fontEngine->ref.ref(); + } + +#if QT_CONFIG(texthtmlparser) + bool needsPathNode = mainGradient != nullptr + || svgFont != nullptr + || styleResolver->currentStrokeGradient() != nullptr; +#endif + for (const auto *tspan : node->tspans()) { + if (!tspan) { + text += QStringLiteral("<br>"); + continue; + } + + // Note: We cannot get the font directly from the style, since this does + // not apply the weight, since this is relative and depends on current state. + handleBaseNodeSetup(tspan); + QFont font = styleResolver->painter().font(); + + QString styleTagContent; + + if ((font.resolveMask() & QFont::FamilyResolved) + || (font.resolveMask() & QFont::FamiliesResolved)) { + styleTagContent += QStringLiteral("font-family: %1;").arg(font.family()); + } + + if (font.resolveMask() & QFont::WeightResolved + && font.weight() != QFont::Normal + && font.weight() != QFont::Bold) { + styleTagContent += QStringLiteral("font-weight: %1;").arg(int(font.weight())); + } + + if (font.resolveMask() & QFont::SizeResolved) { + // Pixel size stored as point size in SVG parser + styleTagContent += QStringLiteral("font-size: %1px;").arg(int(font.pointSizeF())); + } + + if (font.resolveMask() & QFont::CapitalizationResolved + && font.capitalization() == QFont::SmallCaps) { + styleTagContent += QStringLiteral("font-variant: small-caps;"); + } + + if (styleResolver->currentFillGradient() != nullptr + && styleResolver->currentFillGradient() != mainGradient) { + const QGradient grad = styleResolver->applyOpacityToGradient(*styleResolver->currentFillGradient(), styleResolver->currentFillOpacity()); + styleTagContent += gradientCssDescription(&grad) + u';'; +#if QT_CONFIG(texthtmlparser) + needsPathNode = true; +#endif + } + + const QColor currentStrokeColor = styleResolver->currentStrokeColor(); + if (currentStrokeColor.alpha() > 0) { + QString strokeColor = colorCssDescription(currentStrokeColor); + styleTagContent += QStringLiteral("-qt-stroke-color:%1;").arg(strokeColor); + styleTagContent += QStringLiteral("-qt-stroke-width:%1px;").arg(styleResolver->currentStrokeWidth()); + styleTagContent += QStringLiteral("-qt-stroke-dasharray:%1;").arg(dashArrayString(styleResolver->currentStroke().dashPattern())); + styleTagContent += QStringLiteral("-qt-stroke-dashoffset:%1;").arg(styleResolver->currentStroke().dashOffset()); + styleTagContent += QStringLiteral("-qt-stroke-lineCap:%1;").arg(capStyleName(styleResolver->currentStroke().capStyle())); + styleTagContent += QStringLiteral("-qt-stroke-lineJoin:%1;").arg(joinStyleName(styleResolver->currentStroke().joinStyle())); + if (styleResolver->currentStroke().joinStyle() == Qt::MiterJoin || styleResolver->currentStroke().joinStyle() == Qt::SvgMiterJoin) + styleTagContent += QStringLiteral("-qt-stroke-miterlimit:%1;").arg(styleResolver->currentStroke().miterLimit()); +#if QT_CONFIG(texthtmlparser) + needsPathNode = true; +#endif + } + + if (tspan->whitespaceMode() == QSvgText::Preserve && !preserveWhiteSpace) + styleTagContent += QStringLiteral("white-space: pre-wrap;"); + + QString content = tspan->text().toHtmlEscaped(); + content.replace(QLatin1Char('\t'), QLatin1Char(' ')); + content.replace(QLatin1Char('\n'), QLatin1Char(' ')); + + bool fontTag = false; + if (!tspan->style().fill.isDefault()) { + auto &b = tspan->style().fill->qbrush(); + qCDebug(lcQuickVectorImage) << "tspan FILL:" << b; + if (b.style() != Qt::NoBrush) + { + if (qFuzzyCompare(b.color().alphaF() + 1.0, 2.0)) + { + QString spanColor = b.color().name(); + fontTag = !spanColor.isEmpty(); + if (fontTag) + text += QStringLiteral("<font color=\"%1\">").arg(spanColor); + } else { + QString spanColor = colorCssDescription(b.color()); + styleTagContent += QStringLiteral("color:%1").arg(spanColor); + } + } + } + + needsRichText = needsRichText || !styleTagContent.isEmpty(); + if (!styleTagContent.isEmpty()) + text += QStringLiteral("<span style=\"%1\">").arg(styleTagContent); + + if (font.resolveMask() & QFont::WeightResolved && font.bold()) + text += QStringLiteral("<b>"); + + if (font.resolveMask() & QFont::StyleResolved && font.italic()) + text += QStringLiteral("<i>"); + + if (font.resolveMask() & QFont::CapitalizationResolved) { + switch (font.capitalization()) { + case QFont::AllLowercase: + content = content.toLower(); + break; + case QFont::AllUppercase: + content = content.toUpper(); + break; + case QFont::Capitalize: + // ### We need to iterate over the string and do the title case conversion, + // since this is not part of QString. + qCWarning(lcQuickVectorImage) << "Title case not implemented for tspan"; + break; + default: + break; + } + } + text += content; + if (fontTag) + text += QStringLiteral("</font>"); + + if (font.resolveMask() & QFont::StyleResolved && font.italic()) + text += QStringLiteral("</i>"); + + if (font.resolveMask() & QFont::WeightResolved && font.bold()) + text += QStringLiteral("</b>"); + + if (!styleTagContent.isEmpty()) + text += QStringLiteral("</span>"); + + handleBaseNodeEnd(tspan); + } + + if (preserveWhiteSpace && (needsRichText || styleResolver->currentFillGradient() != nullptr)) + text = QStringLiteral("<span style=\"white-space: pre-wrap\">") + text + QStringLiteral("</span>"); + + QFont font = styleResolver->painter().font(); + if (font.pixelSize() <= 0 && font.pointSize() > 0) + font.setPixelSize(font.pointSize()); // Pixel size stored as point size by SVG parser + +#if QT_CONFIG(texthtmlparser) + if (needsPathNode) { + QTextDocument document; + document.setHtml(text); + if (isTextArea && node->size().width() > 0) + document.setTextWidth(node->size().width()); + document.setDefaultFont(font); + document.pageCount(); // Force layout + + QTextBlock block = document.firstBlock(); + while (block.isValid()) { + QTextLayout *lout = block.layout(); + + if (lout != nullptr) { + // If this block has requested the current SVG font, we override it + // (note that this limits the text to one svg font, but this is also the case + // in the QPainter at the moment, and needs a more centralized solution in Qt Svg + // first) + QFont blockFont = block.charFormat().font(); + if (svgFont != nullptr + && blockFont.family() == svgFont->m_familyName) { + QRawFont rawFont; + QRawFontPrivate *rawFontD = QRawFontPrivate::get(rawFont); + rawFontD->setFontEngine(fontEngine->cloneWithSize(blockFont.pixelSize())); + + lout->setRawFont(rawFont); + } + + auto addPathForFormat = [&](QPainterPath p, QTextCharFormat fmt) { + PathNodeInfo info; + fillCommonNodeInfo(node, info); + auto fillStyle = node->style().fill; + if (fillStyle) + info.fillRule = fillStyle->fillRule(); + + if (fmt.hasProperty(QTextCharFormat::ForegroundBrush)) { + info.fillColor = fmt.foreground().color(); + if (fmt.foreground().gradient() != nullptr && fmt.foreground().gradient()->type() != QGradient::NoGradient) + info.grad = *fmt.foreground().gradient(); + } else { + info.fillColor = styleResolver->currentFillColor(); + } + + info.painterPath = p; + + const QGradient *strokeGradient = styleResolver->currentStrokeGradient(); + QPen pen; + if (fmt.hasProperty(QTextCharFormat::TextOutline)) { + pen = fmt.textOutline(); + if (strokeGradient == nullptr) { + info.strokeStyle = StrokeStyle::fromPen(pen); + info.strokeStyle.color = pen.color(); + } + } else { + pen = styleResolver->currentStroke(); + if (strokeGradient == nullptr) { + info.strokeStyle = StrokeStyle::fromPen(pen); + info.strokeStyle.color = styleResolver->currentStrokeColor(); + } + } + + if (info.grad.type() == QGradient::NoGradient && styleResolver->currentFillGradient() != nullptr) + info.grad = styleResolver->applyOpacityToGradient(*styleResolver->currentFillGradient(), styleResolver->currentFillOpacity()); + + info.fillTransform = styleResolver->currentFillTransform(); + + m_generator->generatePath(info); + + if (strokeGradient != nullptr) { + PathNodeInfo strokeInfo; + fillCommonNodeInfo(node, strokeInfo); + + strokeInfo.grad = *strokeGradient; + + QPainterPathStroker stroker(pen); + strokeInfo.painterPath = stroker.createStroke(p); + m_generator->generatePath(strokeInfo); + } + }; + + qreal baselineOffset = -QFontMetricsF(font).ascent(); + if (lout->lineCount() > 0 && lout->lineAt(0).isValid()) + baselineOffset = -lout->lineAt(0).ascent(); + + const QPointF baselineTranslation(0.0, baselineOffset); + auto glyphsToPath = [&](QList<QGlyphRun> glyphRuns, qreal width) { + QList<QPainterPath> paths; + for (const QGlyphRun &glyphRun : glyphRuns) { + QRawFont font = glyphRun.rawFont(); + QList<quint32> glyphIndexes = glyphRun.glyphIndexes(); + QList<QPointF> positions = glyphRun.positions(); + + for (qsizetype j = 0; j < glyphIndexes.size(); ++j) { + quint32 glyphIndex = glyphIndexes.at(j); + const QPointF &pos = positions.at(j); + + QPainterPath p = font.pathForGlyph(glyphIndex); + p.translate(pos + node->position() + baselineTranslation); + if (styleResolver->states().textAnchor == Qt::AlignHCenter) + p.translate(QPointF(-0.5 * width, 0)); + else if (styleResolver->states().textAnchor == Qt::AlignRight) + p.translate(QPointF(-width, 0)); + paths.append(p); + } + } + + return paths; + }; + + QList<QTextLayout::FormatRange> formats = block.textFormats(); + for (int i = 0; i < formats.size(); ++i) { + QTextLayout::FormatRange range = formats.at(i); + + QList<QGlyphRun> glyphRuns = lout->glyphRuns(range.start, range.length); + QList<QPainterPath> paths = glyphsToPath(glyphRuns, lout->minimumWidth()); + for (const QPainterPath &path : paths) + addPathForFormat(path, range.format); + } + } + + block = block.next(); + } + } else +#endif + { + TextNodeInfo info; + fillCommonNodeInfo(node, info); + + info.position = node->position(); + info.size = node->size(); + info.font = font; + info.text = text; + info.isTextArea = isTextArea; + info.needsRichText = needsRichText; + info.fillColor = styleResolver->currentFillColor(); + info.alignment = styleResolver->states().textAnchor; + info.strokeColor = styleResolver->currentStrokeColor(); + + m_generator->generateTextNode(info); + } + + handleBaseNodeEnd(node); + + if (fontEngine != nullptr) { + fontEngine->ref.deref(); + Q_ASSERT(fontEngine->ref.loadRelaxed() == 0); + delete fontEngine; + } +} + +void QSvgVisitorImpl::visitUseNode(const QSvgUse *node) +{ + QSvgNode *link = node->link(); + if (!link) + return; + + handleBaseNodeSetup(node); + UseNodeInfo info; + fillCommonNodeInfo(node, info); + + info.stage = StructureNodeStage::Start; + info.startPos = node->start(); + + m_generator->generateUseNode(info); + + QSvgVisitor::traverse(link); + + info.stage = StructureNodeStage::End; + m_generator->generateUseNode(info); + handleBaseNodeEnd(node); +} + +bool QSvgVisitorImpl::visitDefsNodeStart(const QSvgDefs *node) +{ + Q_UNUSED(node) + + return m_generator->generateDefsNode(NodeInfo{}); +} + +bool QSvgVisitorImpl::visitStructureNodeStart(const QSvgStructureNode *node) +{ + constexpr bool forceSeparatePaths = false; + handleBaseNodeSetup(node); + + StructureNodeInfo info; + + fillCommonNodeInfo(node, info); + info.forceSeparatePaths = forceSeparatePaths; + info.isPathContainer = isPathContainer(node); + info.stage = StructureNodeStage::Start; + + return m_generator->generateStructureNode(info); +} + +void QSvgVisitorImpl::visitStructureNodeEnd(const QSvgStructureNode *node) +{ + handleBaseNodeEnd(node); + // qCDebug(lcQuickVectorGraphics) << "REVERT" << node->nodeId() << node->type() << (m_styleResolver->painter().pen().style() != Qt::NoPen) << m_styleResolver->painter().pen().color().name() + // << (m_styleResolver->painter().pen().brush().style() != Qt::NoBrush) << m_styleResolver->painter().pen().brush().color().name(); + + StructureNodeInfo info; + fillCommonNodeInfo(node, info); + info.stage = StructureNodeStage::End; + + m_generator->generateStructureNode(info); +} + +bool QSvgVisitorImpl::visitDocumentNodeStart(const QSvgTinyDocument *node) +{ + handleBaseNodeSetup(node); + + StructureNodeInfo info; + fillCommonNodeInfo(node, info); + + const QSvgTinyDocument *doc = static_cast<const QSvgTinyDocument *>(node); + info.size = doc->size(); + info.viewBox = doc->viewBox(); + info.isPathContainer = isPathContainer(node); + info.stage = StructureNodeStage::Start; + + return m_generator->generateRootNode(info); +} + +void QSvgVisitorImpl::visitDocumentNodeEnd(const QSvgTinyDocument *node) +{ + handleBaseNodeEnd(node); + qCDebug(lcQuickVectorImage) << "REVERT" << node->nodeId() << node->type() << (styleResolver->painter().pen().style() != Qt::NoPen) + << styleResolver->painter().pen().color().name() << (styleResolver->painter().pen().brush().style() != Qt::NoBrush) + << styleResolver->painter().pen().brush().color().name(); + + StructureNodeInfo info; + fillCommonNodeInfo(node, info); + info.stage = StructureNodeStage::End; + + m_generator->generateRootNode(info); +} + +void QSvgVisitorImpl::fillCommonNodeInfo(const QSvgNode *node, NodeInfo &info) +{ + info.nodeId = node->nodeId(); + info.typeName = node->typeName(); + info.isDefaultTransform = node->style().transform.isDefault(); + info.transform = !info.isDefaultTransform ? node->style().transform->qtransform() : QTransform(); + info.isDefaultOpacity = node->style().opacity.isDefault(); + info.opacity = !info.isDefaultOpacity ? node->style().opacity->opacity() : 1.0; + info.isVisible = node->isVisible(); + info.isDisplayed = node->displayMode() != QSvgNode::DisplayMode::NoneMode; +} + +void QSvgVisitorImpl::handleBaseNodeSetup(const QSvgNode *node) +{ + qCDebug(lcQuickVectorImage) << "Before SETUP" << node << "fill" << styleResolver->currentFillColor() + << "stroke" << styleResolver->currentStrokeColor() << styleResolver->currentStrokeWidth() + << node->nodeId() << " type: " << node->typeName() << " " << node->type(); + + node->applyStyle(&styleResolver->painter(), styleResolver->states()); + + qCDebug(lcQuickVectorImage) << "After SETUP" << node << "fill" << styleResolver->currentFillColor() + << "stroke" << styleResolver->currentStrokeColor() + << styleResolver->currentStrokeWidth() << node->nodeId(); +} + +void QSvgVisitorImpl::handleBaseNode(const QSvgNode *node) +{ + NodeInfo info; + fillCommonNodeInfo(node, info); + + m_generator->generateNodeBase(info); +} + +void QSvgVisitorImpl::handleBaseNodeEnd(const QSvgNode *node) +{ + node->revertStyle(&styleResolver->painter(), styleResolver->states()); + + qCDebug(lcQuickVectorImage) << "After END" << node << "fill" << styleResolver->currentFillColor() + << "stroke" << styleResolver->currentStrokeColor() << styleResolver->currentStrokeWidth() + << node->nodeId(); +} + +void QSvgVisitorImpl::handlePathNode(const QSvgNode *node, const QPainterPath &path) +{ + handleBaseNodeSetup(node); + + PathNodeInfo info; + fillCommonNodeInfo(node, info); + auto fillStyle = node->style().fill; + if (fillStyle) + info.fillRule = fillStyle->fillRule(); + + const QGradient *strokeGradient = styleResolver->currentStrokeGradient(); + + info.painterPath = path; + info.fillColor = styleResolver->currentFillColor(); + if (strokeGradient == nullptr) { + info.strokeStyle = StrokeStyle::fromPen(styleResolver->currentStroke()); + info.strokeStyle.color = styleResolver->currentStrokeColor(); + } + if (styleResolver->currentFillGradient() != nullptr) + info.grad = styleResolver->applyOpacityToGradient(*styleResolver->currentFillGradient(), styleResolver->currentFillOpacity()); + info.fillTransform = styleResolver->currentFillTransform(); + + m_generator->generatePath(info); + + if (strokeGradient != nullptr) { + PathNodeInfo strokeInfo; + fillCommonNodeInfo(node, strokeInfo); + + strokeInfo.grad = *strokeGradient; + + QPainterPathStroker stroker(styleResolver->currentStroke()); + strokeInfo.painterPath = stroker.createStroke(path); + m_generator->generatePath(strokeInfo); + } + + handleBaseNodeEnd(node); +} + +QT_END_NAMESPACE diff --git a/src/quickvectorimage/generator/qsvgvisitorimpl_p.h b/src/quickvectorimage/generator/qsvgvisitorimpl_p.h new file mode 100644 index 0000000000..2113510d9b --- /dev/null +++ b/src/quickvectorimage/generator/qsvgvisitorimpl_p.h @@ -0,0 +1,69 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSVGVISITORIMPL_P_H +#define QSVGVISITORIMPL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtSvg/private/qsvgvisitor_p.h> +#include "qquickgenerator_p.h" + +QT_BEGIN_NAMESPACE + +class QTextStream; +class QSvgTinyDocument; +class QString; +class QQuickItem; + +class QSvgVisitorImpl : public QSvgVisitor +{ +public: + QSvgVisitorImpl(const QString svgFileName, QQuickGenerator *generator); + bool traverse(); + +protected: + void visitNode(const QSvgNode *node) override; + void visitImageNode(const QSvgImage *node) override; + void visitRectNode(const QSvgRect *node) override; + void visitEllipseNode(const QSvgEllipse *node) override; + void visitPathNode(const QSvgPath *node) override; + void visitLineNode(const QSvgLine *node) override; + void visitPolygonNode(const QSvgPolygon *node) override; + void visitPolylineNode(const QSvgPolyline *node) override; + void visitTextNode(const QSvgText *node) override; + void visitUseNode(const QSvgUse *node) override; + bool visitDefsNodeStart(const QSvgDefs *node) override; + bool visitStructureNodeStart(const QSvgStructureNode *node) override; + void visitStructureNodeEnd(const QSvgStructureNode *node) override; + + bool visitDocumentNodeStart(const QSvgTinyDocument *node) override; + void visitDocumentNodeEnd(const QSvgTinyDocument *node) override; + +private: + void fillCommonNodeInfo(const QSvgNode *node, NodeInfo &info); + void handleBaseNodeSetup(const QSvgNode *node); + void handleBaseNode(const QSvgNode *node); + void handleBaseNodeEnd(const QSvgNode *node); + void handlePathNode(const QSvgNode *node, const QPainterPath &path); + void outputShapePath(QPainterPath pathCopy, const PathNodeInfo &info); + static QString gradientCssDescription(const QGradient *gradient); + static QString colorCssDescription(QColor color); + +private: + QString m_svgFileName; + QQuickGenerator *m_generator; +}; + +QT_END_NAMESPACE + +#endif // QSVGVISITORIMPL_P_H diff --git a/src/quickvectorimage/generator/utils_p.h b/src/quickvectorimage/generator/utils_p.h new file mode 100644 index 0000000000..bb65ee2b69 --- /dev/null +++ b/src/quickvectorimage/generator/utils_p.h @@ -0,0 +1,264 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef UTILS_P_H +#define UTILS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qquicktranslate_p.h> +#include <private/qquickitem_p.h> +#include <private/qsvgnode_p.h> + +#include <private/qquadpath_p.h> +#include <private/qsvgvisitor_p.h> + +QT_BEGIN_NAMESPACE + +namespace QQuickVectorImageGenerator::Utils +{ + +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; +}; + +inline 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; +} + +inline 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; +} + +// 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. +inline 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)); +} + +inline 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; +} + +inline QString toSvgString(const QQuadPath &path) +{ + QString svgPathString; + QTextStream strm(&svgPathString); + path.iterateElements([&](const QQuadPath::Element &e, int) { + 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; +} + +inline QString strokeCapStyleString(Qt::PenCapStyle strokeCapStyle) +{ + QString capStyle; + switch (strokeCapStyle) { + case Qt::FlatCap: + capStyle = QStringLiteral("ShapePath.FlatCap"); + break; + case Qt::SquareCap: + capStyle = QStringLiteral("ShapePath.SquareCap"); + break; + case Qt::RoundCap: + capStyle = QStringLiteral("ShapePath.RoundCap"); + break; + default: + Q_UNREACHABLE(); + break; + } + + return capStyle; +} + +inline QString strokeJoinStyleString(Qt::PenJoinStyle strokeJoinStyle) +{ + QString joinStyle; + switch (strokeJoinStyle) { + case Qt::MiterJoin: + joinStyle = QStringLiteral("ShapePath.MiterJoin"); + break; + case Qt::BevelJoin: + joinStyle = QStringLiteral("ShapePath.BevelJoin"); + break; + case Qt::RoundJoin: + joinStyle = QStringLiteral("ShapePath.RoundJoin"); + break; + default: + //TODO: Add support for SvgMiter case + Q_UNREACHABLE(); + break; + } + + return joinStyle; +} + +template<typename T> +inline QString listString(QList<T> list) +{ + if (list.isEmpty()) + return QStringLiteral("[]"); + + QString listString; + QTextStream stream(&listString); + stream << "["; + + if (list.length() > 1) { + for (int i = 0; i < list.length() - 1; i++) { + T v = list[i]; + stream << v << ", "; + } + } + + stream << list.last() << "]"; + return listString; +} + +} + +QT_END_NAMESPACE + +#endif // UTILS_P_H |