diff --git a/src/gui/image/qimage.cpp b/src/gui/image/qimage.cpp index 08798c46645..9d9aa316494 100644 --- a/src/gui/image/qimage.cpp +++ b/src/gui/image/qimage.cpp @@ -5000,6 +5000,9 @@ void QImage::setColorSpace(const QColorSpace &colorSpace) return; if (d->colorSpace == colorSpace) return; + if (colorSpace.isValid() && !qt_compatibleColorModel(pixelFormat().colorModel(), colorSpace.colorModel())) + return; + detachMetadata(false); if (d) d->colorSpace = colorSpace; @@ -5012,13 +5015,14 @@ void QImage::setColorSpace(const QColorSpace &colorSpace) If the image has no valid color space, the method does nothing. + \note If \a colorSpace is not compatible with the current format, the image + will be converted to one that is. + \sa convertedToColorSpace(), setColorSpace() */ void QImage::convertToColorSpace(const QColorSpace &colorSpace) { - if (!d) - return; - if (!d->colorSpace.isValid()) + if (!d || !d->colorSpace.isValid()) return; if (!colorSpace.isValidTarget()) { qWarning() << "QImage::convertToColorSpace: Output colorspace is not valid"; @@ -5026,10 +5030,46 @@ void QImage::convertToColorSpace(const QColorSpace &colorSpace) } if (d->colorSpace == colorSpace) return; + if (!qt_compatibleColorModel(pixelFormat().colorModel(), colorSpace.colorModel())) { + *this = convertedToColorSpace(colorSpace); + return; + } applyColorTransform(d->colorSpace.transformationToColorSpace(colorSpace)); d->colorSpace = colorSpace; } +/*! + \since 6.8 + + Converts the image to \a colorSpace and \a format. + + If the image has no valid color space, the method does nothing, + nor if the color space is not compatible with with the format. + + The specified image conversion \a flags control how the image data + is handled during the format conversion process. + + \sa convertedToColorSpace(), setColorSpace() +*/ +void QImage::convertToColorSpace(const QColorSpace &colorSpace, QImage::Format format, Qt::ImageConversionFlags flags) +{ + if (!d || !d->colorSpace.isValid()) + return; + if (!colorSpace.isValidTarget()) { + qWarning() << "QImage::convertToColorSpace: Output colorspace is not valid"; + return; + } + if (!qt_compatibleColorModel(toPixelFormat(format).colorModel(), colorSpace.colorModel())) { + qWarning() << "QImage::convertToColorSpace: Color space is not compatible with format"; + return; + } + + if (d->colorSpace == colorSpace) + return convertTo(format, flags); + applyColorTransform(d->colorSpace.transformationToColorSpace(colorSpace), format, flags); + d->colorSpace = colorSpace; +} + /*! \since 5.14 @@ -5037,13 +5077,16 @@ void QImage::convertToColorSpace(const QColorSpace &colorSpace) If the image has no valid color space, a null QImage is returned. - \sa convertToColorSpace() + \note If \a colorSpace is not compatible with the current format, + the returned image will also be converted to a format this is. + For more control over returned image format, see the three argument + overload of this method. + + \sa convertToColorSpace(), colorTransformed() */ QImage QImage::convertedToColorSpace(const QColorSpace &colorSpace) const { - if (!d) - return QImage(); - if (!d->colorSpace.isValid()) + if (!d || !d->colorSpace.isValid()) return QImage(); if (!colorSpace.isValidTarget()) { qWarning() << "QImage::convertedToColorSpace: Output colorspace is not valid"; @@ -5051,8 +5094,39 @@ QImage QImage::convertedToColorSpace(const QColorSpace &colorSpace) const } if (d->colorSpace == colorSpace) return *this; - QImage image = copy(); - image.convertToColorSpace(colorSpace); + QImage image = colorTransformed(d->colorSpace.transformationToColorSpace(colorSpace)); + image.setColorSpace(colorSpace); + return image; +} + +/*! + \since 6.8 + + Returns the image converted to \a colorSpace and \a format. + + If the image has no valid color space, a null QImage is returned. + + The specified image conversion \a flags control how the image data + is handled during the format conversion process. + + \sa colorTransformed() +*/ +QImage QImage::convertedToColorSpace(const QColorSpace &colorSpace, QImage::Format format, Qt::ImageConversionFlags flags) const +{ + if (!d || !d->colorSpace.isValid()) + return QImage(); + if (!colorSpace.isValidTarget()) { + qWarning() << "QImage::convertedToColorSpace: Output colorspace is not valid"; + return QImage(); + } + if (!qt_compatibleColorModel(toPixelFormat(format).colorModel(), colorSpace.colorModel())) { + qWarning() << "QImage::convertedToColorSpace: Color space is not compatible with format"; + return QImage(); + } + if (d->colorSpace == colorSpace) + return convertedTo(format, flags); + QImage image = colorTransformed(d->colorSpace.transformationToColorSpace(colorSpace), format, flags); + image.setColorSpace(colorSpace); return image; } @@ -5095,7 +5169,8 @@ void QImage::applyColorTransform(const QColorTransform &transform) && oldFormat != QImage::Format_RGBA64_Premultiplied) convertTo(QImage::Format_RGBA64); } else if (oldFormat != QImage::Format_ARGB32 && oldFormat != QImage::Format_RGB32 - && oldFormat != QImage::Format_ARGB32_Premultiplied) { + && oldFormat != QImage::Format_ARGB32_Premultiplied + && oldFormat != QImage::Format_Grayscale8 && oldFormat != QImage::Format_Grayscale16) { if (hasAlphaChannel()) convertTo(QImage::Format_ARGB32); else @@ -5109,6 +5184,8 @@ void QImage::applyColorTransform(const QColorTransform &transform) case Format_RGBA32FPx4_Premultiplied: flags = QColorTransformPrivate::Premultiplied; break; + case Format_Grayscale8: + case Format_Grayscale16: case Format_RGB32: case Format_RGBX64: case Format_RGBX32FPx4: @@ -5124,7 +5201,21 @@ void QImage::applyColorTransform(const QColorTransform &transform) std::function transformSegment; - if (qt_fpColorPrecision(format())) { + if (format() == Format_Grayscale8) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + uint8_t *scanline = reinterpret_cast(d->data + y * d->bytes_per_line); + QColorTransformPrivate::get(transform)->applyGray(scanline, scanline, width(), flags); + } + }; + } else if (format() == Format_Grayscale16) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + uint16_t *scanline = reinterpret_cast(d->data + y * d->bytes_per_line); + QColorTransformPrivate::get(transform)->applyGray(scanline, scanline, width(), flags); + } + }; + } else if (qt_fpColorPrecision(format())) { transformSegment = [&](int yStart, int yEnd) { for (int y = yStart; y < yEnd; ++y) { QRgbaFloat32 *scanline = reinterpret_cast(d->data + y * d->bytes_per_line); @@ -5171,24 +5262,389 @@ void QImage::applyColorTransform(const QColorTransform &transform) *this = std::move(*this).convertToFormat(oldFormat); } +/*! + \since 6.8 + + Applies the color transformation \a transform to all pixels in the image, and converts the format of the image to \a toFormat. + + The specified image conversion \a flags control how the image data + is handled during the format conversion process. +*/ +void QImage::applyColorTransform(const QColorTransform &transform, QImage::Format toFormat, Qt::ImageConversionFlags flags) +{ + if (!d) + return; + if (transform.isIdentity()) + return convertTo(toFormat, flags); + + *this = colorTransformed(transform, toFormat, flags); +} + /*! \since 6.4 Returns the image color transformed using \a transform on all pixels in the image. + \note If \a transform has a source color space which is incompatible with the format of this image, + returns a null QImage. If \a transform has a target color space which is incompatible with the format + of this image, the image will also be converted to a compatible format. For more control about the + choice of the target pixel format, see the three argument overload of this method. + \sa applyColorTransform() */ QImage QImage::colorTransformed(const QColorTransform &transform) const & { - if (!d || !d->colorSpace.isValid()) + if (!d) return QImage(); if (transform.isIdentity()) return *this; + + QColorSpace::ColorModel inColorModel = QColorTransformPrivate::get(transform)->colorSpaceIn->colorModel; + QColorSpace::ColorModel outColorModel = QColorTransformPrivate::get(transform)->colorSpaceOut->colorModel; + if (!qt_compatibleColorModel(pixelFormat().colorModel(), inColorModel)) { + qWarning() << "QImage::colorTransformed: Invalid input color space for transform"; + return QImage(); + } + if (!qt_compatibleColorModel(pixelFormat().colorModel(), outColorModel)) { + // All model switching transforms are opaque in at least one end. + switch (outColorModel) { + case QColorSpace::ColorModel::Rgb: + return colorTransformed(transform, qt_highColorPrecision(format(), true) ? QImage::Format_RGBX64 : QImage::Format_RGB32); + case QColorSpace::ColorModel::Gray: + return colorTransformed(transform, qt_highColorPrecision(format(), true) ? QImage::Format_Grayscale16 : QImage::Format_Grayscale8); + case QColorSpace::ColorModel::Undefined: + break; + } + return QImage(); + } + QImage image = copy(); image.applyColorTransform(transform); return image; } +static bool isRgb32Data(QImage::Format f) +{ + switch (f) { + case QImage::Format_RGB32: + case QImage::Format_ARGB32: + case QImage::Format_ARGB32_Premultiplied: + return true; + default: + break; + } + return false; +} + +static bool isRgb64Data(QImage::Format f) +{ + switch (f) { + case QImage::Format_RGBX64: + case QImage::Format_RGBA64: + case QImage::Format_RGBA64_Premultiplied: + return true; + default: + break; + } + return false; +} + +static bool isRgb32fpx4Data(QImage::Format f) +{ + switch (f) { + case QImage::Format_RGBX32FPx4: + case QImage::Format_RGBA32FPx4: + case QImage::Format_RGBA32FPx4_Premultiplied: + return true; + default: + break; + } + return false; +} + +/*! + \since 6.8 + + Returns the image color transformed using \a transform on all pixels in the image, returning an image of format \a toFormat. + + The specified image conversion \a flags control how the image data + is handled during the format conversion process. + + \note If \a transform has a source color space which is incompatible with the format of this image, + or a target color space that is incompatible with \a toFormat, returns a null QImage. + + \sa applyColorTransform() +*/ +QImage QImage::colorTransformed(const QColorTransform &transform, QImage::Format toFormat, Qt::ImageConversionFlags flags) const & +{ + if (!d) + return QImage(); + if (toFormat == QImage::Format_Invalid) + toFormat = format(); + if (transform.isIdentity()) + return convertedTo(toFormat, flags); + + QColorSpace::ColorModel inColorModel = QColorTransformPrivate::get(transform)->colorSpaceIn->colorModel; + QColorSpace::ColorModel outColorModel = QColorTransformPrivate::get(transform)->colorSpaceOut->colorModel; + if (!qt_compatibleColorModel(pixelFormat().colorModel(), inColorModel)) { + qWarning() << "QImage::colorTransformed: Invalid input color space for transform"; + return QImage(); + } + if (!qt_compatibleColorModel(toPixelFormat(toFormat).colorModel(), outColorModel)) { + qWarning() << "QImage::colorTransformed: Invalid output color space for transform"; + return QImage(); + } + + QImage fromImage = *this; + + QImage::Format tmpFormat = toFormat; + switch (toFormat) { + case QImage::Format_RGB32: + case QImage::Format_ARGB32: + case QImage::Format_ARGB32_Premultiplied: + case QImage::Format_RGBX32FPx4: + case QImage::Format_RGBA32FPx4: + case QImage::Format_RGBA32FPx4_Premultiplied: + case QImage::Format_RGBX64: + case QImage::Format_RGBA64: + case QImage::Format_RGBA64_Premultiplied: + case QImage::Format_Grayscale8: + case QImage::Format_Grayscale16: + // can be output natively + break; + case QImage::Format_RGB16: + case QImage::Format_RGB444: + case QImage::Format_RGB555: + case QImage::Format_RGB666: + case QImage::Format_RGB888: + case QImage::Format_BGR888: + case QImage::Format_RGBX8888: + case QImage::Format_CMYK8888: + tmpFormat = QImage::Format_RGB32; + break; + case QImage::Format_Mono: + case QImage::Format_MonoLSB: + case QImage::Format_Indexed8: + case QImage::Format_ARGB8565_Premultiplied: + case QImage::Format_ARGB6666_Premultiplied: + case QImage::Format_ARGB8555_Premultiplied: + case QImage::Format_ARGB4444_Premultiplied: + case QImage::Format_RGBA8888: + case QImage::Format_RGBA8888_Premultiplied: + tmpFormat = QImage::Format_ARGB32; + break; + case QImage::Format_BGR30: + case QImage::Format_RGB30: + tmpFormat = QImage::Format_RGBX64; + break; + case QImage::Format_A2BGR30_Premultiplied: + case QImage::Format_A2RGB30_Premultiplied: + tmpFormat = QImage::Format_RGBA64; + break; + case QImage::Format_RGBX16FPx4: + case QImage::Format_RGBA16FPx4: + case QImage::Format_RGBA16FPx4_Premultiplied: + tmpFormat = QImage::Format_RGBA32FPx4; + break; + case QImage::Format_Alpha8: + return convertedTo(QImage::Format_Alpha8); + case QImage::Format_Invalid: + case QImage::NImageFormats: + Q_UNREACHABLE(); + break; + } + QColorSpace::ColorModel inColorData = qt_csColorData(pixelFormat().colorModel()); + QColorSpace::ColorModel outColorData = qt_csColorData(toPixelFormat(toFormat).colorModel()); + // Ensure only precision increasing transforms + if (inColorData != outColorData) { + if (fromImage.format() == QImage::Format_Grayscale8 && outColorData == QColorSpace::ColorModel::Rgb) + tmpFormat = QImage::Format_RGB32; + else if (tmpFormat == QImage::Format_Grayscale8 && qt_highColorPrecision(fromImage.format())) + tmpFormat = QImage::Format_Grayscale16; + else if (fromImage.format() == QImage::Format_Grayscale16 && outColorData == QColorSpace::ColorModel::Rgb) + tmpFormat = QImage::Format_RGBX64; + } else { + if (tmpFormat == QImage::Format_Grayscale8 && fromImage.format() == QImage::Format_Grayscale16) + tmpFormat = QImage::Format_Grayscale16; + else if (qt_fpColorPrecision(fromImage.format()) && !qt_fpColorPrecision(tmpFormat)) + tmpFormat = QImage::Format_RGBA32FPx4; + else if (isRgb32Data(tmpFormat) && qt_highColorPrecision(fromImage.format(), true)) + tmpFormat = QImage::Format_RGBA64; + } + + QImage toImage(size(), tmpFormat); + copyMetadata(&toImage, *this); + + std::function transformSegment; + QColorTransformPrivate::TransformFlags transFlags = QColorTransformPrivate::Unpremultiplied; + + if (inColorData != outColorData) { + // Needs color model switching transform + if (inColorData == QColorSpace::ColorModel::Gray && outColorData == QColorSpace::ColorModel::Rgb) { + // Gray -> RGB + if (format() == QImage::Format_Grayscale8) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const quint8 *in_scanline = reinterpret_cast(d->data + y * d->bytes_per_line); + QRgb *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyGray(out_scanline, in_scanline, width(), QColorTransformPrivate::InputOpaque); + } + }; + } else { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const quint16 *in_scanline = reinterpret_cast(d->data + y * d->bytes_per_line); + QRgba64 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyGray(out_scanline, in_scanline, width(), QColorTransformPrivate::InputOpaque); + } + }; + } + } else if (inColorData == QColorSpace::ColorModel::Rgb && outColorData == QColorSpace::ColorModel::Gray) { + // RGB -> Gray + if (tmpFormat == QImage::Format_Grayscale8) { + fromImage.convertTo(QImage::Format_RGB32); + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const QRgb *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + quint8 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyReturnGray(out_scanline, in_scanline, width(), QColorTransformPrivate::InputOpaque); + } + }; + } else { + fromImage.convertTo(QImage::Format_RGBX64); + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const QRgba64 *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + quint16 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyReturnGray(out_scanline, in_scanline, width(), QColorTransformPrivate::InputOpaque); + } + }; + } + } else { + Q_UNREACHABLE(); + } + } else { + // Conversion on same color model + if (pixelFormat().colorModel() == QPixelFormat::Indexed) { + for (int i = 0; i < d->colortable.size(); ++i) + fromImage.d->colortable[i] = transform.map(d->colortable[i]); + return fromImage.convertedTo(toFormat, flags); + } + + QImage::Format oldFormat = format(); + if (qt_fpColorPrecision(oldFormat)) { + if (oldFormat != QImage::Format_RGBX32FPx4 && oldFormat != QImage::Format_RGBA32FPx4 + && oldFormat != QImage::Format_RGBA32FPx4_Premultiplied) + fromImage.convertTo(QImage::Format_RGBA32FPx4); + } else if (qt_highColorPrecision(oldFormat, true)) { + if (oldFormat != QImage::Format_RGBX64 && oldFormat != QImage::Format_RGBA64 + && oldFormat != QImage::Format_RGBA64_Premultiplied && oldFormat != QImage::Format_Grayscale16) + fromImage.convertTo(QImage::Format_RGBA64); + } else if (oldFormat != QImage::Format_ARGB32 && oldFormat != QImage::Format_RGB32 + && oldFormat != QImage::Format_ARGB32_Premultiplied + && oldFormat != QImage::Format_Grayscale8 && oldFormat != QImage::Format_Grayscale16) { + if (hasAlphaChannel()) + fromImage.convertTo(QImage::Format_ARGB32); + else + fromImage.convertTo(QImage::Format_RGB32); + } + + if (!fromImage.hasAlphaChannel()) + transFlags = QColorTransformPrivate::InputOpaque; + else if (qPixelLayouts[fromImage.format()].premultiplied) + transFlags = QColorTransformPrivate::Premultiplied; + + if (fromImage.format() == Format_Grayscale8) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const quint8 *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + if (tmpFormat == Format_Grayscale8) { + quint8 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyGray(out_scanline, in_scanline, width(), transFlags); + } else { + Q_ASSERT(tmpFormat == Format_Grayscale16); + quint16 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyGray(out_scanline, in_scanline, width(), transFlags); + } + } + }; + } else if (fromImage.format() == Format_Grayscale16) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const quint16 *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + quint16 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->applyGray(out_scanline, in_scanline, width(), transFlags); + } + }; + } else if (isRgb32fpx4Data(fromImage.format())) { + transformSegment = [&](int yStart, int yEnd) { + Q_ASSERT(isRgb32fpx4Data(tmpFormat)); + for (int y = yStart; y < yEnd; ++y) { + const QRgbaFloat32 *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + QRgbaFloat32 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } + }; + } else if (isRgb64Data(fromImage.format())) { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const QRgba64 *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + if (isRgb32fpx4Data(tmpFormat)) { + QRgbaFloat32 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } else { + Q_ASSERT(isRgb64Data(tmpFormat)); + QRgba64 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } + } + }; + } else { + transformSegment = [&](int yStart, int yEnd) { + for (int y = yStart; y < yEnd; ++y) { + const QRgb *in_scanline = reinterpret_cast(fromImage.bits() + y * fromImage.bytesPerLine()); + if (isRgb32fpx4Data(tmpFormat)) { + QRgbaFloat32 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } else if (isRgb64Data(tmpFormat)) { + QRgba64 *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } else { + Q_ASSERT(isRgb32Data(tmpFormat)); + QRgb *out_scanline = reinterpret_cast(toImage.bits() + y * toImage.bytesPerLine()); + QColorTransformPrivate::get(transform)->apply(out_scanline, in_scanline, width(), transFlags); + } + } + }; + } + } + +#if QT_CONFIG(thread) && !defined(Q_OS_WASM) + int segments = (qsizetype(width()) * height()) >> 16; + segments = std::min(segments, height()); + QThreadPool *threadPool = QThreadPoolPrivate::qtGuiInstance(); + if (segments > 1 && threadPool && !threadPool->contains(QThread::currentThread())) { + QSemaphore semaphore; + int y = 0; + for (int i = 0; i < segments; ++i) { + int yn = (height() - y) / (segments - i); + threadPool->start([&, y, yn]() { + transformSegment(y, y + yn); + semaphore.release(1); + }); + y += yn; + } + semaphore.acquire(segments); + } else +#endif + transformSegment(0, height()); + + if (tmpFormat != toFormat) + toImage.convertTo(toFormat); + + return toImage; +} + /*! \since 6.4 \overload @@ -5199,12 +5655,46 @@ QImage QImage::colorTransformed(const QColorTransform &transform) const & */ QImage QImage::colorTransformed(const QColorTransform &transform) && { - if (!d || !d->colorSpace.isValid()) + if (!d) return QImage(); + + QColorSpace::ColorModel inColorModel = QColorTransformPrivate::get(transform)->colorSpaceIn->colorModel; + QColorSpace::ColorModel outColorModel = QColorTransformPrivate::get(transform)->colorSpaceOut->colorModel; + if (!qt_compatibleColorModel(pixelFormat().colorModel(), inColorModel)) { + qWarning() << "QImage::colorTransformed: Invalid input color space for transform"; + return QImage(); + } + if (!qt_compatibleColorModel(pixelFormat().colorModel(), outColorModel)) { + // There is currently no inplace conversion of both colorspace and format, so just use the normal version. + switch (outColorModel) { + case QColorSpace::ColorModel::Rgb: + return colorTransformed(transform, qt_highColorPrecision(format(), true) ? QImage::Format_RGBX64 : QImage::Format_RGB32); + case QColorSpace::ColorModel::Gray: + return colorTransformed(transform, qt_highColorPrecision(format(), true) ? QImage::Format_Grayscale16 : QImage::Format_Grayscale8); + case QColorSpace::ColorModel::Undefined: + break; + } + return QImage(); + } + applyColorTransform(transform); return std::move(*this); } +/*! + \since 6.8 + \overload + + Returns the image color transformed using \a transform on all pixels in the image. + + \sa applyColorTransform() +*/ +QImage QImage::colorTransformed(const QColorTransform &transform, QImage::Format format, Qt::ImageConversionFlags flags) && +{ + // There is currently no inplace conversion of both colorspace and format, so just use the normal version. + return colorTransformed(transform, format, flags); +} + bool QImageData::convertInPlace(QImage::Format newFormat, Qt::ImageConversionFlags flags) { if (format == newFormat) diff --git a/src/gui/image/qimage.h b/src/gui/image/qimage.h index 1208e9ae67b..cba50e5e4cc 100644 --- a/src/gui/image/qimage.h +++ b/src/gui/image/qimage.h @@ -231,13 +231,18 @@ public: void invertPixels(InvertMode = InvertRgb); QColorSpace colorSpace() const; - [[nodiscard]] QImage convertedToColorSpace(const QColorSpace &) const; - void convertToColorSpace(const QColorSpace &); - void setColorSpace(const QColorSpace &); + [[nodiscard]] QImage convertedToColorSpace(const QColorSpace &colorSpace) const; + [[nodiscard]] QImage convertedToColorSpace(const QColorSpace &colorSpace, QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) const; + void convertToColorSpace(const QColorSpace &colorSpace); + void convertToColorSpace(const QColorSpace &colorSpace, QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor); + void setColorSpace(const QColorSpace &colorSpace); QImage colorTransformed(const QColorTransform &transform) const &; + QImage colorTransformed(const QColorTransform &transform, QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) const &; QImage colorTransformed(const QColorTransform &transform) &&; + QImage colorTransformed(const QColorTransform &transform, QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor) &&; void applyColorTransform(const QColorTransform &transform); + void applyColorTransform(const QColorTransform &transform, QImage::Format format, Qt::ImageConversionFlags flags = Qt::AutoColor); bool load(QIODevice *device, const char *format); bool load(const QString &fileName, const char *format = nullptr); diff --git a/src/gui/image/qimage_p.h b/src/gui/image/qimage_p.h index 2edf03a4f2b..9957e93f312 100644 --- a/src/gui/image/qimage_p.h +++ b/src/gui/image/qimage_p.h @@ -437,6 +437,43 @@ inline bool qt_fpColorPrecision(QImage::Format format) return false; } +inline QColorSpace::ColorModel qt_csColorData(QPixelFormat::ColorModel format) +{ + switch (format) { + case QPixelFormat::ColorModel::RGB: + case QPixelFormat::ColorModel::BGR: + case QPixelFormat::ColorModel::Indexed: + return QColorSpace::ColorModel::Rgb; + case QPixelFormat::ColorModel::Alpha: + return QColorSpace::ColorModel::Undefined; // No valid colors + case QPixelFormat::ColorModel::Grayscale: + return QColorSpace::ColorModel::Gray; + default: + break; + } + return QColorSpace::ColorModel::Undefined; +} + +inline bool qt_compatibleColorModel(QPixelFormat::ColorModel data, QColorSpace::ColorModel cs) +{ + QColorSpace::ColorModel dataCs = qt_csColorData(data); + + if (data == QPixelFormat::ColorModel::Alpha) + return true; // Alpha data has no colors and can be handled by any color space + + if (cs == QColorSpace::ColorModel::Undefined || dataCs == QColorSpace::ColorModel::Undefined) + return false; + + if (dataCs == cs) + return true; // Matching color models + + if (cs == QColorSpace::ColorModel::Rgb) + // Can apply RGB CS to Gray data + return dataCs == QColorSpace::ColorModel::Gray; + + return false; +} + inline QImage::Format qt_maybeDataCompatibleAlphaVersion(QImage::Format format) { switch (format) { diff --git a/src/gui/painting/qcolormatrix_p.h b/src/gui/painting/qcolormatrix_p.h index e586e0dadcf..2e6e6b58827 100644 --- a/src/gui/painting/qcolormatrix_p.h +++ b/src/gui/painting/qcolormatrix_p.h @@ -64,6 +64,14 @@ public: constexpr QColorVector operator-(const QColorVector &v) const { return QColorVector(x - v.x, y - v.y, z - v.z); } void operator+=(const QColorVector &v) { x += v.x; y += v.y; z += v.z; } + QPointF toChromaticity() const + { + if (isNull()) + return QPointF(); + float mag = 1.0f / (x + y + z); + return QPointF(x * mag, y * mag); + } + // Common whitepoints: static constexpr QPointF D50Chromaticity() { return QPointF(0.34567, 0.35850); } static constexpr QPointF D65Chromaticity() { return QPointF(0.31271, 0.32902); } @@ -279,6 +287,32 @@ public: { 0.0f, v.y, 0.0f }, { 0.0f, 0.0f, v.z } }; } + static QColorMatrix chromaticAdaptation(const QColorVector &whitePoint) + { + constexpr QColorVector whitePointD50 = QColorVector::D50(); + if (whitePoint != whitePointD50) { + // A chromatic adaptation to map a white point to XYZ D50. + + // The Bradford method chromatic adaptation matrix: + const QColorMatrix abrad = { { 0.8951f, -0.7502f, 0.0389f }, + { 0.2664f, 1.7135f, -0.0685f }, + { -0.1614f, 0.0367f, 1.0296f } }; + const QColorMatrix abradinv = { { 0.9869929f, 0.4323053f, -0.0085287f }, + { -0.1470543f, 0.5183603f, 0.0400428f }, + { 0.1599627f, 0.0492912f, 0.9684867f } }; + + const QColorVector srcCone = abrad.map(whitePoint); + if (srcCone.x && srcCone.y && srcCone.z) { + const QColorVector dstCone = abrad.map(whitePointD50); + const QColorMatrix wToD50 = { { dstCone.x / srcCone.x, 0, 0 }, + { 0, dstCone.y / srcCone.y, 0 }, + { 0, 0, dstCone.z / srcCone.z } }; + return abradinv * (wToD50 * abrad); + } + } + return QColorMatrix::identity(); + } + // These are used to recognize matrices from ICC profiles: static QColorMatrix toXyzFromSRgb() { diff --git a/src/gui/painting/qcolorspace.cpp b/src/gui/painting/qcolorspace.cpp index 334587fd316..044468194ad 100644 --- a/src/gui/painting/qcolorspace.cpp +++ b/src/gui/painting/qcolorspace.cpp @@ -95,34 +95,7 @@ QColorMatrix QColorSpacePrimaries::toXyzMatrix() const toXyz = toXyz * QColorMatrix::fromScale(whiteScale); // But we want a conversion to XYZ relative to D50 - QColorVector wXyzD50 = QColorVector::D50(); - - if (wXyz != wXyzD50) { - // Do chromatic adaptation to map our white point to XYZ D50. - - // The Bradford method chromatic adaptation matrix: - QColorMatrix abrad = { { 0.8951f, -0.7502f, 0.0389f }, - { 0.2664f, 1.7135f, -0.0685f }, - { -0.1614f, 0.0367f, 1.0296f } }; - QColorMatrix abradinv = { { 0.9869929f, 0.4323053f, -0.0085287f }, - { -0.1470543f, 0.5183603f, 0.0400428f }, - { 0.1599627f, 0.0492912f, 0.9684867f } }; - - QColorVector srcCone = abrad.map(wXyz); - QColorVector dstCone = abrad.map(wXyzD50); - - if (srcCone.x && srcCone.y && srcCone.z) { - QColorMatrix wToD50 = { { dstCone.x / srcCone.x, 0, 0 }, - { 0, dstCone.y / srcCone.y, 0 }, - { 0, 0, dstCone.z / srcCone.z } }; - - - QColorMatrix chromaticAdaptation = abradinv * (wToD50 * abrad); - toXyz = chromaticAdaptation * toXyz; - } else { - toXyz.r = {0, 0, 0}; // set to invalid value - } - } + toXyz = QColorMatrix::chromaticAdaptation(wXyz) * toXyz; return toXyz; } @@ -133,6 +106,7 @@ QColorSpacePrivate::QColorSpacePrivate() QColorSpacePrivate::QColorSpacePrivate(QColorSpace::NamedColorSpace namedColorSpace) : namedColorSpace(namedColorSpace) + , colorModel(QColorSpace::ColorModel::Rgb) { switch (namedColorSpace) { case QColorSpace::SRgb: @@ -170,6 +144,7 @@ QColorSpacePrivate::QColorSpacePrivate(QColorSpace::NamedColorSpace namedColorSp QColorSpacePrivate::QColorSpacePrivate(QColorSpace::Primaries primaries, QColorSpace::TransferFunction transferFunction, float gamma) : primaries(primaries) , transferFunction(transferFunction) + , colorModel(QColorSpace::ColorModel::Rgb) , gamma(gamma) { identifyColorSpace(); @@ -181,18 +156,45 @@ QColorSpacePrivate::QColorSpacePrivate(const QColorSpacePrimaries &primaries, float gamma) : primaries(QColorSpace::Primaries::Custom) , transferFunction(transferFunction) + , colorModel(QColorSpace::ColorModel::Rgb) , gamma(gamma) + , whitePoint(primaries.whitePoint) { Q_ASSERT(primaries.areValid()); toXyz = primaries.toXyzMatrix(); - whitePoint = QColorVector(primaries.whitePoint); identifyColorSpace(); setTransferFunction(); } +QColorSpacePrivate::QColorSpacePrivate(const QPointF &whitePoint, + QColorSpace::TransferFunction transferFunction, + float gamma) + : primaries(QColorSpace::Primaries::Custom) + , transferFunction(transferFunction) + , colorModel(QColorSpace::ColorModel::Gray) + , gamma(gamma) + , whitePoint(whitePoint) +{ + toXyz = QColorMatrix::chromaticAdaptation(this->whitePoint); + setTransferFunction(); +} + +QColorSpacePrivate::QColorSpacePrivate(const QPointF &whitePoint, const QList &transferFunctionTable) + : primaries(QColorSpace::Primaries::Custom) + , transferFunction(QColorSpace::TransferFunction::Custom) + , colorModel(QColorSpace::ColorModel::Gray) + , gamma(0) + , whitePoint(whitePoint) +{ + toXyz = QColorMatrix::chromaticAdaptation(this->whitePoint); + setTransferFunctionTable(transferFunctionTable); + setTransferFunction(); +} + QColorSpacePrivate::QColorSpacePrivate(QColorSpace::Primaries primaries, const QList &transferFunctionTable) : primaries(primaries) , transferFunction(QColorSpace::TransferFunction::Custom) + , colorModel(QColorSpace::ColorModel::Rgb) , gamma(0) { setTransferFunctionTable(transferFunctionTable); @@ -203,11 +205,12 @@ QColorSpacePrivate::QColorSpacePrivate(QColorSpace::Primaries primaries, const Q QColorSpacePrivate::QColorSpacePrivate(const QColorSpacePrimaries &primaries, const QList &transferFunctionTable) : primaries(QColorSpace::Primaries::Custom) , transferFunction(QColorSpace::TransferFunction::Custom) + , colorModel(QColorSpace::ColorModel::Rgb) , gamma(0) + , whitePoint(primaries.whitePoint) { Q_ASSERT(primaries.areValid()); toXyz = primaries.toXyzMatrix(); - whitePoint = QColorVector(primaries.whitePoint); setTransferFunctionTable(transferFunctionTable); identifyColorSpace(); initialize(); @@ -219,6 +222,7 @@ QColorSpacePrivate::QColorSpacePrivate(const QColorSpacePrimaries &primaries, const QList &blueTransferFunctionTable) : primaries(QColorSpace::Primaries::Custom) , transferFunction(QColorSpace::TransferFunction::Custom) + , colorModel(QColorSpace::ColorModel::Rgb) , gamma(0) { Q_ASSERT(primaries.areValid()); @@ -437,8 +441,9 @@ QColorTransform QColorSpacePrivate::transformationToXYZ() const transform.d = ptr; ptr->colorSpaceIn = this; ptr->colorSpaceOut = this; + // Convert to XYZ relative to our white point, not the regular D50 white point. if (isThreeComponentMatrix()) - ptr->colorMatrix = toXyz; + ptr->colorMatrix = QColorMatrix::chromaticAdaptation(whitePoint).inverted() * toXyz; else ptr->colorMatrix = QColorMatrix::identity(); return transform; @@ -483,9 +488,10 @@ void QColorSpacePrivate::clearElementListProcessingForEdit() A color space can generally speaking be conceived as a combination of set of primary colors and a transfer function. The primaries defines the axes of the color space, and the transfer function how values are mapped on the axes. - The primaries are defined by three primary colors that represent exactly how red, green, - and blue look in this particular color space, and a white color that represents where - and how bright pure white is. The range of colors expressible by the primary colors is + The primaries are for ColorModel::Rgb color spaces defined by three primary colors that + represent exactly how red, green, and blue look in this particular color space, and a white + color that represents where and how bright pure white is. For grayscale color spaces, only + a single white primary is needed. The range of colors expressible by the primary colors is called the gamut, and a color space that can represent a wider range of colors is also known as a wide-gamut color space. @@ -554,6 +560,17 @@ void QColorSpacePrivate::clearElementListProcessingForEdit() either primaries or transfer function on a color space on this type it will reset to an empty ThreeComponentMatrix form. */ +/*! + \enum QColorSpace::ColorModel + \since 6.8 + + Defines the color model used by the color space data. + + \value Undefined No color model + \value Rgb An RGB color model with red, green, and blue colors. Can apply to RGB and grayscale data. + \value Gray A gray scale color model. Can only apply to grayscale data. +*/ + /*! \fn QColorSpace::QColorSpace() @@ -616,6 +633,28 @@ QColorSpace::QColorSpace(QColorSpace::Primaries gamut, const QList &tr { } +/*! + Creates a custom grayscale color space with the white point \a whitePoint, using the transfer function \a transferFunction and + optionally \a gamma. + + \since 6.8 +*/ +QColorSpace::QColorSpace(const QPointF &whitePoint, TransferFunction transferFunction, float gamma) + : d_ptr(new QColorSpacePrivate(whitePoint, transferFunction, gamma)) +{ +} + +/*! + Creates a custom grayscale color space with white point \a whitePoint, and using the custom transfer function described by + \a transferFunctionTable. + + \since 6.8 +*/ +QColorSpace::QColorSpace(const QPointF &whitePoint, const QList &transferFunctionTable) + : d_ptr(new QColorSpacePrivate(whitePoint, transferFunctionTable)) +{ +} + /*! Creates a custom colorspace with a primaries based on the chromaticities of the primary colors \a whitePoint, \a redPoint, \a greenPoint and \a bluePoint, and using the transfer function \a transferFunction and optionally \a gamma. @@ -869,6 +908,7 @@ void QColorSpace::setPrimaries(QColorSpace::Primaries primariesId) d_ptr->iccProfile = {}; d_ptr->description = QString(); d_ptr->primaries = primariesId; + d_ptr->colorModel = QColorSpace::ColorModel::Rgb; d_ptr->identifyColorSpace(); d_ptr->setToXyzMatrix(); } @@ -898,14 +938,67 @@ void QColorSpace::setPrimaries(const QPointF &whitePoint, const QPointF &redPoin d_ptr->iccProfile = {}; d_ptr->description = QString(); d_ptr->primaries = QColorSpace::Primaries::Custom; + d_ptr->colorModel = QColorSpace::ColorModel::Rgb; d_ptr->toXyz = toXyz; d_ptr->whitePoint = QColorVector(primaries.whitePoint); d_ptr->identifyColorSpace(); } /*! + Returns the white point used for this color space. Returns a null QPointF if not defined. + \since 6.8 +*/ +QPointF QColorSpace::whitePoint() const +{ + if (Q_UNLIKELY(!d_ptr)) + return QPointF(); + return d_ptr->whitePoint.toChromaticity(); +} + +/*! + Sets the white point to used for this color space to \a whitePoint. + + \since 6.8 +*/ +void QColorSpace::setWhitePoint(const QPointF &whitePoint) +{ + if (Q_UNLIKELY(!d_ptr)) { + d_ptr = new QColorSpacePrivate(whitePoint, TransferFunction::Custom, 0.0f); + return; + } + if (QColorVector(whitePoint) == d_ptr->whitePoint) + return; + detach(); + if (d_ptr->transformModel == TransformModel::ElementListProcessing) + d_ptr->clearElementListProcessingForEdit(); + d_ptr->iccProfile = {}; + d_ptr->description = QString(); + d_ptr->primaries = QColorSpace::Primaries::Custom; + // An RGB color model stays RGB, a gray stays gray, but an undefined one can now be considered gray + if (d_ptr->colorModel == QColorSpace::ColorModel::Undefined) + d_ptr->colorModel = QColorSpace::ColorModel::Gray; + QColorVector wXyz(whitePoint); + if (d_ptr->transformModel == QColorSpace::TransformModel::ThreeComponentMatrix) { + if (d_ptr->colorModel == QColorSpace::ColorModel::Rgb) { + // Rescale toXyz to new whitepoint + QColorMatrix chad = QColorMatrix::chromaticAdaptation(d_ptr->whitePoint); + QColorMatrix rawToXyz = chad.inverted() * d_ptr->toXyz; + QColorVector whiteScale = rawToXyz.inverted().map(wXyz); + rawToXyz = rawToXyz * QColorMatrix::fromScale(whiteScale); + d_ptr->toXyz = QColorMatrix::chromaticAdaptation(wXyz) * rawToXyz; + } else if (d_ptr->colorModel == QColorSpace::ColorModel::Gray) { + d_ptr->toXyz = QColorMatrix::chromaticAdaptation(wXyz); + } + } + d_ptr->whitePoint = wXyz; + d_ptr->identifyColorSpace(); +} + +/*! Returns the transfrom processing model used for this color space. + + \since 6.8 */ QColorSpace::TransformModel QColorSpace::transformModel() const noexcept { @@ -914,6 +1007,18 @@ QColorSpace::TransformModel QColorSpace::transformModel() const noexcept return d_ptr->transformModel; } +/*! + Returns the color model this color space can represent + + \since 6.8 +*/ +QColorSpace::ColorModel QColorSpace::colorModel() const noexcept +{ + if (Q_UNLIKELY(!d_ptr)) + return QColorSpace::ColorModel::Undefined; + return d_ptr->colorModel; +} + /*! \internal */ @@ -1007,8 +1112,13 @@ bool QColorSpacePrivate::isValid() const noexcept return !mAB.isEmpty(); if (!toXyz.isValid()) return false; - if (!trc[0].isValid() || !trc[1].isValid() || !trc[2].isValid()) - return false; + if (colorModel == QColorSpace::ColorModel::Gray) { + if (!trc[0].isValid()) + return false; + } else { + if (!trc[0].isValid() || !trc[1].isValid() || !trc[2].isValid()) + return false; + } return true; } diff --git a/src/gui/painting/qcolorspace.h b/src/gui/painting/qcolorspace.h index 848bc0898e1..cbb1435c569 100644 --- a/src/gui/painting/qcolorspace.h +++ b/src/gui/painting/qcolorspace.h @@ -50,9 +50,17 @@ public: ElementListProcessing, }; Q_ENUM(TransformModel) + enum class ColorModel : uint8_t { + Undefined = 0, + Rgb = 1, + Gray = 2, + }; + Q_ENUM(ColorModel) QColorSpace() noexcept = default; QColorSpace(NamedColorSpace namedColorSpace); + QColorSpace(const QPointF &whitePoint, TransferFunction transferFunction, float gamma = 0.0f); + QColorSpace(const QPointF &whitePoint, const QList &transferFunctionTable); QColorSpace(Primaries primaries, TransferFunction transferFunction, float gamma = 0.0f); QColorSpace(Primaries primaries, float gamma); QColorSpace(Primaries primaries, const QList &transferFunctionTable); @@ -104,8 +112,11 @@ public: void setPrimaries(Primaries primariesId); void setPrimaries(const QPointF &whitePoint, const QPointF &redPoint, const QPointF &greenPoint, const QPointF &bluePoint); + void setWhitePoint(const QPointF &whitePoint); + QPointF whitePoint() const; TransformModel transformModel() const noexcept; + ColorModel colorModel() const noexcept; void detach(); bool isValid() const noexcept; bool isValidTarget() const noexcept; diff --git a/src/gui/painting/qcolorspace_p.h b/src/gui/painting/qcolorspace_p.h index 03872ab2c50..ac09825a695 100644 --- a/src/gui/painting/qcolorspace_p.h +++ b/src/gui/painting/qcolorspace_p.h @@ -66,6 +66,8 @@ public: const QList &redTransferFunctionTable, const QList &greenTransferFunctionTable, const QList &blueRransferFunctionTable); + QColorSpacePrivate(const QPointF &whitePoint, QColorSpace::TransferFunction transferFunction, float gamma); + QColorSpacePrivate(const QPointF &whitePoint, const QList &transferFunctionTable); QColorSpacePrivate(const QColorSpacePrivate &other) = default; static const QColorSpacePrivate *get(const QColorSpace &colorSpace) @@ -101,6 +103,7 @@ public: QColorSpace::Primaries primaries = QColorSpace::Primaries::Custom; QColorSpace::TransferFunction transferFunction = QColorSpace::TransferFunction::Custom; QColorSpace::TransformModel transformModel = QColorSpace::TransformModel::ThreeComponentMatrix; + QColorSpace::ColorModel colorModel = QColorSpace::ColorModel::Undefined; float gamma = 0.0f; QColorVector whitePoint; diff --git a/src/gui/painting/qcolortransfertable_p.h b/src/gui/painting/qcolortransfertable_p.h index 5b1bd674762..d857e3942ed 100644 --- a/src/gui/painting/qcolortransfertable_p.h +++ b/src/gui/painting/qcolortransfertable_p.h @@ -97,6 +97,8 @@ public: float apply(float x) const { + if (isEmpty()) + return x; x = std::clamp(x, 0.0f, 1.0f); x *= m_tableSize - 1; const uint32_t lo = static_cast(std::floor(x)); diff --git a/src/gui/painting/qcolortransform.cpp b/src/gui/painting/qcolortransform.cpp index 8d578d7af3a..884e3383040 100644 --- a/src/gui/painting/qcolortransform.cpp +++ b/src/gui/painting/qcolortransform.cpp @@ -346,6 +346,39 @@ static void applyMatrix(QColorVector *buffer, const qsizetype len, const QColorM #endif } +template +static void clampIfNeeded(QColorVector *buffer, const qsizetype len) +{ + if constexpr (doClamp != DoClamp) + return; +#if defined(__SSE2__) + const __m128 minV = _mm_set1_ps(0.0f); + const __m128 maxV = _mm_set1_ps(1.0f); + for (qsizetype j = 0; j < len; ++j) { + __m128 c = _mm_loadu_ps(&buffer[j].x); + c = _mm_min_ps(c, maxV); + c = _mm_max_ps(c, minV); + _mm_storeu_ps(&buffer[j].x, c); + } +#elif defined(__ARM_NEON__) + const float32x4_t minV = vdupq_n_f32(0.0f); + const float32x4_t maxV = vdupq_n_f32(1.0f); + for (qsizetype j = 0; j < len; ++j) { + float32x4_t c = vld1q_f32(&buffer[j].x); + c = vminq_f32(c, maxV); + c = vmaxq_f32(c, minV); + vst1q_f32(&buffer[j].x, c); + } +#else + for (qsizetype j = 0; j < len; ++j) { + const QColorVector cv = buffer[j]; + buffer[j].x = std::clamp(cv.x, 0.f, 1.f); + buffer[j].y = std::clamp(cv.y, 0.f, 1.f); + buffer[j].z = std::clamp(cv.z, 0.f, 1.f); + } +#endif +} + #if defined(__SSE2__) || defined(__ARM_NEON__) template static constexpr inline bool isArgb(); @@ -362,8 +395,24 @@ inline int getAlpha(const QRgb &p) template<> inline int getAlpha(const QRgba64 &p) { return p.alpha(); } + #endif +template +static float getAlphaF(const T &); +template<> float getAlphaF(const QRgb &r) +{ + return qAlpha(r) * (1.f / 255.f); +} +template<> float getAlphaF(const QRgba64 &r) +{ + return r.alpha() * (1.f / 65535.f); +} +template<> float getAlphaF(const QRgbaFloat32 &r) +{ + return r.a; +} + template static void loadPremultiplied(QColorVector *buffer, const T *src, const qsizetype len, const QColorTransformPrivate *d_ptr); template @@ -768,15 +817,16 @@ inline void storeP(QRgba64 &p, __m128i &v, int a) #endif } -template -static void storePremultiplied(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +template, void>> +static void storePremultiplied(D *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { const __m128 v4080 = _mm_set1_ps(4080.f); const __m128 iFF00 = _mm_set1_ps(1.0f / (255 * 256)); - constexpr bool isARGB = isArgb(); + constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { - const int a = getAlpha(src[i]); + const int a = getAlpha(src[i]); __m128 vf = _mm_loadu_ps(&buffer[i].x); __m128i v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); __m128 va = _mm_mul_ps(_mm_set1_ps(a), iFF00); @@ -789,21 +839,21 @@ static void storePremultiplied(T *dst, const T *src, const QColorVector *buffer, vf = _mm_cvtepi32_ps(v); vf = _mm_mul_ps(vf, va); v = _mm_cvtps_epi32(vf); - storeP(dst[i], v, a); + storeP(dst[i], v, a); } } -template<> -void storePremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, - const QColorVector *buffer, const qsizetype len, - const QColorTransformPrivate *d_ptr) +template +static void storePremultiplied(QRgbaFloat32 *dst, const S *src, + const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) { const __m128 v4080 = _mm_set1_ps(4080.f); const __m128 vZero = _mm_set1_ps(0.0f); const __m128 vOne = _mm_set1_ps(1.0f); const __m128 viFF00 = _mm_set1_ps(1.0f / (255 * 256)); for (qsizetype i = 0; i < len; ++i) { - const float a = src[i].a; + const float a = getAlphaF(src[i]); __m128 va = _mm_set1_ps(a); __m128 vf = _mm_loadu_ps(&buffer[i].x); const __m128 under = _mm_cmplt_ps(vf, vZero); @@ -850,14 +900,15 @@ inline void storePU(QRgba64 &p, __m128i &v, int a) _mm_storel_epi64((__m128i *)&p, v); } -template -static void storeUnpremultiplied(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +template, void>> +static void storeUnpremultiplied(D *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { const __m128 v4080 = _mm_set1_ps(4080.f); - constexpr bool isARGB = isArgb(); + constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { - const int a = getAlpha(src[i]); + const int a = getAlpha(src[i]); __m128 vf = _mm_loadu_ps(&buffer[i].x); __m128i v = _mm_cvtps_epi32(_mm_mul_ps(vf, v4080)); const int ridx = _mm_extract_epi16(v, 0); @@ -867,21 +918,21 @@ static void storeUnpremultiplied(T *dst, const T *src, const QColorVector *buffe v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[0]->m_fromLinear[ridx], isARGB ? 2 : 0); v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[1]->m_fromLinear[gidx], 1); v = _mm_insert_epi16(v, d_ptr->colorSpaceOut->lut[2]->m_fromLinear[bidx], isARGB ? 0 : 2); - storePU(dst[i], v, a); + storePU(dst[i], v, a); } } -template<> -void storeUnpremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, - const QColorVector *buffer, const qsizetype len, - const QColorTransformPrivate *d_ptr) +template +void storeUnpremultiplied(QRgbaFloat32 *dst, const S *src, + const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) { const __m128 v4080 = _mm_set1_ps(4080.f); const __m128 vZero = _mm_set1_ps(0.0f); const __m128 vOne = _mm_set1_ps(1.0f); const __m128 viFF00 = _mm_set1_ps(1.0f / (255 * 256)); for (qsizetype i = 0; i < len; ++i) { - const float a = src[i].a; + const float a = getAlphaF(src[i]); __m128 vf = _mm_loadu_ps(&buffer[i].x); const __m128 under = _mm_cmplt_ps(vf, vZero); const __m128 over = _mm_cmpgt_ps(vf, vOne); @@ -907,10 +958,9 @@ void storeUnpremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *s } template -static void storeOpaque(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +static void storeOpaque(T *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); const __m128 v4080 = _mm_set1_ps(4080.f); constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { @@ -928,11 +978,10 @@ static void storeOpaque(T *dst, const T *src, const QColorVector *buffer, const } template<> -void storeOpaque(QRgbaFloat32 *dst, const QRgbaFloat32 *src, +void storeOpaque(QRgbaFloat32 *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); const __m128 v4080 = _mm_set1_ps(4080.f); const __m128 vZero = _mm_set1_ps(0.0f); const __m128 vOne = _mm_set1_ps(1.0f); @@ -976,14 +1025,15 @@ inline void storeP(QRgba64 &p, const uint16x4_t &v) vst1_u16((uint16_t *)&p, v); } -template -static void storePremultiplied(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +template, void>> +static void storePremultiplied(D *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { const float iFF00 = 1.0f / (255 * 256); - constexpr bool isARGB = isArgb(); + constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { - const int a = getAlpha(src[i]); + const int a = getAlpha(src[i]); float32x4_t vf = vld1q_f32(&buffer[i].x); uint32x4_t v = vcvtq_u32_f32(vaddq_f32(vmulq_n_f32(vf, 4080.f), vdupq_n_f32(0.5f))); const int ridx = vgetq_lane_u32(v, 0); @@ -998,7 +1048,7 @@ static void storePremultiplied(T *dst, const T *src, const QColorVector *buffer, v = vcvtq_u32_f32(vf); uint16x4_t v16 = vmovn_u32(v); v16 = vset_lane_u16(a, v16, 3); - storeP(dst[i], v16); + storeP(dst[i], v16); } } @@ -1020,13 +1070,14 @@ inline void storePU(QRgba64 &p, uint16x4_t &v, int a) vst1_u16((uint16_t *)&p, v); } -template -static void storeUnpremultiplied(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +template, void>> +static void storeUnpremultiplied(D *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - constexpr bool isARGB = isArgb(); + constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { - const int a = getAlpha(src[i]); + const int a = getAlpha(src[i]); float32x4_t vf = vld1q_f32(&buffer[i].x); uint16x4_t v = vmovn_u32(vcvtq_u32_f32(vaddq_f32(vmulq_n_f32(vf, 4080.f), vdupq_n_f32(0.5f)))); const int ridx = vget_lane_u16(v, 0); @@ -1035,15 +1086,14 @@ static void storeUnpremultiplied(T *dst, const T *src, const QColorVector *buffe v = vset_lane_u16(d_ptr->colorSpaceOut->lut[0]->m_fromLinear[ridx], v, isARGB ? 2 : 0); v = vset_lane_u16(d_ptr->colorSpaceOut->lut[1]->m_fromLinear[gidx], v, 1); v = vset_lane_u16(d_ptr->colorSpaceOut->lut[2]->m_fromLinear[bidx], v, isARGB ? 0 : 2); - storePU(dst[i], v, a); + storePU(dst[i], v, a); } } template -static void storeOpaque(T *dst, const T *src, const QColorVector *buffer, const qsizetype len, +static void storeOpaque(T *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); constexpr bool isARGB = isArgb(); for (qsizetype i = 0; i < len; ++i) { float32x4_t vf = vld1q_f32(&buffer[i].x); @@ -1082,10 +1132,9 @@ static void storeUnpremultiplied(QRgb *dst, const QRgb *src, const QColorVector } } -static void storeOpaque(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len, +static void storeOpaque(QRgb *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); for (qsizetype i = 0; i < len; ++i) { const int r = d_ptr->colorSpaceOut->lut[0]->u8FromLinearF32(buffer[i].x); const int g = d_ptr->colorSpaceOut->lut[1]->u8FromLinearF32(buffer[i].y); @@ -1094,11 +1143,12 @@ static void storeOpaque(QRgb *dst, const QRgb *src, const QColorVector *buffer, } } -static void storePremultiplied(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, +template +static void storePremultiplied(QRgba64 *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { for (qsizetype i = 0; i < len; ++i) { - const int a = src[i].alpha(); + const int a = getAlphaF(src[i]) * 65535.f; const float fa = a / (255.0f * 256.0f); const float r = d_ptr->colorSpaceOut->lut[0]->m_fromLinear[int(buffer[i].x * 4080.0f + 0.5f)]; const float g = d_ptr->colorSpaceOut->lut[1]->m_fromLinear[int(buffer[i].y * 4080.0f + 0.5f)]; @@ -1107,21 +1157,22 @@ static void storePremultiplied(QRgba64 *dst, const QRgba64 *src, const QColorVec } } -static void storeUnpremultiplied(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, +template +static void storeUnpremultiplied(QRgba64 *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { for (qsizetype i = 0; i < len; ++i) { + const int a = getAlphaF(src[i]) * 65535.f; const int r = d_ptr->colorSpaceOut->lut[0]->u16FromLinearF32(buffer[i].x); const int g = d_ptr->colorSpaceOut->lut[1]->u16FromLinearF32(buffer[i].y); const int b = d_ptr->colorSpaceOut->lut[2]->u16FromLinearF32(buffer[i].z); - dst[i] = qRgba64(r, g, b, src[i].alpha()); + dst[i] = qRgba64(r, g, b, a); } } -static void storeOpaque(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, +static void storeOpaque(QRgba64 *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); for (qsizetype i = 0; i < len; ++i) { const int r = d_ptr->colorSpaceOut->lut[0]->u16FromLinearF32(buffer[i].x); const int g = d_ptr->colorSpaceOut->lut[1]->u16FromLinearF32(buffer[i].y); @@ -1131,11 +1182,12 @@ static void storeOpaque(QRgba64 *dst, const QRgba64 *src, const QColorVector *bu } #endif #if !defined(__SSE2__) -static void storePremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, const QColorVector *buffer, +template +static void storePremultiplied(QRgbaFloat32 *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { for (qsizetype i = 0; i < len; ++i) { - const float a = src[i].a; + const float a = getAlphaF(src[i]); dst[i].r = d_ptr->colorSpaceOut->trc[0].applyInverseExtended(buffer[i].x) * a; dst[i].g = d_ptr->colorSpaceOut->trc[1].applyInverseExtended(buffer[i].y) * a; dst[i].b = d_ptr->colorSpaceOut->trc[2].applyInverseExtended(buffer[i].z) * a; @@ -1143,11 +1195,12 @@ static void storePremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, const } } -static void storeUnpremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, const QColorVector *buffer, +template +static void storeUnpremultiplied(QRgbaFloat32 *dst, const S *src, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { for (qsizetype i = 0; i < len; ++i) { - const float a = src[i].a; + const float a = getAlphaF(src[i]); dst[i].r = d_ptr->colorSpaceOut->trc[0].applyInverseExtended(buffer[i].x); dst[i].g = d_ptr->colorSpaceOut->trc[1].applyInverseExtended(buffer[i].y); dst[i].b = d_ptr->colorSpaceOut->trc[2].applyInverseExtended(buffer[i].z); @@ -1155,10 +1208,9 @@ static void storeUnpremultiplied(QRgbaFloat32 *dst, const QRgbaFloat32 *src, con } } -static void storeOpaque(QRgbaFloat32 *dst, const QRgbaFloat32 *src, const QColorVector *buffer, const qsizetype len, +static void storeOpaque(QRgbaFloat32 *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); for (qsizetype i = 0; i < len; ++i) { dst[i].r = d_ptr->colorSpaceOut->trc[0].applyInverseExtended(buffer[i].x); dst[i].g = d_ptr->colorSpaceOut->trc[1].applyInverseExtended(buffer[i].y); @@ -1167,20 +1219,35 @@ static void storeOpaque(QRgbaFloat32 *dst, const QRgbaFloat32 *src, const QColor } } #endif -static void storeGray(quint8 *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len, - const QColorTransformPrivate *d_ptr) + +static void loadGray(QColorVector *buffer, const quint8 *src, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); - for (qsizetype i = 0; i < len; ++i) - dst[i] = d_ptr->colorSpaceOut->lut[1]->u8FromLinearF32(buffer[i].y); + for (qsizetype i = 0; i < len; ++i) { + const float y = d_ptr->colorSpaceIn->lut[0]->u8ToLinearF32(src[i]); + buffer[i] = d_ptr->colorSpaceIn->whitePoint * y; + } } -static void storeGray(quint16 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len, +static void loadGray(QColorVector *buffer, const quint16 *src, const qsizetype len, const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) { + const float y = d_ptr->colorSpaceIn->lut[0]->u16ToLinearF32(src[i]); + buffer[i] = d_ptr->colorSpaceIn->whitePoint * y; + } +} + +static void storeOpaque(quint8 *dst, const QColorVector *buffer, const qsizetype len, const QColorTransformPrivate *d_ptr) { - Q_UNUSED(src); for (qsizetype i = 0; i < len; ++i) - dst[i] = d_ptr->colorSpaceOut->lut[1]->u16FromLinearF32(buffer[i].y); + dst[i] = d_ptr->colorSpaceOut->lut[0]->u8FromLinearF32(buffer[i].y); +} + +static void storeOpaque(quint16 *dst, const QColorVector *buffer, const qsizetype len, + const QColorTransformPrivate *d_ptr) +{ + for (qsizetype i = 0; i < len; ++i) + dst[i] = d_ptr->colorSpaceOut->lut[0]->u16FromLinearF32(buffer[i].y); } static constexpr qsizetype WorkBlockSize = 256; @@ -1255,7 +1322,19 @@ void loadPremultipliedLUT(QColorVector *buffer, const QRgbaFloat32 *src, const q } } -static void storeUnpremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) +template +static void storeUnpremultipliedLUT(QRgb *dst, const T *, const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int r = buffer[i].x * 255.f; + const int g = buffer[i].y * 255.f; + const int b = buffer[i].z * 255.f; + dst[i] = 0xff000000 | (r << 16) | (g << 8) | (b << 0); + } +} + +template<> +void storeUnpremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { const int r = buffer[i].x * 255.f; @@ -1265,8 +1344,34 @@ static void storeUnpremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVect } } -static void storeUnpremultipliedLUT(QRgba64 *dst, const QRgba64 *src, +template +static void storeUnpremultipliedLUT(QRgba64 *dst, const T *, const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int r = buffer[i].x * 65535.f; + const int g = buffer[i].y * 65535.f; + const int b = buffer[i].z * 65535.f; + dst[i] = qRgba64(r, g, b, 65535); + } +} + +template<> +void storeUnpremultipliedLUT(QRgba64 *dst, const QRgb *src, + const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]) * 257; + const int r = buffer[i].x * 65535.f; + const int g = buffer[i].y * 65535.f; + const int b = buffer[i].z * 65535.f; + dst[i] = qRgba64(r, g, b, a); + } +} + +template<> +void storeUnpremultipliedLUT(QRgba64 *dst, const QRgba64 *src, + const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { const int r = buffer[i].x * 65535.f; @@ -1276,18 +1381,23 @@ static void storeUnpremultipliedLUT(QRgba64 *dst, const QRgba64 *src, } } -static void storeUnpremultipliedLUT(QRgbaFloat32 *dst, const QRgbaFloat32 *src, +template +static void storeUnpremultipliedLUT(QRgbaFloat32 *dst, const T *src, const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { const float r = buffer[i].x; const float g = buffer[i].y; const float b = buffer[i].z; - dst[i] = QRgbaFloat32{r, g, b, src[i].a}; + dst[i] = QRgbaFloat32{r, g, b, getAlphaF(src[i])}; } } -static void storePremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) +template +static void storePremultipliedLUT(QRgb *, const T *, const QColorVector *, const qsizetype); + +template<> +void storePremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { const int a = qAlpha(src[i]); @@ -1298,8 +1408,23 @@ static void storePremultipliedLUT(QRgb *dst, const QRgb *src, const QColorVector } } -static void storePremultipliedLUT(QRgba64 *dst, const QRgba64 *src, - const QColorVector *buffer, const qsizetype len) +template +static void storePremultipliedLUT(QRgba64 *, const T *, const QColorVector *, const qsizetype); + +template<> +void storePremultipliedLUT(QRgba64 *dst, const QRgb *src, const QColorVector *buffer, const qsizetype len) +{ + for (qsizetype i = 0; i < len; ++i) { + const int a = qAlpha(src[i]) * 257; + const int r = buffer[i].x * a; + const int g = buffer[i].y * a; + const int b = buffer[i].z * a; + dst[i] = qRgba64(r, g, b, a); + } +} + +template<> +void storePremultipliedLUT(QRgba64 *dst, const QRgba64 *src, const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { const int a = src[i].alpha(); @@ -1310,11 +1435,11 @@ static void storePremultipliedLUT(QRgba64 *dst, const QRgba64 *src, } } -static void storePremultipliedLUT(QRgbaFloat32 *dst, const QRgbaFloat32 *src, - const QColorVector *buffer, const qsizetype len) +template +static void storePremultipliedLUT(QRgbaFloat32 *dst, const T *src, const QColorVector *buffer, const qsizetype len) { for (qsizetype i = 0; i < len; ++i) { - const float a = src[i].a; + const float a = getAlphaF(src[i]); const float r = buffer[i].x * a; const float g = buffer[i].y * a; const float b = buffer[i].z * a; @@ -1372,6 +1497,9 @@ QColorVector QColorTransformPrivate::map(QColorVector c) const for (auto &&element : colorSpaceIn->mAB) std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); } + c.x = std::clamp(c.x, 0.0f, 1.0f); + c.y = std::clamp(c.y, 0.0f, 1.0f); + c.z = std::clamp(c.z, 0.0f, 1.0f); // Match Profile Connection Spaces (PCS): if (colorSpaceOut->isPcsLab && !colorSpaceIn->isPcsLab) @@ -1380,11 +1508,12 @@ QColorVector QColorTransformPrivate::map(QColorVector c) const c = c.labToXyz(); if (colorSpaceOut->isThreeComponentMatrix()) { - if (!colorSpaceIn->isThreeComponentMatrix()) + if (!colorSpaceIn->isThreeComponentMatrix()) { c = colorMatrix.map(c); - c.x = std::clamp(c.x, 0.0f, 1.0f); - c.y = std::clamp(c.y, 0.0f, 1.0f); - c.z = std::clamp(c.z, 0.0f, 1.0f); + c.x = std::clamp(c.x, 0.0f, 1.0f); + c.y = std::clamp(c.y, 0.0f, 1.0f); + c.z = std::clamp(c.z, 0.0f, 1.0f); + } if (colorSpaceOut->lut.generated.loadAcquire()) { c.x = colorSpaceOut->lut[0]->fromLinear(c.x); c.y = colorSpaceOut->lut[1]->fromLinear(c.y); @@ -1398,6 +1527,9 @@ QColorVector QColorTransformPrivate::map(QColorVector c) const // Do element based conversion for (auto &&element : colorSpaceOut->mBA) std::visit([&c](auto &&elm) { visitElement(elm, &c, 1); }, element); + c.x = std::clamp(c.x, 0.0f, 1.0f); + c.y = std::clamp(c.y, 0.0f, 1.0f); + c.z = std::clamp(c.z, 0.0f, 1.0f); } return c; } @@ -1438,8 +1570,8 @@ QColorVector QColorTransformPrivate::mapExtended(QColorVector c) const return c; } -template -void QColorTransformPrivate::applyConvertIn(const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const +template +void QColorTransformPrivate::applyConvertIn(const S *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const { if (colorSpaceIn->isThreeComponentMatrix()) { if (flags & InputPremultiplied) @@ -1455,20 +1587,24 @@ void QColorTransformPrivate::applyConvertIn(const T *src, QColorVector *buffer, else loadUnpremultipliedLUT(buffer, src, len); + if constexpr (std::is_same_v || std::is_same_v) + clampIfNeeded(buffer, len); + // Do element based conversion for (auto &&element : colorSpaceIn->mAB) std::visit([&buffer, len](auto &&elm) { visitElement(elm, buffer, len); }, element); } } -template -void QColorTransformPrivate::applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const +template +void QColorTransformPrivate::applyConvertOut(D *dst, const S *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const { + constexpr ApplyMatrixForm doClamp = (std::is_same_v || std::is_same_v) ? DoNotClamp : DoClamp; if (colorSpaceOut->isThreeComponentMatrix()) { - applyMatrix(buffer, len, colorMatrix); // colorMatrix should have the latter half only. + applyMatrix(buffer, len, colorMatrix); // colorMatrix should have the latter half only. if (flags & InputOpaque) - storeOpaque(dst, src, buffer, len, this); + storeOpaque(dst, buffer, len, this); else if (flags & OutputPremultiplied) storePremultiplied(dst, src, buffer, len, this); else @@ -1478,11 +1614,7 @@ void QColorTransformPrivate::applyConvertOut(T *dst, const T *src, QColorVector for (auto &&element : colorSpaceOut->mBA) std::visit([&buffer, len](auto &&elm) { visitElement(elm, buffer, len); }, element); - for (qsizetype j = 0; j < len; ++j) { - buffer[j].x = std::clamp(buffer[j].x, 0.f, 1.f); - buffer[j].y = std::clamp(buffer[j].y, 0.f, 1.f); - buffer[j].z = std::clamp(buffer[j].z, 0.f, 1.f); - } + clampIfNeeded(buffer, len); if (flags & OutputPremultiplied) storePremultipliedLUT(dst, src, buffer, len); @@ -1491,8 +1623,8 @@ void QColorTransformPrivate::applyConvertOut(T *dst, const T *src, QColorVector } } -template -void QColorTransformPrivate::applyElementListTransform(T *dst, const T *src, qsizetype count, TransformFlags flags) const +template +void QColorTransformPrivate::applyElementListTransform(D *dst, const S *src, qsizetype count, TransformFlags flags) const { Q_ASSERT(!colorSpaceIn->isThreeComponentMatrix() || !colorSpaceOut->isThreeComponentMatrix()); @@ -1526,8 +1658,8 @@ void QColorTransformPrivate::applyElementListTransform(T *dst, const T *src, qsi } } -template -void QColorTransformPrivate::applyThreeComponentMatrix(T *dst, const T *src, qsizetype count, TransformFlags flags) const +template +void QColorTransformPrivate::applyThreeComponentMatrix(D *dst, const S *src, qsizetype count, TransformFlags flags) const { Q_ASSERT(colorSpaceIn->isThreeComponentMatrix() && colorSpaceOut->isThreeComponentMatrix()); @@ -1538,7 +1670,7 @@ void QColorTransformPrivate::applyThreeComponentMatrix(T *dst, const T *src, qsi updateLutsOut(); bool doApplyMatrix = !colorMatrix.isIdentity(); - constexpr ApplyMatrixForm doClamp = (std::is_same_v || std::is_same_v) ? DoNotClamp : DoClamp; + constexpr ApplyMatrixForm doClamp = (std::is_same_v || std::is_same_v) ? DoNotClamp : DoClamp; QUninitialized buffer; qsizetype i = 0; @@ -1551,9 +1683,11 @@ void QColorTransformPrivate::applyThreeComponentMatrix(T *dst, const T *src, qsi if (doApplyMatrix) applyMatrix(buffer, len, colorMatrix); + else + clampIfNeeded(buffer, len); if (flags & InputOpaque) - storeOpaque(dst + i, src + i, buffer, len, this); + storeOpaque(dst + i, buffer, len, this); else if (flags & OutputPremultiplied) storePremultiplied(dst + i, src + i, buffer, len, this); else @@ -1563,13 +1697,22 @@ void QColorTransformPrivate::applyThreeComponentMatrix(T *dst, const T *src, qsi } } -template -void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const +/*! + \internal + Applies the color transformation on \a count S pixels starting from + \a src and stores the result in \a dst as D pixels . + + Assumes unpremultiplied data by default. Set \a flags to change defaults. + + \sa prepare() +*/ +template +void QColorTransformPrivate::apply(D *dst, const S *src, qsizetype count, TransformFlags flags) const { - if (colorSpaceIn->isThreeComponentMatrix() && colorSpaceOut->isThreeComponentMatrix()) - applyThreeComponentMatrix(dst, src, count, flags); + if (isThreeComponentMatrix()) + applyThreeComponentMatrix(dst, src, count, flags); else - applyElementListTransform(dst, src, count, flags); + applyElementListTransform(dst, src, count, flags); } /*! @@ -1580,6 +1723,7 @@ void QColorTransformPrivate::apply(T *dst, const T *src, qsizetype count, Transf template void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const { + Q_ASSERT(colorSpaceOut->isThreeComponentMatrix()); if (!colorSpaceIn->isThreeComponentMatrix()) { QUninitialized buffer; @@ -1595,7 +1739,8 @@ void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype cou for (auto &&element : colorSpaceIn->mAB) std::visit([&](auto &&elm) { visitElement(elm, buffer, len); }, element); - storeGray(dst + i, src + i, buffer, len, this); + applyMatrix(buffer, len, colorMatrix); + storeOpaque(dst + i, buffer, len, this); i += len; } @@ -1620,17 +1765,69 @@ void QColorTransformPrivate::applyReturnGray(D *dst, const S *src, qsizetype cou applyMatrix(buffer, len, colorMatrix); - storeGray(dst + i, src + i, buffer, len, this); + storeOpaque(dst + i, buffer, len, this); i += len; } } +/*! + \internal +*/ +template +void QColorTransformPrivate::applyGray(D *dst, const S *src, qsizetype count, TransformFlags) const +{ + Q_ASSERT(colorSpaceIn->isThreeComponentMatrix()); + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { + if (!colorSpaceOut->isThreeComponentMatrix()) { + QUninitialized buffer; + + qsizetype i = 0; + while (i < count) { + const qsizetype len = qMin(count - i, WorkBlockSize); + loadGray(buffer, src + i, len, this); + + applyMatrix(buffer, len, colorMatrix); + + // Do element based conversion + for (auto &&element : colorSpaceOut->mBA) + std::visit([&buffer, len](auto &&elm) { visitElement(elm, buffer, len); }, element); + + clampIfNeeded(buffer, len); + + storeUnpremultipliedLUT(dst, src, buffer, len); // input is always opaque + + i += len; + } + return; + } + } + Q_ASSERT(colorSpaceOut->isThreeComponentMatrix()); + if (!colorMatrix.isValid()) + return; + + updateLutsIn(); + updateLutsOut(); + + QUninitialized buffer; + + qsizetype i = 0; + while (i < count) { + const qsizetype len = qMin(count - i, WorkBlockSize); + loadGray(buffer, src + i, len, this); + + applyMatrix(buffer, len, colorMatrix); + + storeOpaque(dst + i, buffer, len, this); + i += len; + } +} + /*! \internal \enum QColorTransformPrivate::TransformFlag - Defines how the transform is to be applied. + Defines how the transform should handle alpha values. \value Unpremultiplied The input and output should both be unpremultiplied. \value InputOpaque The input is guaranteed to be opaque. @@ -1654,58 +1851,21 @@ void QColorTransformPrivate::prepare() updateLutsOut(); } -/*! - \internal - Applies the color transformation on \a count QRgb pixels starting from - \a src and stores the result in \a dst. - - Thread-safe if prepare() has been called first. - - Assumes unpremultiplied data by default. Set \a flags to change defaults. - - \sa prepare() -*/ -void QColorTransformPrivate::apply(QRgb *dst, const QRgb *src, qsizetype count, TransformFlags flags) const -{ - apply(dst, src, count, flags); -} - -/*! - \internal - Applies the color transformation on \a count QRgba64 pixels starting from - \a src and stores the result in \a dst. - - Thread-safe if prepare() has been called first. - - Assumes unpremultiplied data by default. Set \a flags to change defaults. - - \sa prepare() -*/ -void QColorTransformPrivate::apply(QRgba64 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const -{ - apply(dst, src, count, flags); -} - -/*! - \internal - Applies the color transformation on \a count QRgbaFloat32 pixels starting from - \a src and stores the result in \a dst. - - Thread-safe if prepare() has been called first. - - Assumes unpremultiplied data by default. Set \a flags to change defaults. - - \sa prepare() -*/ -void QColorTransformPrivate::apply(QRgbaFloat32 *dst, const QRgbaFloat32 *src, qsizetype count, - TransformFlags flags) const -{ - apply(dst, src, count, flags); -} - - +// Only allow versions increasing precision template void QColorTransformPrivate::applyReturnGray(quint8 *dst, const QRgb *src, qsizetype count, TransformFlags flags) const; template void QColorTransformPrivate::applyReturnGray(quint16 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::applyGray(quint8 *dst, const quint8 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::applyGray(quint16 *dst, const quint8 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::applyGray(quint16 *dst, const quint16 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::applyGray(QRgb *dst, const quint8 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::applyGray(QRgba64 *dst, const quint16 *src, qsizetype count, TransformFlags flags) const; + +template void QColorTransformPrivate::apply(QRgb *dst, const QRgb *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::apply(QRgba64 *dst, const QRgb *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::apply(QRgba64 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::apply(QRgbaFloat32 *dst, const QRgb *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::apply(QRgbaFloat32 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags) const; +template void QColorTransformPrivate::apply(QRgbaFloat32 *dst, const QRgbaFloat32 *src, qsizetype count, TransformFlags flags) const; bool QColorTransformPrivate::isThreeComponentMatrix() const { diff --git a/src/gui/painting/qcolortransform_p.h b/src/gui/painting/qcolortransform_p.h index 1d54aced1b8..92b8b3cc20b 100644 --- a/src/gui/painting/qcolortransform_p.h +++ b/src/gui/painting/qcolortransform_p.h @@ -51,26 +51,22 @@ public: QColorVector map(QColorVector color) const; QColorVector mapExtended(QColorVector color) const; - void apply(QRgb *dst, const QRgb *src, qsizetype count, TransformFlags flags = Unpremultiplied) const; - void apply(QRgba64 *dst, const QRgba64 *src, qsizetype count, TransformFlags flags = Unpremultiplied) const; - void apply(QRgbaFloat32 *dst, const QRgbaFloat32 *src, qsizetype count, - TransformFlags flags = Unpremultiplied) const; - - template - void apply(T *dst, const T *src, qsizetype count, TransformFlags flags) const; - - template - void applyConvertIn(const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; - template - void applyConvertOut(T *dst, const T *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; - template - void applyElementListTransform(T *dst, const T *src, qsizetype count, TransformFlags flags) const; - template - void applyThreeComponentMatrix(T *dst, const T *src, qsizetype count, TransformFlags flags) const; - + template + void apply(D *dst, const S *src, qsizetype count, TransformFlags flags) const; + template + void applyGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const; template void applyReturnGray(D *dst, const S *src, qsizetype count, TransformFlags flags) const; +private: + template + void applyConvertIn(const S *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; + template + void applyConvertOut(D *dst, const S *src, QColorVector *buffer, qsizetype len, TransformFlags flags) const; + template + void applyElementListTransform(D *dst, const S *src, qsizetype count, TransformFlags flags) const; + template + void applyThreeComponentMatrix(D *dst, const S *src, qsizetype count, TransformFlags flags) const; }; QT_END_NAMESPACE diff --git a/src/gui/painting/qicc.cpp b/src/gui/painting/qicc.cpp index dcebdfd1b0b..316ac352ee4 100644 --- a/src/gui/painting/qicc.cpp +++ b/src/gui/painting/qicc.cpp @@ -782,7 +782,7 @@ static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColo colorSpacePrivate->mAB.append(inTableElement); if (!clutElement.isEmpty()) colorSpacePrivate->mAB.append(clutElement); - if (!outTableIsLinear) + if (!outTableIsLinear || colorSpacePrivate->mAB.isEmpty()) colorSpacePrivate->mAB.append(outTableElement); } else { // The matrix is only to be applied if the input color-space is XYZ @@ -792,7 +792,7 @@ static bool parseLutData(const QByteArray &data, const TagEntry &tagEntry, QColo colorSpacePrivate->mBA.append(inTableElement); if (!clutElement.isEmpty()) colorSpacePrivate->mBA.append(clutElement); - if (!outTableIsLinear) + if (!outTableIsLinear || colorSpacePrivate->mBA.isEmpty()) colorSpacePrivate->mBA.append(outTableElement); } return true; @@ -964,7 +964,7 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo if (!offsetElement.isNull()) colorSpacePrivate->mAB.append(std::move(offsetElement)); } - if (!bCurvesAreLinear) + if (!bCurvesAreLinear|| colorSpacePrivate->mAB.isEmpty()) colorSpacePrivate->mAB.append(std::move(bTableElement)); } else { if (!bCurvesAreLinear) @@ -983,6 +983,8 @@ static bool parseMabData(const QByteArray &data, const TagEntry &tagEntry, QColo if (!aCurvesAreLinear) colorSpacePrivate->mBA.append(std::move(aTableElement)); } + if (colorSpacePrivate->mBA.isEmpty()) // Ensure non-empty to indicate valid empty transform + colorSpacePrivate->mBA.append(std::move(bTableElement)); } return true; @@ -1269,9 +1271,11 @@ bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace) if (header.inputColorSpace == uint(ColorSpaceType::Rgb)) { if (!parseRgbMatrix(data, tagIndex, colorspaceDPtr)) return false; + colorspaceDPtr->colorModel = QColorSpace::ColorModel::Rgb; } else if (header.inputColorSpace == uint(ColorSpaceType::Gray)) { if (!parseGrayMatrix(data, tagIndex, colorspaceDPtr)) return false; + colorspaceDPtr->colorModel = QColorSpace::ColorModel::Gray; } else { Q_UNREACHABLE(); } @@ -1285,6 +1289,7 @@ bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace) } else { colorspaceDPtr->isPcsLab = (header.pcs == uint(Tag::Lab_)); colorspaceDPtr->transformModel = QColorSpace::TransformModel::ElementListProcessing; + colorspaceDPtr->colorModel = QColorSpace::ColorModel::Rgb; // Only parse the default perceptual transform for now if (!parseA2B(data, tagIndex[Tag::A2B0], colorspaceDPtr, true)) @@ -1313,6 +1318,7 @@ bool fromIccProfile(const QByteArray &data, QColorSpace *colorSpace) colorspaceDPtr->iccProfile = data; + Q_ASSERT(colorspaceDPtr->isValid()); return true; } diff --git a/tests/auto/gui/image/qimage/tst_qimage.cpp b/tests/auto/gui/image/qimage/tst_qimage.cpp index 3177fa84b16..1abca9e1883 100644 --- a/tests/auto/gui/image/qimage/tst_qimage.cpp +++ b/tests/auto/gui/image/qimage/tst_qimage.cpp @@ -169,6 +169,9 @@ private slots: void largeInplaceRgbConversion_data(); void largeInplaceRgbConversion(); + void colorSpaceRgbConversion_data(); + void colorSpaceRgbConversion(); + void deepCopyWhenPaintingActive(); void scaled_QTBUG19157(); @@ -3244,6 +3247,81 @@ void tst_QImage::largeInplaceRgbConversion() } } +void tst_QImage::colorSpaceRgbConversion_data() +{ + QTest::addColumn("fromFormat"); + QTest::addColumn("toFormat"); + + // The various possible code paths for color space conversions compatible with RGB color spaces: + QImage::Format formats[] = { + QImage::Format_RGB32, + QImage::Format_ARGB32, + QImage::Format_ARGB32_Premultiplied, + QImage::Format_RGBX64, + QImage::Format_RGBA64, + QImage::Format_RGBA64_Premultiplied, + QImage::Format_RGBX32FPx4, + QImage::Format_RGBA32FPx4, + QImage::Format_RGBA32FPx4_Premultiplied, + QImage::Format_Grayscale8, + QImage::Format_Grayscale16, + }; + + for (auto fromFormat : formats) { + const QLatin1String formatI = formatToString(fromFormat); + for (auto toFormat : formats) { + QTest::addRow("%s -> %s", formatI.data(), formatToString(toFormat).data()) + << fromFormat << toFormat; + } + } +} + +void tst_QImage::colorSpaceRgbConversion() +{ + // Test that all color space conversions work + QFETCH(QImage::Format, fromFormat); + QFETCH(QImage::Format, toFormat); + + bool srcGrayscale = fromFormat == QImage::Format_Grayscale8 || fromFormat == QImage::Format_Grayscale16; + bool dstGrayscale = toFormat == QImage::Format_Grayscale8 || toFormat == QImage::Format_Grayscale16; + + QImage image(16, 16, fromFormat); + image.setColorSpace(QColorSpace::SRgb); + + for (int i = 0; i < image.height(); ++i) { + for (int j = 0; j < image.width(); ++j) { + if (srcGrayscale || dstGrayscale) + image.setPixel(j, i, qRgb((i + j) * 8, (i + j) * 8, (i + j) * 8)); + else + image.setPixel(j, i, qRgb(j * 16, i * 16, (i + j) * 8)); + } + } + + QImage imageConverted = image.convertedToColorSpace(QColorSpace::DisplayP3, toFormat); + QCOMPARE(imageConverted.format(), toFormat); + QCOMPARE(imageConverted.size(), image.size()); + if (dstGrayscale) { + int gray = 0; + for (int x = 0; x < image.width(); ++x) { + int newGray = qGray(imageConverted.pixel(x, 6)); + QCOMPARE_GE(newGray, gray); + gray = newGray; + } + } else { + int red = 0; + int blue = 0; + for (int x = 0; x < image.width(); ++x) { + int newRed = qRed(imageConverted.pixel(x, 5)); + int newBlue = qBlue(imageConverted.pixel(x, 7)); + QCOMPARE_GE(newBlue, blue); + QCOMPARE_GE(newRed, red); + blue = newBlue; + red = newRed; + } + } +} + + void tst_QImage::deepCopyWhenPaintingActive() { QImage image(64, 64, QImage::Format_ARGB32_Premultiplied); diff --git a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp index 780c264f069..6cecf2ca70d 100644 --- a/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp +++ b/tests/auto/gui/painting/qcolorspace/tst_qcolorspace.cpp @@ -42,9 +42,12 @@ private slots: void imageConversionOverLargerGamut(); void imageConversionOverLargerGamut2_data(); void imageConversionOverLargerGamut2(); + void imageConversionOverAnyGamutFP_data(); + void imageConversionOverAnyGamutFP(); + void imageConversionOverAnyGamutFP2_data(); + void imageConversionOverAnyGamutFP2(); void imageConversionOverNonThreeComponentMatrix_data(); void imageConversionOverNonThreeComponentMatrix(); - void loadImage(); void primaries(); @@ -63,6 +66,11 @@ private slots: void transferFunctionTable(); void description(); + void whitePoint_data(); + void whitePoint(); + void setWhitePoint(); + void grayColorSpace(); + void grayColorSpaceEffectivelySRgb(); }; tst_QColorSpace::tst_QColorSpace() @@ -530,6 +538,80 @@ void tst_QColorSpace::imageConversionOverLargerGamut2() QVERIFY(resultImage.pixelColor(0, 255).greenF() > 1.0f); } +void tst_QColorSpace::imageConversionOverAnyGamutFP_data() +{ + QTest::addColumn("fromColorSpace"); + QTest::addColumn("toColorSpace"); + + QTest::newRow("sRGB -> Display-P3") << QColorSpace::SRgb << QColorSpace::DisplayP3; + QTest::newRow("sRGB -> Adobe RGB") << QColorSpace::SRgb << QColorSpace::AdobeRgb; + QTest::newRow("sRGB -> ProPhoto RGB") << QColorSpace::SRgb << QColorSpace::ProPhotoRgb; + QTest::newRow("Adobe RGB -> sRGB") << QColorSpace::AdobeRgb << QColorSpace::SRgb; + QTest::newRow("Adobe RGB -> Display-P3") << QColorSpace::AdobeRgb << QColorSpace::DisplayP3; + QTest::newRow("Adobe RGB -> ProPhoto RGB") << QColorSpace::AdobeRgb << QColorSpace::ProPhotoRgb; + QTest::newRow("Display-P3 -> sRGB") << QColorSpace::DisplayP3 << QColorSpace::SRgb; + QTest::newRow("Display-P3 -> Adobe RGB") << QColorSpace::DisplayP3 << QColorSpace::AdobeRgb; + QTest::newRow("Display-P3 -> ProPhoto RGB") << QColorSpace::DisplayP3 << QColorSpace::ProPhotoRgb; +} + +void tst_QColorSpace::imageConversionOverAnyGamutFP() +{ + QFETCH(QColorSpace::NamedColorSpace, fromColorSpace); + QFETCH(QColorSpace::NamedColorSpace, toColorSpace); + + QColorSpace csfrom(fromColorSpace); + QColorSpace csto(toColorSpace); + csfrom.setTransferFunction(QColorSpace::TransferFunction::Linear); + csto.setTransferFunction(QColorSpace::TransferFunction::Linear); + + QImage testImage(256, 256, QImage::Format_RGBX32FPx4); + testImage.setColorSpace(csfrom); + for (int y = 0; y < 256; ++y) + for (int x = 0; x < 256; ++x) + testImage.setPixel(x, y, qRgb(x, y, 0)); + + QImage resultImage = testImage.convertedToColorSpace(csto); + resultImage.convertToColorSpace(csfrom); + + for (int y = 0; y < 256; ++y) { + for (int x = 0; x < 256; ++x) { + QCOMPARE(resultImage.pixel(x, y), testImage.pixel(x, y)); + } + } +} + +void tst_QColorSpace::imageConversionOverAnyGamutFP2_data() +{ + imageConversionOverAnyGamutFP_data(); +} + +void tst_QColorSpace::imageConversionOverAnyGamutFP2() +{ + QFETCH(QColorSpace::NamedColorSpace, fromColorSpace); + QFETCH(QColorSpace::NamedColorSpace, toColorSpace); + + // Same as imageConversionOverAnyGamutFP but using format switching transform + QColorSpace csfrom(fromColorSpace); + QColorSpace csto(toColorSpace); + csfrom.setTransferFunction(QColorSpace::TransferFunction::Linear); + csto.setTransferFunction(QColorSpace::TransferFunction::Linear); + + QImage testImage(256, 256, QImage::Format_RGB32); + testImage.setColorSpace(csfrom); + for (int y = 0; y < 256; ++y) + for (int x = 0; x < 256; ++x) + testImage.setPixel(x, y, qRgb(x, y, 0)); + + QImage resultImage = testImage.convertedToColorSpace(csto, QImage::Format_RGBX32FPx4); + resultImage.convertToColorSpace(csfrom, QImage::Format_RGB32); + + for (int y = 0; y < 256; ++y) { + for (int x = 0; x < 256; ++x) { + QCOMPARE(resultImage.pixel(x, y), testImage.pixel(x, y)); + } + } +} + void tst_QColorSpace::imageConversionOverNonThreeComponentMatrix_data() { QTest::addColumn("fromColorSpace"); @@ -569,6 +651,7 @@ void tst_QColorSpace::imageConversionOverNonThreeComponentMatrix() testImage.setPixel(x, y, qRgb(x, y, 0)); QImage resultImage = testImage.convertedToColorSpace(toColorSpace); + QCOMPARE(resultImage.size(), testImage.size()); for (int y = 0; y < 256; ++y) { int lastRed = 0; for (int x = 0; x < 256; ++x) { @@ -834,5 +917,92 @@ void tst_QColorSpace::description() QCOMPARE(srgb.description(), QLatin1String("Linear sRGB")); // Set to empty returns default behavior } +void tst_QColorSpace::whitePoint_data() +{ + QTest::addColumn("namedColorSpace"); + QTest::addColumn("whitePoint"); + + QTest::newRow("sRGB") << QColorSpace::SRgb << QColorVector::D65Chromaticity(); + QTest::newRow("Adobe RGB") << QColorSpace::AdobeRgb << QColorVector::D65Chromaticity(); + QTest::newRow("Display-P3") << QColorSpace::DisplayP3 << QColorVector::D65Chromaticity(); + QTest::newRow("ProPhoto RGB") << QColorSpace::ProPhotoRgb << QColorVector::D50Chromaticity(); +} + +void tst_QColorSpace::whitePoint() +{ + QFETCH(QColorSpace::NamedColorSpace, namedColorSpace); + QFETCH(QPointF, whitePoint); + + QColorSpace colorSpace(namedColorSpace); + QPointF wpt = colorSpace.whitePoint(); + QCOMPARE_LE(qAbs(wpt.x() - whitePoint.x()), 0.0000001); + QCOMPARE_LE(qAbs(wpt.y() - whitePoint.y()), 0.0000001); +} + +void tst_QColorSpace::setWhitePoint() +{ + QColorSpace colorSpace(QColorSpace::SRgb); + colorSpace.setWhitePoint(QPointF(0.33, 0.33)); + QCOMPARE_NE(colorSpace, QColorSpace(QColorSpace::SRgb)); + colorSpace.setWhitePoint(QColorVector::D65Chromaticity()); + // Check our matrix manipulations returned us to where we came from + QCOMPARE(colorSpace, QColorSpace(QColorSpace::SRgb)); +} + +void tst_QColorSpace::grayColorSpace() +{ + QColorSpace spc; + QCOMPARE(spc.colorModel(), QColorSpace::ColorModel::Undefined); + QVERIFY(!spc.isValid()); + spc.setWhitePoint(QColorVector::D65Chromaticity()); + spc.setTransferFunction(QColorSpace::TransferFunction::SRgb); + QVERIFY(spc.isValid()); + QCOMPARE(spc.colorModel(), QColorSpace::ColorModel::Gray); + + QColorSpace spc2(QColorVector::D65Chromaticity(), QColorSpace::TransferFunction::SRgb); + QVERIFY(spc2.isValid()); + QCOMPARE(spc2.colorModel(), QColorSpace::ColorModel::Gray); + QCOMPARE(spc, spc2); + + QImage rgbImage(1, 8, QImage::Format_RGB32); + QImage grayImage(1, 255, QImage::Format_Grayscale8); + // RGB images can not have gray color space + rgbImage.setColorSpace(spc2); + grayImage.setColorSpace(spc2); + QCOMPARE_NE(rgbImage.colorSpace(), spc2); + QCOMPARE(grayImage.colorSpace(), spc2); + // But gray images can have RGB color space + rgbImage.setColorSpace(QColorSpace::SRgb); + grayImage.setColorSpace(QColorSpace::SRgb); + QCOMPARE(rgbImage.colorSpace(), QColorSpace(QColorSpace::SRgb)); + QCOMPARE(grayImage.colorSpace(), QColorSpace(QColorSpace::SRgb)); + + // While we can not set a grayscale color space on rgb image, we can convert to one + QImage grayImage2 = rgbImage.convertedToColorSpace(spc2); + QCOMPARE(grayImage2.colorSpace(), spc2); + QCOMPARE(grayImage2.format(), QImage::Format_Grayscale8); +} + +void tst_QColorSpace::grayColorSpaceEffectivelySRgb() +{ + // Test grayscale colorspace conversion by making a gray color space that should act like sRGB on gray values. + QColorSpace sRgb(QColorSpace::SRgb); + QColorSpace sRgbGray(QColorVector::D65Chromaticity(), QColorSpace::TransferFunction::SRgb); + + QImage grayImage1(256, 1, QImage::Format_Grayscale8); + QImage grayImage2(256, 1, QImage::Format_Grayscale8); + for (int i = 0; i < 256; ++i) { + grayImage1.bits()[i] = i; + grayImage2.bits()[i] = i; + } + grayImage1.setColorSpace(sRgb); + grayImage2.setColorSpace(sRgbGray); + + QImage rgbImage1 = grayImage1.convertedTo(QImage::Format_RGB32); + QImage rgbImage2 = grayImage2.convertedToColorSpace(sRgb, QImage::Format_RGB32); + + QCOMPARE(rgbImage1, rgbImage2); +} + QTEST_MAIN(tst_QColorSpace) #include "tst_qcolorspace.moc" diff --git a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp index 3bd94785ef2..5055b572294 100644 --- a/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp +++ b/tests/libfuzzer/gui/painting/qcolorspace/fromiccprofile/main.cpp @@ -5,6 +5,7 @@ #include #include +#include extern "C" int LLVMFuzzerTestOneInput(const char *data, size_t size) { // to reduce noise and increase speed @@ -28,7 +29,12 @@ extern "C" int LLVMFuzzerTestOneInput(const char *data, size_t size) { Q_UNUSED(b); QRgb color = 0xfaf8fa00; color = trans1.map(color); + QImage img(16, 2, cs.colorModel() == QColorSpace::ColorModel::Rgb ? QImage::Format_RGB32 : QImage::Format_Grayscale8); + img.setColorSpace(cs); + QImage img2 = img.convertedToColorSpace(QColorSpace::SRgb); if (cs.isValidTarget()) { + QImage img3 = img2.convertedToColorSpace(cs); + QColorTransform trans2 = QColorSpace(QColorSpace::SRgb).transformationToColorSpace(cs); bool a = (trans1 == trans2); Q_UNUSED(a);