mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-13 08:14:13 +02:00
Build and work on Qt before 6.8
QPdfWriter::setDocumentId() is new in Qt 6.8, QTimeZone::UTC in 6.5 and QHashSeed in 6.2, so the build failed on Qt 6.4 (Ubuntu 24.04). Qt before 6.8 also writes no /ModDate and no XMP metadata, and setDocumentDate() then gave up and left the time of the export in the file. Those two are now rewritten when present, and no document id is set where Qt writes none. tst_pdfreproducible also checks that the xref table still matches the file after the dates are rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJw3u5AdKQgQVPug8CzuMQ
This commit is contained in:
@@ -165,7 +165,7 @@ QDateTime sourceDateEpoch()
|
||||
err << "SOURCE_DATE_EPOCH '" << value << "' is not a number of seconds; ignored.\n";
|
||||
return {};
|
||||
}
|
||||
return QDateTime::fromSecsSinceEpoch(seconds, QTimeZone::UTC);
|
||||
return QDateTime::fromSecsSinceEpoch(seconds, QTimeZone::utc());
|
||||
}
|
||||
|
||||
int exportPdf(QETProject &project, const QString &output,
|
||||
@@ -190,8 +190,10 @@ int exportPdf(QETProject &project, const QString &output,
|
||||
// SOURCE_DATE_EPOCH (reproducible-builds.org) asks for the same file
|
||||
// from the same input: the time it names instead of now, and a document
|
||||
// id from the project file instead of a random one. Qt has no setter for
|
||||
// the dates, so they are rewritten once the file is written.
|
||||
// the dates, so they are rewritten once the file is written. Before
|
||||
// Qt 6.8 there is no document id to set: it is only written for PDF/A.
|
||||
const QDateTime sourceDate = sourceDateEpoch();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
if (sourceDate.isValid()) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
QFile file(project.filePath());
|
||||
@@ -199,6 +201,7 @@ int exportPdf(QETProject &project, const QString &output,
|
||||
hash.addData(&file);
|
||||
writer.setDocumentId(QUuid::createUuidV5(project.uuid(), hash.result()));
|
||||
}
|
||||
#endif
|
||||
|
||||
QPainter painter;
|
||||
bool first = true;
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
#include <QApplication>
|
||||
#include <QDomImplementation>
|
||||
#include <QFont>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
||||
#include <QHashSeed>
|
||||
#endif
|
||||
|
||||
#include <QSettings>
|
||||
#include <QStyleFactory>
|
||||
@@ -102,7 +104,11 @@ int main(int argc, char **argv)
|
||||
// in QHash order). Setting QT_HASH_SEED alone came too late: Qt reads it
|
||||
// once, when the first hash is made, and that happens before main().
|
||||
// The variable is still set for the processes QElectroTech starts.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
|
||||
QHashSeed::setDeterministicGlobalSeed();
|
||||
#else
|
||||
qSetGlobalQHashSeed(0);
|
||||
#endif
|
||||
qputenv("QT_HASH_SEED", "0");
|
||||
//Some setup, notably to use with QSetting.
|
||||
QCoreApplication::setOrganizationName("QElectroTech");
|
||||
|
||||
+39
-33
@@ -430,51 +430,57 @@ void setDocumentDate(const QString &pdfPath, const QDateTime &when)
|
||||
struct Edit { int start; int end; QByteArray text; };
|
||||
QList<Edit> edits;
|
||||
|
||||
// The document information: "/CreationDate (D:...)" and "/ModDate (D:...)".
|
||||
// Its other strings are UTF-16, so they cannot hold these keys.
|
||||
// The document information: "/CreationDate (D:...)" and, from Qt 6.8,
|
||||
// "/ModDate (D:...)". Its other strings are UTF-16, so they cannot hold
|
||||
// these keys.
|
||||
for (const QByteArray &key : {QByteArray("/CreationDate "), QByteArray("/ModDate ")}) {
|
||||
const int k = data.indexOf(key);
|
||||
if (k == -1) return;
|
||||
if (k == -1) {
|
||||
if (key.startsWith("/ModDate")) continue;
|
||||
return;
|
||||
}
|
||||
const int start = k + key.size();
|
||||
const int end = data.indexOf(')', start);
|
||||
if (end == -1 || data.at(start) != '(') return;
|
||||
edits.append({start, end + 1, pdfDate});
|
||||
}
|
||||
|
||||
// The XMP metadata stream, which Qt writes uncompressed: its three
|
||||
// dates, and its /Length, which changes with them.
|
||||
// The XMP metadata stream, which Qt writes uncompressed from Qt 6.8
|
||||
// (before, only for PDF/A): its three dates, and its /Length, which
|
||||
// changes with them.
|
||||
const QByteArray metadata("/Type /Metadata /Subtype /XML");
|
||||
const int m = data.indexOf(metadata);
|
||||
if (m == -1) return;
|
||||
const QByteArray lengthKey("/Length ");
|
||||
const int l = data.indexOf(lengthKey, m);
|
||||
const int streamStart = data.indexOf("stream\n", m);
|
||||
if (l == -1 || streamStart == -1 || l > streamStart) return;
|
||||
const int lengthStart = l + lengthKey.size();
|
||||
int lengthEnd = lengthStart;
|
||||
while (lengthEnd < data.size() && QChar(data.at(lengthEnd)).isDigit())
|
||||
++lengthEnd;
|
||||
const int streamEnd = data.indexOf("endstream", streamStart);
|
||||
if (lengthEnd == lengthStart || streamEnd == -1) return;
|
||||
if (m != -1) {
|
||||
const QByteArray lengthKey("/Length ");
|
||||
const int l = data.indexOf(lengthKey, m);
|
||||
const int streamStart = data.indexOf("stream\n", m);
|
||||
if (l == -1 || streamStart == -1 || l > streamStart) return;
|
||||
const int lengthStart = l + lengthKey.size();
|
||||
int lengthEnd = lengthStart;
|
||||
while (lengthEnd < data.size() && QChar(data.at(lengthEnd)).isDigit())
|
||||
++lengthEnd;
|
||||
const int streamEnd = data.indexOf("endstream", streamStart);
|
||||
if (lengthEnd == lengthStart || streamEnd == -1) return;
|
||||
|
||||
// The last of each in the stream: the title and author come before
|
||||
// the dates, and are text a project could fill with anything.
|
||||
int xmpDelta = 0;
|
||||
QList<Edit> xmpEdits;
|
||||
for (const QByteArray &attr : {QByteArray("xmp:CreateDate=\""),
|
||||
QByteArray("xmp:ModifyDate=\""),
|
||||
QByteArray("xmp:MetadataDate=\"")}) {
|
||||
const int a = data.lastIndexOf(attr, streamEnd);
|
||||
if (a < streamStart) return;
|
||||
const int start = a + attr.size();
|
||||
const int end = data.indexOf('"', start);
|
||||
if (end == -1 || end > streamEnd) return;
|
||||
xmpEdits.append({start, end, xmpDate});
|
||||
xmpDelta += xmpDate.size() - (end - start);
|
||||
// The last of each in the stream: the title and author come before
|
||||
// the dates, and are text a project could fill with anything.
|
||||
int xmpDelta = 0;
|
||||
QList<Edit> xmpEdits;
|
||||
for (const QByteArray &attr : {QByteArray("xmp:CreateDate=\""),
|
||||
QByteArray("xmp:ModifyDate=\""),
|
||||
QByteArray("xmp:MetadataDate=\"")}) {
|
||||
const int a = data.lastIndexOf(attr, streamEnd);
|
||||
if (a < streamStart) return;
|
||||
const int start = a + attr.size();
|
||||
const int end = data.indexOf('"', start);
|
||||
if (end == -1 || end > streamEnd) return;
|
||||
xmpEdits.append({start, end, xmpDate});
|
||||
xmpDelta += xmpDate.size() - (end - start);
|
||||
}
|
||||
const int length = data.mid(lengthStart, lengthEnd - lengthStart).toInt();
|
||||
edits.append({lengthStart, lengthEnd, QByteArray::number(length + xmpDelta)});
|
||||
edits.append(xmpEdits);
|
||||
}
|
||||
const int length = data.mid(lengthStart, lengthEnd - lengthStart).toInt();
|
||||
edits.append({lengthStart, lengthEnd, QByteArray::number(length + xmpDelta)});
|
||||
edits.append(xmpEdits);
|
||||
|
||||
std::sort(edits.begin(), edits.end(),
|
||||
[](const Edit &a, const Edit &b) { return a.start < b.start; });
|
||||
|
||||
@@ -51,6 +51,31 @@ class tst_pdfreproducible : public QObject
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
/// Every object the xref table lists starts where the table says: the
|
||||
/// dates are rewritten after Qt writes the file, which moves them.
|
||||
static bool xrefMatches(const QByteArray &pdf)
|
||||
{
|
||||
const int sx = pdf.lastIndexOf("startxref");
|
||||
if (sx == -1)
|
||||
return false;
|
||||
const int xref = pdf.mid(sx + 9).trimmed().split('\n').value(0).toInt();
|
||||
if (!pdf.mid(xref).startsWith("xref"))
|
||||
return false;
|
||||
const QList<QByteArray> lines = pdf.mid(xref).split('\n');
|
||||
const QList<QByteArray> header = lines.value(1).split(' ');
|
||||
const int first = header.value(0).toInt();
|
||||
const int count = header.value(1).toInt();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const QList<QByteArray> entry = lines.value(2 + i).split(' ');
|
||||
if (entry.value(2) != "n")
|
||||
continue;
|
||||
const QByteArray obj = QByteArray::number(first + i) + " 0 obj";
|
||||
if (pdf.mid(entry.value(0).toInt(), obj.size()) != obj)
|
||||
return false;
|
||||
}
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
@@ -69,7 +94,10 @@ private slots:
|
||||
const QByteArray first = exportPdf(project, "1700000000");
|
||||
QVERIFY(!first.isEmpty());
|
||||
QVERIFY(first.contains("/CreationDate (D:20231114221320Z)"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
QVERIFY(first.contains("xmp:CreateDate=\"2023-11-14T22:13:20Z\""));
|
||||
#endif
|
||||
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
|
||||
for (int i = 0; i < 3; ++i)
|
||||
QVERIFY2(exportPdf(project, "1700000000") == first,
|
||||
"two exports of the same project differ");
|
||||
|
||||
Reference in New Issue
Block a user