New features for QPdfWriter

Added new API setDocumentXmpMetadata/documentXmpMetadata and
addFileAttachment

[ChangeLog][QtGui][QPdfWriter] New API to provide external document XMP
metadata and attach files to PDF.

Fixes: QTBUG-78651
Fixes: QTBUG-78764
Change-Id: Ic0b37e8d12899f907001db469080594c14c87655
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
bb10
Jordi Pujol Foyo 2019-12-13 14:27:11 +01:00
parent 6227a0d2e2
commit 9d6b4d1142
5 changed files with 218 additions and 32 deletions

View File

@ -1390,6 +1390,18 @@ void QPdfEngine::setPdfVersion(PdfVersion version)
d->pdfVersion = version;
}
void QPdfEngine::setDocumentXmpMetadata(const QByteArray &xmpMetadata)
{
Q_D(QPdfEngine);
d->xmpDocumentMetadata = xmpMetadata;
}
QByteArray QPdfEngine::documentXmpMetadata() const
{
Q_D(const QPdfEngine);
return d->xmpDocumentMetadata;
}
void QPdfEngine::setPageLayout(const QPageLayout &pageLayout)
{
Q_D(QPdfEngine);
@ -1520,6 +1532,8 @@ bool QPdfEngine::begin(QPaintDevice *pdev)
d->xrefPositions.clear();
d->pageRoot = 0;
d->embeddedfilesRoot = 0;
d->namesRoot = 0;
d->catalog = 0;
d->info = 0;
d->graphicsState = 0;
@ -1555,10 +1569,18 @@ bool QPdfEngine::end()
d->outDevice = nullptr;
}
d->fileCache.clear();
setActive(false);
return true;
}
void QPdfEngine::addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType)
{
Q_D(QPdfEngine);
d->fileCache.push_back({fileName, data, mimeType});
}
QPdfEnginePrivate::~QPdfEnginePrivate()
{
qDeleteAll(fonts);
@ -1586,13 +1608,19 @@ void QPdfEnginePrivate::writeHeader()
int metaDataObj = -1;
int outputIntentObj = -1;
if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty()) {
metaDataObj = writeXmpDcumentMetaData();
}
if (pdfVersion == QPdfEngine::Version_A1b) {
metaDataObj = writeXmpMetaData();
outputIntentObj = writeOutputIntent();
}
catalog = addXrefEntry(-1);
pageRoot = requestObject();
if (!fileCache.isEmpty()) {
namesRoot = requestObject();
embeddedfilesRoot = requestObject();
}
// catalog
{
@ -1602,10 +1630,15 @@ void QPdfEnginePrivate::writeHeader()
<< "/Type /Catalog\n"
<< "/Pages " << pageRoot << "0 R\n";
if (pdfVersion == QPdfEngine::Version_A1b) {
s << "/OutputIntents [" << outputIntentObj << "0 R]\n";
// Embedded files, if any
if (!fileCache.isEmpty())
s << "/Names " << embeddedfilesRoot << "0 R\n";
if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty())
s << "/Metadata " << metaDataObj << "0 R\n";
}
if (pdfVersion == QPdfEngine::Version_A1b)
s << "/OutputIntents [" << outputIntentObj << "0 R]\n";
s << ">>\n"
<< "endobj\n";
@ -1613,6 +1646,12 @@ void QPdfEnginePrivate::writeHeader()
write(catalog);
}
if (!fileCache.isEmpty()) {
addXrefEntry(embeddedfilesRoot);
xprintf("<</EmbeddedFiles %d 0 R>>\n"
"endobj\n", namesRoot);
}
// graphics state
graphicsState = addXrefEntry(-1);
xprintf("<<\n"
@ -1664,39 +1703,45 @@ void QPdfEnginePrivate::writeInfo()
"endobj\n");
}
int QPdfEnginePrivate::writeXmpMetaData()
int QPdfEnginePrivate::writeXmpDcumentMetaData()
{
const int metaDataObj = addXrefEntry(-1);
QByteArray metaDataContent;
const QString producer(QString::fromLatin1("Qt " QT_VERSION_STR));
if (xmpDocumentMetadata.isEmpty()) {
const QString producer(QString::fromLatin1("Qt " QT_VERSION_STR));
const QDateTime now = QDateTime::currentDateTime();
const QDate date = now.date();
const QTime time = now.time();
const QString timeStr =
QString::asprintf("%d-%02d-%02dT%02d:%02d:%02d",
date.year(), date.month(), date.day(),
time.hour(), time.minute(), time.second());
const QDateTime now = QDateTime::currentDateTime();
const QDate date = now.date();
const QTime time = now.time();
const QString timeStr =
QString::asprintf("%d-%02d-%02dT%02d:%02d:%02d",
date.year(), date.month(), date.day(),
time.hour(), time.minute(), time.second());
const int offset = now.offsetFromUtc();
const int hours = (offset / 60) / 60;
const int mins = (offset / 60) % 60;
QString tzStr;
if (offset < 0)
tzStr = QString::asprintf("-%02d:%02d", -hours, -mins);
else if (offset > 0)
tzStr = QString::asprintf("+%02d:%02d", hours , mins);
const int offset = now.offsetFromUtc();
const int hours = (offset / 60) / 60;
const int mins = (offset / 60) % 60;
QString tzStr;
if (offset < 0)
tzStr = QString::asprintf("-%02d:%02d", -hours, -mins);
else if (offset > 0)
tzStr = QString::asprintf("+%02d:%02d", hours , mins);
else
tzStr = QLatin1String("Z");
const QString metaDataDate = timeStr + tzStr;
QFile metaDataFile(QLatin1String(":/qpdf/qpdfa_metadata.xml"));
metaDataFile.open(QIODevice::ReadOnly);
metaDataContent = QString::fromUtf8(metaDataFile.readAll()).arg(producer.toHtmlEscaped(),
title.toHtmlEscaped(),
creator.toHtmlEscaped(),
metaDataDate).toUtf8();
}
else
tzStr = QLatin1String("Z");
metaDataContent = xmpDocumentMetadata;
const QString metaDataDate = timeStr + tzStr;
QFile metaDataFile(QLatin1String(":/qpdf/qpdfa_metadata.xml"));
metaDataFile.open(QIODevice::ReadOnly);
const QByteArray metaDataContent = QString::fromUtf8(metaDataFile.readAll()).arg(producer.toHtmlEscaped(),
title.toHtmlEscaped(),
creator.toHtmlEscaped(),
metaDataDate).toUtf8();
xprintf("<<\n"
"/Type /Metadata /Subtype /XML\n"
"/Length %d\n"
@ -1774,6 +1819,56 @@ void QPdfEnginePrivate::writePageRoot()
"endobj\n");
}
void QPdfEnginePrivate::writeAttachmentRoot()
{
if (fileCache.isEmpty())
return;
QVector<int> attachments;
const int size = fileCache.size();
for (int i = 0; i < size; ++i) {
auto attachment = fileCache.at(i);
const int attachmentID = addXrefEntry(-1);
xprintf("<<\n");
if (do_compress)
xprintf("/Filter /FlateDecode\n");
const int lenobj = requestObject();
xprintf("/Length %d 0 R\n", lenobj);
int len = 0;
xprintf(">>\nstream\n");
len = writeCompressed(attachment.data);
xprintf("\nendstream\n"
"endobj\n");
addXrefEntry(lenobj);
xprintf("%d\n"
"endobj\n", len);
attachments.push_back(addXrefEntry(-1));
xprintf("<<\n"
"/F (%s)", attachment.fileName.toLatin1().constData());
xprintf("\n/EF <</F %d 0 R>>\n"
"/Type/Filespec\n"
, attachmentID);
if (!attachment.mimeType.isEmpty())
xprintf("/Subtype/%s\n",
attachment.mimeType.replace(QLatin1String("/"),
QLatin1String("#2F")).toLatin1().constData());
xprintf(">>\nendobj\n");
}
// names
addXrefEntry(namesRoot);
xprintf("<</Names[");
for (int i = 0; i < size; ++i) {
auto attachment = fileCache.at(i);
printString(attachment.fileName);
xprintf("%d 0 R\n", attachments.at(i));
}
xprintf("]>>\n"
"endobj\n");
}
void QPdfEnginePrivate::embedFont(QFontSubset *font)
{
@ -2031,6 +2126,8 @@ void QPdfEnginePrivate::writeTail()
writePage();
writeFonts();
writePageRoot();
writeAttachmentRoot();
addXrefEntry(xrefPositions.size(),false);
xprintf("xref\n"
"0 %d\n"

View File

@ -187,6 +187,11 @@ public:
void setPdfVersion(PdfVersion version);
void setDocumentXmpMetadata(const QByteArray &xmpMetadata);
QByteArray documentXmpMetadata() const;
void addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType);
// reimplementations QPaintEngine
bool begin(QPaintDevice *pdev) override;
bool end() override;
@ -297,9 +302,10 @@ private:
int createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha);
void writeInfo();
int writeXmpMetaData();
int writeXmpDcumentMetaData();
int writeOutputIntent();
void writePageRoot();
void writeAttachmentRoot();
void writeFonts();
void embedFont(QFontSubset *font);
qreal calcUserUnit() const;
@ -324,11 +330,22 @@ private:
inline int writeCompressed(const QByteArray &data) { return writeCompressed(data.constData(), data.length()); }
int writeCompressed(QIODevice *dev);
struct AttachmentInfo
{
AttachmentInfo (const QString &fileName, const QByteArray &data, const QString &mimeType)
: fileName(fileName), data(data), mimeType(mimeType) {}
QString fileName;
QByteArray data;
QString mimeType;
};
// various PDF objects
int pageRoot, catalog, info, graphicsState, patternColorSpace;
int pageRoot, embeddedfilesRoot, namesRoot, catalog, info, graphicsState, patternColorSpace;
QVector<uint> pages;
QHash<qint64, uint> imageCache;
QHash<QPair<uint, uint>, uint > alphaCache;
QVector<AttachmentInfo> fileCache;
QByteArray xmpDocumentMetadata;
};
QT_END_NAMESPACE

