File indexing completed on 2024-04-28 03:54:41
0001 /* 0002 The high dynamic range EXR format support for QImage. 0003 0004 SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net> 0005 SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com> 0006 0007 SPDX-License-Identifier: LGPL-2.0-or-later 0008 */ 0009 0010 /* *** EXR_USE_LEGACY_CONVERSIONS *** 0011 * If defined, the result image is an 8-bit RGB(A) converted 0012 * without icc profiles. Otherwise, a 16-bit images is generated. 0013 * NOTE: The use of legacy conversions are discouraged due to 0014 * imprecise image result. 0015 */ 0016 //#define EXR_USE_LEGACY_CONVERSIONS // default commented -> you should define it in your cmake file 0017 0018 /* *** EXR_CONVERT_TO_SRGB *** 0019 * If defined, the linear data is converted to sRGB on read to accommodate 0020 * programs that do not support color profiles. 0021 * Otherwise the data are kept as is and it is the display program that 0022 * must convert to the monitor profile. 0023 * NOTE: If EXR_USE_LEGACY_CONVERSIONS is active, this is ignored. 0024 */ 0025 //#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file 0026 0027 /* *** EXR_STORE_XMP_ATTRIBUTE *** 0028 * If defined, disables the stores XMP values in a non-standard attribute named "xmp". 0029 * The QImage metadata used is "XML:com.adobe.xmp". 0030 * NOTE: The use of non-standard attributes is possible but discouraged by the specification. However, 0031 * metadata is essential for good image management and programs like darktable also set this 0032 * attribute. Gimp reads the "xmp" attribute and Darktable writes it as well. 0033 */ 0034 //#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file 0035 0036 /* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT *** 0037 * The maximum size in pixel allowed by the plugin. 0038 */ 0039 #ifndef EXR_MAX_IMAGE_WIDTH 0040 #define EXR_MAX_IMAGE_WIDTH 300000 0041 #endif 0042 #ifndef EXR_MAX_IMAGE_HEIGHT 0043 #define EXR_MAX_IMAGE_HEIGHT EXR_MAX_IMAGE_WIDTH 0044 #endif 0045 0046 /* *** EXR_LINES_PER_BLOCK *** 0047 * Allows certain compression schemes to work in multithreading 0048 * Requires up to "LINES_PER_BLOCK * MAX_IMAGE_WIDTH * 8" 0049 * additional RAM (e.g. if 128, up to 307MiB of RAM). 0050 * There is a performance gain with the following parameters (based on empirical tests): 0051 * - PIZ compression needs 64+ lines 0052 * - ZIPS compression needs 8+ lines 0053 * - ZIP compression needs 32+ lines 0054 * - Others not tested 0055 * 0056 * NOTE: The OpenEXR documentation states that the higher the better :) 0057 */ 0058 #ifndef EXR_LINES_PER_BLOCK 0059 #define EXR_LINES_PER_BLOCK 128 0060 #endif 0061 0062 #include "exr_p.h" 0063 #include "scanlineconverter_p.h" 0064 #include "util_p.h" 0065 0066 #include <IexThrowErrnoExc.h> 0067 #include <ImathBox.h> 0068 #include <ImfArray.h> 0069 #include <ImfBoxAttribute.h> 0070 #include <ImfChannelListAttribute.h> 0071 #include <ImfCompressionAttribute.h> 0072 #include <ImfConvert.h> 0073 #include <ImfFloatAttribute.h> 0074 #include <ImfInputFile.h> 0075 #include <ImfInt64.h> 0076 #include <ImfIntAttribute.h> 0077 #include <ImfLineOrderAttribute.h> 0078 #include <ImfPreviewImage.h> 0079 #include <ImfRgbaFile.h> 0080 #include <ImfStandardAttributes.h> 0081 #include <ImfVersion.h> 0082 0083 #include <iostream> 0084 0085 #include <QColorSpace> 0086 #include <QDataStream> 0087 #include <QDebug> 0088 #include <QFloat16> 0089 #include <QImage> 0090 #include <QImageIOPlugin> 0091 #include <QLocale> 0092 #include <QThread> 0093 #include <QTimeZone> 0094 0095 // Allow the code to works on all QT versions supported by KDE 0096 // project (Qt 5.15 and Qt 6.x) to easy backports fixes. 0097 #if !defined(EXR_USE_LEGACY_CONVERSIONS) 0098 // If uncommented, the image is rendered in a float16 format, the result is very precise 0099 #define EXR_USE_QT6_FLOAT_IMAGE // default uncommented 0100 #endif 0101 0102 class K_IStream : public Imf::IStream 0103 { 0104 public: 0105 K_IStream(QIODevice *dev, const QByteArray &fileName) 0106 : IStream(fileName.data()) 0107 , m_dev(dev) 0108 { 0109 } 0110 0111 bool read(char c[], int n) override; 0112 #if OPENEXR_VERSION_MAJOR > 2 0113 uint64_t tellg() override; 0114 void seekg(uint64_t pos) override; 0115 #else 0116 Imf::Int64 tellg() override; 0117 void seekg(Imf::Int64 pos) override; 0118 #endif 0119 void clear() override; 0120 0121 private: 0122 QIODevice *m_dev; 0123 }; 0124 0125 bool K_IStream::read(char c[], int n) 0126 { 0127 qint64 result = m_dev->read(c, n); 0128 if (result > 0) { 0129 return true; 0130 } else if (result == 0) { 0131 throw Iex::InputExc("Unexpected end of file"); 0132 } else { // negative value { 0133 Iex::throwErrnoExc("Error in read", result); 0134 } 0135 return false; 0136 } 0137 0138 #if OPENEXR_VERSION_MAJOR > 2 0139 uint64_t K_IStream::tellg() 0140 #else 0141 Imf::Int64 K_IStream::tellg() 0142 #endif 0143 { 0144 return m_dev->pos(); 0145 } 0146 0147 #if OPENEXR_VERSION_MAJOR > 2 0148 void K_IStream::seekg(uint64_t pos) 0149 #else 0150 void K_IStream::seekg(Imf::Int64 pos) 0151 #endif 0152 { 0153 m_dev->seek(pos); 0154 } 0155 0156 void K_IStream::clear() 0157 { 0158 // TODO 0159 } 0160 0161 class K_OStream : public Imf::OStream 0162 { 0163 public: 0164 K_OStream(QIODevice *dev, const QByteArray &fileName) 0165 : OStream(fileName.data()) 0166 , m_dev(dev) 0167 { 0168 } 0169 0170 void write(const char c[/*n*/], int n) override; 0171 #if OPENEXR_VERSION_MAJOR > 2 0172 uint64_t tellp() override; 0173 void seekp(uint64_t pos) override; 0174 #else 0175 Imf::Int64 tellp() override; 0176 void seekp(Imf::Int64 pos) override; 0177 #endif 0178 0179 private: 0180 QIODevice *m_dev; 0181 }; 0182 0183 void K_OStream::write(const char c[], int n) 0184 { 0185 qint64 result = m_dev->write(c, n); 0186 if (result > 0) { 0187 return; 0188 } else { // negative value { 0189 Iex::throwErrnoExc("Error in write", result); 0190 } 0191 return; 0192 } 0193 0194 #if OPENEXR_VERSION_MAJOR > 2 0195 uint64_t K_OStream::tellp() 0196 #else 0197 Imf::Int64 K_OStream::tellg() 0198 #endif 0199 { 0200 return m_dev->pos(); 0201 } 0202 0203 #if OPENEXR_VERSION_MAJOR > 2 0204 void K_OStream::seekp(uint64_t pos) 0205 #else 0206 void K_OStream::seekg(Imf::Int64 pos) 0207 #endif 0208 { 0209 m_dev->seek(pos); 0210 } 0211 0212 #ifdef EXR_USE_LEGACY_CONVERSIONS 0213 // source: https://openexr.com/en/latest/ReadingAndWritingImageFiles.html 0214 inline unsigned char gamma(float x) 0215 { 0216 x = std::pow(5.5555f * std::max(0.f, x), 0.4545f) * 84.66f; 0217 return (unsigned char)qBound(0.f, x, 255.f); 0218 } 0219 inline QRgb RgbaToQrgba(struct Imf::Rgba &imagePixel) 0220 { 0221 return qRgba(gamma(float(imagePixel.r)), 0222 gamma(float(imagePixel.g)), 0223 gamma(float(imagePixel.b)), 0224 (unsigned char)(qBound(0.f, imagePixel.a * 255.f, 255.f) + 0.5f)); 0225 } 0226 #endif 0227 0228 EXRHandler::EXRHandler() 0229 : m_compressionRatio(-1) 0230 , m_quality(-1) 0231 , m_imageNumber(0) 0232 , m_imageCount(0) 0233 , m_startPos(-1) 0234 { 0235 // Set the number of threads to use (0 is allowed) 0236 Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2); 0237 } 0238 0239 bool EXRHandler::canRead() const 0240 { 0241 if (canRead(device())) { 0242 setFormat("exr"); 0243 return true; 0244 } 0245 return false; 0246 } 0247 0248 static QImage::Format imageFormat(const Imf::RgbaInputFile &file) 0249 { 0250 auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A; 0251 #if defined(EXR_USE_LEGACY_CONVERSIONS) 0252 return (isRgba ? QImage::Format_ARGB32 : QImage::Format_RGB32); 0253 #elif defined(EXR_USE_QT6_FLOAT_IMAGE) 0254 return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4); 0255 #else 0256 return (isRgba ? QImage::Format_RGBA64 : QImage::Format_RGBX64); 0257 #endif 0258 } 0259 0260 /*! 0261 * \brief viewList 0262 * \param header 0263 * \return The list of available views. 0264 */ 0265 static QStringList viewList(const Imf::Header &h) 0266 { 0267 QStringList l; 0268 if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>("multiView")) { 0269 for (auto &&v : views->value()) { 0270 l << QString::fromStdString(v); 0271 } 0272 } 0273 return l; 0274 } 0275 0276 #ifdef QT_DEBUG 0277 void printAttributes(const Imf::Header &h) 0278 { 0279 for (auto i = h.begin(); i != h.end(); ++i) { 0280 qDebug() << i.name(); 0281 } 0282 } 0283 #endif 0284 0285 /*! 0286 * \brief readMetadata 0287 * Reads EXR attributes from the \a header and set its as metadata in the \a image. 0288 */ 0289 static void readMetadata(const Imf::Header &header, QImage &image) 0290 { 0291 // set some useful metadata 0292 if (auto comments = header.findTypedAttribute<Imf::StringAttribute>("comments")) { 0293 image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value())); 0294 } 0295 0296 if (auto owner = header.findTypedAttribute<Imf::StringAttribute>("owner")) { 0297 image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value())); 0298 } 0299 0300 if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>("latitude")) { 0301 image.setText(QStringLiteral("Latitude"), QLocale::c().toString(lat->value())); 0302 } 0303 0304 if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>("longitude")) { 0305 image.setText(QStringLiteral("Longitude"), QLocale::c().toString(lon->value())); 0306 } 0307 0308 if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>("altitude")) { 0309 image.setText(QStringLiteral("Altitude"), QLocale::c().toString(alt->value())); 0310 } 0311 0312 if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>("capDate")) { 0313 float off = 0; 0314 if (auto utcOffset = header.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) { 0315 off = utcOffset->value(); 0316 } 0317 auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss")); 0318 if (dateTime.isValid()) { 0319 dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off)); 0320 image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate)); 0321 } 0322 } 0323 0324 if (auto xDensity = header.findTypedAttribute<Imf::FloatAttribute>("xDensity")) { 0325 float par = 1; 0326 if (auto pixelAspectRatio = header.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) { 0327 par = pixelAspectRatio->value(); 0328 } 0329 image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54)); 0330 image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54)); 0331 } 0332 0333 // Non-standard attribute 0334 if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>("xmp")) { 0335 image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromStdString(xmp->value())); 0336 } 0337 0338 /* TODO: OpenEXR 3.2 metadata 0339 * 0340 * New Optional Standard Attributes: 0341 * - Support automated editorial workflow: 0342 * reelName, imageCounter, ascFramingDecisionList 0343 * 0344 * - Support forensics (“which other shots used that camera and lens before the camera firmware was updated?”): 0345 * cameraMake, cameraModel, cameraSerialNumber, cameraFirmware, cameraUuid, cameraLabel, lensMake, lensModel, 0346 * lensSerialNumber, lensFirmware, cameraColorBalance 0347 * 0348 * -Support pickup shots (reproduce critical camera settings): 0349 * shutterAngle, cameraCCTSetting, cameraTintSetting 0350 * 0351 * - Support metadata-driven match move: 0352 * sensorCenterOffset, sensorOverallDimensions, sensorPhotositePitch, sensorAcquisitionRectanglenominalFocalLength, 0353 * effectiveFocalLength, pinholeFocalLength, entrancePupilOffset, tStop(complementing existing 'aperture') 0354 */ 0355 } 0356 0357 /*! 0358 * \brief readColorSpace 0359 * Reads EXR chromaticities from the \a header and set its as color profile in the \a image. 0360 */ 0361 static void readColorSpace(const Imf::Header &header, QImage &image) 0362 { 0363 // final color operations 0364 #ifndef EXR_USE_LEGACY_CONVERSIONS 0365 0366 QColorSpace cs; 0367 if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>("chromaticities")) { 0368 auto &&v = chroma->value(); 0369 cs = QColorSpace(QPointF(v.white.x, v.white.y), 0370 QPointF(v.red.x, v.red.y), 0371 QPointF(v.green.x, v.green.y), 0372 QPointF(v.blue.x, v.blue.y), 0373 QColorSpace::TransferFunction::Linear); 0374 } 0375 if (!cs.isValid()) { 0376 cs = QColorSpace(QColorSpace::SRgbLinear); 0377 } 0378 image.setColorSpace(cs); 0379 0380 #ifdef EXR_CONVERT_TO_SRGB 0381 image.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); 0382 #endif 0383 0384 #endif // !EXR_USE_LEGACY_CONVERSIONS 0385 } 0386 0387 bool EXRHandler::read(QImage *outImage) 0388 { 0389 try { 0390 auto d = device(); 0391 0392 // set the image position after the first run. 0393 if (!d->isSequential()) { 0394 if (m_startPos < 0) { 0395 m_startPos = d->pos(); 0396 } else { 0397 d->seek(m_startPos); 0398 } 0399 } 0400 0401 K_IStream istr(d, QByteArray()); 0402 Imf::RgbaInputFile file(istr); 0403 auto &&header = file.header(); 0404 0405 // set the image to load 0406 if (m_imageNumber > -1) { 0407 auto views = viewList(header); 0408 if (m_imageNumber < views.count()) { 0409 file.setLayerName(views.at(m_imageNumber).toStdString()); 0410 } 0411 } 0412 0413 // get image info 0414 Imath::Box2i dw = file.dataWindow(); 0415 qint32 width = dw.max.x - dw.min.x + 1; 0416 qint32 height = dw.max.y - dw.min.y + 1; 0417 0418 // limiting the maximum image size on a reasonable size (as done in other plugins) 0419 if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) { 0420 qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px"; 0421 return false; 0422 } 0423 0424 // creating the image 0425 QImage image = imageAlloc(width, height, imageFormat(file)); 0426 if (image.isNull()) { 0427 qWarning() << "Failed to allocate image, invalid size?" << QSize(width, height); 0428 return false; 0429 } 0430 0431 Imf::Array2D<Imf::Rgba> pixels; 0432 pixels.resizeErase(EXR_LINES_PER_BLOCK, width); 0433 bool isRgba = image.hasAlphaChannel(); 0434 0435 // somehow copy pixels into image 0436 for (int y = 0, n = 0; y < height; y += n) { 0437 auto my = dw.min.y + y; 0438 if (my > dw.max.y) { // paranoia check 0439 break; 0440 } 0441 0442 file.setFrameBuffer(&pixels[0][0] - dw.min.x - qint64(my) * width, 1, width); 0443 file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y)); 0444 0445 for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) { 0446 #if defined(EXR_USE_LEGACY_CONVERSIONS) 0447 Q_UNUSED(isRgba) 0448 auto scanLine = reinterpret_cast<QRgb *>(image.scanLine(y + n)); 0449 for (int x = 0; x < width; ++x) { 0450 *(scanLine + x) = RgbaToQrgba(pixels[n][x]); 0451 } 0452 #elif defined(EXR_USE_QT6_FLOAT_IMAGE) 0453 auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y + n)); 0454 for (int x = 0; x < width; ++x) { 0455 auto xcs = x * 4; 0456 *(scanLine + xcs) = qfloat16(qBound(0.f, float(pixels[n][x].r), 1.f)); 0457 *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[n][x].g), 1.f)); 0458 *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[n][x].b), 1.f)); 0459 *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[n][x].a), 1.f) : 1.f); 0460 } 0461 #else 0462 auto scanLine = reinterpret_cast<QRgba64 *>(image.scanLine(y + n)); 0463 for (int x = 0; x < width; ++x) { 0464 *(scanLine + x) = QRgba64::fromRgba64(quint16(qBound(0.f, float(pixels[n][x].r) * 65535.f + 0.5f, 65535.f)), 0465 quint16(qBound(0.f, float(pixels[n][x].g) * 65535.f + 0.5f, 65535.f)), 0466 quint16(qBound(0.f, float(pixels[n][x].b) * 65535.f + 0.5f, 65535.f)), 0467 isRgba ? quint16(qBound(0.f, float(pixels[n][x].a) * 65535.f + 0.5f, 65535.f)) : quint16(65535)); 0468 } 0469 #endif 0470 } 0471 } 0472 0473 // set some useful metadata 0474 readMetadata(header, image); 0475 // final color operations 0476 readColorSpace(header, image); 0477 0478 *outImage = image; 0479 0480 return true; 0481 } catch (const std::exception &) { 0482 return false; 0483 } 0484 } 0485 0486 /*! 0487 * \brief makePreview 0488 * Creates a preview of maximum 256 x 256 pixels from the \a image. 0489 */ 0490 bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels) 0491 { 0492 auto w = image.width(); 0493 auto h = image.height(); 0494 0495 QImage preview; 0496 if (w > h) { 0497 preview = image.scaledToWidth(256).convertToFormat(QImage::Format_ARGB32); 0498 } else { 0499 preview = image.scaledToHeight(256).convertToFormat(QImage::Format_ARGB32); 0500 } 0501 if (preview.isNull()) { 0502 return false; 0503 } 0504 0505 w = preview.width(); 0506 h = preview.height(); 0507 pixels.resizeErase(h, w); 0508 preview.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); 0509 0510 for (int y = 0; y < h; ++y) { 0511 auto scanLine = reinterpret_cast<const QRgb *>(preview.constScanLine(y)); 0512 for (int x = 0; x < w; ++x) { 0513 auto &&out = pixels[y][x]; 0514 out.r = qRed(*(scanLine + x)); 0515 out.g = qGreen(*(scanLine + x)); 0516 out.b = qBlue(*(scanLine + x)); 0517 out.a = qAlpha(*(scanLine + x)); 0518 } 0519 } 0520 0521 return true; 0522 } 0523 0524 /*! 0525 * \brief setMetadata 0526 * Reades the metadata from \a image and set its as attributes in the \a header. 0527 */ 0528 static void setMetadata(const QImage &image, Imf::Header &header) 0529 { 0530 auto dateTime = QDateTime::currentDateTime(); 0531 for (auto &&key : image.textKeys()) { 0532 auto text = image.text(key); 0533 if (!key.compare(QStringLiteral("Comment"), Qt::CaseInsensitive)) { 0534 header.insert("comments", Imf::StringAttribute(text.toStdString())); 0535 } 0536 0537 if (!key.compare(QStringLiteral("Owner"), Qt::CaseInsensitive)) { 0538 header.insert("owner", Imf::StringAttribute(text.toStdString())); 0539 } 0540 0541 // clang-format off 0542 if (!key.compare(QStringLiteral("Latitude"), Qt::CaseInsensitive) || 0543 !key.compare(QStringLiteral("Longitude"), Qt::CaseInsensitive) || 0544 !key.compare(QStringLiteral("Altitude"), Qt::CaseInsensitive)) { 0545 // clang-format on 0546 auto ok = false; 0547 auto value = QLocale::c().toFloat(text, &ok); 0548 if (ok) { 0549 header.insert(qPrintable(key.toLower()), Imf::FloatAttribute(value)); 0550 } 0551 } 0552 0553 if (!key.compare(QStringLiteral("CreationDate"), Qt::CaseInsensitive)) { 0554 auto dt = QDateTime::fromString(text, Qt::ISODate); 0555 if (dt.isValid()) { 0556 dateTime = dt; 0557 } 0558 } 0559 0560 #ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute! 0561 if (!key.compare(QStringLiteral("XML:com.adobe.xmp"), Qt::CaseInsensitive)) { 0562 header.insert("xmp", Imf::StringAttribute(text.toStdString())); 0563 } 0564 #endif 0565 } 0566 if (dateTime.isValid()) { 0567 header.insert("capDate", Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString())); 0568 header.insert("utcOffset", Imf::FloatAttribute(dateTime.offsetFromUtc())); 0569 } 0570 0571 if (image.dotsPerMeterX() && image.dotsPerMeterY()) { 0572 header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f)); 0573 header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterX()) / float(image.dotsPerMeterY()))); 0574 } 0575 0576 // set default chroma (default constructor ITU-R BT.709-3 -> sRGB) 0577 // The image is converted to Linear sRGB so, the chroma is the default EXR value. 0578 // If a file doesn’t have a chromaticities attribute, display software should assume that the 0579 // file’s primaries and the white point match Rec. ITU-R BT.709-3. 0580 // header.insert("chromaticities", Imf::ChromaticitiesAttribute(Imf::Chromaticities())); 0581 0582 // TODO: EXR 3.2 attributes (see readMetadata()) 0583 } 0584 0585 bool EXRHandler::write(const QImage &image) 0586 { 0587 try { 0588 // create EXR header 0589 qint32 width = image.width(); 0590 qint32 height = image.height(); 0591 0592 // limiting the maximum image size on a reasonable size (as done in other plugins) 0593 if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) { 0594 qWarning() << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px"; 0595 return false; 0596 } 0597 0598 Imf::Header header(width, height); 0599 // set compression scheme (forcing PIZ as default) 0600 header.compression() = Imf::Compression::PIZ_COMPRESSION; 0601 if (m_compressionRatio >= qint32(Imf::Compression::NO_COMPRESSION) && m_compressionRatio < qint32(Imf::Compression::NUM_COMPRESSION_METHODS)) { 0602 header.compression() = Imf::Compression(m_compressionRatio); 0603 } 0604 // set the DCT quality (used by DCT compressions only) 0605 if (m_quality > -1 && m_quality <= 100) { 0606 header.dwaCompressionLevel() = float(m_quality); 0607 } 0608 // make ZIP compression fast (used by ZIP compressions) 0609 header.zipCompressionLevel() = 1; 0610 0611 // set preview (don't set it for small images) 0612 if (width > 1024 || height > 1024) { 0613 Imf::Array2D<Imf::PreviewRgba> previewPixels; 0614 if (makePreview(image, previewPixels)) { 0615 header.setPreviewImage(Imf::PreviewImage(previewPixels.width(), previewPixels.height(), &previewPixels[0][0])); 0616 } 0617 } 0618 0619 // set metadata (EXR attributes) 0620 setMetadata(image, header); 0621 0622 // write the EXR 0623 K_OStream ostr(device(), QByteArray()); 0624 auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB; 0625 if (image.format() == QImage::Format_Mono || 0626 image.format() == QImage::Format_MonoLSB || 0627 image.format() == QImage::Format_Grayscale16 || 0628 image.format() == QImage::Format_Grayscale8) { 0629 channelsType = Imf::RgbaChannels::WRITE_Y; 0630 } 0631 Imf::RgbaOutputFile file(ostr, header, channelsType); 0632 Imf::Array2D<Imf::Rgba> pixels; 0633 pixels.resizeErase(EXR_LINES_PER_BLOCK, width); 0634 0635 // convert the image and write into the stream 0636 #if defined(EXR_USE_QT6_FLOAT_IMAGE) 0637 auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4; 0638 #else 0639 auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA64 : QImage::Format_RGBX64; 0640 #endif 0641 ScanLineConverter slc(convFormat); 0642 slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb)); 0643 slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear)); 0644 for (int y = 0, n = 0; y < height; y += n) { 0645 for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, height - y); ++n) { 0646 #if defined(EXR_USE_QT6_FLOAT_IMAGE) 0647 auto scanLine = reinterpret_cast<const qfloat16 *>(slc.convertedScanLine(image, y + n)); 0648 if (scanLine == nullptr) { 0649 return false; 0650 } 0651 for (int x = 0; x < width; ++x) { 0652 auto xcs = x * 4; 0653 pixels[n][x].r = float(*(scanLine + xcs)); 0654 pixels[n][x].g = float(*(scanLine + xcs + 1)); 0655 pixels[n][x].b = float(*(scanLine + xcs + 2)); 0656 pixels[n][x].a = float(*(scanLine + xcs + 3)); 0657 } 0658 #else 0659 auto scanLine = reinterpret_cast<const QRgba64 *>(slc.convertedScanLine(image, y + n)); 0660 if (scanLine == nullptr) { 0661 return false; 0662 } 0663 for (int x = 0; x < width; ++x) { 0664 pixels[n][x].r = float((scanLine + x)->red() / 65535.f); 0665 pixels[n][x].g = float((scanLine + x)->green() / 65535.f); 0666 pixels[n][x].b = float((scanLine + x)->blue() / 65535.f); 0667 pixels[n][x].a = float((scanLine + x)->alpha() / 65535.f); 0668 } 0669 #endif 0670 } 0671 file.setFrameBuffer(&pixels[0][0] - qint64(y) * width, 1, width); 0672 file.writePixels(n); 0673 } 0674 } catch (const std::exception &) { 0675 return false; 0676 } 0677 0678 return true; 0679 } 0680 0681 void EXRHandler::setOption(ImageOption option, const QVariant &value) 0682 { 0683 if (option == QImageIOHandler::CompressionRatio) { 0684 auto ok = false; 0685 auto cr = value.toInt(&ok); 0686 if (ok) { 0687 m_compressionRatio = cr; 0688 } 0689 } 0690 if (option == QImageIOHandler::Quality) { 0691 auto ok = false; 0692 auto q = value.toInt(&ok); 0693 if (ok) { 0694 m_quality = q; 0695 } 0696 } 0697 } 0698 0699 bool EXRHandler::supportsOption(ImageOption option) const 0700 { 0701 if (option == QImageIOHandler::Size) { 0702 return true; 0703 } 0704 if (option == QImageIOHandler::ImageFormat) { 0705 return true; 0706 } 0707 if (option == QImageIOHandler::CompressionRatio) { 0708 return true; 0709 } 0710 if (option == QImageIOHandler::Quality) { 0711 return true; 0712 } 0713 return false; 0714 } 0715 0716 QVariant EXRHandler::option(ImageOption option) const 0717 { 0718 QVariant v; 0719 0720 if (option == QImageIOHandler::Size) { 0721 if (auto d = device()) { 0722 // transactions works on both random and sequential devices 0723 d->startTransaction(); 0724 try { 0725 K_IStream istr(d, QByteArray()); 0726 Imf::RgbaInputFile file(istr); 0727 if (m_imageNumber > -1) { // set the image to read 0728 auto views = viewList(file.header()); 0729 if (m_imageNumber < views.count()) { 0730 file.setLayerName(views.at(m_imageNumber).toStdString()); 0731 } 0732 } 0733 Imath::Box2i dw = file.dataWindow(); 0734 v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)); 0735 } catch (const std::exception &) { 0736 // broken file or unsupported version 0737 } 0738 d->rollbackTransaction(); 0739 } 0740 } 0741 0742 if (option == QImageIOHandler::ImageFormat) { 0743 if (auto d = device()) { 0744 // transactions works on both random and sequential devices 0745 d->startTransaction(); 0746 try { 0747 K_IStream istr(d, QByteArray()); 0748 Imf::RgbaInputFile file(istr); 0749 v = QVariant::fromValue(imageFormat(file)); 0750 } catch (const std::exception &) { 0751 // broken file or unsupported version 0752 } 0753 d->rollbackTransaction(); 0754 } 0755 } 0756 0757 if (option == QImageIOHandler::CompressionRatio) { 0758 v = QVariant(m_compressionRatio); 0759 } 0760 0761 if (option == QImageIOHandler::Quality) { 0762 v = QVariant(m_quality); 0763 } 0764 0765 return v; 0766 } 0767 0768 bool EXRHandler::jumpToNextImage() 0769 { 0770 return jumpToImage(m_imageNumber + 1); 0771 } 0772 0773 bool EXRHandler::jumpToImage(int imageNumber) 0774 { 0775 if (imageNumber < 0 || imageNumber >= imageCount()) { 0776 return false; 0777 } 0778 m_imageNumber = imageNumber; 0779 return true; 0780 } 0781 0782 int EXRHandler::imageCount() const 0783 { 0784 // NOTE: image count is cached for performance reason 0785 auto &&count = m_imageCount; 0786 if (count > 0) { 0787 return count; 0788 } 0789 0790 count = QImageIOHandler::imageCount(); 0791 0792 auto d = device(); 0793 d->startTransaction(); 0794 0795 try { 0796 K_IStream istr(d, QByteArray()); 0797 Imf::RgbaInputFile file(istr); 0798 auto views = viewList(file.header()); 0799 if (!views.isEmpty()) { 0800 count = views.size(); 0801 } 0802 } catch (const std::exception &) { 0803 // do nothing 0804 } 0805 0806 d->rollbackTransaction(); 0807 0808 return count; 0809 } 0810 0811 int EXRHandler::currentImageNumber() const 0812 { 0813 return m_imageNumber; 0814 } 0815 0816 bool EXRHandler::canRead(QIODevice *device) 0817 { 0818 if (!device) { 0819 qWarning("EXRHandler::canRead() called with no device"); 0820 return false; 0821 } 0822 0823 const QByteArray head = device->peek(4); 0824 0825 return Imf::isImfMagic(head.data()); 0826 } 0827 0828 QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const 0829 { 0830 if (format == "exr") { 0831 return Capabilities(CanRead | CanWrite); 0832 } 0833 if (!format.isEmpty()) { 0834 return {}; 0835 } 0836 if (!device->isOpen()) { 0837 return {}; 0838 } 0839 0840 Capabilities cap; 0841 if (device->isReadable() && EXRHandler::canRead(device)) { 0842 cap |= CanRead; 0843 } 0844 if (device->isWritable()) { 0845 cap |= CanWrite; 0846 } 0847 return cap; 0848 } 0849 0850 QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const 0851 { 0852 QImageIOHandler *handler = new EXRHandler; 0853 handler->setDevice(device); 0854 handler->setFormat(format); 0855 return handler; 0856 } 0857 0858 #include "moc_exr_p.cpp"