diff --git a/src/plugins/imageformats/jpeg/qjpeghandler.cpp b/src/plugins/imageformats/jpeg/qjpeghandler.cpp index 6bc7712c61..9fc49104e5 100644 --- a/src/plugins/imageformats/jpeg/qjpeghandler.cpp +++ b/src/plugins/imageformats/jpeg/qjpeghandler.cpp @@ -172,9 +172,14 @@ inline static bool read_jpeg_format(QImage::Format &format, j_decompress_ptr cin format = QImage::Format_Grayscale8; break; case 3: - case 4: format = QImage::Format_RGB32; break; + case 4: + if (cinfo->out_color_space == JCS_CMYK) + format = QImage::Format_CMYK32; + else + format = QImage::Format_RGB32; + break; default: result = false; break; @@ -192,9 +197,14 @@ static bool ensureValidImage(QImage *dest, struct jpeg_decompress_struct *info, format = QImage::Format_Grayscale8; break; case 3: - case 4: format = QImage::Format_RGB32; break; + case 4: + if (info->out_color_space == JCS_CMYK) + format = QImage::Format_CMYK32; + else + format = QImage::Format_RGB32; + break; default: return false; // unsupported format } @@ -206,7 +216,7 @@ static bool read_jpeg_image(QImage *outImage, QSize scaledSize, QRect scaledClipRect, QRect clipRect, int quality, Rgb888ToRgb32Converter converter, - j_decompress_ptr info, struct my_error_mgr* err ) + j_decompress_ptr info, struct my_error_mgr* err, bool invertCMYK) { if (!setjmp(err->setjmp_buffer)) { // -1 means default quality. @@ -348,6 +358,17 @@ static bool read_jpeg_image(QImage *outImage, QRgb *out = (QRgb*)outImage->scanLine(y); converter(out, in, clip.width()); } else if (info->out_color_space == JCS_CMYK) { + uchar *in = rows[0] + clip.x() * 4; + quint32 *out = (quint32*)outImage->scanLine(y); + if (invertCMYK) { + for (int i = 0; i < clip.width(); ++i) { + *out++ = 0xffffffffu - (in[0] | in[1] << 8 | in[2] << 16 | in[3] << 24); + in += 4; + } + } else { + memcpy(out, in, clip.width() * 4); + } +#if 0 // Convert CMYK->RGB. uchar *in = rows[0] + clip.x() * 4; QRgb *out = (QRgb*)outImage->scanLine(y); @@ -357,6 +378,7 @@ static bool read_jpeg_image(QImage *outImage, k * in[2] / 255); in += 4; } +#endif } else if (info->output_components == 1) { // Grayscale. memcpy(outImage->scanLine(y), @@ -493,7 +515,8 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, int sourceQuality, const QString &description, bool optimize, - bool progressive) + bool progressive, + bool invertCMYK) { bool success = false; const QList cmap = image.colorTable(); @@ -538,6 +561,10 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, cinfo.input_components = 1; cinfo.in_color_space = JCS_GRAYSCALE; break; + case QImage::Format_CMYK32: + cinfo.input_components = 4; + cinfo.in_color_space = JCS_CMYK; + break; default: cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; @@ -570,7 +597,7 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, jpeg_start_compress(&cinfo, TRUE); set_text(image, &cinfo, description); - if (cinfo.in_color_space == JCS_RGB) + if (cinfo.in_color_space == JCS_RGB || cinfo.in_color_space == JCS_CMYK) write_icc_profile(image, &cinfo); row_pointer[0] = new uchar[cinfo.image_width*cinfo.input_components]; @@ -654,6 +681,17 @@ static bool do_write_jpeg_image(struct jpeg_compress_struct &cinfo, } } break; + case QImage::Format_CMYK32: { + auto *cmykIn = reinterpret_cast(image.constScanLine(cinfo.next_scanline)); + auto *cmykOut = reinterpret_cast(row); + if (invertCMYK) { + for (int i = 0; i < w; ++i) + cmykOut[i] = 0xffffffffu - cmykIn[i]; + } else { + memcpy(cmykOut, cmykIn, w * 4); + } + break; + } default: { // (Testing shows that this way is actually faster than converting to RGB888 + memcpy) @@ -689,7 +727,8 @@ static bool write_jpeg_image(const QImage &image, int sourceQuality, const QString &description, bool optimize, - bool progressive) + bool progressive, + bool invertCMYK) { // protect these objects from the setjmp/longjmp pair inside // do_write_jpeg_image (by making them non-local). @@ -700,7 +739,7 @@ static bool write_jpeg_image(const QImage &image, const bool success = do_write_jpeg_image(cinfo, row_pointer, image, device, sourceQuality, description, - optimize, progressive); + optimize, progressive, invertCMYK); delete [] row_pointer[0]; return success; @@ -745,6 +784,21 @@ public: QStringList readTexts; QByteArray iccProfile; + // Photoshop historically invertes the quantities in CMYK JPEG files: + // 0 means 100% ink, 255 means no ink. Every reader does the same, + // for compatibility reasons. + // Use such an interpretation by default, but also offer the alternative + // of not inverting the channels. + // This is just a "fancy" API; it could be reduced to a boolean setting + // for CMYK files. + enum class SubType { + Automatic, + Inverted_CMYK, + CMYK, + NSubTypes + }; + SubType subType = SubType::Automatic; + struct jpeg_decompress_struct info; struct my_jpeg_source_mgr * iod_src; struct my_error_mgr err; @@ -759,6 +813,14 @@ public: QJpegHandler *q; }; +static const char SupportedJPEGSubtypes[][14] = { + "Automatic", + "Inverted_CMYK", + "CMYK" +}; + +static_assert(std::size(SupportedJPEGSubtypes) == size_t(QJpegHandlerPrivate::SubType::NSubTypes)); + static bool readExifHeader(QDataStream &stream) { char prefix[6]; @@ -975,7 +1037,8 @@ bool QJpegHandlerPrivate::read(QImage *image) if (state == ReadHeader) { - bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err); + const bool invertCMYK = subType != QJpegHandlerPrivate::SubType::CMYK; + bool success = read_jpeg_image(image, scaledSize, scaledClipRect, clipRect, quality, rgb888ToRgb32ConverterPtr, &info, &err, invertCMYK); if (success) { for (int i = 0; i < readTexts.size()-1; i+=2) image->setText(readTexts.at(i), readTexts.at(i+1)); @@ -1061,13 +1124,14 @@ extern void qt_imageTransform(QImage &src, QImageIOHandler::Transformations orie bool QJpegHandler::write(const QImage &image) { + const bool invertCMYK = d->subType != QJpegHandlerPrivate::SubType::CMYK; if (d->transformation != QImageIOHandler::TransformationNone) { // We don't support writing EXIF headers so apply the transform to the data. QImage img = image; qt_imageTransform(img, d->transformation); - return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive); + return write_jpeg_image(img, device(), d->quality, d->description, d->optimize, d->progressive, invertCMYK); } - return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive); + return write_jpeg_image(image, device(), d->quality, d->description, d->optimize, d->progressive, invertCMYK); } bool QJpegHandler::supportsOption(ImageOption option) const @@ -1078,6 +1142,8 @@ bool QJpegHandler::supportsOption(ImageOption option) const || option == ClipRect || option == Description || option == Size + || option == SubType + || option == SupportedSubTypes || option == ImageFormat || option == OptimizedWrite || option == ProgressiveScanWrite @@ -1101,6 +1167,13 @@ QVariant QJpegHandler::option(ImageOption option) const case Size: d->readJpegHeader(device()); return d->size; + case SubType: + return QByteArray(SupportedJPEGSubtypes[int(d->subType)]); + case SupportedSubTypes: { + QByteArrayList list(std::begin(SupportedJPEGSubtypes), + std::end(SupportedJPEGSubtypes)); + return QVariant::fromValue(list); + } case ImageFormat: d->readJpegHeader(device()); return d->format; @@ -1136,6 +1209,16 @@ void QJpegHandler::setOption(ImageOption option, const QVariant &value) case Description: d->description = value.toString(); break; + case SubType: { + const QByteArray subType = value.toByteArray(); + for (size_t i = 0; i < std::size(SupportedJPEGSubtypes); ++i) { + if (subType == SupportedJPEGSubtypes[i]) { + d->subType = QJpegHandlerPrivate::SubType(i); + break; + } + } + break; + } case OptimizedWrite: d->optimize = value.toBool(); break; diff --git a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp index 6d875ec0ab..4ea874cec0 100644 --- a/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp +++ b/tests/auto/gui/image/qimagereader/tst_qimagereader.cpp @@ -303,25 +303,52 @@ void tst_QImageReader::jpegRgbCmyk() QImage image1(prefix + QLatin1String("YCbCr_cmyk.jpg")); QImage image2(prefix + QLatin1String("YCbCr_cmyk.png")); - if (image1 != image2) { - // first, do some obvious tests - QCOMPARE(image1.height(), image2.height()); - QCOMPARE(image1.width(), image2.width()); - QCOMPARE(image1.format(), image2.format()); - QCOMPARE(image1.format(), QImage::Format_RGB32); + QVERIFY(!image1.isNull()); + QVERIFY(!image2.isNull()); - // compare all the pixels with a slack of 3. This ignores rounding errors - // in libjpeg/libpng, where some versions sacrifice accuracy for speed. - for (int h = 0; h < image1.height(); ++h) { - const uchar *s1 = image1.constScanLine(h); - const uchar *s2 = image2.constScanLine(h); - for (int w = 0; w < image1.width() * 4; ++w) { - if (*s1 != *s2) { - QVERIFY2(qAbs(*s1 - *s2) <= 3, qPrintable(QString("images differ in line %1, col %2 (image1: %3, image2: %4)").arg(h).arg(w).arg(*s1, 0, 16).arg(*s2, 0, 16))); - } - s1++; - s2++; - } + QCOMPARE(image1.height(), image2.height()); + QCOMPARE(image1.width(), image2.width()); + + QCOMPARE(image1.format(), QImage::Format_CMYK32); + QCOMPARE(image2.format(), QImage::Format_RGB32); + + // compare all the pixels with a slack of 3. This ignores rounding errors + // in libjpeg/libpng, where some versions sacrifice accuracy for speed. + const auto fuzzyCompareColors = [](const QColor &c1, const QColor &c2) { + int c1rgba[4]; + int c2rgba[4]; + + c1.getRgb(c1rgba + 0, + c1rgba + 1, + c1rgba + 2, + c1rgba + 3); + + c2.getRgb(c2rgba + 0, + c2rgba + 1, + c2rgba + 2, + c2rgba + 3); + + const auto fuzzyCompare = [](int a, int b) { + return qAbs(a - b) <= 3; + }; + + return fuzzyCompare(c1rgba[0], c2rgba[0]) && + fuzzyCompare(c1rgba[1], c2rgba[1]) && + fuzzyCompare(c1rgba[2], c2rgba[2]) && + fuzzyCompare(c1rgba[3], c2rgba[3]); + }; + + for (int h = 0; h < image1.height(); ++h) { + const uchar *sl1 = image1.constScanLine(h); + const uchar *sl2 = image2.constScanLine(h); + for (int w = 0; w < image1.width(); ++w) { + const uchar *s1 = sl1 + w * 4; + const uchar *s2 = sl2 + w * 4; + + QColor c1 = QColor::fromCmyk(s1[0], s1[1], s1[2], s1[3]); + QColor c2 = QColor::fromRgb(s2[2], s2[1], s2[0]); + QVERIFY2(fuzzyCompareColors(c1, c2), + qPrintable(QString("images differ in line %1, col %2").arg(h).arg(w))); } } } @@ -589,7 +616,7 @@ void tst_QImageReader::imageFormat_data() QTest::newRow("ppm-4") << QString("test.ppm") << QByteArray("ppm") << QImage::Format_RGB32; QTest::newRow("jpeg-1") << QString("beavis.jpg") << QByteArray("jpeg") << QImage::Format_Grayscale8; - QTest::newRow("jpeg-2") << QString("YCbCr_cmyk.jpg") << QByteArray("jpeg") << QImage::Format_RGB32; + QTest::newRow("jpeg-2") << QString("YCbCr_cmyk.jpg") << QByteArray("jpeg") << QImage::Format_CMYK32; QTest::newRow("jpeg-3") << QString("YCbCr_rgb.jpg") << QByteArray("jpeg") << QImage::Format_RGB32; QTest::newRow("gif-1") << QString("earth.gif") << QByteArray("gif") << QImage::Format_Invalid;