From 527b0f3b2d5abcabbdbff3a1217f1d7f2ccbeba5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 5 Oct 2026 21:43:28 +1300 Subject: [PATCH] Fix repeatable PDF changing with the output folder (#1178) Rewriting the links copied the old trailer up to %%EOF, so the old startxref stayed in the file before the new one. Its offset is the size the file had before the rewrite, and the links carry the output path, so the same project exported to two folders gave different bytes, and a different document id. Both rewrites now copy the trailer dictionary only, through one helper; the print window's rewrite no longer leaves a stray %%EOF either. Co-Authored-By: Claude Opus 5.5 --- sources/pdf_links.cpp | 53 ++++++++++------------------ tests/qttest/tst_pdfreproducible.cpp | 26 ++++++++++++-- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/sources/pdf_links.cpp b/sources/pdf_links.cpp index 9b591305f..af892836a 100644 --- a/sources/pdf_links.cpp +++ b/sources/pdf_links.cpp @@ -194,6 +194,22 @@ void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram, engine->drawHyperlink(link.rect, link.url); } +/// The trailer dictionary of the xref table at \a xrefStart, from +/// "trailer" up to its startxref, which the caller writes again with the +/// new offset. Copying the old startxref too left a second one in the +/// file, with the size the file had before its links were rewritten, so +/// the bytes depended on the output path the links carry (#1178). +static QByteArray trailerDictionary(const QByteArray &data, int xrefStart) +{ + const int start = data.indexOf("trailer", xrefStart); + if (start == -1) + return "trailer\n<<>>"; + const int end = data.indexOf("startxref", start); + if (end == -1) + return "trailer\n<<>>"; + return data.mid(start, end - start).trimmed(); +} + void convertUriToGoTo(const QString &pdfPath) { // --- 1. Read raw bytes --- @@ -383,19 +399,7 @@ void convertUriToGoTo(const QString &pdfPath) } } - // Find trailer dict from the original xref section - int trailerPos = data.indexOf("trailer", xrefStart); - int trailerEnd = -1; - if (trailerPos != -1) { - trailerEnd = data.indexOf("%%EOF", trailerPos); - if (trailerEnd != -1) trailerEnd += 5; - } - - QByteArray trailer; - if (trailerPos != -1 && trailerEnd != -1) - trailer = data.mid(trailerPos, trailerEnd - trailerPos); - else - trailer = "trailer\n<<>>\n%%EOF"; + const QByteArray trailer = trailerDictionary(data, xrefStart); int newXrefOffset = body.size(); @@ -814,18 +818,7 @@ void convertComponentInfoAnnotations(const QString &pdfPath, } // Copy trailer and bump /Size to account for the new XObject - QByteArray trailer; - { - int tPos = data.indexOf("trailer", xrefStart); - if (tPos != -1) { - int tEnd = data.indexOf("%%EOF", tPos); - if (tEnd != -1) tEnd += 5; - if (tEnd != -1) - trailer = data.mid(tPos, tEnd - tPos); - } - } - if (trailer.isEmpty()) - trailer = "trailer\n<<>>\n%%EOF"; + QByteArray trailer = trailerDictionary(data, xrefStart); // Bump /Size: original was maxObjNum+1, now it's emptyXObjNum+1 { @@ -842,16 +835,6 @@ void convertComponentInfoAnnotations(const QString &pdfPath, } } - // Remove duplicate startxref if present in copied trailer - { - int stPos = trailer.indexOf("\nstartxref\n"); - if (stPos != -1) - trailer = trailer.left(stPos); - // Ensure trailer ends with %%EOF - if (!trailer.endsWith("%%EOF\n")) - trailer += "\n%%EOF\n"; - } - QByteArray result; result.reserve(out.size() + xref.size() + trailer.size() + 64); result += out; diff --git a/tests/qttest/tst_pdfreproducible.cpp b/tests/qttest/tst_pdfreproducible.cpp index a1f37450d..05bd946e1 100644 --- a/tests/qttest/tst_pdfreproducible.cpp +++ b/tests/qttest/tst_pdfreproducible.cpp @@ -22,12 +22,17 @@ class tst_pdfreproducible : public QObject QTemporaryDir m_dir; int m_run = 0; - QByteArray exportPdf(const QString &project, const QByteArray &epoch) + QByteArray exportPdf(const QString &project, const QByteArray &epoch, + const QString &folder = QString()) { const int run = m_run++; const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run)); const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run)); - const QString out = m_dir.filePath(QStringLiteral("out%1.pdf").arg(run)); + const QString out = folder.isEmpty() + ? m_dir.filePath(QStringLiteral("out%1.pdf").arg(run)) + : m_dir.filePath(folder + QStringLiteral("/same.pdf")); + if (!folder.isEmpty()) + QDir().mkpath(m_dir.filePath(folder)); QDir().mkpath(home); QDir().mkpath(tmp); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); @@ -145,6 +150,23 @@ private slots: "the same drawing with new uuids gives a different PDF"); } + void sameBytesInAnyFolder() + { + // #1178: the links carry the output path, and rewriting them left + // the old startxref in the file, with the size the file had before. + const QString project = + QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/iso_sfc_example.qet"); + QVERIFY2(QFile::exists(project), "examples/iso_sfc_example.qet not found"); + + const QByteArray first = exportPdf(project, "1700000000", QStringLiteral("p")); + QVERIFY(!first.isEmpty()); + QVERIFY(first.contains("/GoTo")); + QCOMPARE(first.count("startxref"), 1); + QVERIFY2(xrefMatches(first), "the xref table does not match the file"); + QVERIFY2(exportPdf(project, "1700000000", QStringLiteral("longer-folder-name")) == first, + "the same project gives a different PDF in another folder"); + } + void nowWithoutTheVariable() { const QString project =