diff --git a/sources/container/qetcontainer.cpp b/sources/container/qetcontainer.cpp index 95f306c0e..19a7615b6 100644 --- a/sources/container/qetcontainer.cpp +++ b/sources/container/qetcontainer.cpp @@ -208,6 +208,13 @@ QString QetContainer::pictureFile(const QDomElement &image) return image.attribute(ImageFileAttribute); } +void QetContainer::inlinePicture(QDomElement image, const QByteArray &bytes) +{ + image.removeAttribute(ImageFileAttribute); + image.insertBefore(image.ownerDocument().createTextNode(QString::fromLatin1(bytes.toBase64())), + image.firstChild()); +} + /** @brief QetContainer::ownUuids @return true if every folio, symbol and wire of @p project has a uuid diff --git a/sources/container/qetcontainer.h b/sources/container/qetcontainer.h index 7447fafc3..bdfd37c55 100644 --- a/sources/container/qetcontainer.h +++ b/sources/container/qetcontainer.h @@ -84,6 +84,9 @@ namespace QetContainer QString *error = nullptr); /// The file join() left the picture of @p image in, or an empty string QString pictureFile(const QDomElement &image); + /// @p image, which names its picture's file (pictureFile()), given + /// the picture itself, @p bytes, as a document read whole has it + void inlinePicture(QDomElement image, const QByteArray &bytes); /// Take what QElectroTech computes out of a folio (format 4) void dropComputed(QDomElement diagram, bool terminal_lists); diff --git a/sources/diagram.cpp b/sources/diagram.cpp index b3eae505e..5dc44f471 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -881,6 +881,17 @@ QDomElement Diagram::deferredXmlWithHeader(QDomDocument &document) for (QDomElement child = items.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement()) if (!header_children.contains(child.tagName())) folio.appendChild(document.importNode(child, true)); + //Its pictures written in, as building it writes them. A folio read + //from a .qetz names its pictures' files instead, which the next + //.qetz written from this document would leave out: that project + //then refused to open, "picture ... is missing" (F118). + for (QDomElement image : QET::findInDomElement(folio, QStringLiteral("images"), QStringLiteral("image"))) { + const QString file = QetContainer::pictureFile(image); + if (file.isEmpty() || !m_project) continue; + const QByteArray bytes = m_project->containerPicture(file); + if (!bytes.isEmpty()) + QetContainer::inlinePicture(image, bytes); + } return folio; } diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index f271bce96..9b589d403 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -943,6 +943,18 @@ if(QET_HAS_SCRIPTING) "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # A .qetz folio whose title only changed keeps its pictures when saved + # with folios built when needed (F118), on examples/grafcet.qet. + add_executable( + tst_qetzheaderpictures + tst_qetzheaderpictures.cpp) + add_test(NAME tst_qetzheaderpictures COMMAND tst_qetzheaderpictures) + add_dependencies(tst_qetzheaderpictures qelectrotech) + target_link_libraries(tst_qetzheaderpictures PRIVATE Qt::Test) + target_compile_definitions(tst_qetzheaderpictures PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + # qet.elementTerminals() lists each terminal's uuid and qet.terminalIndex() # turns element uuid + terminal uuid back into the index the conductor # calls take, on examples/perceuse.qet (two terminals at one point). diff --git a/tests/qttest/tst_qetzheaderpictures.cpp b/tests/qttest/tst_qetzheaderpictures.cpp new file mode 100644 index 000000000..49bd4013f --- /dev/null +++ b/tests/qttest/tst_qetzheaderpictures.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include + +// A folio of a .qetz whose title only changed is saved from its items as +// read, not built (Diagram::deferredXmlWithHeader()). Those items name +// their pictures' files in the .qetz, which the new .qetz left out: the +// project saved then refused to open, 'picture "images/..." is missing' +// (FINDINGS F118). Runs scripts through the real binary's --run on a copy +// of examples/grafcet.qet given a picture on its second folio. +class tst_qetzheaderpictures : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Run @p script on @p project; its output + QString run(const QString &script, const QString &project, bool every_folio_built) + { + const QString path = m_dir.filePath(QStringLiteral("script.js")); + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) return QString(); + f.write(script.toUtf8()); + f.close(); + const QString home = m_dir.filePath(QStringLiteral("home")); + QDir().mkpath(home); + 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()); + if (every_folio_built) + env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); + else + env.remove(QStringLiteral("QET_EAGER_FOLIOS")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), path, project}); + proc.waitForFinished(120000); + return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void titleChangeKeepsPictures() + { + // A 1x1 PNG + const QString png = m_dir.filePath(QStringLiteral("dot.png")); + QFile p(png); + QVERIFY(p.open(QIODevice::WriteOnly)); + p.write(QByteArray::fromBase64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==")); + p.close(); + const QString with_picture = m_dir.filePath(QStringLiteral("picture.qetz")); + const QString titled = m_dir.filePath(QStringLiteral("titled.qetz")); + QString out = run(QStringLiteral("qet.log('ADDED ' + qet.addImage(1, '%1', 100, 100));" + "qet.save('%2');").arg(png, with_picture), + QStringLiteral(QET_EXAMPLES_DIR "/grafcet.qet"), true); + QVERIFY2(out.contains(QStringLiteral("ADDED 0")), qPrintable(out)); + QVERIFY(QFile::exists(with_picture)); + + // Only the title of the folio with the picture, folios built + // when needed + out = run(QStringLiteral("qet.log('SET ' + qet.setFolioProperty(1, 'title', 'Edited'));" + "qet.save('%1');").arg(titled), with_picture, false); + QVERIFY2(out.contains(QStringLiteral("SET true")), qPrintable(out)); + QVERIFY(QFile::exists(titled)); + + // Opens, with its picture and the new title + out = run(QStringLiteral("qet.log('PICTURES ' + qet.images(1).length);" + "qet.log('TITLE ' + qet.folioTitle(1));"), titled, true); + QVERIFY2(out.contains(QStringLiteral("PICTURES 1")), qPrintable(out)); + QVERIFY2(out.contains(QStringLiteral("TITLE Edited")), qPrintable(out)); + } +}; + +QTEST_APPLESS_MAIN(tst_qetzheaderpictures) + +#include "tst_qetzheaderpictures.moc"