1/*
2 The high dynamic range EXR format support for QImage.
3
4 SPDX-FileCopyrightText: 2003 Brad Hards <bradh@frogmouth.net>
5 SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "exr_p.h"
11#include "scanlineconverter_p.h"
12#include "util_p.h"
13
14/* *** EXR_CONVERT_TO_SRGB ***
15 * If defined, the linear data is converted to sRGB on read to accommodate
16 * programs that do not support color profiles.
17 * Otherwise the data are kept as is and it is the display program that
18 * must convert to the monitor profile.
19 */
20//#define EXR_CONVERT_TO_SRGB // default: commented -> you should define it in your cmake file
21
22/* *** EXR_STORE_XMP_ATTRIBUTE ***
23 * If defined, disables the stores XMP values in a non-standard attribute named "xmp".
24 * The QImage metadata used is "XML:com.adobe.xmp".
25 * NOTE: The use of non-standard attributes is possible but discouraged by the specification. However,
26 * metadata is essential for good image management and programs like darktable also set this
27 * attribute. Gimp reads the "xmp" attribute and Darktable writes it as well.
28 */
29//#define EXR_DISABLE_XMP_ATTRIBUTE // default: commented -> you should define it in your cmake file
30
31/* *** EXR_MAX_IMAGE_WIDTH and EXR_MAX_IMAGE_HEIGHT ***
32 * The maximum size in pixel allowed by the plugin.
33 */
34#ifndef EXR_MAX_IMAGE_WIDTH
35#define EXR_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
36#endif
37#ifndef EXR_MAX_IMAGE_HEIGHT
38#define EXR_MAX_IMAGE_HEIGHT EXR_MAX_IMAGE_WIDTH
39#endif
40
41/* *** EXR_LINES_PER_BLOCK ***
42 * Allows certain compression schemes to work in multithreading
43 * Requires up to "LINES_PER_BLOCK * MAX_IMAGE_WIDTH * 8"
44 * additional RAM (e.g. if 128, up to 307MiB of RAM).
45 * There is a performance gain with the following parameters (based on empirical tests):
46 * - PIZ compression needs 64+ lines
47 * - ZIPS compression needs 8+ lines
48 * - ZIP compression needs 32+ lines
49 * - Others not tested
50 *
51 * NOTE: The OpenEXR documentation states that the higher the better :)
52 */
53#ifndef EXR_LINES_PER_BLOCK
54#define EXR_LINES_PER_BLOCK 128
55#endif
56
57#include <IexThrowErrnoExc.h>
58#include <ImathBox.h>
59#include <ImfArray.h>
60#include <ImfBoxAttribute.h>
61#include <ImfChannelListAttribute.h>
62#include <ImfCompressionAttribute.h>
63#include <ImfConvert.h>
64#include <ImfFloatAttribute.h>
65#include <ImfInputFile.h>
66#include <ImfInt64.h>
67#include <ImfIntAttribute.h>
68#include <ImfLineOrderAttribute.h>
69#include <ImfPreviewImage.h>
70#include <ImfRgbaFile.h>
71#include <ImfStandardAttributes.h>
72#include <ImfVersion.h>
73
74#include <iostream>
75
76#include <QColorSpace>
77#include <QDataStream>
78#include <QFloat16>
79#include <QImage>
80#include <QImageIOPlugin>
81#include <QLocale>
82#include <QLoggingCategory>
83#include <QThread>
84#include <QTimeZone>
85
86#ifdef QT_DEBUG
87Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtDebugMsg)
88#else
89Q_LOGGING_CATEGORY(LOG_EXRPLUGIN, "kf.imageformats.plugins.exr", QtWarningMsg)
90#endif
91
92class K_IStream : public Imf::IStream
93{
94public:
95 K_IStream(QIODevice *dev)
96 : IStream("K_IStream")
97 , m_dev(dev)
98 {
99 }
100
101 bool read(char c[], int n) override;
102#if OPENEXR_VERSION_MAJOR > 2
103 uint64_t tellg() override;
104 void seekg(uint64_t pos) override;
105#else
106 Imf::Int64 tellg() override;
107 void seekg(Imf::Int64 pos) override;
108#endif
109 void clear() override;
110
111private:
112 QIODevice *m_dev;
113};
114
115bool K_IStream::read(char c[], int n)
116{
117 qint64 result = m_dev->read(data: c, maxlen: n);
118 if (result > 0) {
119 return true;
120 } else if (result == 0) {
121 throw Iex::InputExc("Unexpected end of file");
122 } else { // negative value {
123 Iex::throwErrnoExc(txt: "Error in read", errnum: result);
124 }
125 return false;
126}
127
128#if OPENEXR_VERSION_MAJOR > 2
129uint64_t K_IStream::tellg()
130#else
131Imf::Int64 K_IStream::tellg()
132#endif
133{
134 return m_dev->pos();
135}
136
137#if OPENEXR_VERSION_MAJOR > 2
138void K_IStream::seekg(uint64_t pos)
139#else
140void K_IStream::seekg(Imf::Int64 pos)
141#endif
142{
143 m_dev->seek(pos);
144}
145
146void K_IStream::clear()
147{
148 // TODO
149}
150
151class K_OStream : public Imf::OStream
152{
153public:
154 K_OStream(QIODevice *dev)
155 : OStream("K_OStream")
156 , m_dev(dev)
157 {
158 }
159
160 void write(const char c[/*n*/], int n) override;
161#if OPENEXR_VERSION_MAJOR > 2
162 uint64_t tellp() override;
163 void seekp(uint64_t pos) override;
164#else
165 Imf::Int64 tellp() override;
166 void seekp(Imf::Int64 pos) override;
167#endif
168
169private:
170 QIODevice *m_dev;
171};
172
173void K_OStream::write(const char c[], int n)
174{
175 qint64 result = m_dev->write(data: c, len: n);
176 if (result > 0) {
177 return;
178 } else { // negative value {
179 Iex::throwErrnoExc(txt: "Error in write", errnum: result);
180 }
181 return;
182}
183
184#if OPENEXR_VERSION_MAJOR > 2
185uint64_t K_OStream::tellp()
186#else
187Imf::Int64 K_OStream::tellg()
188#endif
189{
190 return m_dev->pos();
191}
192
193#if OPENEXR_VERSION_MAJOR > 2
194void K_OStream::seekp(uint64_t pos)
195#else
196void K_OStream::seekg(Imf::Int64 pos)
197#endif
198{
199 m_dev->seek(pos);
200}
201
202EXRHandler::EXRHandler()
203 : m_compressionRatio(-1)
204 , m_quality(-1)
205 , m_imageNumber(0)
206 , m_imageCount(0)
207 , m_startPos(-1)
208{
209 // Set the number of threads to use (0 is allowed)
210 Imf::setGlobalThreadCount(QThread::idealThreadCount() / 2);
211}
212
213bool EXRHandler::canRead() const
214{
215 if (canRead(device: device())) {
216 setFormat("exr");
217 return true;
218 }
219 return false;
220}
221
222static QImage::Format imageFormat(const Imf::RgbaInputFile &file)
223{
224 auto isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
225 return (isRgba ? QImage::Format_RGBA16FPx4 : QImage::Format_RGBX16FPx4);
226}
227
228/*!
229 * \brief viewList
230 * \param header
231 * \return The list of available views.
232 */
233static QStringList viewList(const Imf::Header &h)
234{
235 QStringList l;
236 if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>(name: "multiView")) {
237 for (auto &&v : views->value()) {
238 l << QString::fromStdString(s: v);
239 }
240 }
241 return l;
242}
243
244#ifdef QT_DEBUG
245static void printAttributes(const Imf::Header &h)
246{
247 for (auto i = h.begin(); i != h.end(); ++i) {
248 qCDebug(LOG_EXRPLUGIN) << i.name();
249 }
250}
251#endif
252
253/*!
254 * \brief readMetadata
255 * Reads EXR attributes from the \a header and set its as metadata in the \a image.
256 */
257static void readMetadata(const Imf::Header &header, QImage &image)
258{
259 // set some useful metadata
260 if (auto comments = header.findTypedAttribute<Imf::StringAttribute>(name: "comments")) {
261 image.setText(QStringLiteral(META_KEY_COMMENT), value: QString::fromStdString(s: comments->value()));
262 }
263
264 if (auto owner = header.findTypedAttribute<Imf::StringAttribute>(name: "owner")) {
265 image.setText(QStringLiteral(META_KEY_OWNER), value: QString::fromStdString(s: owner->value()));
266 }
267
268 if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>(name: "latitude")) {
269 image.setText(QStringLiteral(META_KEY_LATITUDE), value: QLocale::c().toString(f: lat->value()));
270 }
271
272 if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>(name: "longitude")) {
273 image.setText(QStringLiteral(META_KEY_LONGITUDE), value: QLocale::c().toString(f: lon->value()));
274 }
275
276 if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>(name: "altitude")) {
277 image.setText(QStringLiteral(META_KEY_ALTITUDE), value: QLocale::c().toString(f: alt->value()));
278 }
279
280 if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>(name: "capDate")) {
281 float off = 0;
282 if (auto utcOffset = header.findTypedAttribute<Imf::FloatAttribute>(name: "utcOffset")) {
283 off = utcOffset->value();
284 }
285 auto dateTime = QDateTime::fromString(string: QString::fromStdString(s: capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
286 if (dateTime.isValid()) {
287 dateTime.setTimeZone(toZone: QTimeZone::fromSecondsAheadOfUtc(offset: off));
288 image.setText(QStringLiteral(META_KEY_CREATIONDATE), value: dateTime.toString(format: Qt::ISODate));
289 }
290 }
291
292 if (auto xDensity = header.findTypedAttribute<Imf::FloatAttribute>(name: "xDensity")) {
293 float par = 1;
294 if (auto pixelAspectRatio = header.findTypedAttribute<Imf::FloatAttribute>(name: "pixelAspectRatio")) {
295 par = pixelAspectRatio->value();
296 }
297 auto hres = dpi2ppm(dpi: xDensity->value());
298 if (hres > 0) {
299 image.setDotsPerMeterX(hres);
300 }
301 auto vres = dpi2ppm(dpi: xDensity->value() * par);
302 if (vres > 0) {
303 image.setDotsPerMeterY(vres);
304 }
305 }
306
307 // Non-standard attribute
308 if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>(name: "xmp")) {
309 image.setText(QStringLiteral(META_KEY_XMP_ADOBE), value: QString::fromStdString(s: xmp->value()));
310 }
311
312 // camera metadata
313 if (auto manufacturer = header.findTypedAttribute<Imf::StringAttribute>(name: "cameraMake")) {
314 image.setText(QStringLiteral(META_KEY_MANUFACTURER), value: QString::fromStdString(s: manufacturer->value()));
315 }
316 if (auto model = header.findTypedAttribute<Imf::StringAttribute>(name: "cameraModel")) {
317 image.setText(QStringLiteral(META_KEY_MODEL), value: QString::fromStdString(s: model->value()));
318 }
319 if (auto serial = header.findTypedAttribute<Imf::StringAttribute>(name: "cameraSerialNumber")) {
320 image.setText(QStringLiteral(META_KEY_SERIALNUMBER), value: QString::fromStdString(s: serial->value()));
321 }
322
323 // lens metadata
324 if (auto manufacturer = header.findTypedAttribute<Imf::StringAttribute>(name: "lensMake")) {
325 image.setText(QStringLiteral(META_KEY_LENS_MANUFACTURER), value: QString::fromStdString(s: manufacturer->value()));
326 }
327 if (auto model = header.findTypedAttribute<Imf::StringAttribute>(name: "lensModel")) {
328 image.setText(QStringLiteral(META_KEY_LENS_MODEL), value: QString::fromStdString(s: model->value()));
329 }
330 if (auto serial = header.findTypedAttribute<Imf::StringAttribute>(name: "lensSerialNumber")) {
331 image.setText(QStringLiteral(META_KEY_LENS_SERIALNUMBER), value: QString::fromStdString(s: serial->value()));
332 }
333}
334
335/*!
336 * \brief readColorSpace
337 * Reads EXR chromaticities from the \a header and set its as color profile in the \a image.
338 */
339static void readColorSpace(const Imf::Header &header, QImage &image)
340{
341 // final color operations
342 QColorSpace cs;
343 if (auto chroma = header.findTypedAttribute<Imf::ChromaticitiesAttribute>(name: "chromaticities")) {
344 auto &&v = chroma->value();
345 cs = QColorSpace(QPointF(v.white.x, v.white.y),
346 QPointF(v.red.x, v.red.y),
347 QPointF(v.green.x, v.green.y),
348 QPointF(v.blue.x, v.blue.y),
349 QColorSpace::TransferFunction::Linear);
350 }
351 if (!cs.isValid()) {
352 cs = QColorSpace(QColorSpace::SRgbLinear);
353 }
354 image.setColorSpace(cs);
355
356#ifdef EXR_CONVERT_TO_SRGB
357 image.convertToColorSpace(QColorSpace(QColorSpace::SRgb));
358#endif
359}
360
361bool EXRHandler::read(QImage *outImage)
362{
363 try {
364 auto d = device();
365
366 // set the image position after the first run.
367 if (!d->isSequential()) {
368 if (m_startPos < 0) {
369 m_startPos = d->pos();
370 } else {
371 d->seek(pos: m_startPos);
372 }
373 }
374
375 K_IStream istr(d);
376 Imf::RgbaInputFile file(istr);
377 auto &&header = file.header();
378
379 // set the image to load
380 if (m_imageNumber > -1) {
381 auto views = viewList(h: header);
382 if (m_imageNumber < views.count()) {
383 file.setLayerName(views.at(i: m_imageNumber).toStdString());
384 }
385 }
386
387 // get image info
388 Imath::Box2i dw = file.dataWindow();
389 qint32 width = dw.max.x - dw.min.x + 1;
390 qint32 height = dw.max.y - dw.min.y + 1;
391
392 // limiting the maximum image size on a reasonable size (as done in other plugins)
393 if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
394 qCWarning(LOG_EXRPLUGIN) << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
395 return false;
396 }
397
398 // creating the image
399 QImage image = imageAlloc(width, height, format: imageFormat(file));
400 if (image.isNull()) {
401 qCWarning(LOG_EXRPLUGIN) << "Failed to allocate image, invalid size?" << QSize(width, height);
402 return false;
403 }
404
405 Imf::Array2D<Imf::Rgba> pixels;
406 pixels.resizeErase(EXR_LINES_PER_BLOCK, sizeY: width);
407 std::memset(s: pixels[0], c: 0, n: sizeof(Imf::Rgba) * EXR_LINES_PER_BLOCK * width);
408 bool isRgba = image.hasAlphaChannel();
409
410 for (int y = 0, n = 0; y < height; y += n) {
411 auto my = dw.min.y + y;
412 if (my > dw.max.y) { // paranoia check
413 break;
414 }
415
416 file.setFrameBuffer(base: &pixels[0][0] - dw.min.x - qint64(my) * width, xStride: 1, yStride: width);
417 file.readPixels(scanLine1: my, scanLine2: std::min(a: my + EXR_LINES_PER_BLOCK - 1, b: dw.max.y));
418
419 for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, b: height - y); ++n) {
420 auto scanLine = reinterpret_cast<qfloat16 *>(image.scanLine(y + n));
421 for (int x = 0; x < width; ++x) {
422 auto xcs = x * 4;
423 *(scanLine + xcs) = qfloat16(float(pixels[n][x].r));
424 *(scanLine + xcs + 1) = qfloat16(float(pixels[n][x].g));
425 *(scanLine + xcs + 2) = qfloat16(float(pixels[n][x].b));
426 *(scanLine + xcs + 3) = qfloat16(isRgba ? std::clamp(val: float(pixels[n][x].a), lo: 0.f, hi: 1.f) : 1.f);
427 }
428 }
429 }
430
431 // set some useful metadata
432 readMetadata(header, image);
433 // final color operations
434 readColorSpace(header, image);
435
436 *outImage = image;
437
438 return true;
439 } catch (const std::exception &) {
440 return false;
441 }
442}
443
444/*!
445 * \brief makePreview
446 * Creates a preview of maximum 256 x 256 pixels from the \a image.
447 */
448bool makePreview(const QImage &image, Imf::Array2D<Imf::PreviewRgba> &pixels)
449{
450 auto w = image.width();
451 auto h = image.height();
452
453 QImage preview;
454 if (w > h) {
455 preview = image.scaledToWidth(w: 256).convertToFormat(f: QImage::Format_ARGB32);
456 } else {
457 preview = image.scaledToHeight(h: 256).convertToFormat(f: QImage::Format_ARGB32);
458 }
459 if (preview.isNull()) {
460 return false;
461 }
462
463 w = preview.width();
464 h = preview.height();
465 pixels.resizeErase(sizeX: h, sizeY: w);
466 preview.convertToColorSpace(colorSpace: QColorSpace(QColorSpace::SRgb));
467
468 for (int y = 0; y < h; ++y) {
469 auto scanLine = reinterpret_cast<const QRgb *>(preview.constScanLine(y));
470 for (int x = 0; x < w; ++x) {
471 auto &&out = pixels[y][x];
472 out.r = qRed(rgb: *(scanLine + x));
473 out.g = qGreen(rgb: *(scanLine + x));
474 out.b = qBlue(rgb: *(scanLine + x));
475 out.a = qAlpha(rgb: *(scanLine + x));
476 }
477 }
478
479 return true;
480}
481
482/*!
483 * \brief setMetadata
484 * Reades the metadata from \a image and set its as attributes in the \a header.
485 */
486static void setMetadata(const QImage &image, Imf::Header &header)
487{
488 auto dateTime = QDateTime::currentDateTime();
489 for (auto &&key : image.textKeys()) {
490 auto text = image.text(key);
491 if (!key.compare(QStringLiteral(META_KEY_COMMENT), cs: Qt::CaseInsensitive)) {
492 header.insert(name: "comments", attribute: Imf::StringAttribute(text.toStdString()));
493 }
494
495 if (!key.compare(QStringLiteral(META_KEY_OWNER), cs: Qt::CaseInsensitive)) {
496 header.insert(name: "owner", attribute: Imf::StringAttribute(text.toStdString()));
497 }
498
499 // clang-format off
500 if (!key.compare(QStringLiteral(META_KEY_LATITUDE), cs: Qt::CaseInsensitive) ||
501 !key.compare(QStringLiteral(META_KEY_LONGITUDE), cs: Qt::CaseInsensitive) ||
502 !key.compare(QStringLiteral(META_KEY_ALTITUDE), cs: Qt::CaseInsensitive)) {
503 // clang-format on
504 auto ok = false;
505 auto value = QLocale::c().toFloat(s: text, ok: &ok);
506 if (ok) {
507 header.insert(qPrintable(key.toLower()), attribute: Imf::FloatAttribute(value));
508 }
509 }
510
511 if (!key.compare(QStringLiteral(META_KEY_CREATIONDATE), cs: Qt::CaseInsensitive)) {
512 auto dt = QDateTime::fromString(string: text, format: Qt::ISODate);
513 if (dt.isValid()) {
514 dateTime = dt;
515 }
516 }
517
518#ifndef EXR_DISABLE_XMP_ATTRIBUTE // warning: Non-standard attribute!
519 if (!key.compare(QStringLiteral(META_KEY_XMP_ADOBE), cs: Qt::CaseInsensitive)) {
520 header.insert(name: "xmp", attribute: Imf::StringAttribute(text.toStdString()));
521 }
522#endif
523
524 if (!key.compare(QStringLiteral(META_KEY_MANUFACTURER), cs: Qt::CaseInsensitive)) {
525 header.insert(name: "cameraMake", attribute: Imf::StringAttribute(text.toStdString()));
526 }
527 if (!key.compare(QStringLiteral(META_KEY_MODEL), cs: Qt::CaseInsensitive)) {
528 header.insert(name: "cameraModel", attribute: Imf::StringAttribute(text.toStdString()));
529 }
530 if (!key.compare(QStringLiteral(META_KEY_SERIALNUMBER), cs: Qt::CaseInsensitive)) {
531 header.insert(name: "cameraSerialNumber", attribute: Imf::StringAttribute(text.toStdString()));
532 }
533
534 if (!key.compare(QStringLiteral(META_KEY_LENS_MANUFACTURER), cs: Qt::CaseInsensitive)) {
535 header.insert(name: "lensMake", attribute: Imf::StringAttribute(text.toStdString()));
536 }
537 if (!key.compare(QStringLiteral(META_KEY_LENS_MODEL), cs: Qt::CaseInsensitive)) {
538 header.insert(name: "lensModel", attribute: Imf::StringAttribute(text.toStdString()));
539 }
540 if (!key.compare(QStringLiteral(META_KEY_LENS_SERIALNUMBER), cs: Qt::CaseInsensitive)) {
541 header.insert(name: "lensSerialNumber", attribute: Imf::StringAttribute(text.toStdString()));
542 }
543 }
544 if (dateTime.isValid()) {
545 header.insert(name: "capDate", attribute: Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString()));
546 header.insert(name: "utcOffset", attribute: Imf::FloatAttribute(dateTime.offsetFromUtc()));
547 }
548
549 if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
550 header.insert(name: "xDensity", attribute: Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
551 header.insert(name: "pixelAspectRatio", attribute: Imf::FloatAttribute(float(image.dotsPerMeterY()) / float(image.dotsPerMeterX())));
552 }
553
554 // set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
555 // The image is converted to Linear sRGB so, the chroma is the default EXR value.
556 // If a file doesn’t have a chromaticities attribute, display software should assume that the
557 // file’s primaries and the white point match Rec. ITU-R BT.709-3.
558 // header.insert("chromaticities", Imf::ChromaticitiesAttribute(Imf::Chromaticities()));
559}
560
561bool EXRHandler::write(const QImage &image)
562{
563 try {
564 // create EXR header
565 qint32 width = image.width();
566 qint32 height = image.height();
567
568 // limiting the maximum image size on a reasonable size (as done in other plugins)
569 if (width > EXR_MAX_IMAGE_WIDTH || height > EXR_MAX_IMAGE_HEIGHT) {
570 qCWarning(LOG_EXRPLUGIN) << "The maximum image size is limited to" << EXR_MAX_IMAGE_WIDTH << "x" << EXR_MAX_IMAGE_HEIGHT << "px";
571 return false;
572 }
573
574 Imf::Header header(width, height);
575 // set compression scheme (forcing PIZ as default)
576 header.compression() = Imf::Compression::PIZ_COMPRESSION;
577 if (m_compressionRatio >= qint32(Imf::Compression::NO_COMPRESSION) && m_compressionRatio < qint32(Imf::Compression::NUM_COMPRESSION_METHODS)) {
578 header.compression() = Imf::Compression(m_compressionRatio);
579 }
580 // set the DCT quality (used by DCT compressions only)
581 if (m_quality > -1 && m_quality <= 100) {
582 header.dwaCompressionLevel() = float(m_quality);
583 }
584 // make ZIP compression fast (used by ZIP compressions)
585 header.zipCompressionLevel() = 1;
586
587 // set preview (don't set it for small images)
588 if (width > 1024 || height > 1024) {
589 Imf::Array2D<Imf::PreviewRgba> previewPixels;
590 if (makePreview(image, pixels&: previewPixels)) {
591 header.setPreviewImage(Imf::PreviewImage(previewPixels.width(), previewPixels.height(), &previewPixels[0][0]));
592 }
593 }
594
595 // set metadata (EXR attributes)
596 setMetadata(image, header);
597
598 // write the EXR
599 K_OStream ostr(device());
600 auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
601 if (image.format() == QImage::Format_Mono ||
602 image.format() == QImage::Format_MonoLSB ||
603 image.format() == QImage::Format_Grayscale16 ||
604 image.format() == QImage::Format_Grayscale8) {
605 channelsType = Imf::RgbaChannels::WRITE_Y;
606 }
607 Imf::RgbaOutputFile file(ostr, header, channelsType);
608 Imf::Array2D<Imf::Rgba> pixels;
609 pixels.resizeErase(EXR_LINES_PER_BLOCK, sizeY: width);
610
611 // convert the image and write into the stream
612 auto convFormat = image.hasAlphaChannel() ? QImage::Format_RGBA32FPx4 : QImage::Format_RGBX32FPx4;
613 ScanLineConverter slc(convFormat);
614 slc.setDefaultSourceColorSpace(QColorSpace(QColorSpace::SRgb));
615 slc.setTargetColorSpace(QColorSpace(QColorSpace::SRgbLinear));
616 for (int y = 0, n = 0; y < height; y += n) {
617 for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, b: height - y); ++n) {
618 auto scanLine = reinterpret_cast<const float *>(slc.convertedScanLine(image, y: y + n));
619 if (scanLine == nullptr) {
620 return false;
621 }
622 for (int x = 0; x < width; ++x) {
623 auto xcs = x * 4;
624 pixels[n][x].r = float(*(scanLine + xcs));
625 pixels[n][x].g = float(*(scanLine + xcs + 1));
626 pixels[n][x].b = float(*(scanLine + xcs + 2));
627 pixels[n][x].a = float(*(scanLine + xcs + 3));
628 }
629 }
630 file.setFrameBuffer(base: &pixels[0][0] - qint64(y) * width, xStride: 1, yStride: width);
631 file.writePixels(numScanLines: n);
632 }
633 } catch (const std::exception &) {
634 return false;
635 }
636
637 return true;
638}
639
640void EXRHandler::setOption(ImageOption option, const QVariant &value)
641{
642 if (option == QImageIOHandler::CompressionRatio) {
643 auto ok = false;
644 auto cr = value.toInt(ok: &ok);
645 if (ok) {
646 m_compressionRatio = cr;
647 }
648 }
649 if (option == QImageIOHandler::Quality) {
650 auto ok = false;
651 auto q = value.toInt(ok: &ok);
652 if (ok) {
653 m_quality = q;
654 }
655 }
656}
657
658bool EXRHandler::supportsOption(ImageOption option) const
659{
660 if (option == QImageIOHandler::Size) {
661 if (auto d = device())
662 return !d->isSequential();
663 }
664 if (option == QImageIOHandler::ImageFormat) {
665 if (auto d = device())
666 return !d->isSequential();
667 }
668 if (option == QImageIOHandler::CompressionRatio) {
669 return true;
670 }
671 if (option == QImageIOHandler::Quality) {
672 return true;
673 }
674 return false;
675}
676
677QVariant EXRHandler::option(ImageOption option) const
678{
679 QVariant v;
680
681 if (option == QImageIOHandler::Size) {
682 if (auto d = device()) {
683 // transactions works on both random and sequential devices
684 d->startTransaction();
685 if (m_startPos > -1) {
686 d->seek(pos: m_startPos);
687 }
688 try {
689 K_IStream istr(d);
690 Imf::RgbaInputFile file(istr);
691 if (m_imageNumber > -1) { // set the image to read
692 auto views = viewList(h: file.header());
693 if (m_imageNumber < views.count()) {
694 file.setLayerName(views.at(i: m_imageNumber).toStdString());
695 }
696 }
697 Imath::Box2i dw = file.dataWindow();
698 v = QVariant(QSize(dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1));
699 } catch (const std::exception &) {
700 // broken file or unsupported version
701 }
702 d->rollbackTransaction();
703 }
704 }
705
706 if (option == QImageIOHandler::ImageFormat) {
707 if (auto d = device()) {
708 // transactions works on both random and sequential devices
709 d->startTransaction();
710 if (m_startPos > -1) {
711 d->seek(pos: m_startPos);
712 }
713 try {
714 K_IStream istr(d);
715 Imf::RgbaInputFile file(istr);
716 v = QVariant::fromValue(value: imageFormat(file));
717 } catch (const std::exception &) {
718 // broken file or unsupported version
719 }
720 d->rollbackTransaction();
721 }
722 }
723
724 if (option == QImageIOHandler::CompressionRatio) {
725 v = QVariant(m_compressionRatio);
726 }
727
728 if (option == QImageIOHandler::Quality) {
729 v = QVariant(m_quality);
730 }
731
732 return v;
733}
734
735bool EXRHandler::jumpToNextImage()
736{
737 return jumpToImage(imageNumber: m_imageNumber + 1);
738}
739
740bool EXRHandler::jumpToImage(int imageNumber)
741{
742 if (imageNumber < 0 || imageNumber >= imageCount()) {
743 return false;
744 }
745 m_imageNumber = imageNumber;
746 return true;
747}
748
749int EXRHandler::imageCount() const
750{
751 // NOTE: image count is cached for performance reason
752 auto &&count = m_imageCount;
753 if (count > 0) {
754 return count;
755 }
756
757 count = QImageIOHandler::imageCount();
758
759 auto d = device();
760 d->startTransaction();
761
762 try {
763 K_IStream istr(d);
764 Imf::RgbaInputFile file(istr);
765 auto views = viewList(h: file.header());
766 if (!views.isEmpty()) {
767 count = views.size();
768 }
769 } catch (const std::exception &) {
770 // do nothing
771 }
772
773 d->rollbackTransaction();
774
775 return count;
776}
777
778int EXRHandler::currentImageNumber() const
779{
780 return m_imageNumber;
781}
782
783bool EXRHandler::canRead(QIODevice *device)
784{
785 if (!device) {
786 qCWarning(LOG_EXRPLUGIN) << "EXRHandler::canRead() called with no device";
787 return false;
788 }
789
790#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2
791 // openexpr >= 3.3 uses seek and tell extensively
792 if (device->isSequential()) {
793 return false;
794 }
795#endif
796
797 const QByteArray head = device->peek(maxlen: 4);
798
799 return Imf::isImfMagic(bytes: head.data());
800}
801
802QImageIOPlugin::Capabilities EXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
803{
804 if (format == "exr") {
805 return Capabilities(CanRead | CanWrite);
806 }
807 if (!format.isEmpty()) {
808 return {};
809 }
810 if (!device->isOpen()) {
811 return {};
812 }
813
814 Capabilities cap;
815 if (device->isReadable() && EXRHandler::canRead(device)) {
816 cap |= CanRead;
817 }
818 if (device->isWritable()) {
819 cap |= CanWrite;
820 }
821 return cap;
822}
823
824QImageIOHandler *EXRPlugin::create(QIODevice *device, const QByteArray &format) const
825{
826 QImageIOHandler *handler = new EXRHandler;
827 handler->setDevice(device);
828 handler->setFormat(format);
829 return handler;
830}
831
832#include "moc_exr_p.cpp"
833

source code of kimageformats/src/imageformats/exr.cpp