View File

@ -266,6 +266,50 @@ int QPdfWriter::resolution() const
return d->engine->resolution();
}
/*!
\since 5.15
Sets the document metadata. This metadata is not influenced by the setTitle / setCreator methods,
so is up to the user to keep it consistent.
\a xmpMetadata contains XML formatted metadata to embed into the PDF file.
\sa documentXmpMetadata()
*/
void QPdfWriter::setDocumentXmpMetadata(const QByteArray &xmpMetadata)
{
Q_D(const QPdfWriter);
d->engine->setDocumentXmpMetadata(xmpMetadata);
}
/*!
\since 5.15
Gets the document metadata, as it was provided with a call to setDocumentXmpMetadata. It will not
return the default metadata.
\sa setDocumentXmpMetadata()
*/
QByteArray QPdfWriter::documentXmpMetadata() const
{
Q_D(const QPdfWriter);
return d->engine->documentXmpMetadata();
}
/*!
\since 5.15
Adds \a fileName attachment to the PDF with (optional) \a mimeType
\a data contains the raw file data to embed into the PDF file.
*/
void QPdfWriter::addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType)
{
Q_D(QPdfWriter);
d->engine->addFileAttachment(fileName, data, mimeType);
}
// Defined in QPagedPaintDevice but non-virtual, add QPdfWriter specific doc here
#ifdef Q_QDOC
/*!

View File

@ -75,6 +75,11 @@ public:
void setResolution(int resolution);
int resolution() const;
void setDocumentXmpMetadata(const QByteArray &xmpMetadata);
QByteArray documentXmpMetadata() const;
void addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType = QString());
#ifdef Q_QDOC
bool setPageLayout(const QPageLayout &pageLayout);
bool setPageSize(const QPageSize &pageSize);

View File

@ -112,6 +112,29 @@ void tst_QPdfWriter::basics()
QCOMPARE(writer.pageLayout().fullRect(QPageLayout::Millimeter), QRectF(0, 0, 297, 210));
QCOMPARE(writer.pageLayout().paintRect(QPageLayout::Millimeter), QRectF(50, 50, 197, 110));
QByteArray metadata (
"<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>\n"
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n"
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\"\n"
" <rdf:Description xmlns:dc=\"http://purl.org/dc/elements/1.1/\" rdf:about=\"\">\n"
" <dc:title>\n"
" <rdf:Alt>\n"
" <rdf:li xml:lang=\"x-default\">TITLE</rdf:li>\n"
" </rdf:Alt>\n"
" </dc:title>\n"
" </rdf:Description>\n"
" <rdf:Description xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\" rdf:about="" xmp:CreatorTool=\"OUR_OWN_XMP\" xmp:CreateDate=\"2019-12-16T00:00:00+01:00\" xmp:ModifyDate=\"2019-12-16T00:00:00+01:00\"/>\n"
" <rdf:Description xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\" rdf:about="" pdf:Producer=\"MetaType Info Producer\"/>\n"
" <rdf:Description xmlns:pdfaid=\"http://www.aiim.org/pdfa/ns/id/\" rdf:about=\"THI IS ALL ABOUT\" pdfaid:part=\"1\" pdfaid:conformance=\"B\"/>\n"
" </rdf:RDF>\n"
"</x:xmpmeta>\n"
"<?xpacket end='w'?>\n"
);
QCOMPARE(writer.documentXmpMetadata(), QByteArray());
writer.setDocumentXmpMetadata(metadata);
QCOMPARE(writer.documentXmpMetadata(), metadata);
}
// Test the old page metrics methods, see also QPrinter tests for the same.