From 899a2df42d7ce1ee8cdeee24f76d18114ae950be Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Mon, 28 Aug 2023 17:29:34 +0000 Subject: [PATCH] exr: multiple fixes (kf5) The code is the same of MR !170 (master) but test cases are slightly different due to rounding. * Support for images with transparency * Precise colorspace conversion using QT color spaces * Set the correct resolution * Set useful metadata * Creates 16-bits images * Speed improvements --- autotests/read/exr/rgb-gimp.png | Bin 1940 -> 2196 bytes src/imageformats/exr.cpp | 205 +++++++++++++++++++------------- 2 files changed, 124 insertions(+), 81 deletions(-) diff --git a/autotests/read/exr/rgb-gimp.png b/autotests/read/exr/rgb-gimp.png index e87f80b7a88b44686f4634f3a39e245be604f406..5b06d554730e33d57e6a839146f3e6611719700f 100644 GIT binary patch literal 2196 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 + 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;