From fc41cf9f5def61efc3fa0ae00dea837a2fff6a1a Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 21 Sep 2026 21:32:03 +1200 Subject: [PATCH] Let a script place an image images() addImage() setImageScale() setImageRotation() deleteImage() addImage reads a file the way the "add image" tool does after its file dialog and pushes the same AddGraphicsObjectCommand. The pixels are copied into the project, which DiagramImageItem::toXml writes inline, so the saved file does not refer to the original path: verified by moving the source file away and reloading, where the image and its scale came back. The price is that the project grows by about the size of the image, so files over 10 MB are refused; so are unreadable files, non-images and missing paths, each with its own reason. Scaling sets both axes as one undo step. Images are addressed by index in a position-sorted listing like texts and shapes, by the on-screen bounding box -- so because an image turns and scales about its centre, scaling or rotating one can change where it sorts, and the header says to re-list after either. Observed: a 2x scale moved a listed top-left from (100,100) to (68,84), which is that, not a displacement. Co-Authored-By: Claude Sonnet 5 --- sources/scripting/qetscriptapi.cpp | 142 +++++++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 17 ++++ 2 files changed, 159 insertions(+) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 80be1acb4..a82ceb7e9 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -31,6 +31,7 @@ #include "../qetproject.h" #include "../qetresult.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/diagramimageitem.h" #include "../qetgraphicsitem/independenttextitem.h" #include "../qetgraphicsitem/qetshapeitem.h" #include "../TerminalStrip/UndoCommand/addterminalstripcommand.h" @@ -54,6 +55,9 @@ #include #include #include +#include +#include +#include #include #include @@ -1735,6 +1739,144 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name) return true; } +QList QetScriptApi::sortedImages(int folioIndex) const +{ + if (!m_project) return {}; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; + DiagramContent content(diagrams.at(folioIndex), false); + return sortedByPosition(content.m_images); +} + +QStringList QetScriptApi::images(int folioIndex) const +{ + QStringList out; + const QList list = sortedImages(folioIndex); + for (int i = 0 ; i < list.count() ; ++i) + { + DiagramImageItem *item = list.at(i); + const QPixmap px = item->pixmap(); + const QPointF at = item->sceneBoundingRect().topLeft(); + out << QStringLiteral("%1: %2x%3 px at (%4, %5) scale=%6 rotation=%7") + .arg(i).arg(px.width()).arg(px.height()).arg(at.x()).arg(at.y()) + .arg(item->scaleFactorX()).arg(item->rotationAngle()); + } + return out; +} + +/** + @brief QetScriptApi::addImage + Place a picture from a file, as the "add image" tool does after its file + dialog. The pixels are copied into the project (DiagramImageItem::toXml + writes them inline), so the file need not exist afterwards. + @return the image's index in images(), or -1 +*/ +int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, double y) +{ + if (!m_project) return -1; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.addImage: project is read-only")); + return -1; + } + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; + + const QFileInfo info(filePath); + if (!info.isFile()) { + log(QStringLiteral("qet.addImage: '%1' is not a file").arg(filePath)); + return -1; + } + constexpr qint64 max_bytes = 10LL * 1024 * 1024; + if (info.size() > max_bytes) { + log(QStringLiteral("qet.addImage: '%1' is %2 bytes; images are embedded in the project, " + "so files over 10 MB are refused").arg(filePath).arg(info.size())); + return -1; + } + const QImage image(filePath); + if (image.isNull()) { + log(QStringLiteral("qet.addImage: '%1' could not be read as an image").arg(filePath)); + return -1; + } + + Diagram *diagram = diagrams.at(folioIndex); + auto *item = new DiagramImageItem(QPixmap::fromImage(image)); + diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); + return sortedImages(folioIndex).indexOf(item); +} + +bool QetScriptApi::setImageScale(int folioIndex, int imageIndex, double factor) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setImageScale: project is read-only")); + return false; + } + if (factor <= 0) { + log(QStringLiteral("qet.setImageScale: the factor must be positive")); + return false; + } + const QList list = sortedImages(folioIndex); + if (imageIndex < 0 || imageIndex >= list.count()) { + log(QStringLiteral("qet.setImageScale: folio %1 has %2 image(s), no index %3") + .arg(folioIndex).arg(list.count()).arg(imageIndex)); + return false; + } + DiagramImageItem *item = list.at(imageIndex); + if (item->scaleFactorX() == factor && item->scaleFactorY() == factor) return true; + + // Both axes, one undo step: a script that scales an image means the + // image, not one axis of it. + m_project->undoStack()->beginMacro(QObject::tr("Redimensionner une image")); + m_project->undoStack()->push(new QPropertyUndoCommand(item, "scaleFactorX", + QVariant(item->scaleFactorX()), QVariant(factor))); + m_project->undoStack()->push(new QPropertyUndoCommand(item, "scaleFactorY", + QVariant(item->scaleFactorY()), QVariant(factor))); + m_project->undoStack()->endMacro(); + return true; +} + +bool QetScriptApi::setImageRotation(int folioIndex, int imageIndex, double angle) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setImageRotation: project is read-only")); + return false; + } + const QList list = sortedImages(folioIndex); + if (imageIndex < 0 || imageIndex >= list.count()) { + log(QStringLiteral("qet.setImageRotation: folio %1 has %2 image(s), no index %3") + .arg(folioIndex).arg(list.count()).arg(imageIndex)); + return false; + } + DiagramImageItem *item = list.at(imageIndex); + if (item->rotationAngle() == angle) return true; + auto *cmd = new QPropertyUndoCommand(item, "rotationAngle", + QVariant(item->rotationAngle()), QVariant(angle)); + cmd->setText(QObject::tr("Pivoter une image")); + m_project->undoStack()->push(cmd); + return true; +} + +bool QetScriptApi::deleteImage(int folioIndex, int imageIndex) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.deleteImage: project is read-only")); + return false; + } + const QList list = sortedImages(folioIndex); + if (imageIndex < 0 || imageIndex >= list.count()) { + log(QStringLiteral("qet.deleteImage: folio %1 has %2 image(s), no index %3") + .arg(folioIndex).arg(list.count()).arg(imageIndex)); + return false; + } + Diagram *diagram = m_project->diagrams().at(folioIndex); + DiagramContent content; + content.m_images << list.at(imageIndex); + diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); + return true; +} + int QetScriptApi::addFolio() { if (!m_project) return -1; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 8e832e92e..8ae3f2852 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -30,6 +30,7 @@ class Terminal; class Conductor; class IndependentTextItem; class QetShapeItem; +class DiagramImageItem; /** @brief The QetScriptApi class @@ -173,6 +174,14 @@ class QetShapeItem; because the application itself does it through direct project calls and only the counter advance is on the undo stack; the numbering actually applied to a conductor is. + - @b Images: place a picture from a file. The pixels are copied into + the project, which stores them inline in the .qet -- the saved file + does not refer to the original path, so it opens on another machine, + and it grows by roughly the size of the image, which is why files + over 10 MB are refused. Images are addressed by index in a + position-sorted listing, like texts and shapes -- by the on-screen + bounding box, so scaling or rotating an image, which turns about its + centre, can change where it sorts. Re-list after either. - @b Navigating and @b messaging: select an element, zoom the active view, and show the user a message. Deliberately narrow: selection and messaging work with no view at all (headless `--run`); zoom is a no-op @@ -309,6 +318,13 @@ class QetScriptApi : public QObject Q_INVOKABLE bool removeAutoNum(const QString &kind, const QString &name); Q_INVOKABLE bool useConductorAutoNum(int folioIndex, const QString &name); + // -- images, embedded in the project -- + Q_INVOKABLE QStringList images(int folioIndex) const; + Q_INVOKABLE int addImage(int folioIndex, const QString &filePath, double x, double y); + Q_INVOKABLE bool setImageScale(int folioIndex, int imageIndex, double factor); + Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle); + Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex); + // -- folios -- Q_INVOKABLE int addFolio(); Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title); @@ -338,6 +354,7 @@ class QetScriptApi : public QObject const QString &caller); QList sortedTexts(int folioIndex) const; QList sortedShapes(int folioIndex) const; + QList sortedImages(int folioIndex) const; IndependentTextItem *findText(int folioIndex, int textIndex, const QString &caller); bool setInfoKey(int folioIndex, const QString &elementUuid, const QString &key, const QString &value, const QString &caller);