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);