From 7d63a1d8fac63585abc874f87daa195f8b262dee Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Mon, 28 Aug 2023 17:30:50 +0000 Subject: [PATCH] exr: multiple fixes - Support for images with transparency - Precise colorspace conversion using QT color spaces - Set the correct resolution - Set useful metadata - Support for RGBX16FPx4 format in Qt 6 - Speed improvements ![image](/uploads/bb36492d71acce4995e8b4229a813031/image.png) --- autotests/read/exr/rgb-gimp.png | Bin 1940 -> 1898 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..37673a96b7eeb78be3190eb7bdf95fc6ca4e199b 100644 GIT binary patch delta 1895 zcmV-t2blPj59$t(7=I7~00022h%w**000DMK}|sb0I_P@oOkj7008)DLqkw=Qb$4n z0C=3Okg-kzK@>&Lf)pB!g~m{BVX3jT1TAQcl8uFEwYvh%qMMmb^aqT!jjgfb51{7@ zNJt0;A3=an^JWR`G<4kL&79ns%-r|D8AV}B=St9eOO#i+aetp`&n}kyP{-wJ_0IMz zIj|U-6$Y2)tu6PqXD00(qQO+^Ri2onz|J5ob3nE(I<^+`lQRCt`#m|JWdR~g5DXJ#*B zuQ$7n(u;UZaf;)tFR^FWyK{J0x2lz>j$w-0-{Uu>;V zA6uMdz5Nj+@u10Kffh}m1z1`n2sMFWRt(V8rdpDOSh~cB!QiT)sg6Xr;^-_*8WL%! zXLjo(2G|7@uFeJqx)On3?B_$AQiv(aXWW~df#C;%I^ZpWQ zhd#wUg{r=ZWlYnIYe>JG3Hs^eFBbr2xuy+v7tdBhzUX(7VMbRJV2pK)=t_FzY%s!I z?c{2(%kPoV>Uv(TJYBk|D+-gGtUTx6(N62)@PF^>h<yidg->a82$3Ve!oNdq)(D07RgwYf%dB3)Ef$aI{N!clPlFv7jCe%uyx><<<-97 zWT6k_i8=TEj9XiXL7%PSug9K z%YP+1K6!B{&vk8ZS)VFi)N9&7cVPp^f?FCgs|m(`RpvQ7aD8Y7mykU3 z+B>ja2C(^gQZlM< zUD+zH1%JP?U+&ZjL&N?Kc`QGs2M$dXZhxuG-=jBICoerReJqQ^Xtlw1dAyX6OFBh2 zYYSbVd;Ok#3n^CD8nQS*LLQ88i4wZ4DQY^S{qm;n_A^YxZS-{W)92!6i ztI*UsfT#Vb<;imoWz<-6|+r#U_>%*^|I_#f4_4e@V!*35aY}2-ulZJNY`#7&(`autagS-9h{_!BoDygZ1v#|2XBy--PD!r_s227N`Qxz!hcE30$gS^Nf#%}Yoyb^Fm$8bfy=sLO16mytbY-+EPqQA zvw$QCLfs;tES@TZmwV~B%Szd$3FEWqLjjM1RC_)2j8!V9^6*=g#g z$Q((s(w<&%W+7$)bwbU^u)cobX+OdFmnU>Yr^v`AIau0AhJJBzV?9B}EFg`M7N3%} zxi3{P-jH7SN0wO#@y{`NGJDjXc>*m7eO0G=lXJiu=gBxr4 zKYQbJ8Ki9)8Rjuy`2mQu4bwR<<&&r(SK4m;0n8 zi7xQUlzE0?wpih2P{@LIUe=^D0pja?k$C2fDdw*17%z(jRT0>p$ z|90?n<=avodvz=?qcyKGquYM;q@Upv3%fv^g%oQK7%UDMR?AwM;edB=-{4Wh_EI?* z)*3f;o`}}6JT6VlQ>X)%I>o}8!s)=XE`vXg@Nn`WbvkHii**BtOJ-SKKZuy8P+B~( zy3y|wk0f2}Ih>|Lihp}n5vhYK?JHB9#GHY7JUqD+JSltSH!mh+owV(bNhE97O_DUJ zW%yf~m<6;aeHKsP*{|1hC}>-=lixKw>{iM8GYbASZtq{A+MU2e#ryzHeSpET3|HssOdHRtXx0$;^2O1 z+iRs>acQwhyqG^4`)M?Z%M<=iSuNkc*@in+O>nb=4 zg5U$h(aA~CMI`>W6)Mu+%gy=tbI-{={|z8*o0{ddi~*Kibbk}EsGQC$$?Y%bKp)yT z2Ncb7W)~LXXgWUc5D@WpRMcv|ui*$~bx#8VVe*vbxJ9z~R(!4K2;L=!wY-ugpOHsg z9+3PXbhXB>f^$`tM9r|9O3aZ(O*!|6)jzD|U5h+Pj$|#H^`$g<#xhIV@7Y9GA>Cd~ zGWK%~z(WEtM1LVeMG31gQN$>tP5&C_jaBpJQDBb7r-L$FX5^4(?g-TqtLlsfb)C~h zS=_;YZ#`pTyjDFF1zMkd|9uE(-vYiXzW;UP``^bv@CmrI>-mL4VETi4rLIK|fbI?8 z{Hm_0JHW*z(EI2pu2PkPT#{h`Z>K<42I#o~I+ts{{eQm4$vu!-=XG5Jd;7pJ_jj)} z@nW#*MUoD9>|KY&uICtCDCAZ8!@uVTB}H>6+OXw>00009a7bBm000ie000ie0hKEb z8vpmU%Pg`Z9zsEW zo&>BD?~N_vfNB!3y(f~U{zH8xNOQG4H5AEg3{LJ$ch zsYL5Fp}d5@lq1Kot;y5~?hrzAk>y^zc)BQR%Qp(8xQFrKaIg+ji-{ecxUP$B2Rn|b z5vDv=DrwA0-k(L@brv8~LAWBULam)_S*>(s7tHy)q)vGssFGG!hLtjkn}p+uW#b`I zmVfpfUW>9zElV+!K%r7n^Cp8lUNFUE!A3&%?$N+H+bFu7FQtRb#VY(g!t7jwq@-?! zDbSF-i>48SOsSTy>UN@XbL4Y~Ho{{6O3xPJ%UDh&S!mVc8H0vZ4Kjjdm(BZvMK%$t~ zFjfZ7tEVN>I6ZU^);IL!nN)T2b&jubR}vA+NPb)-3IfO0Q)j!TGuzk?-gM^K?!Pnt z1L%DU@zof%j`O9SalJusze2RpZy>1j<~isU^9C(zV;FFKLJJgrtc^P8Ff%nq%rtl(oBf7IvzoYN-}h(I8QQl>G%7-0<$ASOdX zG&ZSd#i`Q15gGX@F~oQZ#H5Xof`2v)z*ofl0YC(!0S-FSa}quAY|?oO40p}UnqKnV zVMY$)ZWul99DD6*)XBFRe}+lx%dStB^P?dPK*nsaWk$R~_R8 zq3Ue~`5*74H`^onPE6BhroXr7tr^36rmT4?1F5O;mgpk_0HjXmb!Fp^nQrNFwb1+Y z=#3=tILIaeb@bGH=2a3Feaz*gyh}d6C+!J<2!%;}_65zV$)S9e$MukqQ{gmnJ7}a% zFREwv%9*$?Miu;Ku{2w+gnwovXgK~**ULePp#og|+Ho4LGv$z?e*nAy7b-LfU%09W z1O$v6Xci%;r1P{{)4yk>$`oF3odUaz0LW$`;xl;w5pp4+EW|_~iu{&A3Je11^rZq6 z2;kH+tsCQe?%}25>}f1|v*dacnES|IyNXa^t~>HJT>-hQwibiC0gb+>4{2DX`ykXMLDOU?Uq@yY(6 zXT;b!+W(74&lJ`?{@%E(wJWl0nVj_=IEYK`&hea5h=d7Z>3?VoFpkr%)$Vc;Kwfec zcGUR-00a&`AG`n%1dH}w)-6I>9lvk~cL0=BPC2D>)<6UYN#_1~6Tq!2nF-r_W3Pcx z$?m20oml`$kxfAk 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;