File indexing completed on 2024-04-28 15:25:39
0001 /* 0002 KImageIO Routines to read (and perhaps in the future, write) images 0003 in the high dynamic range EXR format. 0004 0005 SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net> 0006 SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com> 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 /* *** EXR_USE_LEGACY_CONVERSIONS *** 0012 * If defined, the result image is an 8-bit RGB(A) converted 0013 * without icc profiles. Otherwise, a 16-bit images is generated. 0014 * NOTE: The use of legacy conversions are discouraged due to 0015 * imprecise image result. 0016 */ 0017 //#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file 0018 0019 /* *** EXR_ALLOW_LINEAR_COLORSPACE *** 0020 * If defined, the linear data is kept and it is the display program that 0021 * must convert to the monitor profile. Otherwise the data is converted to sRGB 0022 * to accommodate programs that do not support color profiles. 0023 * NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored. 0024 */ 0025 //#define EXR_ALLOW_LINEAR_COLORSPACE // default: commented -> you should define it in your cmake file 0026 0027 #include "exr_p.h" 0028 #include "util_p.h" 0029 0030 #include <IexThrowErrnoExc.h> 0031 #include <ImathBox.h> 0032 #include <ImfArray.h> 0033 #include <ImfBoxAttribute.h> 0034 #include <ImfChannelListAttribute.h> 0035 #include <ImfCompressionAttribute.h> 0036 #include <ImfConvert.h> 0037 #include <ImfFloatAttribute.h> 0038 #include <ImfInputFile.h> 0039 #include <ImfInt64.h> 0040 #include <ImfIntAttribute.h> 0041 #include <ImfLineOrderAttribute.h> 0042 #include <ImfRgbaFile.h> 0043 #include <ImfStandardAttributes.h> 0044 #include <ImfStringAttribute.h> 0045 #include <ImfVecAttribute.h> 0046 #include <ImfVersion.h> 0047 0048 #include <iostream> 0049 0050 #include <QColorSpace> 0051 #include <QDataStream> 0052 #include <QDebug> 0053 #include <QFloat16> 0054 #include <QImage> 0055 #include <QImageIOPlugin> 0056 0057 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) 0058 #include <QTimeZone> 0059 #endif 0060 0061 // Allow the code to works on all QT versions supported by KDE 0062 // project (Qt 5.15 and Qt 6.x) to easy backports fixes. 0063 #if (QT_VERSION_MAJOR >= 6) && !defined(EXR_USE_LEGACY_CONVERSIONS) 0064 // If uncommented, the image is rendered in a float16 format, the result is very precise 0065 #define EXR_USE_QT6_FLOAT_IMAGE // default uncommented 0066 #endif 0067 0068 class K_IStream : public Imf::IStream 0069 { 0070 public: 0071 K_IStream(QIODevice *dev, const QByteArray &fileName) 0072 : IStream(fileName.data()) 0073 , m_dev(dev) 0074 { 0075 } 0076 0077 bool read(char c[], int n) override; 0078 #if OPENEXR_VERSION_MAJOR > 2 0079 uint64_t tellg() override; 0080 void seekg(uint64_t pos) override; 0081 #else 0082 Imf::Int64 tellg() override; 0083 void seekg(Imf::Int64 pos) override; 0084 #endif 0085 void clear() override; 0086 0087 private: 0088 QIODevice *m_dev; 0089 }; 0090 0091 bool K_IStream::read(char c[], int n) 0092 { 0093 qint64 result = m_dev->read(c, n); 0094 if (result > 0) { 0095 return true; 0096 } else if (result == 0) { 0097 throw Iex::InputExc("Unexpected end of file"); 0098 } else { // negative value { 0099 Iex::throwErrnoExc("Error in read", result); 0100 } 0101 return false; 0102 } 0103 0104 #if OPENEXR_VERSION_MAJOR > 2 0105 uint64_t K_IStream::tellg() 0106 #else 0107 Imf::Int64 K_IStream::tellg() 0108 #endif 0109 { 0110 return m_dev->pos(); 0111 } 0112 0113 #if OPENEXR_VERSION_MAJOR > 2 0114 void K_IStream::seekg(uint64_t pos) 0115 #else 0116 void K_IStream::seekg(Imf::Int64 pos) 0117 #endif 0118 { 0119 m_dev->seek(pos); 0120 } 0121 0122 void K_IStream::clear() 0123 { 0124 // TODO 0125 } 0126 0127 #ifdef EXR_USE_LEGACY_CONVERSIONS 0128 // source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html 0129 inline unsigned char gamma(float x) 0130 { 0131 x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f; 0132 return (unsigned char)qBound(0.f, x, 255.f); 0133 } 0134 inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel) 0135 { 0136 return qRgba(gamma(float(imagePixel.r)), 0137 gamma(float(imagePixel.g)), 0138 gamma(float(imagePixel.b)), 0139 (unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f)); 0140 } 0141 #endif 0142 0143 EXRHandler::EXRHandler() 0144 { 0145 } 0146 0147 bool EXRHandler::canRead() const 0148 { 0149 if (canRead(device())) { 0150 setFormat("exr"); 0151 return true; 0152 } 0153 return false; 0154 } 0155 0156 bool EXRHandler::read(QImage *outImage) 0157 { 0158 try { 0159 int width; 0160 int height; 0161 0162 K_IStream istr(device(), QByteArray()); 0163 Imf::RgbaInputFile file(istr); 0164 Imath::Box2i dw = file.dataWindow(); 0165 bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A; 0166 0167 width = dw.max.x - dw.min.x + 1; 0168 height = dw.max.y - dw.min.y + 1; 0169 0170 #if defined(EXR_USE_LEGACY_CONVERSIONS) 0171 QImage image = imageAlloc(width, height, isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32); 0172 #elif defined(EXR_USE_QT6_FLOAT_IMAGE) 0173 QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4); 0174 #else 0175 QImage image = imageAlloc(width, height, isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64); 0176 #endif 0177 if (image.isNull()) { 0178 qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height); 0179 return false; 0180 } 0181 0182 // set some useful metadata 0183 auto &&h = file.header(); 0184 if (auto comments = h.findTypedAttribute<Imf::StringAttribute>("comments")) { 0185 image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value())); 0186 } 0187 if (auto owner = h.findTypedAttribute<Imf::StringAttribute>("owner")) { 0188 image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value())); 0189 } 0190 if (auto capDate = h.findTypedAttribute<Imf::StringAttribute>("capDate")) { 0191 float off = 0; 0192 if (auto utcOffset = h.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) { 0193 off = utcOffset->value(); 0194 } 0195 auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss")); 0196 if (dateTime.isValid()) { 0197 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) 0198 dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off)); 0199 #else 0200 dateTime.setOffsetFromUtc(off); 0201 #endif 0202 image.setText(QStringLiteral("Date"), dateTime.toString(Qt::ISODate)); 0203 } 0204 } 0205 if (auto xDensity = h.findTypedAttribute<Imf::FloatAttribute>("xDensity")) { 0206 float par = 1; 0207 if (auto pixelAspectRatio = h.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) { 0208 par = pixelAspectRatio->value(); 0209 } 0210 image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54)); 0211 image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54)); 0212 } 0213 0214 Imf::Array<Imf::Rgba> pixels; 0215 pixels.resizeErase(width); 0216 0217 // somehow copy pixels into image 0218 for (int y = 0; y < height; ++y) { 0219 auto my = dw.min.y + y; 0220 if (my <= dw.max.y) { // paranoia check 0221 file.setFrameBuffer(&pixels[0] - dw.min.x - qint64(my) * width, 1, width); 0222 file.readPixels(my, my); 0223 0224 #if defined(EXR_USE_LEGACY_CONVERSIONS) 0225 auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y)); 0226 for (int x = 0; x < width; ++x) { 0227 *(scanLine + x) = RgbaToQrgba(pixels[x]); 0228 } 0229 #elif defined(EXR_USE_QT6_FLOAT_IMAGE) 0230 auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y)); 0231 for (int x = 0; x < width; ++x) { 0232 auto xcs = x * 4; 0233 *(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[x].r), 1.f)); 0234 *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[x].g), 1.f)); 0235 *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[x].b), 1.f)); 0236 *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[x].a), 1.f) : 1.f); 0237 } 0238 #else 0239 auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y)); 0240 for (int x = 0; x < width; ++x) { 0241 *(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[x].r) * 65535.f + 0.5f, 65535.f)), 0242 quint16(qBound(0.f, float(pixels[x].g) * 65535.f + 0.5f, 65535.f)), 0243 quint16(qBound(0.f, float(pixels[x].b) * 65535.f + 0.5f, 65535.f)), 0244 isRgba ? quint16(qBound(0.f, float(pixels[x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535)); 0245 } 0246 #endif 0247 } 0248 } 0249 0250 // final color operations 0251 #ifndef EXR_USE_LEGACY_CONVERSIONS 0252 image.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); 0253 #ifndef EXR_ALLOW_LINEAR_COLORSPACE 0254 image.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); 0255 #endif // !EXR_ALLOW_LINEAR_COLORSPACE 0256 #endif // !EXR_USE_LEGACY_CONVERSIONS 0257 0258 *outImage = image; 0259 0260 return true; 0261 } catch (const std::exception &exc) { 0262 // qDebug() << exc.what(); 0263 return false; 0264 } 0265 } 0266 0267 bool EXRHandler::canRead(QIODevice *device) 0268 { 0269 if (!device) { 0270 qWarning("EXRHandler::canRead() called with no device"); 0271 return false; 0272 } 0273 0274 const QByteArray head = device->peek(4); 0275 0276 return Imf::isImfMagic(head.data()); 0277 } 0278 0279 QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const 0280 { 0281 if (format == "exr") { 0282 return Capabilities(CanRead); 0283 } 0284 if (!format.isEmpty()) { 0285 return {}; 0286 } 0287 if (!device->isOpen()) { 0288 return {}; 0289 } 0290 0291 Capabilities cap; 0292 if (device->isReadable() && EXRHandler::canRead(device)) { 0293 cap |= CanRead; 0294 } 0295 return cap; 0296 } 0297 0298 QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const 0299 { 0300 QImageIOHandler *handler = new EXRHandler; 0301 handler->setDevice(device); 0302 handler->setFormat(format); 0303 return handler; 0304 } 0305 0306 #include "moc_exr_p.cpp"