diff --git a/autotests/read/exr/rgb-gimp.png b/autotests/read/exr/rgb-gimp.png index e87f80b..37673a9 100644 Binary files a/autotests/read/exr/rgb-gimp.png and b/autotests/read/exr/rgb-gimp.png differ diff --git a/src/imageformats/exr.cpp b/src/imageformats/exr.cpp index 207ab17..a219ee0 100644 --- a/src/imageformats/exr.cpp +++ b/src/imageformats/exr.cpp @@ -3,10 +3,27 @@ in the high dynamic range EXR format. SPDX-FileCopyrightText: 2003 Brad Hards + SPDX-FileCopyrightText: 2023 Mirco Miranda SPDX-License-Identifier: LGPL-2.0-or-later */ +/* *** EXR_USE_LEGACY_CONVERSIONS *** + * If defined, the result image is an 8-bit RGB(A) converted + * without icc profiles. Otherwise, a 16-bit images is generated. + * NOTE: The use of legacy conversions are discouraged due to + * imprecise image result. + */ +//#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file + +/* *** EXR_ALLOW_LINEAR_COLORSPACE *** + * If defined, the linear data is kept and it is the display program that + * must convert to the monitor profile. Otherwise the data is converted to sRGB + * to accommodate programs that do not support color profiles. + * NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored. + */ +//#define EXR_ALLOW_LINEAR_COLORSPACE // default: commented -> you should define it in your cmake file + #include "exr_p.h" #include "util_p.h" @@ -30,11 +47,24 @@ #include +#include #include #include +#include #include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +#include +#endif + +// Allow the code to works on all QT versions supported by KDE +// project (Qt 5.15 and Qt 6.x) to easy backports fixes. +#if (QT_VERSION_MAJOR >= 6) && !defined(EXR_USE_LEGACY_CONVERSIONS) +// If uncommented, the image is rendered in a float16 format, the result is very precise +#define EXR_USE_QT6_FLOAT_IMAGE // default uncommented +#endif + class K_IStream : public Imf::IStream { public: @@ -94,77 +124,21 @@ void K_IStream::clear() // TODO } -/* this does a conversion from the ILM Half (equal to Nvidia Half) - * format into the normal 32 bit pixel format. Process is from the - * ILM code. - */ -QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel) +#ifdef EXR_USE_LEGACY_CONVERSIONS +// source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html +inline unsigned char gamma(float x) { - float r; - float g; - float b; - float a; - - // 1) Compensate for fogging by subtracting defog - // from the raw pixel values. - // Response: We work with defog of 0.0, so this is a no-op - - // 2) Multiply the defogged pixel values by - // 2^(exposure + 2.47393). - // Response: We work with exposure of 0.0. - // (2^2.47393) is 5.55555 - r = imagePixel.r * 5.55555; - g = imagePixel.g * 5.55555; - b = imagePixel.b * 5.55555; - a = imagePixel.a * 5.55555; - - // 3) Values, which are now 1.0, are called "middle gray". - // If defog and exposure are both set to 0.0, then - // middle gray corresponds to a raw pixel value of 0.18. - // In step 6, middle gray values will be mapped to an - // intensity 3.5 f-stops below the display's maximum - // intensity. - // Response: no apparent content. - - // 4) Apply a knee function. The knee function has two - // parameters, kneeLow and kneeHigh. Pixel values - // below 2^kneeLow are not changed by the knee - // function. Pixel values above kneeLow are lowered - // according to a logarithmic curve, such that the - // value 2^kneeHigh is mapped to 2^3.5 (in step 6, - // this value will be mapped to the display's - // maximum intensity). - // Response: kneeLow = 0.0 (2^0.0 => 1); kneeHigh = 5.0 (2^5 =>32) - if (r > 1.0) { - r = 1.0 + std::log((r - 1.0) * 0.184874 + 1) / 0.184874; - } - if (g > 1.0) { - g = 1.0 + std::log((g - 1.0) * 0.184874 + 1) / 0.184874; - } - if (b > 1.0) { - b = 1.0 + std::log((b - 1.0) * 0.184874 + 1) / 0.184874; - } - if (a > 1.0) { - a = 1.0 + std::log((a - 1.0) * 0.184874 + 1) / 0.184874; - } - // - // 5) Gamma-correct the pixel values, assuming that the - // screen's gamma is 0.4545 (or 1/2.2). - r = std::pow(r, 0.4545); - g = std::pow(g, 0.4545); - b = std::pow(b, 0.4545); - a = std::pow(a, 0.4545); - - // 6) Scale the values such that pixels middle gray - // pixels are mapped to 84.66 (or 3.5 f-stops below - // the display's maximum intensity). - // - // 7) Clamp the values to [0, 255]. - return qRgba((unsigned char)(Imath::clamp(r * 84.66f, 0.f, 255.f)), - (unsigned char)(Imath::clamp(g * 84.66f, 0.f, 255.f)), - (unsigned char)(Imath::clamp(b * 84.66f, 0.f, 255.f)), - (unsigned char)(Imath::clamp(a * 84.66f, 0.f, 255.f))); + x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f; + return (unsigned char)qBound(0.f, x, 255.f); } +inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel) +{ + return qRgba(gamma(float(imagePixel.r)), + gamma(float(imagePixel.g)), + gamma(float(imagePixel.b)), + (unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f)); +} +#endif EXRHandler::EXRHandler() { @@ -188,29 +162,98 @@ bool EXRHandler::read(QImage *outImage) K_IStream istr(device(), QByteArray()); Imf::RgbaInputFile file(istr); Imath::Box2i dw = file.dataWindow(); + bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A; width = dw.max.x - dw.min.x + 1; height = dw.max.y - dw.min.y + 1; - QImage image = imageAlloc(width, height, QImage::Format_RGB32); +#if defined(EXR_USE_LEGACY_CONVERSIONS) + QImage image = imageAlloc(width, height, isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32); +#elif defined(EXR_USE_QT6_FLOAT_IMAGE) + QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4); +#else + QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64); +#endif if (image.isNull()) { qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height); return false; } - Imf::Array2D pixels; - pixels.resizeErase(height, width); - - file.setFrameBuffer(&pixels[0][0] - dw.min.x - dw.min.y * width, 1, width); - file.readPixels(dw.min.y, dw.max.y); - - // somehow copy pixels into image - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // copy pixels(x,y) into image(x,y) - image.setPixel(x, y, RgbaToQrgba(pixels[y][x])); + // set some useful metadata + auto &&h = file.header(); + if (auto comments = h.findTypedAttribute("comments")) { + image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value())); + } + if (auto owner = h.findTypedAttribute("owner")) { + image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value())); + } + if (auto capDate = h.findTypedAttribute("capDate")) { + float off = 0; + if (auto utcOffset = h.findTypedAttribute("utcOffset")) { + off = utcOffset->value(); + } + auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss")); + if (dateTime.isValid()) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off)); +#else + dateTime.setOffsetFromUtc(off); +#endif + image.setText(QStringLiteral("Date"), dateTime.toString(Qt::ISODate)); } } + if (auto xDensity = h.findTypedAttribute("xDensity")) { + float par = 1; + if (auto pixelAspectRatio = h.findTypedAttribute("pixelAspectRatio")) { + par = pixelAspectRatio->value(); + } + image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54)); + image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54)); + } + + Imf::Array pixels; + pixels.resizeErase(width); + + // somehow copy pixels into image + for (int y = 0; y < height; ++y) { + auto my = dw.min.y + y; + if (my <= dw.max.y) { // paranoia check + file.setFrameBuffer(&pixels[0] - dw.min.x - qint64(my) * width, 1, width); + file.readPixels(my, my); + +#if defined(EXR_USE_LEGACY_CONVERSIONS) + auto scanLine = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < width; ++x) { + *(scanLine + x) = RgbaToQrgba(pixels[x]); + } +#elif defined(EXR_USE_QT6_FLOAT_IMAGE) + auto scanLine = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < width; ++x) { + auto xcs = x * 4; + *(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[x].r), 1.f)); + *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[x].g), 1.f)); + *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[x].b), 1.f)); + *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[x].a), 1.f) : 1.f); + } +#else + auto scanLine = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < width; ++x) { + *(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[x].r) * 65535.f + 0.5f, 65535.f)), + quint16(qBound(0.f, float(pixels[x].g) * 65535.f + 0.5f, 65535.f)), + quint16(qBound(0.f, float(pixels[x].b) * 65535.f + 0.5f, 65535.f)), + isRgba ? quint16(qBound(0.f, float(pixels[x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535)); + } +#endif + } + } + + // final color operations +#ifndef EXR_USE_LEGACY_CONVERSIONS + image.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); +#ifndef EXR_ALLOW_LINEAR_COLORSPACE + image.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); +#endif // !EXR_ALLOW_LINEAR_COLORSPACE +#endif // !EXR_USE_LEGACY_CONVERSIONS *outImage = image;