/* 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 . */ #include #include #include #include #include #include #include #include #include /** Before #1310, a project saved after a crop was undone kept the undone , with the uncropped picture shown. Loading such a file has to drop that crop: the picture shown is always exactly the crop's size, so a picture of another size means the crop is stale. Otherwise the next edit of the picture applies the crop again. Runs the real binary: a script adds and crops a picture and saves; the test then puts another picture where the shown one is, and lets the binary load and save that file again. */ class tst_imagestalecrop : public QObject { Q_OBJECT QTemporaryDir m_dir; // Runs the binary on @p project with a script that saves it to @p out. bool 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 false; 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}); return proc.waitForFinished(120000) && proc.exitStatus() == QProcess::NormalExit; } // Opens @p in and saves it to @p out. bool resave(const QString &in, const QString &out) { return run(QStringLiteral("qet.save('%1');\n").arg(out), in) && QFile::exists(out); } static QByteArray read(const QString &path) { QFile f(path); return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray(); } static QByteArray png(const QSize &size) { QImage img(size, QImage::Format_RGB32); img.fill(Qt::darkGreen); QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); img.save(&buffer, "PNG"); return bytes.toBase64(); } // The size of the saved original (), or an empty size. static QSize baseSize(const QByteArray &xml) { const QRegularExpression base(QStringLiteral("\\s*([A-Za-z0-9+/=]+)")); const QRegularExpressionMatch m = base.match(QString::fromLatin1(xml)); if (!m.hasMatch()) return {}; return QImage::fromData(QByteArray::fromBase64(m.captured(1).toLatin1())).size(); } // @p xml with the shown picture (the element's first text) // replaced by a PNG of @p size. static QByteArray withShown(QByteArray xml, const QSize &size) { const QRegularExpression shown(QStringLiteral("(]*>\\s*)([A-Za-z0-9+/=]+)")); const QRegularExpressionMatch m = shown.match(QString::fromLatin1(xml)); if (!m.hasMatch()) return {}; return xml.replace(m.capturedStart(2), m.capturedLength(2), png(size)); } QString m_cropped; // a picture of 40 x 30, cropped to 10,5,20,10 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); const QString pic = m_dir.filePath(QStringLiteral("pic.png")); QVERIFY(img.save(pic)); m_cropped = m_dir.filePath(QStringLiteral("cropped.qet")); QVERIFY(run(QStringLiteral(R"JS( var i = qet.addImage(0, '%1', 100, 100); qet.cropImage(0, i, 10, 5, 20, 10); qet.save('%2'); )JS").arg(pic, m_cropped), QStringLiteral(QET_EXAMPLES_DIR "/741.qet"))); const QByteArray xml = read(m_cropped); QCOMPARE(xml.count("")); } // A valid crop is kept, byte for byte. void validCropIsKept() { const QString out = m_dir.filePath(QStringLiteral("valid-out.qet")); QVERIFY(resave(m_cropped, out)); const QByteArray xml = read(out); // Attributes in any order: Qt before 6.5 writes them unordered. const QRegularExpression crop(QStringLiteral("]*\\bx=\"10\")(?=[^>]*\\by=\"5\")" "(?=[^>]*\\bw=\"20\")(?=[^>]*\\bh=\"10\")")); QVERIFY2(crop.match(QString::fromLatin1(xml)).hasMatch(), "the valid crop was lost"); QVERIFY(xml.contains("")); } // The shown picture is the whole original (40 x 30): the crop was // undone before the file was saved. It is dropped, and with nothing // else to remember the original is not saved twice any more. void staleCropOfWholeOriginalIsDropped() { const QString stale = m_dir.filePath(QStringLiteral("stale.qet")); const QByteArray xml = withShown(read(m_cropped), QSize(40, 30)); QVERIFY(!xml.isEmpty()); QFile f(stale); QVERIFY(f.open(QIODevice::WriteOnly)); f.write(xml); f.close(); const QString out = m_dir.filePath(QStringLiteral("stale-out.qet")); QVERIFY(resave(stale, out)); const QByteArray saved = read(out); QCOMPARE(saved.count("")); } // The shown picture matches neither the crop nor the original: it is // what the user saw, so it becomes the original -- a crop made after // loading cuts it, not the old original. void shownPictureOfOtherSizeBecomesTheOriginal() { const QString odd = m_dir.filePath(QStringLiteral("odd.qet")); const QByteArray xml = withShown(read(m_cropped), QSize(25, 25)); QVERIFY(!xml.isEmpty()); QFile f(odd); QVERIFY(f.open(QIODevice::WriteOnly)); f.write(xml); f.close(); const QString out = m_dir.filePath(QStringLiteral("odd-out.qet")); QVERIFY(resave(odd, out)); const QByteArray saved = read(out); QVERIFY2(!saved.contains("")); const QString recropped = m_dir.filePath(QStringLiteral("odd-recropped.qet")); QVERIFY(run(QStringLiteral("qet.cropImage(0, 0, 0, 0, 10, 10);\nqet.save('%1');\n") .arg(recropped), odd)); QCOMPARE(baseSize(read(recropped)), QSize(25, 25)); } }; QTEST_MAIN(tst_imagestalecrop) #include "tst_imagestalecrop.moc"