diff options
author | Tobias Koenig <tobias.koenig@kdab.com> | 2017-02-14 10:11:19 +0100 |
---|---|---|
committer | Tobias Koenig <tobias.koenig@kdab.com> | 2017-03-28 07:02:05 +0000 |
commit | 6c32927f8cf226b8158dd25f4b92a6853dadb8b6 (patch) | |
tree | 7fcd1dcfa76c05d59fb6f115d133308dfe2922e2 /src/gui/painting/qpdf.cpp | |
parent | 3398d9d40cb0dae2dc2a1a4f7dc3b4b9cceae903 (diff) |
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>
Diffstat (limited to 'src/gui/painting/qpdf.cpp')
-rw-r--r-- | src/gui/painting/qpdf.cpp | 288 |
1 files changed, 262 insertions, 26 deletions
diff --git a/src/gui/painting/qpdf.cpp b/src/gui/painting/qpdf.cpp index bac9740892..fdbc803174 100644 --- a/src/gui/painting/qpdf.cpp +++ b/src/gui/painting/qpdf.cpp @@ -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); |