Full range HDR support

EXR, HDR, JXR and PFM formats support High Dynamic Range images (FP values grater than 1).

In summary, here is the list of changes:

    EXR, HDR, JXR and PFM: When working with FP formats, the clamp between 0 and 1 ​​is no longer done.
    EXR: Removed old SDR code and conversions. Due to the lack of a QImage Gray FP format, Gray images are output as RGB FP (recently added code for Qt 6.8 has been removed).
    PFM: Due to the lack of a QImage Gray FP format, Gray images are output as RGB FP.
    HDR: Added rotation and exposure support.

With this patch, EXR, JXR, HDR, PFM behave like Qt's TIFF plugin when working with FP images.
This commit is contained in:
Mirco Miranda
2024-06-20 15:45:08 +02:00
committed by Albert Astals Cid
parent 4c0f49295b
commit f5a6de7280
24 changed files with 715 additions and 309 deletions

View File

@ -436,6 +436,13 @@ public:
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
<< QImage::Format_CMYK8888
#endif
#ifndef JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA16FPx4
<< QImage::Format_RGBX16FPx4
<< QImage::Format_RGBA32FPx4
<< QImage::Format_RGBA32FPx4_Premultiplied
<< QImage::Format_RGBX32FPx4
#endif // JXR_DENY_FLOAT_IMAGE
<< QImage::Format_RGBA64
<< QImage::Format_RGBA64_Premultiplied
<< QImage::Format_RGBA8888
@ -476,7 +483,18 @@ public:
} else {
qi = qi.convertToFormat(alpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
}
#ifndef JXR_DENY_FLOAT_IMAGE
} else if(qi.format() == QImage::Format_RGBA16FPx4 ||
qi.format() == QImage::Format_RGBX16FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4 ||
qi.format() == QImage::Format_RGBA32FPx4_Premultiplied ||
qi.format() == QImage::Format_RGBX32FPx4) {
auto cs = qi.colorSpace();
if (cs.isValid() && cs.transferFunction() != QColorSpace::TransferFunction::Linear) {
qi = qi.convertedToColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
}
#endif // JXR_DENY_FLOAT_IMAGE
return qi;
}
@ -759,35 +777,6 @@ private:
}
};
template<class T>
inline T scRGBTosRGB(T f)
{
// convert from linear scRGB to non-linear sRGB
if (f <= T(0)) {
return T(0);
}
if (f <= T(0.0031308f)) {
return qBound(T(0), f * T(12.92f), T(1));
}
if (f < T(1)) {
return qBound(T(0), T(1.055f) * T(pow(f, T(1.0) / T(2.4))) - T(0.055), T(1));
}
return T(1);
}
template<class T>
inline T alpha_scRGBTosRGB(T f)
{
// alpha is converted differently than RGB in scRGB
if (f <= T(0)) {
return T(0);
}
if (f < T(1.0)) {
return T(f);
}
return T(1);
}
bool JXRHandler::read(QImage *outImage)
{
if (!d->initForReading(device())) {
@ -841,13 +830,11 @@ bool JXRHandler::read(QImage *outImage)
} else { // additional buffer needed
qint64 convStrideSize = (img.width() * d->pDecoder->WMP.wmiI.cBitsPerUnit + 7) / 8;
qint64 buffSize = convStrideSize * img.height();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qint64 limit = QImageReader::allocationLimit();
if (limit && (buffSize + img.sizeInBytes()) > limit * 1024 * 1024) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::read() unable to covert due to allocation limit set:" << limit << "MiB";
return false;
}
#endif
QVector<quint8> ba(buffSize);
if (auto err = pConverter->Copy(pConverter, &rect, ba.data(), convStrideSize)) {
PKFormatConverter_Release(&pConverter);
@ -866,34 +853,24 @@ bool JXRHandler::read(QImage *outImage)
d->setTextMetadata(img);
#ifndef JXR_DENY_FLOAT_IMAGE
// JXR float are stored in scRGB -> range -0,5 to 7,5 (source Wikipedia)
if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
// JXR float are stored in scRGB.
if (img.format() == QImage::Format_RGBX16FPx4 || img.format() == QImage::Format_RGBA16FPx4 || img.format() == QImage::Format_RGBA16FPx4_Premultiplied ||
img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4 || img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
auto hasAlpha = img.hasAlphaChannel();
for (qint32 y = 0, h = img.height(); y < h; ++y) {
qfloat16 *line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
for (int x = 0, w = img.width(); x < w; ++x) {
const auto x4 = x * 4;
line[x4 + 0] = scRGBTosRGB(line[x4 + 0]);
line[x4 + 1] = scRGBTosRGB(line[x4 + 1]);
line[x4 + 2] = scRGBTosRGB(line[x4 + 2]);
line[x4 + 3] = hasAlpha ? alpha_scRGBTosRGB(line[x4 + 3]) : qfloat16(1);
if (img.depth() == 64) {
auto line = reinterpret_cast<qfloat16 *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], qfloat16(0), qfloat16(1)) : qfloat16(1);
} else {
auto line = reinterpret_cast<float *>(img.scanLine(y));
for (int x = 0, w = img.width() * 4; x < w; x += 4)
line[x + 3] = hasAlpha ? std::clamp(line[x + 3], float(0), float(1)) : float(1);
}
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
} else if (img.format() == QImage::Format_RGBX32FPx4 || img.format() == QImage::Format_RGBA32FPx4
|| img.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
auto hasAlpha = img.hasAlphaChannel();
for (qint32 y = 0, h = img.height(); y < h; ++y) {
float *line = reinterpret_cast<float *>(img.scanLine(y));
for (int x = 0, w = img.width(); x < w; ++x) {
const auto x4 = x * 4;
line[x4 + 0] = scRGBTosRGB(line[x4 + 0]);
line[x4 + 1] = scRGBTosRGB(line[x4 + 1]);
line[x4 + 2] = scRGBTosRGB(line[x4 + 2]);
line[x4 + 3] = hasAlpha ? alpha_scRGBTosRGB(line[x4 + 3]) : float(1);
}
if(!img.colorSpace().isValid()) {
img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
}
img.setColorSpace(QColorSpace(QColorSpace::SRgb));
}
#endif
@ -922,19 +899,11 @@ bool JXRHandler::write(const QImage &image)
#ifndef JXR_DISABLE_BGRA_HACK
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppBGRA;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qi = qi.rgbSwapped();
#else
qi.rgbSwap();
#endif
}
if (IsEqualGUID(jxlfmt, GUID_PKPixelFormat32bppPRGBA)) {
jxlfmt = GUID_PKPixelFormat32bppPBGRA;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
qi = qi.rgbSwapped();
#else
qi.rgbSwap();
#endif
}
#endif
@ -968,7 +937,7 @@ bool JXRHandler::write(const QImage &image)
}
// setting metadata (a failure of setting metadata doesn't stop the encoding)
auto cs = image.colorSpace().iccProfile();
auto cs = qi.colorSpace().iccProfile();
if (!cs.isEmpty()) {
if (auto err = d->pEncoder->SetColorContext(d->pEncoder, reinterpret_cast<quint8 *>(cs.data()), cs.size())) {
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting ICC profile:" << err;
@ -1043,28 +1012,28 @@ QVariant JXRHandler::option(ImageOption option) const
if (d->initForReading(device())) {
switch (d->pDecoder->WMP.oOrientationFromContainer) {
case O_FLIPV:
v = QImageIOHandler::TransformationFlip;
v = int(QImageIOHandler::TransformationFlip);
break;
case O_FLIPH:
v = QImageIOHandler::TransformationMirror;
v = int(QImageIOHandler::TransformationMirror);
break;
case O_FLIPVH:
v = QImageIOHandler::TransformationRotate180;
v = int(QImageIOHandler::TransformationRotate180);
break;
case O_RCW:
v = QImageIOHandler::TransformationRotate90;
v = int(QImageIOHandler::TransformationRotate90);
break;
case O_RCW_FLIPV:
v = QImageIOHandler::TransformationFlipAndRotate90;
v = int(QImageIOHandler::TransformationFlipAndRotate90);
break;
case O_RCW_FLIPH:
v = QImageIOHandler::TransformationMirrorAndRotate90;
v = int(QImageIOHandler::TransformationMirrorAndRotate90);
break;
case O_RCW_FLIPVH:
v = QImageIOHandler::TransformationRotate270;
v = int(QImageIOHandler::TransformationRotate270);
break;
default:
v = QImageIOHandler::TransformationNone;
v = int(QImageIOHandler::TransformationNone);
break;
}
}