diff options
author | Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> | 2023-10-18 18:42:05 +0200 |
---|---|---|
committer | Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> | 2024-02-02 16:06:45 +0100 |
commit | 0092f06a648ba29c9643ae19a88ed2eb6ea1300f (patch) | |
tree | 2923ebdfc8023b8da36faf0e4c91d99808cde93e /src/gui/painting | |
parent | efc579145ef33655a4aaad5a41124cb103bfbe7a (diff) |
Add CMYK support for pens/fills in the PDF engine
Insofar, painting with a CMYK color (pen/brush) was completely ignored
by QPdfWriter, although the PDF format can faithfully represent CMYK
colors.
This commit adds support for CMYK colors in the PDF engine. The support
is opt-in, in the name of backwards compatibility; an enumeration on
QPdfWriter controls the output.
QPrinter was using a hidden hook in QPdfEngine in order to do grayscale
printing; this hook can now be made public API through the same
enumeration.
This work has been kindly sponsored by the QGIS project
(https://qgis.org/).
[ChangeLog][QtGui][QPdfWriter] QPdfWriter can now use CMYK colors
directly, without converting them into RGB colors.
Change-Id: Ia27c19ec81a58ab68ddc8b9c89c4e57d7d637301
Reviewed-by: Lars Knoll <lars@knoll.priv.no>
Diffstat (limited to 'src/gui/painting')
-rw-r--r-- | src/gui/painting/qpdf.cpp | 241 | ||||
-rw-r--r-- | src/gui/painting/qpdf_p.h | 37 | ||||
-rw-r--r-- | src/gui/painting/qpdfwriter.cpp | 46 | ||||
-rw-r--r-- | src/gui/painting/qpdfwriter.h | 12 |
4 files changed, 290 insertions, 46 deletions
diff --git a/src/gui/painting/qpdf.cpp b/src/gui/painting/qpdf.cpp index d49249b1dd..f44a95ae7c 100644 --- a/src/gui/painting/qpdf.cpp +++ b/src/gui/painting/qpdf.cpp @@ -44,7 +44,7 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -inline QPaintEngine::PaintEngineFeatures qt_pdf_decide_features() +constexpr QPaintEngine::PaintEngineFeatures qt_pdf_decide_features() { QPaintEngine::PaintEngineFeatures f = QPaintEngine::AllFeatures; f &= ~(QPaintEngine::PorterDuff @@ -1239,17 +1239,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; @@ -1303,18 +1294,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"; @@ -1454,9 +1436,9 @@ int QPdfEngine::metric(QPaintDevice::PaintDeviceMetric metricType) const QPdfEnginePrivate::QPdfEnginePrivate() : clipEnabled(false), allClipped(false), hasPen(true), hasBrush(false), simplePen(false), needsTransform(false), pdfVersion(QPdfEngine::Version_1_4), + colorModel(QPdfEngine::ColorModel::RGB), outDevice(nullptr), ownsDevice(false), embedFonts(true), - grayscale(false), m_pageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(10, 10, 10, 10)) { initResources(); @@ -1511,7 +1493,9 @@ bool QPdfEngine::begin(QPaintDevice *pdev) 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; @@ -1629,10 +1613,99 @@ 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: + case QPdfEngine::ColorModel::Grayscale: + 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::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 << gray << 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() @@ -2078,12 +2151,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)); @@ -2396,7 +2475,22 @@ 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: + case QPdfEngine::ColorModel::Grayscale: + *stream << "/DeviceRGB\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()) { @@ -2408,6 +2502,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); @@ -2423,8 +2546,30 @@ 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: + case QPdfEngine::ColorModel::Grayscale: + // For backwards compatibility, Grayscale emits RGB colors + 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::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"; @@ -2492,7 +2637,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) @@ -2538,17 +2684,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); @@ -2606,18 +2757,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); @@ -2856,6 +3012,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()) { diff --git a/src/gui/painting/qpdf_p.h b/src/gui/painting/qpdf_p.h index 2c70ddf664..b97d0df31f 100644 --- a/src/gui/painting/qpdf_p.h +++ b/src/gui/painting/qpdf_p.h @@ -142,7 +142,7 @@ public: }; QPdfEngine(); - QPdfEngine(QPdfEnginePrivate &d); + explicit QPdfEngine(QPdfEnginePrivate &d); ~QPdfEngine() {} void setOutputFilename(const QString &filename); @@ -157,6 +157,18 @@ public: void addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType); + // keep in sync with QPdfWriter + enum class ColorModel + { + RGB, + Grayscale, + CMYK, + Auto, + }; + + ColorModel colorModel() const; + void setColorModel(ColorModel model); + // reimplementations QPaintEngine bool begin(QPaintDevice *pdev) override; bool end() override; @@ -240,6 +252,7 @@ public: bool needsTransform; qreal opacity; QPdfEngine::PdfVersion pdfVersion; + QPdfEngine::ColorModel colorModel; QHash<QFontEngine::FaceId, QFontSubset *> fonts; @@ -255,7 +268,6 @@ public: QString creator; bool embedFonts; int resolution; - bool grayscale; // Page layout: size, orientation and margins QPageLayout m_pageLayout; @@ -265,8 +277,22 @@ private: int generateGradientShader(const QGradient *gradient, const QTransform &matrix, bool alpha = false); int generateLinearGradientShader(const QLinearGradient *lg, const QTransform &matrix, bool alpha); int generateRadialGradientShader(const QRadialGradient *gradient, const QTransform &matrix, bool alpha); - int createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha); + struct ShadingFunctionResult + { + int function; + QPdfEngine::ColorModel colorModel; + void writeColorSpace(QPdf::ByteStream *stream) const; + }; + ShadingFunctionResult createShadingFunction(const QGradient *gradient, int from, int to, bool reflect, bool alpha); + + enum class ColorDomain { + Stroking, + NonStroking, + NonStrokingPattern, + }; + QPdfEngine::ColorModel colorModelForColor(const QColor &color) const; + void writeColor(ColorDomain domain, const QColor &color); void writeInfo(); int writeXmpDcumentMetaData(); int writeOutputIntent(); @@ -316,7 +342,10 @@ private: // various PDF objects int pageRoot, namesRoot, destsRoot, attachmentsRoot, catalog, info; - int graphicsState, patternColorSpace; + int graphicsState; + int patternColorSpaceRGB; + int patternColorSpaceGrayscale; + int patternColorSpaceCMYK; QList<uint> pages; QHash<qint64, uint> imageCache; QHash<QPair<uint, uint>, uint > alphaCache; diff --git a/src/gui/painting/qpdfwriter.cpp b/src/gui/painting/qpdfwriter.cpp index f28a460d7c..b3b6f3d310 100644 --- a/src/gui/painting/qpdfwriter.cpp +++ b/src/gui/painting/qpdfwriter.cpp @@ -295,6 +295,52 @@ bool QPdfWriter::newPage() return d->engine->newPage(); } +/*! + \enum QPdfWriter::ColorModel + \since 6.8 + + This enumeration describes the way in which the PDF engine interprets + stroking and filling colors, set as a QPainter's pen or brush (via + QPen and QBrush). + + \value RGB All colors are converted to RGB and saved as such in the + PDF. This is the default. + + \value Grayscale All colors are converted to grayscale. For backwards + compatibility, they are emitted in the PDF output as RGB colors, with + identical quantities of red, green and blue. + + \value CMYK All colors are converted to CMYK and saved as such. + + \value Auto RGB colors are emitted as RGB; CMYK colors are emitted as + CMYK. Colors of any other color spec are converted to RGB. + + \sa QColor, QGradient +*/ + +/*! + \since 6.8 + + Returns the color model used by this PDF writer. + The default is QPdfWriter::ColorModel::RGB. +*/ +QPdfWriter::ColorModel QPdfWriter::colorModel() const +{ + Q_D(const QPdfWriter); + return static_cast<ColorModel>(d->engine->d_func()->colorModel); +} + +/*! + \since 6.8 + + Sets the color model used by this PDF writer to \a model. +*/ +void QPdfWriter::setColorModel(ColorModel model) +{ + Q_D(QPdfWriter); + d->engine->d_func()->colorModel = static_cast<QPdfEngine::ColorModel>(model); +} + QT_END_NAMESPACE #include "moc_qpdfwriter.cpp" diff --git a/src/gui/painting/qpdfwriter.h b/src/gui/painting/qpdfwriter.h index 5885c4ef1a..1a4b607b66 100644 --- a/src/gui/painting/qpdfwriter.h +++ b/src/gui/painting/qpdfwriter.h @@ -44,6 +44,18 @@ public: void addFileAttachment(const QString &fileName, const QByteArray &data, const QString &mimeType = QString()); + enum class ColorModel + { + RGB, + Grayscale, + CMYK, + Auto, + }; + Q_ENUM(ColorModel) + + ColorModel colorModel() const; + void setColorModel(ColorModel model); + protected: QPaintEngine *paintEngine() const override; int metric(PaintDeviceMetric id) const override; |