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>
This commit is contained in:
Giuseppe D'Angelo 2023-10-18 18:42:05 +02:00
parent efc579145e
commit 0092f06a64
8 changed files with 324 additions and 51 deletions

View File

@ -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()) {

View File

@ -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;

View File

@ -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"

View File

@ -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;

View File

@ -249,9 +249,12 @@ void QCupsPrintEnginePrivate::changePrinter(const QString &newPrinter)
duplex = m_printDevice.defaultDuplexMode();
duplexRequestedExplicitly = false;
}
QPrint::ColorMode colorMode = grayscale ? QPrint::GrayScale : QPrint::Color;
if (!m_printDevice.supportedColorModes().contains(colorMode))
grayscale = m_printDevice.defaultColorMode() == QPrint::GrayScale;
QPrint::ColorMode colorMode = static_cast<QPrint::ColorMode>(printerColorMode());
if (!m_printDevice.supportedColorModes().contains(colorMode)) {
colorModel = (m_printDevice.defaultColorMode() == QPrint::GrayScale)
? QPdfEngine::ColorModel::Grayscale
: QPdfEngine::ColorModel::RGB;
}
// Get the equivalent page size for this printer as supported names may be different
if (m_printDevice.supportedPageSize(m_pageLayout.pageSize()).isValid())

View File

@ -68,6 +68,7 @@ namespace QPrint {
DuplexShortSide
};
// Note: Keep in sync with QPrinter::ColorMode
enum ColorMode {
GrayScale,
Color

View File

@ -104,7 +104,14 @@ void QPdfPrintEngine::setProperty(PrintEnginePropertyKey key, const QVariant &va
d->collate = value.toBool();
break;
case PPK_ColorMode:
d->grayscale = (QPrinter::ColorMode(value.toInt()) == QPrinter::GrayScale);
switch (QPrinter::ColorMode(value.toInt())) {
case QPrinter::GrayScale:
d->colorModel = QPdfEngine::ColorModel::Grayscale;
break;
case QPrinter::Color:
d->colorModel = QPdfEngine::ColorModel::RGB;
break;
}
break;
case PPK_Creator:
d->creator = value.toString();
@ -221,7 +228,7 @@ QVariant QPdfPrintEngine::property(PrintEnginePropertyKey key) const
ret = d->collate;
break;
case PPK_ColorMode:
ret = d->grayscale ? QPrinter::GrayScale : QPrinter::Color;
ret = d->printerColorMode();
break;
case PPK_Creator:
ret = d->creator;
@ -367,6 +374,22 @@ QPdfPrintEnginePrivate::~QPdfPrintEnginePrivate()
{
}
QPrinter::ColorMode QPdfPrintEnginePrivate::printerColorMode() const
{
switch (colorModel) {
case QPdfEngine::ColorModel::RGB:
case QPdfEngine::ColorModel::CMYK:
case QPdfEngine::ColorModel::Auto:
return QPrinter::Color;
case QPdfEngine::ColorModel::Grayscale:
return QPrinter::GrayScale;
}
Q_UNREACHABLE();
return QPrinter::Color;
}
QT_END_NAMESPACE
#endif // QT_NO_PRINTER

View File

@ -79,6 +79,8 @@ public:
QPdfPrintEnginePrivate(QPrinter::PrinterMode m);
~QPdfPrintEnginePrivate();
QPrinter::ColorMode printerColorMode() const;
virtual bool openPrintDevice();
virtual void closePrintDevice();