mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 19:54:13 +02:00
Add qet.cropImage() and qet.imageCrop() to the script API
Crop a picture as the crop tool does, in one undo step, and read its crop rectangle back. tst_imagecropundo uses them on the real binary: a crop that was undone is not saved, and redoing it saves it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3469,6 +3469,42 @@ bool QetScriptApi::setImageRotation(int folioIndex, int imageIndex, double angle
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::cropImage
|
||||
Show only the rectangle (x, y, width, height) of the image's original,
|
||||
in the original's own pixels, as the crop tool does: one undo step,
|
||||
the kept region staying where it is on the folio.
|
||||
*/
|
||||
bool QetScriptApi::cropImage(int folioIndex, int imageIndex, int x, int y, int width, int height)
|
||||
{
|
||||
if (!m_project) return false;
|
||||
if (m_project->isReadOnly()) {
|
||||
log(QStringLiteral("qet.cropImage: project is read-only"));
|
||||
return false;
|
||||
}
|
||||
const QList<DiagramImageItem *> list = sortedImages(folioIndex);
|
||||
if (imageIndex < 0 || imageIndex >= list.count()) {
|
||||
log(QStringLiteral("qet.cropImage: folio %1 has %2 image(s), no index %3")
|
||||
.arg(folioIndex).arg(list.count()).arg(imageIndex));
|
||||
return false;
|
||||
}
|
||||
list.at(imageIndex)->applyCrop(QRect(x, y, width, height));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetScriptApi::imageCrop
|
||||
@return the image's crop rectangle in its original's pixels, as
|
||||
"x,y,width,height", or an empty string for no such image.
|
||||
*/
|
||||
QString QetScriptApi::imageCrop(int folioIndex, int imageIndex) const
|
||||
{
|
||||
const QList<DiagramImageItem *> list = sortedImages(folioIndex);
|
||||
if (imageIndex < 0 || imageIndex >= list.count()) return QString();
|
||||
const QRect r = list.at(imageIndex)->cropRect();
|
||||
return QStringLiteral("%1,%2,%3,%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height());
|
||||
}
|
||||
|
||||
bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
|
||||
{
|
||||
if (!m_project) return false;
|
||||
|
||||
@@ -539,6 +539,8 @@ class QetScriptApi : public QObject
|
||||
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 cropImage(int folioIndex, int imageIndex, int x, int y, int width, int height);
|
||||
Q_INVOKABLE QString imageCrop(int folioIndex, int imageIndex) const;
|
||||
Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex);
|
||||
Q_INVOKABLE int addPdfPage(int folioIndex, const QString &pdfPath, int pageNumber,
|
||||
int dpi, double x, double y);
|
||||
|
||||
@@ -593,6 +593,18 @@ if(QET_HAS_SCRIPTING)
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Undoing a crop of a picture restores its crop rectangle too, so a
|
||||
# project saved after the undo does not record the undone crop.
|
||||
add_executable(
|
||||
tst_imagecropundo
|
||||
tst_imagecropundo.cpp)
|
||||
add_test(NAME tst_imagecropundo COMMAND tst_imagecropundo)
|
||||
add_dependencies(tst_imagecropundo qelectrotech)
|
||||
target_link_libraries(tst_imagecropundo PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_imagecropundo PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# The same symbol placed twice in a project whose collection does not
|
||||
# start with its "import" category (examples/lmdg.qet).
|
||||
add_executable(
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
Undoing a crop restores the crop rectangle too, not only the pixels
|
||||
shown: before, a project saved after Ctrl+Z still recorded the undone
|
||||
crop, and the picture came back cropped once reopened. Runs the real
|
||||
binary on a script: add a picture, crop it, undo, save; redo, save.
|
||||
*/
|
||||
class tst_imagecropundo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QByteArray read(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
QImage img(40, 30, QImage::Format_RGB32);
|
||||
img.fill(Qt::darkGreen);
|
||||
QVERIFY(img.save(m_dir.filePath(QStringLiteral("pic.png"))));
|
||||
}
|
||||
|
||||
void undoneCropIsNotSaved()
|
||||
{
|
||||
const QString undone = m_dir.filePath(QStringLiteral("undone.qet"));
|
||||
const QString redone = m_dir.filePath(QStringLiteral("redone.qet"));
|
||||
const QString script = QStringLiteral(R"JS(
|
||||
var i = qet.addImage(0, '%1', 100, 100);
|
||||
var r = {full: qet.imageCrop(0, i)};
|
||||
r.cropped_ok = qet.cropImage(0, i, 10, 5, 20, 10);
|
||||
r.cropped = qet.imageCrop(0, i);
|
||||
qet.undo();
|
||||
r.undone = qet.imageCrop(0, i);
|
||||
qet.save('%2');
|
||||
qet.redo();
|
||||
r.redone = qet.imageCrop(0, i);
|
||||
qet.save('%3');
|
||||
qet.log('PROBE ' + JSON.stringify(r));
|
||||
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), undone, redone);
|
||||
|
||||
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QCOMPARE(r.value("full").toString(), QStringLiteral("0,0,40,30"));
|
||||
QVERIFY(r.value("cropped_ok").toBool());
|
||||
QCOMPARE(r.value("cropped").toString(), QStringLiteral("10,5,20,10"));
|
||||
QCOMPARE(r.value("undone").toString(), QStringLiteral("0,0,40,30"));
|
||||
QCOMPARE(r.value("redone").toString(), QStringLiteral("10,5,20,10"));
|
||||
|
||||
const QByteArray undone_xml = read(undone);
|
||||
QVERIFY(undone_xml.contains("<image "));
|
||||
QVERIFY2(!undone_xml.contains("<crop "), "an undone crop was saved");
|
||||
const QRegularExpressionMatch crop =
|
||||
QRegularExpression(QStringLiteral("<crop ([^>]*)/>")).match(QString::fromUtf8(read(redone)));
|
||||
QVERIFY2(crop.hasMatch(), "the redone crop was not saved");
|
||||
for (const char *attribute : {R"(x="10")", R"(y="5")", R"(w="20")", R"(h="10")"})
|
||||
QVERIFY2(crop.captured(1).contains(QLatin1String(attribute)), attribute);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_imagecropundo)
|
||||
#include "tst_imagecropundo.moc"
|
||||
Reference in New Issue
Block a user