mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-04 18:04:14 +02:00
Write the same PDF for the same project when SOURCE_DATE_EPOCH is set
Two exports of one project differed in the creation and modification dates (the time of the export, in local time) and in the document id (random). With SOURCE_DATE_EPOCH set (reproducible-builds.org), --export-pdf now uses the time it names, in UTC, and a document id derived from the project: version 5 of the project uuid with the SHA-256 of the project file, so it is the same for the same file and changes when the file does. Qt has no setter for the dates, so PdfLinks::setDocumentDate() rewrites them in the document information and the XMP metadata after the file is written, fixing the XMP /Length and shifting the xref table. Without the variable nothing changes. Only --export-pdf reads it; the print window's PDF export does not, as a person exporting by hand wants the real date. qet_export (misc/qet-mcp) gains "reproducible" and "source_date_epoch". It reports whether the build honoured the variable, since an older one ignores it. tst_pdfreproducible exports examples/741.qet four times and requires identical bytes and the epoch's date; it fails with either this change or the hash seed fix reverted. Projects with cross-reference links also need their links in a fixed order, which is a separate fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,9 @@
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QDomDocument>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
#include <QFileInfo>
|
||||
@@ -59,7 +61,9 @@
|
||||
#include <QSqlQuery>
|
||||
#include <QSvgGenerator>
|
||||
#include <QTextStream>
|
||||
#include <QTimeZone>
|
||||
#include <QTransform>
|
||||
#include <QUuid>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -147,6 +151,23 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
|
||||
diagram->setDrawTerminalNames(was_drawing_terminal_names);
|
||||
}
|
||||
|
||||
/// The time SOURCE_DATE_EPOCH names, in seconds since 1970 UTC, or an
|
||||
/// invalid QDateTime when it is unset. A value that is not a whole number of
|
||||
/// seconds is reported and ignored.
|
||||
QDateTime sourceDateEpoch()
|
||||
{
|
||||
const QByteArray value = qgetenv("SOURCE_DATE_EPOCH");
|
||||
if (value.isEmpty())
|
||||
return {};
|
||||
bool ok = false;
|
||||
const qlonglong seconds = value.toLongLong(&ok);
|
||||
if (!ok || seconds < 0) {
|
||||
err << "SOURCE_DATE_EPOCH '" << value << "' is not a number of seconds; ignored.\n";
|
||||
return {};
|
||||
}
|
||||
return QDateTime::fromSecsSinceEpoch(seconds, QTimeZone::UTC);
|
||||
}
|
||||
|
||||
int exportPdf(QETProject &project, const QString &output,
|
||||
bool showTerminals = false)
|
||||
{
|
||||
@@ -166,6 +187,19 @@ int exportPdf(QETProject &project, const QString &output,
|
||||
writer.setCreator("QElectroTech");
|
||||
writer.setResolution(96);
|
||||
|
||||
// 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.
|
||||
const QDateTime sourceDate = sourceDateEpoch();
|
||||
if (sourceDate.isValid()) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
QFile file(project.filePath());
|
||||
if (file.open(QIODevice::ReadOnly))
|
||||
hash.addData(&file);
|
||||
writer.setDocumentId(QUuid::createUuidV5(project.uuid(), hash.result()));
|
||||
}
|
||||
|
||||
QPainter painter;
|
||||
bool first = true;
|
||||
for (Diagram *diagram : diagrams) {
|
||||
@@ -240,6 +274,8 @@ int exportPdf(QETProject &project, const QString &output,
|
||||
// the cross-references jump inside the document in any PDF viewer.
|
||||
PdfLinks::convertUriToGoTo(output);
|
||||
PdfLinks::removeUnusedPdfxNamespace(output);
|
||||
if (sourceDate.isValid())
|
||||
PdfLinks::setDocumentDate(output, sourceDate);
|
||||
|
||||
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user