diff options
Diffstat (limited to 'src/gui/painting/qpdf.cpp')
-rw-r--r-- | src/gui/painting/qpdf.cpp | 688 |
1 files changed, 465 insertions, 223 deletions
diff --git a/src/gui/painting/qpdf.cpp b/src/gui/painting/qpdf.cpp index 55a1aa0242..38f2a9b803 100644 --- a/src/gui/painting/qpdf.cpp +++ b/src/gui/painting/qpdf.cpp @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtGui module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpdf_p.h" @@ -43,6 +7,7 @@ #include "qplatformdefs.h" +#include <private/qcmyk_p.h> #include <private/qfont_p.h> #include <private/qmath_p.h> #include <private/qpainter_p.h> @@ -78,7 +43,9 @@ static void initResources() QT_BEGIN_NAMESPACE -inline QPaintEngine::PaintEngineFeatures qt_pdf_decide_features() +using namespace Qt::StringLiterals; + +constexpr QPaintEngine::PaintEngineFeatures qt_pdf_decide_features() { QPaintEngine::PaintEngineFeatures f = QPaintEngine::AllFeatures; f &= ~(QPaintEngine::PorterDuff @@ -133,7 +100,7 @@ static void removeTransparencyFromBrush(QBrush &brush) const char *qt_real_to_string(qreal val, char *buf) { const char *ret = buf; - if (qIsNaN(val)) { + if (!qIsFinite(val) || std::abs(val) > std::numeric_limits<quint32>::max()) { *(buf++) = '0'; *(buf++) = ' '; *buf = 0; @@ -144,8 +111,8 @@ const char *qt_real_to_string(qreal val, char *buf) { *(buf++) = '-'; val = -val; } - unsigned int ival = (unsigned int) val; - qreal frac = val - (qreal)ival; + qreal frac = std::modf(val, &val); + quint32 ival(val); int ifrac = (int)(frac * 1000000000); if (ifrac == 1000000000) { @@ -304,13 +271,6 @@ namespace QPdf { dev->open(QIODevice::ReadWrite | QIODevice::Truncate); } - void ByteStream::constructor_helper(QByteArray *ba) - { - delete dev; - dev = new QBuffer(ba); - dev->open(QIODevice::ReadWrite); - } - void ByteStream::prepareBuffer() { Q_ASSERT(!dev->isSequential()); @@ -319,16 +279,17 @@ namespace QPdf { && size > maxMemorySize()) { // Switch to file backing. QTemporaryFile *newFile = new QTemporaryFile; - newFile->open(); - dev->reset(); - while (!dev->atEnd()) { - QByteArray buf = dev->read(chunkSize()); - newFile->write(buf); + if (newFile->open()) { + dev->reset(); + while (!dev->atEnd()) { + QByteArray buf = dev->read(chunkSize()); + newFile->write(buf); + } + delete dev; + dev = newFile; + ba.clear(); + fileBackingActive = true; } - delete dev; - dev = newFile; - ba.clear(); - fileBackingActive = true; } if (dev->pos() != size) { dev->seek(size); @@ -856,14 +817,14 @@ void QPdfEngine::drawRects (const QRectF *rects, int rectCount) if (!d->hasPen && !d->hasBrush) return; - if (d->simplePen || !d->hasPen) { - // draw strokes natively in this case for better output - if (!d->simplePen && !d->stroker.matrix.isIdentity()) + if ((d->simplePen && !d->needsTransform) || !d->hasPen) { + // draw natively in this case for better output + if (!d->hasPen && d->needsTransform) // i.e. this is just a fillrect *d->currentPage << "q\n" << QPdf::generateMatrix(d->stroker.matrix); for (int i = 0; i < rectCount; ++i) *d->currentPage << rects[i].x() << rects[i].y() << rects[i].width() << rects[i].height() << "re\n"; *d->currentPage << (d->hasPen ? (d->hasBrush ? "B\n" : "S\n") : "f\n"); - if (!d->simplePen && !d->stroker.matrix.isIdentity()) + if (!d->hasPen && d->needsTransform) *d->currentPage << "Q\n"; } else { QPainterPath p; @@ -920,7 +881,8 @@ void QPdfEngine::drawPath (const QPainterPath &p) if (d->simplePen) { // draw strokes natively in this case for better output - *d->currentPage << QPdf::generatePath(p, QTransform(), d->hasBrush ? QPdf::FillAndStrokePath : QPdf::StrokePath); + *d->currentPage << QPdf::generatePath(p, d->needsTransform ? d->stroker.matrix : QTransform(), + d->hasBrush ? QPdf::FillAndStrokePath : QPdf::StrokePath); } else { if (d->hasBrush) *d->currentPage << QPdf::generatePath(p, d->stroker.matrix, QPdf::FillPath); @@ -967,7 +929,7 @@ void QPdfEngine::drawPixmap (const QRectF &rectangle, const QPixmap &pixmap, con *d->currentPage << QPdf::generateMatrix(QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(), - rectangle.x(), rectangle.y()) * (d->simplePen ? QTransform() : d->stroker.matrix)); + rectangle.x(), rectangle.y()) * (!d->needsTransform ? QTransform() : d->stroker.matrix)); if (bitmap) { // set current pen as d->brush d->brush = d->pen.brush(); @@ -1007,7 +969,7 @@ void QPdfEngine::drawImage(const QRectF &rectangle, const QImage &image, const Q *d->currentPage << QPdf::generateMatrix(QTransform(rectangle.width() / sr.width(), 0, 0, rectangle.height() / sr.height(), - rectangle.x(), rectangle.y()) * (d->simplePen ? QTransform() : d->stroker.matrix)); + rectangle.x(), rectangle.y()) * (!d->needsTransform ? QTransform() : d->stroker.matrix)); setBrush(); d->currentPage->streamImage(im.width(), im.height(), object); *d->currentPage << "Q\n"; @@ -1056,7 +1018,7 @@ void QPdfEngine::drawTextItem(const QPointF &p, const QTextItem &textItem) } *d->currentPage << "q\n"; - if (!d->simplePen) + if (d->needsTransform) *d->currentPage << QPdf::generateMatrix(d->stroker.matrix); bool hp = d->hasPen; @@ -1114,6 +1076,9 @@ void QPdfEngine::updateState(const QPaintEngineState &state) QPaintEngine::DirtyFlags flags = state.state(); + if (flags & DirtyHints) + flags |= DirtyBrush; + if (flags & DirtyTransform) d->stroker.matrix = state.transform(); @@ -1135,12 +1100,12 @@ void QPdfEngine::updateState(const QPaintEngineState &state) d->pen = state.pen(); } d->hasPen = d->pen.style() != Qt::NoPen; + bool oldCosmetic = d->stroker.cosmeticPen; d->stroker.setPen(d->pen, state.renderHints()); QBrush penBrush = d->pen.brush(); - bool cosmeticPen = d->pen.isCosmetic(); bool oldSimple = d->simplePen; - d->simplePen = (d->hasPen && !cosmeticPen && (penBrush.style() == Qt::SolidPattern) && penBrush.isOpaque() && d->opacity == 1.0); - if (oldSimple != d->simplePen) + d->simplePen = (d->hasPen && (penBrush.style() == Qt::SolidPattern) && penBrush.isOpaque() && d->opacity == 1.0); + if (oldSimple != d->simplePen || oldCosmetic != d->stroker.cosmeticPen) flags |= DirtyTransform; } else if (flags & DirtyHints) { d->stroker.setPen(d->pen, state.renderHints()); @@ -1224,8 +1189,13 @@ void QPdfEngine::setupGraphicsState(QPaintEngine::DirtyFlags flags) if (flags & DirtyTransform) { *d->currentPage << "q\n"; - if (d->simplePen && !d->stroker.matrix.isIdentity()) - *d->currentPage << QPdf::generateMatrix(d->stroker.matrix); + d->needsTransform = false; + if (!d->stroker.matrix.isIdentity()) { + if (d->simplePen && !d->stroker.cosmeticPen) + *d->currentPage << QPdf::generateMatrix(d->stroker.matrix); + else + d->needsTransform = true; // I.e. page-wide xf not set, local xf needed + } } if (flags & DirtyBrush) setBrush(); @@ -1241,20 +1211,18 @@ void QPdfEngine::updateClipPath(const QPainterPath &p, Qt::ClipOperation op) QPainterPath path = d->stroker.matrix.map(p); //qDebug() << "updateClipPath: " << d->stroker.matrix << p.boundingRect() << path.boundingRect() << op; - if (op == Qt::NoClip) { + switch (op) { + case Qt::NoClip: d->clipEnabled = false; d->clips.clear(); - } else if (op == Qt::ReplaceClip) { + break; + case Qt::ReplaceClip: d->clips.clear(); d->clips.append(path); - } else if (op == Qt::IntersectClip) { - d->clips.append(path); - } else { // UniteClip - // ask the painter for the current clipping path. that's the easiest solution - path = painter()->clipPath(); - path = d->stroker.matrix.map(path); - d->clips.clear(); + break; + case Qt::IntersectClip: d->clips.append(path); + break; } } @@ -1266,17 +1234,8 @@ void QPdfEngine::setPen() QBrush b = d->pen.brush(); Q_ASSERT(b.style() == Qt::SolidPattern && b.isOpaque()); - QColor rgba = b.color(); - if (d->grayscale) { - qreal gray = qGray(rgba.rgba())/255.; - *d->currentPage << gray << gray << gray; - } else { - *d->currentPage << rgba.redF() - << rgba.greenF() - << rgba.blueF(); - } + d->writeColor(QPdfEnginePrivate::ColorDomain::Stroking, b.color()); *d->currentPage << "SCN\n"; - *d->currentPage << d->pen.widthF() << "w "; int pdfCapStyle = 0; @@ -1330,18 +1289,9 @@ void QPdfEngine::setBrush() if (!patternObject && !specifyColor) return; - *d->currentPage << (patternObject ? "/PCSp cs " : "/CSp cs "); - if (specifyColor) { - QColor rgba = d->brush.color(); - if (d->grayscale) { - qreal gray = qGray(rgba.rgba())/255.; - *d->currentPage << gray << gray << gray; - } else { - *d->currentPage << rgba.redF() - << rgba.greenF() - << rgba.blueF(); - } - } + const auto domain = patternObject ? QPdfEnginePrivate::ColorDomain::NonStrokingPattern + : QPdfEnginePrivate::ColorDomain::NonStroking; + d->writeColor(domain, specifyColor ? d->brush.color() : QColor()); if (patternObject) *d->currentPage << "/Pat" << patternObject; *d->currentPage << "scn\n"; @@ -1480,10 +1430,10 @@ int QPdfEngine::metric(QPaintDevice::PaintDeviceMetric metricType) const QPdfEnginePrivate::QPdfEnginePrivate() : clipEnabled(false), allClipped(false), hasPen(true), hasBrush(false), simplePen(false), - pdfVersion(QPdfEngine::Version_1_4), + needsTransform(false), pdfVersion(QPdfEngine::Version_1_4), + colorModel(QPdfEngine::ColorModel::Auto), outDevice(nullptr), ownsDevice(false), embedFonts(true), - grayscale(false), m_pageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(10, 10, 10, 10)) { initResources(); @@ -1532,13 +1482,17 @@ bool QPdfEngine::begin(QPaintDevice *pdev) d->xrefPositions.clear(); d->pageRoot = 0; - d->embeddedfilesRoot = 0; d->namesRoot = 0; + d->destsRoot = 0; + d->attachmentsRoot = 0; d->catalog = 0; d->info = 0; d->graphicsState = 0; - d->patternColorSpace = 0; + d->patternColorSpaceRGB = 0; + d->patternColorSpaceGrayscale = 0; + d->patternColorSpaceCMYK = 0; d->simplePen = false; + d->needsTransform = false; d->pages.clear(); d->imageCache.clear(); @@ -1569,6 +1523,7 @@ bool QPdfEngine::end() d->outDevice = nullptr; } + d->destCache.clear(); d->fileCache.clear(); setActive(false); @@ -1617,10 +1572,7 @@ void QPdfEnginePrivate::writeHeader() catalog = addXrefEntry(-1); pageRoot = requestObject(); - if (!fileCache.isEmpty()) { - namesRoot = requestObject(); - embeddedfilesRoot = requestObject(); - } + namesRoot = requestObject(); // catalog { @@ -1628,11 +1580,8 @@ void QPdfEnginePrivate::writeHeader() QPdf::ByteStream s(&catalog); s << "<<\n" << "/Type /Catalog\n" - << "/Pages " << pageRoot << "0 R\n"; - - // Embedded files, if any - if (!fileCache.isEmpty()) - s << "/Names " << embeddedfilesRoot << "0 R\n"; + << "/Pages " << pageRoot << "0 R\n" + << "/Names " << namesRoot << "0 R\n"; if (pdfVersion == QPdfEngine::Version_A1b || !xmpDocumentMetadata.isEmpty()) s << "/Metadata " << metaDataObj << "0 R\n"; @@ -1646,12 +1595,6 @@ 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" @@ -1665,10 +1608,108 @@ void QPdfEnginePrivate::writeHeader() ">>\n" "endobj\n"); - // color space for pattern - patternColorSpace = addXrefEntry(-1); + // color spaces for pattern + patternColorSpaceRGB = addXrefEntry(-1); xprintf("[/Pattern /DeviceRGB]\n" "endobj\n"); + patternColorSpaceGrayscale = addXrefEntry(-1); + xprintf("[/Pattern /DeviceGray]\n" + "endobj\n"); + patternColorSpaceCMYK = addXrefEntry(-1); + xprintf("[/Pattern /DeviceCMYK]\n" + "endobj\n"); +} + +QPdfEngine::ColorModel QPdfEnginePrivate::colorModelForColor(const QColor &color) const +{ + switch (colorModel) { + case QPdfEngine::ColorModel::RGB: + case QPdfEngine::ColorModel::Grayscale: + case QPdfEngine::ColorModel::CMYK: + return colorModel; + case QPdfEngine::ColorModel::Auto: + switch (color.spec()) { + case QColor::Invalid: + case QColor::Rgb: + case QColor::Hsv: + case QColor::Hsl: + case QColor::ExtendedRgb: + return QPdfEngine::ColorModel::RGB; + case QColor::Cmyk: + return QPdfEngine::ColorModel::CMYK; + } + + break; + } + + Q_UNREACHABLE_RETURN(QPdfEngine::ColorModel::RGB); +} + +void QPdfEnginePrivate::writeColor(ColorDomain domain, const QColor &color) +{ + // Switch to the right colorspace. + // For simplicity: do it even if it redundant (= already in that colorspace) + const QPdfEngine::ColorModel actualColorModel = colorModelForColor(color); + + switch (actualColorModel) { + case QPdfEngine::ColorModel::RGB: + switch (domain) { + case ColorDomain::Stroking: + *currentPage << "/CSp CS\n"; break; + case ColorDomain::NonStroking: + *currentPage << "/CSp cs\n"; break; + case ColorDomain::NonStrokingPattern: + *currentPage << "/PCSp cs\n"; break; + } + break; + case QPdfEngine::ColorModel::Grayscale: + switch (domain) { + case ColorDomain::Stroking: + *currentPage << "/CSpg CS\n"; break; + case ColorDomain::NonStroking: + *currentPage << "/CSpg cs\n"; break; + case ColorDomain::NonStrokingPattern: + *currentPage << "/PCSpg cs\n"; break; + } + break; + case QPdfEngine::ColorModel::CMYK: + switch (domain) { + case ColorDomain::Stroking: + *currentPage << "/CSpcmyk CS\n"; break; + case ColorDomain::NonStroking: + *currentPage << "/CSpcmyk cs\n"; break; + case ColorDomain::NonStrokingPattern: + *currentPage << "/PCSpcmyk cs\n"; break; + } + break; + case QPdfEngine::ColorModel::Auto: + Q_UNREACHABLE_RETURN(); + } + + // If we also have a color specified, write it out. + if (!color.isValid()) + return; + + switch (actualColorModel) { + case QPdfEngine::ColorModel::RGB: + *currentPage << color.redF() + << color.greenF() + << color.blueF(); + break; + case QPdfEngine::ColorModel::Grayscale: { + const qreal gray = qGray(color.rgba()) / 255.; + *currentPage << gray; + break; + } + case QPdfEngine::ColorModel::CMYK: + *currentPage << color.cyanF() + << color.magentaF() + << color.yellowF() + << color.blackF(); + break; + case QPdfEngine::ColorModel::Auto: + Q_UNREACHABLE_RETURN(); + } } void QPdfEnginePrivate::writeInfo() @@ -1728,12 +1769,13 @@ int QPdfEnginePrivate::writeXmpDcumentMetaData() else if (offset > 0) tzStr = QString::asprintf("+%02d:%02d", hours , mins); else - tzStr = QLatin1String("Z"); + tzStr = "Z"_L1; const QString metaDataDate = timeStr + tzStr; - QFile metaDataFile(QLatin1String(":/qpdf/qpdfa_metadata.xml")); - metaDataFile.open(QIODevice::ReadOnly); + QFile metaDataFile(":/qpdf/qpdfa_metadata.xml"_L1); + bool ok = metaDataFile.open(QIODevice::ReadOnly); + Q_ASSERT(ok); metaDataContent = QString::fromUtf8(metaDataFile.readAll()).arg(producer.toHtmlEscaped(), title.toHtmlEscaped(), creator.toHtmlEscaped(), @@ -1758,8 +1800,9 @@ int QPdfEnginePrivate::writeOutputIntent() { const int colorProfile = addXrefEntry(-1); { - QFile colorProfileFile(QLatin1String(":/qpdf/sRGB2014.icc")); - colorProfileFile.open(QIODevice::ReadOnly); + QFile colorProfileFile(":/qpdf/sRGB2014.icc"_L1); + bool ok = colorProfileFile.open(QIODevice::ReadOnly); + Q_ASSERT(ok); const QByteArray colorProfileData = colorProfileFile.readAll(); QByteArray data; @@ -1819,6 +1862,39 @@ void QPdfEnginePrivate::writePageRoot() "endobj\n"); } +void QPdfEnginePrivate::writeDestsRoot() +{ + if (destCache.isEmpty()) + return; + + QHash<QString, int> destObjects; + QByteArray xs, ys; + for (const DestInfo &destInfo : std::as_const(destCache)) { + int destObj = addXrefEntry(-1); + xs.setNum(static_cast<double>(destInfo.coords.x()), 'f'); + ys.setNum(static_cast<double>(destInfo.coords.y()), 'f'); + xprintf("[%d 0 R /XYZ %s %s 0]\n", destInfo.pageObj, xs.constData(), ys.constData()); + xprintf("endobj\n"); + destObjects.insert(destInfo.anchor, destObj); + } + + // names + destsRoot = addXrefEntry(-1); + QStringList anchors = destObjects.keys(); + anchors.sort(); + xprintf("<<\n/Limits ["); + printString(anchors.constFirst()); + xprintf(" "); + printString(anchors.constLast()); + xprintf("]\n/Names [\n"); + for (const QString &anchor : std::as_const(anchors)) { + printString(anchor); + xprintf(" %d 0 R\n", destObjects[anchor]); + } + xprintf("]\n>>\n" + "endobj\n"); +} + void QPdfEnginePrivate::writeAttachmentRoot() { if (fileCache.isEmpty()) @@ -1853,13 +1929,12 @@ void QPdfEnginePrivate::writeAttachmentRoot() , attachmentID); if (!attachment.mimeType.isEmpty()) xprintf("/Subtype/%s\n", - attachment.mimeType.replace(QLatin1String("/"), - QLatin1String("#2F")).toLatin1().constData()); + attachment.mimeType.replace("/"_L1, "#2F"_L1).toLatin1().constData()); xprintf(">>\nendobj\n"); } // names - addXrefEntry(namesRoot); + attachmentsRoot = addXrefEntry(-1); xprintf("<</Names["); for (int i = 0; i < size; ++i) { auto attachment = fileCache.at(i); @@ -1870,6 +1945,21 @@ void QPdfEnginePrivate::writeAttachmentRoot() "endobj\n"); } +void QPdfEnginePrivate::writeNamesRoot() +{ + addXrefEntry(namesRoot); + xprintf("<<\n"); + + if (attachmentsRoot) + xprintf("/EmbeddedFiles %d 0 R\n", attachmentsRoot); + + if (destsRoot) + xprintf("/Dests %d 0 R\n", destsRoot); + + xprintf(">>\n"); + xprintf("endobj\n"); +} + void QPdfEnginePrivate::embedFont(QFontSubset *font) { //qDebug() << "embedFont" << font->object_id; @@ -1963,7 +2053,7 @@ void QPdfEnginePrivate::embedFont(QFontSubset *font) addXrefEntry(toUnicode); QByteArray touc = font->createToUnicodeMap(); xprintf("<< /Length %d >>\n" - "stream\n", touc.length()); + "stream\n", touc.size()); write(touc); write("\nendstream\n" "endobj\n"); @@ -1986,7 +2076,7 @@ void QPdfEnginePrivate::embedFont(QFontSubset *font) QByteArray cidSetStream(font->nGlyphs() / 8 + 1, 0); int byteCounter = 0; int bitCounter = 0; - for (int i = 0; i < font->nGlyphs(); ++i) { + for (qsizetype i = 0; i < font->nGlyphs(); ++i) { cidSetStream.data()[byteCounter] |= (1 << (7 - bitCounter)); bitCounter++; @@ -2067,12 +2157,18 @@ void QPdfEnginePrivate::writePage() xprintf("<<\n" "/ColorSpace <<\n" "/PCSp %d 0 R\n" + "/PCSpg %d 0 R\n" + "/PCSpcmyk %d 0 R\n" "/CSp /DeviceRGB\n" "/CSpg /DeviceGray\n" + "/CSpcmyk /DeviceCMYK\n" ">>\n" "/ExtGState <<\n" "/GSa %d 0 R\n", - patternColorSpace, graphicsState); + patternColorSpaceRGB, + patternColorSpaceGrayscale, + patternColorSpaceCMYK, + graphicsState); for (int i = 0; i < currentPage->graphicStates.size(); ++i) xprintf("/GState%d %d 0 R\n", currentPage->graphicStates.at(i), currentPage->graphicStates.at(i)); @@ -2126,7 +2222,9 @@ void QPdfEnginePrivate::writeTail() writePage(); writeFonts(); writePageRoot(); + writeDestsRoot(); writeAttachmentRoot(); + writeNamesRoot(); addXrefEntry(xrefPositions.size(),false); xprintf("xref\n" @@ -2175,7 +2273,7 @@ int QPdfEnginePrivate::addXrefEntry(int object, bool printostr) return object; } -void QPdfEnginePrivate::printString(const QString &string) +void QPdfEnginePrivate::printString(QStringView string) { if (string.isEmpty()) { write("()"); @@ -2186,9 +2284,9 @@ void QPdfEnginePrivate::printString(const QString &string) // Unicode UTF-16 with a Unicode byte order mark as the first character // (0xfeff), with the high-order byte first. QByteArray array("(\xfe\xff"); - const ushort *utf16 = string.utf16(); + const char16_t *utf16 = string.utf16(); - for (int i=0; i < string.size(); ++i) { + for (qsizetype i = 0; i < string.size(); ++i) { char part[2] = {char((*(utf16 + i)) >> 8), char((*(utf16 + i)) & 0xff)}; for(int j=0; j < 2; ++j) { if (part[j] == '(' || part[j] == ')' || part[j] == '\\') @@ -2305,16 +2403,15 @@ int QPdfEnginePrivate::writeCompressed(const char *src, int len) { #ifndef QT_NO_COMPRESS if (do_compress) { - uLongf destLen = len + len/100 + 13; // zlib requirement - Bytef* dest = new Bytef[destLen]; - if (Z_OK == ::compress(dest, &destLen, (const Bytef*) src, (uLongf)len)) { - stream->writeRawData((const char*)dest, destLen); + const QByteArray data = qCompress(reinterpret_cast<const uchar *>(src), len); + constexpr qsizetype HeaderSize = 4; + if (!data.isNull()) { + stream->writeRawData(data.data() + HeaderSize, data.size() - HeaderSize); + len = data.size() - HeaderSize; } else { qWarning("QPdfStream::writeCompressed: Error in compress()"); - destLen = 0; + len = 0; } - delete [] dest; - len = destLen; } else #endif { @@ -2324,7 +2421,7 @@ int QPdfEnginePrivate::writeCompressed(const char *src, int len) return len; } -int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, int depth, +int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, WriteImageOption option, int maskObject, int softMaskObject, bool dct, bool isMono) { int image = addXrefEntry(-1); @@ -2334,7 +2431,8 @@ int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, "/Width %d\n" "/Height %d\n", width, height); - if (depth == 1) { + switch (option) { + case WriteImageOption::Monochrome: if (!isMono) { xprintf("/ImageMask true\n" "/Decode [1 0]\n"); @@ -2342,10 +2440,21 @@ int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, xprintf("/BitsPerComponent 1\n" "/ColorSpace /DeviceGray\n"); } - } else { + break; + case WriteImageOption::Grayscale: + xprintf("/BitsPerComponent 8\n" + "/ColorSpace /DeviceGray\n"); + break; + case WriteImageOption::RGB: + xprintf("/BitsPerComponent 8\n" + "/ColorSpace /DeviceRGB\n"); + break; + case WriteImageOption::CMYK: xprintf("/BitsPerComponent 8\n" - "/ColorSpace %s\n", (depth == 32) ? "/DeviceRGB" : "/DeviceGray"); + "/ColorSpace /DeviceCMYK\n"); + break; } + if (maskObject > 0) xprintf("/Mask %d 0 R\n", maskObject); if (softMaskObject > 0) @@ -2360,7 +2469,7 @@ int QPdfEnginePrivate::writeImage(const QByteArray &data, int width, int height, //qDebug("DCT"); xprintf("/Filter /DCTDecode\n>>\nstream\n"); write(data); - len = data.length(); + len = data.size(); } else { if (do_compress) xprintf("/Filter /FlateDecode\n>>\nstream\n"); @@ -2384,7 +2493,23 @@ struct QGradientBound { }; Q_DECLARE_TYPEINFO(QGradientBound, Q_PRIMITIVE_TYPE); -int QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha) +void QPdfEnginePrivate::ShadingFunctionResult::writeColorSpace(QPdf::ByteStream *stream) const +{ + *stream << "/ColorSpace "; + switch (colorModel) { + case QPdfEngine::ColorModel::RGB: + *stream << "/DeviceRGB\n"; break; + case QPdfEngine::ColorModel::Grayscale: + *stream << "/DeviceGray\n"; break; + case QPdfEngine::ColorModel::CMYK: + *stream << "/DeviceCMYK\n"; break; + case QPdfEngine::ColorModel::Auto: + Q_UNREACHABLE(); break; + } +} + +QPdfEnginePrivate::ShadingFunctionResult +QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha) { QGradientStops stops = gradient->stops(); if (stops.isEmpty()) { @@ -2396,6 +2521,35 @@ int QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from if (stops.at(stops.size() - 1).first < 1) stops.append(QGradientStop(1, stops.at(stops.size() - 1).second)); + // Color to use which colorspace to use + const QColor referenceColor = stops.constFirst().second; + + switch (colorModel) { + case QPdfEngine::ColorModel::RGB: + case QPdfEngine::ColorModel::Grayscale: + case QPdfEngine::ColorModel::CMYK: + break; + case QPdfEngine::ColorModel::Auto: { + // Make sure that all the stops have the same color spec + // (we don't support anything else) + const QColor::Spec referenceSpec = referenceColor.spec(); + bool warned = false; + for (QGradientStop &stop : stops) { + if (stop.second.spec() != referenceSpec) { + if (!warned) { + qWarning("QPdfEngine: unable to create a gradient between colors of different spec"); + warned = true; + } + stop.second = stop.second.convertTo(referenceSpec); + } + } + break; + } + } + + ShadingFunctionResult result; + result.colorModel = colorModelForColor(referenceColor); + QList<int> functions; const int numStops = stops.size(); functions.reserve(numStops - 1); @@ -2411,8 +2565,31 @@ int QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from s << "/C0 [" << stops.at(i).second.alphaF() << "]\n" "/C1 [" << stops.at(i + 1).second.alphaF() << "]\n"; } else { - s << "/C0 [" << stops.at(i).second.redF() << stops.at(i).second.greenF() << stops.at(i).second.blueF() << "]\n" - "/C1 [" << stops.at(i + 1).second.redF() << stops.at(i + 1).second.greenF() << stops.at(i + 1).second.blueF() << "]\n"; + switch (result.colorModel) { + case QPdfEngine::ColorModel::RGB: + s << "/C0 [" << stops.at(i).second.redF() << stops.at(i).second.greenF() << stops.at(i).second.blueF() << "]\n" + "/C1 [" << stops.at(i + 1).second.redF() << stops.at(i + 1).second.greenF() << stops.at(i + 1).second.blueF() << "]\n"; + break; + case QPdfEngine::ColorModel::Grayscale: + s << "/C0 [" << (qGray(stops.at(i).second.rgba()) / 255.) << "]\n" + "/C1 [" << (qGray(stops.at(i + 1).second.rgba()) / 255.) << "]\n"; + break; + case QPdfEngine::ColorModel::CMYK: + s << "/C0 [" << stops.at(i).second.cyanF() + << stops.at(i).second.magentaF() + << stops.at(i).second.yellowF() + << stops.at(i).second.blackF() << "]\n" + "/C1 [" << stops.at(i + 1).second.cyanF() + << stops.at(i + 1).second.magentaF() + << stops.at(i + 1).second.yellowF() + << stops.at(i + 1).second.blackF() << "]\n"; + break; + + case QPdfEngine::ColorModel::Auto: + Q_UNREACHABLE(); + break; + } + } s << ">>\n" "endobj\n"; @@ -2480,7 +2657,8 @@ int QPdfEnginePrivate::createShadingFunction(const QGradient *gradient, int from } else { function = functions.at(0); } - return function; + result.function = function; + return result; } int QPdfEnginePrivate::generateLinearGradientShader(const QLinearGradient *gradient, const QTransform &matrix, bool alpha) @@ -2526,17 +2704,22 @@ int QPdfEnginePrivate::generateLinearGradientShader(const QLinearGradient *gradi } } - int function = createShadingFunction(gradient, from, to, reflect, alpha); + const auto shadingFunctionResult = createShadingFunction(gradient, from, to, reflect, alpha); QByteArray shader; QPdf::ByteStream s(&shader); s << "<<\n" - "/ShadingType 2\n" - "/ColorSpace " << (alpha ? "/DeviceGray\n" : "/DeviceRGB\n") << - "/AntiAlias true\n" + "/ShadingType 2\n"; + + if (alpha) + s << "/ColorSpace /DeviceGray\n"; + else + shadingFunctionResult.writeColorSpace(&s); + + s << "/AntiAlias true\n" "/Coords [" << start.x() << start.y() << stop.x() << stop.y() << "]\n" "/Extend [true true]\n" - "/Function " << function << "0 R\n" + "/Function " << shadingFunctionResult.function << "0 R\n" ">>\n" "endobj\n"; int shaderObject = addXrefEntry(-1); @@ -2594,18 +2777,23 @@ int QPdfEnginePrivate::generateRadialGradientShader(const QRadialGradient *gradi } } - int function = createShadingFunction(gradient, from, to, reflect, alpha); + const auto shadingFunctionResult = createShadingFunction(gradient, from, to, reflect, alpha); QByteArray shader; QPdf::ByteStream s(&shader); s << "<<\n" - "/ShadingType 3\n" - "/ColorSpace " << (alpha ? "/DeviceGray\n" : "/DeviceRGB\n") << - "/AntiAlias true\n" + "/ShadingType 3\n"; + + if (alpha) + s << "/ColorSpace /DeviceGray\n"; + else + shadingFunctionResult.writeColorSpace(&s); + + s << "/AntiAlias true\n" "/Domain [0 1]\n" "/Coords [" << p0.x() << p0.y() << r0 << p1.x() << p1.y() << r1 << "]\n" "/Extend [true true]\n" - "/Function " << function << "0 R\n" + "/Function " << shadingFunctionResult.function << "0 R\n" ">>\n" "endobj\n"; int shaderObject = addXrefEntry(-1); @@ -2691,7 +2879,7 @@ int QPdfEnginePrivate::gradientBrush(const QBrush &b, const QTransform &matrix, "/Shading << /Shader" << alphaShaderObject << alphaShaderObject << "0 R >>\n" ">>\n"; - f << "/Length " << content.length() << "\n" + f << "/Length " << content.size() << "\n" ">>\n" "stream\n" << content @@ -2742,17 +2930,23 @@ int QPdfEnginePrivate::addBrushPattern(const QTransform &m, bool *specifyColor, *specifyColor = true; *gStateObject = 0; - QTransform matrix = m; + const Qt::BrushStyle style = brush.style(); + const bool isCosmetic = style >= Qt::Dense1Pattern && style <= Qt::DiagCrossPattern + && !q->painter()->testRenderHint(QPainter::NonCosmeticBrushPatterns); + QTransform matrix; + if (!isCosmetic) + matrix = m; matrix.translate(brushOrigin.x(), brushOrigin.y()); matrix = matrix * pageMatrix(); - //qDebug() << brushOrigin << matrix; - Qt::BrushStyle style = brush.style(); if (style == Qt::LinearGradientPattern || style == Qt::RadialGradientPattern) {// && style <= Qt::ConicalGradientPattern) { *specifyColor = false; return gradientBrush(brush, matrix, gStateObject); } + if (!isCosmetic) + matrix = brush.transform() * matrix; + if ((!brush.isOpaque() && brush.style() < Qt::LinearGradientPattern) || opacity != 1.0) *gStateObject = addConstantAlphaObject(qRound(brush.color().alpha() * opacity), qRound(pen.color().alpha() * opacity)); @@ -2803,7 +2997,7 @@ int QPdfEnginePrivate::addBrushPattern(const QTransform &m, bool *specifyColor, s << "/XObject << /Im" << imageObject << ' ' << imageObject << "0 R >> "; } s << ">>\n" - "/Length " << pattern.length() << "\n" + "/Length " << pattern.size() << "\n" ">>\n" "stream\n" << pattern @@ -2838,6 +3032,7 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, QImage image = img; QImage::Format format = image.format(); + const bool grayscale = (colorModel == QPdfEngine::ColorModel::Grayscale); if (pdfVersion == QPdfEngine::Version_A1b) { if (image.hasAlphaChannel()) { @@ -2861,7 +3056,7 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, format = QImage::Format_Mono; } else { *bitmap = false; - if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32) { + if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32 && format != QImage::Format_CMYK8888) { image = image.convertToFormat(QImage::Format_ARGB32); format = QImage::Format_ARGB32; } @@ -2869,7 +3064,6 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, int w = image.width(); int h = image.height(); - int d = image.depth(); if (format == QImage::Format_Mono) { int bytesPerLine = (w + 7) >> 3; @@ -2880,7 +3074,7 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, memcpy(rawdata, image.constScanLine(y), bytesPerLine); rawdata += bytesPerLine; } - object = writeImage(data, w, h, d, 0, 0, false, is_monochrome(img.colorTable())); + object = writeImage(data, w, h, WriteImageOption::Monochrome, 0, 0, false, is_monochrome(img.colorTable())); } else { QByteArray softMaskData; bool dct = false; @@ -2892,10 +3086,14 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, QBuffer buffer(&imageData); QImageWriter writer(&buffer, "jpeg"); writer.setQuality(94); + if (format == QImage::Format_CMYK8888) { + // PDFs require CMYK colors not to be inverted in the JPEG encoding + writer.setSubType("CMYK"); + } writer.write(image); dct = true; - if (format != QImage::Format_RGB32) { + if (format != QImage::Format_RGB32 && format != QImage::Format_CMYK8888) { softMaskData.resize(w * h); uchar *sdata = (uchar *)softMaskData.data(); for (int y = 0; y < h; ++y) { @@ -2910,41 +3108,59 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, } } } else { - imageData.resize(grayscale ? w * h : 3 * w * h); - uchar *data = (uchar *)imageData.data(); - softMaskData.resize(w * h); - uchar *sdata = (uchar *)softMaskData.data(); - for (int y = 0; y < h; ++y) { - const QRgb *rgb = (const QRgb *)image.constScanLine(y); + if (format == QImage::Format_CMYK8888) { + imageData.resize(grayscale ? w * h : w * h * 4); + uchar *data = (uchar *)imageData.data(); + const qsizetype bytesPerLine = image.bytesPerLine(); if (grayscale) { - for (int x = 0; x < w; ++x) { - *(data++) = qGray(*rgb); - uchar alpha = qAlpha(*rgb); - *sdata++ = alpha; - hasMask |= (alpha < 255); - hasAlpha |= (alpha != 0 && alpha != 255); - ++rgb; + for (int y = 0; y < h; ++y) { + const uint *cmyk = (const uint *)image.constScanLine(y); + for (int x = 0; x < w; ++x) + *data++ = qGray(QCmyk32::fromCmyk32(*cmyk++).toColor().rgba()); } } else { - for (int x = 0; x < w; ++x) { - *(data++) = qRed(*rgb); - *(data++) = qGreen(*rgb); - *(data++) = qBlue(*rgb); - uchar alpha = qAlpha(*rgb); - *sdata++ = alpha; - hasMask |= (alpha < 255); - hasAlpha |= (alpha != 0 && alpha != 255); - ++rgb; + for (int y = 0; y < h; ++y) { + uchar *start = data + y * w * 4; + memcpy(start, image.constScanLine(y), bytesPerLine); + } + } + } else { + imageData.resize(grayscale ? w * h : 3 * w * h); + uchar *data = (uchar *)imageData.data(); + softMaskData.resize(w * h); + uchar *sdata = (uchar *)softMaskData.data(); + for (int y = 0; y < h; ++y) { + const QRgb *rgb = (const QRgb *)image.constScanLine(y); + if (grayscale) { + for (int x = 0; x < w; ++x) { + *(data++) = qGray(*rgb); + uchar alpha = qAlpha(*rgb); + *sdata++ = alpha; + hasMask |= (alpha < 255); + hasAlpha |= (alpha != 0 && alpha != 255); + ++rgb; + } + } else { + for (int x = 0; x < w; ++x) { + *(data++) = qRed(*rgb); + *(data++) = qGreen(*rgb); + *(data++) = qBlue(*rgb); + uchar alpha = qAlpha(*rgb); + *sdata++ = alpha; + hasMask |= (alpha < 255); + hasAlpha |= (alpha != 0 && alpha != 255); + ++rgb; + } } } } - if (format == QImage::Format_RGB32) + if (format == QImage::Format_RGB32 || format == QImage::Format_CMYK8888) hasAlpha = hasMask = false; } int maskObject = 0; int softMaskObject = 0; if (hasAlpha) { - softMaskObject = writeImage(softMaskData, w, h, 8, 0, 0); + softMaskObject = writeImage(softMaskData, w, h, WriteImageOption::Grayscale, 0, 0); } else if (hasMask) { // dither the soft mask to 1bit and add it. This also helps PDF viewers // without transparency support @@ -2960,9 +3176,18 @@ int QPdfEnginePrivate::addImage(const QImage &img, bool *bitmap, bool lossless, } mdata += bytesPerLine; } - maskObject = writeImage(mask, w, h, 1, 0, 0); + maskObject = writeImage(mask, w, h, WriteImageOption::Monochrome, 0, 0); } - object = writeImage(imageData, w, h, grayscale ? 8 : 32, + + const WriteImageOption option = [&]() { + if (grayscale) + return WriteImageOption::Grayscale; + if (format == QImage::Format_CMYK8888) + return WriteImageOption::CMYK; + return WriteImageOption::RGB; + }(); + + object = writeImage(imageData, w, h, option, maskObject, softMaskObject, dct); } imageCache.insert(serial_no, object); @@ -2973,7 +3198,9 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti) { Q_Q(QPdfEngine); - if (ti.charFormat.isAnchor()) { + const bool isLink = ti.charFormat.hasProperty(QTextFormat::AnchorHref); + const bool isAnchor = ti.charFormat.hasProperty(QTextFormat::AnchorName); + if (isLink || isAnchor) { qreal size = ti.fontEngine->fontDef.pixelSize; int synthesized = ti.fontEngine->synthesized(); qreal stretch = synthesized & QFontEngine::SynthesizedStretch ? ti.fontEngine->fontDef.stretch/100. : 1.; @@ -2993,32 +3220,47 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti) trans.map(0, 0, &x1, &y1); trans.map(ti.width.toReal()/size, (ti.ascent.toReal()-ti.descent.toReal())/size, &x2, &y2); - uint annot = addXrefEntry(-1); - QByteArray x1s, y1s, x2s, y2s; - x1s.setNum(static_cast<double>(x1), 'f'); - y1s.setNum(static_cast<double>(y1), 'f'); - x2s.setNum(static_cast<double>(x2), 'f'); - y2s.setNum(static_cast<double>(y2), 'f'); - QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s; - xprintf("<<\n/Type /Annot\n/Subtype /Link\n"); - - if (pdfVersion == QPdfEngine::Version_A1b) - xprintf("/F 4\n"); // enable print flag, disable all other - - xprintf("/Rect ["); - xprintf(rectData.constData()); + if (isLink) { + uint annot = addXrefEntry(-1); + QByteArray x1s, y1s, x2s, y2s; + x1s.setNum(static_cast<double>(x1), 'f'); + y1s.setNum(static_cast<double>(y1), 'f'); + x2s.setNum(static_cast<double>(x2), 'f'); + y2s.setNum(static_cast<double>(y2), 'f'); + QByteArray rectData = x1s + ' ' + y1s + ' ' + x2s + ' ' + y2s; + xprintf("<<\n/Type /Annot\n/Subtype /Link\n"); + + if (pdfVersion == QPdfEngine::Version_A1b) + xprintf("/F 4\n"); // enable print flag, disable all other + + xprintf("/Rect ["); + xprintf(rectData.constData()); #ifdef Q_DEBUG_PDF_LINKS - xprintf("]\n/Border [16 16 1]\n/A <<\n"); + xprintf("]\n/Border [16 16 1]\n"); #else - xprintf("]\n/Border [0 0 0]\n/A <<\n"); + xprintf("]\n/Border [0 0 0]\n"); #endif - xprintf("/Type /Action\n/S /URI\n/URI (%s)\n", - ti.charFormat.anchorHref().toLatin1().constData()); - xprintf(">>\n>>\n"); - xprintf("endobj\n"); + const QString link = ti.charFormat.anchorHref(); + const bool isInternal = link.startsWith(QLatin1Char('#')); + if (!isInternal) { + xprintf("/A <<\n"); + xprintf("/Type /Action\n/S /URI\n/URI (%s)\n", link.toLatin1().constData()); + xprintf(">>\n"); + } else { + xprintf("/Dest "); + printString(link.sliced(1)); + xprintf("\n"); + } + xprintf(">>\n"); + xprintf("endobj\n"); - if (!currentPage->annotations.contains(annot)) { - currentPage->annotations.append(annot); + if (!currentPage->annotations.contains(annot)) { + currentPage->annotations.append(annot); + } + } else { + const QString anchor = ti.charFormat.anchorNames().constFirst(); + const uint curPage = pages.last(); + destCache.append(DestInfo({ anchor, curPage, QPointF(x1, y2) })); } } @@ -3100,7 +3342,7 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti) x += .3*y; x /= stretch; char buf[5]; - int g = font->addGlyph(glyphs[i]); + qsizetype g = font->addGlyph(glyphs[i]); *currentPage << x - last_x << last_y - y << "Td <" << QPdf::toHex((ushort)g, buf) << "> Tj\n"; last_x = x; @@ -3120,7 +3362,7 @@ void QPdfEnginePrivate::drawTextItem(const QPointF &p, const QTextItemInt &ti) x += .3*y; x /= stretch; char buf[5]; - int g = font->addGlyph(glyphs[i]); + qsizetype g = font->addGlyph(glyphs[i]); *currentPage << x - last_x << last_y - y << "Td <" << QPdf::toHex((ushort)g, buf) << "> Tj\n"; last_x = x; |