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