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