diff options
Diffstat (limited to 'src/quickvectorimage/generator/qquickqmlgenerator.cpp')
-rw-r--r-- | src/quickvectorimage/generator/qquickqmlgenerator.cpp | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/src/quickvectorimage/generator/qquickqmlgenerator.cpp b/src/quickvectorimage/generator/qquickqmlgenerator.cpp new file mode 100644 index 0000000000..fdb59c7ef4 --- /dev/null +++ b/src/quickvectorimage/generator/qquickqmlgenerator.cpp @@ -0,0 +1,578 @@ +// 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 + +Q_DECLARE_LOGGING_CATEGORY(lcQuickVectorImage) + +class GeneratorStream +{ +public: + explicit GeneratorStream(QTextStream *result) + : m_array(new QByteArray()) + , m_stream(new QTextStream(m_array, QIODeviceBase::ReadWrite)) + , m_resultStream(result) + {} + + ~GeneratorStream() + { + if (m_stream) { + m_stream->flush(); + delete m_stream; + } + + if (m_resultStream && m_array && !m_array->isEmpty()) + *m_resultStream << *m_array << Qt::endl; + + delete m_array; + } + + GeneratorStream(GeneratorStream &&other) noexcept + : m_array(std::exchange(other.m_array, nullptr)) + , m_stream(std::exchange(other.m_stream, nullptr)) + , m_resultStream(std::exchange(other.m_resultStream, nullptr)) + {} + GeneratorStream &operator=(GeneratorStream &&other) noexcept + { + std::swap(m_resultStream, other.m_resultStream); + std::swap(m_stream, other.m_stream); + std::swap(m_array, other.m_array); + + return *this; + } + + Q_DISABLE_COPY(GeneratorStream) +private: + template<typename T> + friend const GeneratorStream &operator<<(const GeneratorStream& str, T val); + QByteArray *m_array = nullptr; + QTextStream *m_stream = nullptr; + QTextStream *m_resultStream = nullptr; +}; + +template<typename T> +const GeneratorStream &operator<<(const GeneratorStream& str, T val) +{ + *str.m_stream << val; + return str; +} + +QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName) + : QQuickGenerator(fileName, flags) + , outputFileName(outFileName) +{ + m_stream = new QTextStream(&result); +} + +QQuickQmlGenerator::~QQuickQmlGenerator() +{ + if (!outputFileName.isEmpty()) { + QFile outFile(outputFileName); + outFile.open(QIODevice::WriteOnly); + outFile.write(result); + outFile.close(); + } + + if (lcQuickVectorImage().isDebugEnabled()) { + result.truncate(300); + qCDebug(lcQuickVectorImage).noquote() << result; + } +} + +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 { + const QMatrix4x4 m(info.transform); + { + 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 (!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::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 (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; +} + +QString QQuickQmlGenerator::indent() +{ + return QString().fill(QLatin1Char(' '), m_indentLevel * 4); +} + +GeneratorStream QQuickQmlGenerator::stream() +{ + GeneratorStream strm(m_stream); + strm << indent(); + return strm; +} + +const char *QQuickQmlGenerator::shapeName() const +{ + return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData(); +} + +QT_END_NAMESPACE |