Improve PDF/A-1b support in QPdfWriter

Add new enum QPrinter::PdfVersion to switch QPdfWriter
into PDF/A-1b mode. In that mode
    - meta data are embedded in XMP format
    - a color profile for sRGB is embedded and an OutputIntent defined
    - the ID key is added to the document trailer dictionary
    - a CIDSet entry is added to the font descriptor
    - transparency is removed from pens, brushes and images

Change-Id: Ia8a24d83609b239e716aefc1ba05f07320dbd290
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
bb10
Tobias Koenig 2017-02-14 10:11:19 +01:00
parent 3398d9d40c
commit 6c32927f8c
17 changed files with 414 additions and 30 deletions

10
src/3rdparty/icc/LICENSE.txt vendored Normal file
View File

@ -0,0 +1,10 @@
For the file sRGB2014.icc:
Copyright International Color Consortium, 2015
This profile is made available by the International Color Consortium, and may be copied,
distributed, embedded, made, used, and sold without restriction. Altered versions of this
profile shall have the original identification and copyright information removed and
shall not be misrepresented as the original profile.
(original source location: http://www.color.org/srgbprofiles.xalter)

15
src/3rdparty/icc/qt_attribution.json vendored Normal file
View File

@ -0,0 +1,15 @@
[
{
"Id": "icc-sRGB-color-profile",
"Name": "sRGB color profile icc file",
"QDocModule": "qtgui",
"QtUsage": "Used in Qt Gui (Embedded into PDF/A-1b files generated by QPrinter/QPdfWriter).",
"Files": "sRGB2014.icc",
"Description": "An ICC color profile for PDF/A-1b compatible PDF files.",
"LicenseId": "ICC License",
"License": "International Color Consortium License",
"LicenseFile": "LICENSE.txt",
"Copyright": "Copyright International Color Consortium, 2015"
}
]

BIN
src/3rdparty/icc/sRGB2014.icc vendored Normal file

Binary file not shown.

View File

@ -99,6 +99,9 @@ SOURCES += \
painting/qplatformbackingstore.cpp \
painting/qpathsimplifier.cpp
RESOURCES += \
painting/qpdf.qrc
darwin {
HEADERS += painting/qcoregraphics_p.h
SOURCES += painting/qcoregraphics.mm

View File

@ -243,6 +243,18 @@ QPagedPaintDevicePrivate *QPagedPaintDevice::dd()
Starts a new page. Returns \c true on success.
*/
/*!
\enum QPagedPaintDevice::PdfVersion
The PdfVersion enum describes the version of the PDF file that
is produced by QPrinter or QPdfWriter.
\value PdfFormat_1_4 A PDF 1.4 compatible document is produced.
\value PdfFormat_A1b A PDF/A-1b compatible document is produced.
\since 5.10
*/
/*!
Sets the size of the a page to \a size.

View File

@ -213,6 +213,8 @@ public:
Envelope10 = Comm10E
};
enum PdfVersion { PdfVersion_1_4, PdfVersion_A1b };
// ### Qt6 Make these virtual
bool setPageLayout(const QPageLayout &pageLayout);
bool setPageSize(const QPageSize &pageSize);

View File

@ -42,16 +42,20 @@
#ifndef QT_NO_PDF
#include "qplatformdefs.h"
#include <qdebug.h>
#include <qfile.h>
#include <qtemporaryfile.h>
#include <private/qfont_p.h>
#include <private/qmath_p.h>
#include <private/qpainter_p.h>
#include <qnumeric.h>
#include "private/qfont_p.h"
#include <qbuffer.h>
#include <qcryptographichash.h>
#include <qdatetime.h>
#include <qdebug.h>
#include <qfile.h>
#include <qimagewriter.h>
#include "qbuffer.h"
#include "QtCore/qdatetime.h"
#include <qnumeric.h>
#include <qtemporaryfile.h>
#include <quuid.h>
#ifndef QT_NO_COMPRESS
#include <zlib.h>
@ -79,6 +83,45 @@ inline QPaintEngine::PaintEngineFeatures qt_pdf_decide_features()
return f;
}
extern bool qt_isExtendedRadialGradient(const QBrush &brush);
// helper function to remove transparency from brush in PDF/A-1b mode
static void removeTransparencyFromBrush(QBrush &brush)
{
if (brush.style() == Qt::SolidPattern) {
QColor color = brush.color();
if (color.alpha() != 255) {
color.setAlpha(255);
brush.setColor(color);
}
return;
}
if (qt_isExtendedRadialGradient(brush)) {
brush = QBrush(Qt::black); // the safest we can do so far...
return;
}
if (brush.style() == Qt::LinearGradientPattern
|| brush.style() == Qt::RadialGradientPattern
|| brush.style() == Qt::ConicalGradientPattern) {
QGradientStops stops = brush.gradient()->stops();
for (int i = 0; i < stops.size(); ++i) {
if (stops[i].second.alpha() != 255)
stops[i].second.setAlpha(255);
}
const_cast<QGradient*>(brush.gradient())->setStops(stops);
return;
}
if (brush.style() == Qt::TexturePattern) {
// handled inside QPdfEnginePrivate::addImage() already
return;
}
}
/* also adds a space at the end of the number */
@ -1042,7 +1085,22 @@ void QPdfEngine::updateState(const QPaintEngineState &state)
d->stroker.matrix = state.transform();
if (flags & DirtyPen) {
d->pen = state.pen();
if (d->pdfVersion == QPdfEngine::Version_A1b) {
QPen pen = state.pen();
QColor penColor = pen.color();
if (penColor.alpha() != 255)
penColor.setAlpha(255);
pen.setColor(penColor);
QBrush penBrush = pen.brush();
removeTransparencyFromBrush(penBrush);
pen.setBrush(penBrush);
d->pen = pen;
} else {
d->pen = state.pen();
}
d->hasPen = d->pen.style() != Qt::NoPen;
d->stroker.setPen(d->pen, state.renderHints());
QBrush penBrush = d->pen.brush();
@ -1054,7 +1112,13 @@ void QPdfEngine::updateState(const QPaintEngineState &state)
d->stroker.setPen(d->pen, state.renderHints());
}
if (flags & DirtyBrush) {
d->brush = state.brush();
if (d->pdfVersion == QPdfEngine::Version_A1b) {
QBrush brush = state.brush();
removeTransparencyFromBrush(brush);
d->brush = brush;
} else {
d->brush = state.brush();
}
if (d->brush.color().alpha() == 0 && d->brush.style() == Qt::SolidPattern)
d->brush.setStyle(Qt::NoBrush);
d->hasBrush = d->brush.style() != Qt::NoBrush;
@ -1286,6 +1350,12 @@ int QPdfEngine::resolution() const
return d->resolution;
}
void QPdfEngine::setPdfVersion(PdfVersion version)
{
Q_D(QPdfEngine);
d->pdfVersion = version;
}
void QPdfEngine::setPageLayout(const QPageLayout &pageLayout)
{
Q_D(QPdfEngine);
@ -1364,6 +1434,7 @@ int QPdfEngine::metric(QPaintDevice::PaintDeviceMetric metricType) const
QPdfEnginePrivate::QPdfEnginePrivate()
: clipEnabled(false), allClipped(false), hasPen(true), hasBrush(false), simplePen(false),
pdfVersion(QPdfEngine::Version_1_4),
outDevice(0), ownsDevice(false),
embedFonts(true),
grayscale(false),
@ -1469,13 +1540,34 @@ void QPdfEnginePrivate::writeHeader()
writeInfo();
int metaDataObj = -1;
int outputIntentObj = -1;
if (pdfVersion == QPdfEngine::Version_A1b) {
metaDataObj = writeXmpMetaData();
outputIntentObj = writeOutputIntent();
}
catalog = addXrefEntry(-1);
pageRoot = requestObject();
xprintf("<<\n"
"/Type /Catalog\n"
"/Pages %d 0 R\n"
">>\n"
"endobj\n", pageRoot);
// catalog
{
QByteArray catalog;
QPdf::ByteStream s(&catalog);
s << "<<\n"
<< "/Type /Catalog\n"
<< "/Pages " << pageRoot << "0 R\n";
if (pdfVersion == QPdfEngine::Version_A1b) {
s << "/OutputIntents [" << outputIntentObj << "0 R]\n";
s << "/Metadata " << metaDataObj << "0 R\n";
}
s << ">>\n"
<< "endobj\n";
write(catalog);
}
// graphics state
graphicsState = addXrefEntry(-1);
@ -1528,6 +1620,95 @@ void QPdfEnginePrivate::writeInfo()
"endobj\n");
}
int QPdfEnginePrivate::writeXmpMetaData()
{
const int metaDataObj = addXrefEntry(-1);
const QString producer(QString::fromLatin1("Qt " QT_VERSION_STR));
const QDateTime now = QDateTime::currentDateTime();
const QDate date = now.date();
const QTime time = now.time();
QString timeStr;
timeStr.sprintf("%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.sprintf("-%02d:%02d", -hours, -mins);
else if (offset > 0)
tzStr.sprintf("+%02d:%02d", hours , mins);
else
tzStr = QLatin1String("Z");
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"
">>\n"
"stream\n", metaDataContent.size());
write(metaDataContent);
xprintf("\nendstream\n"
"endobj\n");
return metaDataObj;
}
int QPdfEnginePrivate::writeOutputIntent()
{
const int colorProfile = addXrefEntry(-1);
{
QFile colorProfileFile(QLatin1String(":/qpdf/sRGB2014.icc"));
colorProfileFile.open(QIODevice::ReadOnly);
const QByteArray colorProfileData = colorProfileFile.readAll();
QByteArray data;
QPdf::ByteStream s(&data);
int length_object = requestObject();
s << "<<\n";
s << "/N 3\n";
s << "/Alternate /DeviceRGB\n";
s << "/Length " << length_object << "0 R\n";
s << "/Filter /FlateDecode\n";
s << ">>\n";
s << "stream\n";
write(data);
const int len = writeCompressed(colorProfileData);
write("\nendstream\n"
"endobj\n");
addXrefEntry(length_object);
xprintf("%d\n"
"endobj\n", len);
}
const int outputIntent = addXrefEntry(-1);
{
xprintf("<<\n");
xprintf("/Type /OutputIntent\n");
xprintf("/S/GTS_PDFA1\n");
xprintf("/OutputConditionIdentifier (sRGB_IEC61966-2-1_black_scaled)\n");
xprintf("/DestOutputProfile %d 0 R\n", colorProfile);
xprintf("/Info(sRGB IEC61966 v2.1 with black scaling)\n");
xprintf("/RegistryName(http://www.color.org)\n");
xprintf(">>\n");
xprintf("endobj\n");
}
return outputIntent;
}
void QPdfEnginePrivate::writePageRoot()
{
addXrefEntry(pageRoot);
@ -1569,6 +1750,7 @@ void QPdfEnginePrivate::embedFont(QFontSubset *font)
int fontstream = requestObject();
int cidfont = requestObject();
int toUnicode = requestObject();
int cidset = requestObject();
QFontEngine::Properties properties = font->fontEngine->properties();
QByteArray postscriptName = properties.postscriptName.replace(' ', '_');
@ -1598,6 +1780,7 @@ void QPdfEnginePrivate::embedFont(QFontSubset *font)
"/CapHeight " << properties.capHeight.toReal()*scale << "\n"
"/StemV " << properties.lineWidth.toReal()*scale << "\n"
"/FontFile2 " << fontstream << "0 R\n"
"/CIDSet " << cidset << "0 R\n"
">>\nendobj\n";
write(descriptor);
}
@ -1660,6 +1843,29 @@ void QPdfEnginePrivate::embedFont(QFontSubset *font)
"endobj\n";
write(font);
}
{
QByteArray cidSetStream(font->nGlyphs() / 8 + 1, 0);
int byteCounter = 0;
int bitCounter = 0;
for (int i = 0; i < font->nGlyphs(); ++i) {
cidSetStream.data()[byteCounter] |= (1 << (7 - bitCounter));
bitCounter++;
if (bitCounter == 8) {
bitCounter = 0;
byteCounter++;
}
}
addXrefEntry(cidset);
xprintf("<<\n");
xprintf("/Length %d\n", cidSetStream.size());
xprintf(">>\n");
xprintf("stream\n");
write(cidSetStream);
xprintf("\nendstream\n");
xprintf("endobj\n");
}
}
@ -1769,15 +1975,28 @@ void QPdfEnginePrivate::writeTail()
for (int i = 1; i < xrefPositions.size()-1; ++i)
xprintf("%010d 00000 n \n", xrefPositions[i]);
xprintf("trailer\n"
"<<\n"
"/Size %d\n"
"/Info %d 0 R\n"
"/Root %d 0 R\n"
">>\n"
"startxref\n%d\n"
"%%%%EOF\n",
xrefPositions.size()-1, info, catalog, xrefPositions.constLast());
{
QByteArray trailer;
QPdf::ByteStream s(&trailer);
s << "trailer\n"
<< "<<\n"
<< "/Size " << xrefPositions.size() - 1 << "\n"
<< "/Info " << info << "0 R\n"
<< "/Root " << catalog << "0 R\n";
if (pdfVersion == QPdfEngine::Version_A1b) {
const QString uniqueId = QUuid::createUuid().toString();
const QByteArray fileIdentifier = QCryptographicHash::hash(uniqueId.toLatin1(), QCryptographicHash::Md5).toHex();
s << "/ID [ <" << fileIdentifier << "> <" << fileIdentifier << "> ]\n";
}
s << ">>\n"
<< "startxref\n" << xrefPositions.constLast() << "\n"
<< "%%EOF\n";
write(trailer);
}
}
int QPdfEnginePrivate::addXrefEntry(int object, bool printostr)
@ -1983,7 +2202,7 @@ int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height,
xprintf(">>\nstream\n");
len = writeCompressed(data);
}
xprintf("endstream\n"
xprintf("\nendstream\n"
"endobj\n");
addXrefEntry(lenobj);
xprintf("%d\n"
@ -2309,7 +2528,7 @@ int QPdfEnginePrivate::gradientBrush(const QBrush &b, const QTransform &matrix,
">>\n"
"stream\n"
<< content
<< "endstream\n"
<< "\nendstream\n"
"endobj\n";
int softMaskFormObject = addXrefEntry(-1);
@ -2418,7 +2637,7 @@ int QPdfEnginePrivate::addBrushPattern(const QTransform &m, bool *specifyColor,
">>\n"
"stream\n"
<< pattern
<< "endstream\n"
<< "\nendstream\n"
"endobj\n";
int patternObj = addXrefEntry(-1);
@ -2449,6 +2668,23 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, qint64 serial_n
QImage image = img;
QImage::Format format = image.format();
if (pdfVersion == QPdfEngine::Version_A1b) {
if (image.hasAlphaChannel()) {
// transparent images are not allowed in PDF/A-1b, so we convert it to
// a format without alpha channel first
QImage alphaLessImage(image.width(), image.height(), QImage::Format_RGB32);
alphaLessImage.fill(Qt::white);
QPainter p(&alphaLessImage);
p.drawImage(0, 0, image);
image = alphaLessImage;
format = image.format();
}
}
if (image.depth() == 1 && *bitmap && is_monochrome(img.colorTable())) {
if (format == QImage::Format_MonoLSB)
image = image.convertToFormat(QImage::Format_Mono);

View File

@ -0,0 +1,7 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="qpdf/">
<file>qpdfa_metadata.xml</file>
<file alias="sRGB2014.icc">../../3rdparty/icc/sRGB2014.icc</file>
</qresource>
</RCC>

View File

@ -168,6 +168,12 @@ class Q_GUI_EXPORT QPdfEngine : public QPaintEngine
Q_DECLARE_PRIVATE(QPdfEngine)
friend class QPdfWriter;
public:
enum PdfVersion
{
Version_1_4,
Version_A1b
};
QPdfEngine();
QPdfEngine(QPdfEnginePrivate &d);
~QPdfEngine() {}
@ -177,6 +183,8 @@ public:
void setResolution(int resolution);
int resolution() const;
void setPdfVersion(PdfVersion version);
// reimplementations QPaintEngine
bool begin(QPaintDevice *pdev) Q_DECL_OVERRIDE;
bool end() Q_DECL_OVERRIDE;
@ -258,6 +266,7 @@ public:
bool hasBrush;
bool simplePen;
qreal opacity;
QPdfEngine::PdfVersion pdfVersion;
QHash<QFontEngine::FaceId, QFontSubset *> fonts;
@ -286,6 +295,8 @@ private:
int createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha);
void writeInfo();
int writeXmpMetaData();
int writeOutputIntent();
void writePageRoot();
void writeFonts();
void embedFont(QFontSubset *font);

View File

@ -0,0 +1,16 @@
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/" rdf:about="">
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">%2</rdf:li>
</rdf:Alt>
</dc:title>
</rdf:Description>
<rdf:Description xmlns:xmp="http://ns.adobe.com/xap/1.0/" rdf:about="" xmp:CreatorTool="%3" xmp:CreateDate="%4" xmp:ModifyDate="%4"/>
<rdf:Description xmlns:pdf="http://ns.adobe.com/pdf/1.3/" rdf:about="" pdf:Producer="%1"/>
<rdf:Description xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/" rdf:about="" pdfaid:part="1" pdfaid:conformance="B"/>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end='w'?>

View File

@ -56,6 +56,7 @@ public:
{
engine = new QPdfEngine();
output = 0;
pdfVersion = QPdfWriter::PdfVersion_1_4;
}
~QPdfWriterPrivate()
{
@ -65,6 +66,7 @@ public:
QPdfEngine *engine;
QFile *output;
QPdfWriter::PdfVersion pdfVersion;
};
class QPdfPagedPaintDevicePrivate : public QPagedPaintDevicePrivate
@ -176,6 +178,35 @@ QPdfWriter::~QPdfWriter()
}
/*!
\since 5.10
Sets the PDF version for this writer to \a version.
If \a version is the same value as currently set then no change will be made.
*/
void QPdfWriter::setPdfVersion(PdfVersion version)
{
Q_D(QPdfWriter);
if (d->pdfVersion == version)
return;
d->pdfVersion = version;
d->engine->setPdfVersion(d->pdfVersion == QPdfWriter::PdfVersion_1_4 ? QPdfEngine::Version_1_4 : QPdfEngine::Version_A1b);
}
/*!
\since 5.10
Returns the PDF version for this writer. The default is \c PdfVersion_1_4.
*/
QPdfWriter::PdfVersion QPdfWriter::pdfVersion() const
{
Q_D(const QPdfWriter);
return d->pdfVersion;
}
/*!
Returns the title of the document.
*/

View File

@ -61,6 +61,9 @@ public:
explicit QPdfWriter(QIODevice *device);
~QPdfWriter();
void setPdfVersion(PdfVersion version);
PdfVersion pdfVersion() const;
QString title() const;
void setTitle(const QString &title);

View File

@ -58,10 +58,12 @@
QT_BEGIN_NAMESPACE
QPdfPrintEngine::QPdfPrintEngine(QPrinter::PrinterMode m)
QPdfPrintEngine::QPdfPrintEngine(QPrinter::PrinterMode m, QPdfEngine::PdfVersion version)
: QPdfEngine(*new QPdfPrintEnginePrivate(m))
{
state = QPrinter::Idle;
setPdfVersion(version);
}
QPdfPrintEngine::QPdfPrintEngine(QPdfPrintEnginePrivate &p)

View File

@ -83,7 +83,7 @@ class Q_PRINTSUPPORT_EXPORT QPdfPrintEngine : public QPdfEngine, public QPrintEn
{
Q_DECLARE_PRIVATE(QPdfPrintEngine)
public:
QPdfPrintEngine(QPrinter::PrinterMode m);
QPdfPrintEngine(QPrinter::PrinterMode m, QPdfEngine::PdfVersion version = QPdfEngine::Version_1_4);
virtual ~QPdfPrintEngine();
// reimplementations QPaintEngine

View File

@ -147,7 +147,8 @@ void QPrinterPrivate::initEngines(QPrinter::OutputFormat format, const QPrinterI
printEngine = ps->createNativePrintEngine(printerMode);
paintEngine = ps->createPaintEngine(printEngine, printerMode);
} else {
QPdfPrintEngine *pdfEngine = new QPdfPrintEngine(printerMode);
const auto pdfEngineVersion = (pdfVersion == QPrinter::PdfVersion_1_4 ? QPdfEngine::Version_1_4 : QPdfEngine::Version_A1b);
QPdfPrintEngine *pdfEngine = new QPdfPrintEngine(printerMode, pdfEngineVersion);
paintEngine = pdfEngine;
printEngine = pdfEngine;
}
@ -685,7 +686,37 @@ QPrinter::OutputFormat QPrinter::outputFormat() const
return d->outputFormat;
}
/*!
\since 5.10
Sets the PDF version for this printer to \a version.
If \a version is the same value as currently set then no change will be made.
*/
void QPrinter::setPdfVersion(PdfVersion version)
{
Q_D(QPrinter);
if (d->pdfVersion == version)
return;
d->pdfVersion = version;
if (d->outputFormat == QPrinter::PdfFormat) {
d->changeEngines(d->outputFormat, QPrinterInfo());
}
}
/*!
\since 5.10
Returns the PDF version for this printer. The default is \c PdfVersion_1_4.
*/
QPrinter::PdfVersion QPrinter::pdfVersion() const
{
Q_D(const QPrinter);
return d->pdfVersion;
}
/*! \internal
*/

View File

@ -134,6 +134,9 @@ public:
void setOutputFormat(OutputFormat format);
OutputFormat outputFormat() const;
void setPdfVersion(PdfVersion version);
PdfVersion pdfVersion() const;
void setPrinterName(const QString &);
QString printerName() const;

View File

@ -75,7 +75,8 @@ class Q_PRINTSUPPORT_EXPORT QPrinterPrivate
Q_DECLARE_PUBLIC(QPrinter)
public:
QPrinterPrivate(QPrinter *printer)
: printEngine(0),
: pdfVersion(QPrinter::PdfVersion_1_4),
printEngine(0),
paintEngine(0),
realPrintEngine(0),
realPaintEngine(0),
@ -107,6 +108,7 @@ public:
QPrinter::PrinterMode printerMode;
QPrinter::OutputFormat outputFormat;
QPrinter::PdfVersion pdfVersion;
QPrintEngine *printEngine;
QPaintEngine *paintEngine;