KImageFormats

exr.cpp
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
102class K_IStream : public Imf::IStream
103{
104public:
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
121private:
122 QIODevice *m_dev;
123};
124
125bool K_IStream::read(char c[], int n)
126{
127 qint64 result = m_dev->read(c, 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("Error in read", result);
134 }
135 return false;
136}
137
138#if OPENEXR_VERSION_MAJOR > 2
139uint64_t K_IStream::tellg()
140#else
141Imf::Int64 K_IStream::tellg()
142#endif
143{
144 return m_dev->pos();
145}
146
147#if OPENEXR_VERSION_MAJOR > 2
148void K_IStream::seekg(uint64_t pos)
149#else
150void K_IStream::seekg(Imf::Int64 pos)
151#endif
152{
153 m_dev->seek(pos);
154}
155
156void K_IStream::clear()
157{
158 // TODO
159}
160
161class K_OStream : public Imf::OStream
162{
163public:
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
179private:
180 QIODevice *m_dev;
181};
182
183void K_OStream::write(const char c[], int n)
184{
185 qint64 result = m_dev->write(c, n);
186 if (result > 0) {
187 return;
188 } else { // negative value {
189 Iex::throwErrnoExc("Error in write", result);
190 }
191 return;
192}
193
194#if OPENEXR_VERSION_MAJOR > 2
195uint64_t K_OStream::tellp()
196#else
197Imf::Int64 K_OStream::tellg()
198#endif
199{
200 return m_dev->pos();
201}
202
203#if OPENEXR_VERSION_MAJOR > 2
204void K_OStream::seekp(uint64_t pos)
205#else
206void 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
214inline 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}
219inline 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
228EXRHandler::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
239bool EXRHandler::canRead() const
240{
241 if (canRead(device())) {
242 setFormat("exr");
243 return true;
244 }
245 return false;
246}
247
248static 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 */
265static QStringList viewList(const Imf::Header &h)
266{
267 QStringList l;
268 if (auto views = h.findTypedAttribute<Imf::StringVectorAttribute>("multiView")) {
269 for (auto &&v : views->value()) {
271 }
272 }
273 return l;
274}
275
276#ifdef QT_DEBUG
277void printAttributes(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 */
289static void readMetadata(const Imf::Header &header, QImage &image)
290{
291 // set some useful metadata
292 if (auto comments = header.findTypedAttribute<Imf::StringAttribute>("comments")) {
293 image.setText(QStringLiteral("Comment"), QString::fromStdString(comments->value()));
294 }
295
296 if (auto owner = header.findTypedAttribute<Imf::StringAttribute>("owner")) {
297 image.setText(QStringLiteral("Owner"), QString::fromStdString(owner->value()));
298 }
299
300 if (auto lat = header.findTypedAttribute<Imf::FloatAttribute>("latitude")) {
301 image.setText(QStringLiteral("Latitude"), QLocale::c().toString(lat->value()));
302 }
303
304 if (auto lon = header.findTypedAttribute<Imf::FloatAttribute>("longitude")) {
305 image.setText(QStringLiteral("Longitude"), QLocale::c().toString(lon->value()));
306 }
307
308 if (auto alt = header.findTypedAttribute<Imf::FloatAttribute>("altitude")) {
309 image.setText(QStringLiteral("Altitude"), QLocale::c().toString(alt->value()));
310 }
311
312 if (auto capDate = header.findTypedAttribute<Imf::StringAttribute>("capDate")) {
313 float off = 0;
314 if (auto utcOffset = header.findTypedAttribute<Imf::FloatAttribute>("utcOffset")) {
315 off = utcOffset->value();
316 }
317 auto dateTime = QDateTime::fromString(QString::fromStdString(capDate->value()), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
318 if (dateTime.isValid()) {
319 dateTime.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(off));
320 image.setText(QStringLiteral("CreationDate"), dateTime.toString(Qt::ISODate));
321 }
322 }
323
324 if (auto xDensity = header.findTypedAttribute<Imf::FloatAttribute>("xDensity")) {
325 float par = 1;
326 if (auto pixelAspectRatio = header.findTypedAttribute<Imf::FloatAttribute>("pixelAspectRatio")) {
327 par = pixelAspectRatio->value();
328 }
329 image.setDotsPerMeterX(qRound(xDensity->value() * 100.0 / 2.54));
330 image.setDotsPerMeterY(qRound(xDensity->value() * par * 100.0 / 2.54));
331 }
332
333 // Non-standard attribute
334 if (auto xmp = header.findTypedAttribute<Imf::StringAttribute>("xmp")) {
335 image.setText(QStringLiteral("XML:com.adobe.xmp"), QString::fromStdString(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 */
361static void readColorSpace(const Imf::Header &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>("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()) {
377 }
378 image.setColorSpace(cs);
379
380#ifdef EXR_CONVERT_TO_SRGB
382#endif
383
384#endif // !EXR_USE_LEGACY_CONVERSIONS
385}
386
387bool 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(m_startPos);
398 }
399 }
400
401 K_IStream istr(d, QByteArray());
402 Imf::RgbaInputFile file(istr);
403 auto &&header = file.header();
404
405 // set the image to load
406 if (m_imageNumber > -1) {
407 auto views = viewList(header);
408 if (m_imageNumber < views.count()) {
409 file.setLayerName(views.at(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, 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, 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(&pixels[0][0] - dw.min.x - qint64(my) * width, 1, width);
443 file.readPixels(my, std::min(my + EXR_LINES_PER_BLOCK - 1, dw.max.y));
444
445 for (n = 0; n < std::min(EXR_LINES_PER_BLOCK, 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(0.f, float(pixels[n][x].r), 1.f));
457 *(scanLine + xcs + 1) = qfloat16(qBound(0.f, float(pixels[n][x].g), 1.f));
458 *(scanLine + xcs + 2) = qfloat16(qBound(0.f, float(pixels[n][x].b), 1.f));
459 *(scanLine + xcs + 3) = qfloat16(isRgba ? qBound(0.f, float(pixels[n][x].a), 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 */
490bool 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) {
498 } else {
500 }
501 if (preview.isNull()) {
502 return false;
503 }
504
505 w = preview.width();
506 h = preview.height();
507 pixels.resizeErase(h, w);
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(*(scanLine + x));
515 out.g = qGreen(*(scanLine + x));
516 out.b = qBlue(*(scanLine + x));
517 out.a = qAlpha(*(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 */
528static void setMetadata(const QImage &image, Imf::Header &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"), Qt::CaseInsensitive)) {
534 header.insert("comments", Imf::StringAttribute(text.toStdString()));
535 }
536
537 if (!key.compare(QStringLiteral("Owner"), Qt::CaseInsensitive)) {
538 header.insert("owner", Imf::StringAttribute(text.toStdString()));
539 }
540
541 // clang-format off
542 if (!key.compare(QStringLiteral("Latitude"), Qt::CaseInsensitive) ||
543 !key.compare(QStringLiteral("Longitude"), Qt::CaseInsensitive) ||
544 !key.compare(QStringLiteral("Altitude"), Qt::CaseInsensitive)) {
545 // clang-format on
546 auto ok = false;
547 auto value = QLocale::c().toFloat(text, &ok);
548 if (ok) {
549 header.insert(qPrintable(key.toLower()), Imf::FloatAttribute(value));
550 }
551 }
552
553 if (!key.compare(QStringLiteral("CreationDate"), Qt::CaseInsensitive)) {
554 auto dt = QDateTime::fromString(text, 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"), Qt::CaseInsensitive)) {
562 header.insert("xmp", Imf::StringAttribute(text.toStdString()));
563 }
564#endif
565 }
566 if (dateTime.isValid()) {
567 header.insert("capDate", Imf::StringAttribute(dateTime.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toStdString()));
568 header.insert("utcOffset", Imf::FloatAttribute(dateTime.offsetFromUtc()));
569 }
570
571 if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
572 header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
573 header.insert("pixelAspectRatio", 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
585bool 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 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, 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 ||
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, width);
634
635 // convert the image and write into the stream
636#if defined(EXR_USE_QT6_FLOAT_IMAGE)
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, height - y); ++n) {
646#if defined(EXR_USE_QT6_FLOAT_IMAGE)
647 auto scanLine = reinterpret_cast<const qfloat16 *>(slc.convertedScanLine(image, 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(&pixels[0][0] - qint64(y) * width, 1, width);
672 file.writePixels(n);
673 }
674 } catch (const std::exception &) {
675 return false;
676 }
677
678 return true;
679}
680
681void EXRHandler::setOption(ImageOption option, const QVariant &value)
682{
683 if (option == QImageIOHandler::CompressionRatio) {
684 auto ok = false;
685 auto cr = value.toInt(&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);
693 if (ok) {
694 m_quality = q;
695 }
696 }
697}
698
699bool 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
716QVariant 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(file.header());
729 if (m_imageNumber < views.count()) {
730 file.setLayerName(views.at(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(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
768bool EXRHandler::jumpToNextImage()
769{
770 return jumpToImage(m_imageNumber + 1);
771}
772
773bool EXRHandler::jumpToImage(int imageNumber)
774{
775 if (imageNumber < 0 || imageNumber >= imageCount()) {
776 return false;
777 }
778 m_imageNumber = imageNumber;
779 return true;
780}
781
782int 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
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(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
811int EXRHandler::currentImageNumber() const
812{
813 return m_imageNumber;
814}
815
816bool EXRHandler::canRead(QIODevice *device)
817{
818 if (!device) {
819 qWarning("EXRHandler::canRead() called with no device");
820 return false;
821 }
822
823 const QByteArray head = device->peek(4);
824
825 return Imf::isImfMagic(head.data());
826}
827
828QImageIOPlugin::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
850QImageIOHandler *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"
char * toString(const EngineQuery &query)
KGUIADDONS_EXPORT qreal chroma(const QColor &)
QFlags< Capability > Capabilities
KEDUVOCDOCUMENT_EXPORT QStringList comments(const QString &language=QString())
char * data()
bool isEmpty() const const
bool isValid() const const
QDateTime currentDateTime()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
const uchar * constScanLine(int i) const const
void convertToColorSpace(const QColorSpace &colorSpace)
QImage convertToFormat(Format format, Qt::ImageConversionFlags flags) &&
int dotsPerMeterX() const const
int dotsPerMeterY() const const
Format format() const const
bool hasAlphaChannel() const const
int height() const const
bool isNull() const const
QImage scaledToHeight(int height, Qt::TransformationMode mode) const const
QImage scaledToWidth(int width, Qt::TransformationMode mode) const const
uchar * scanLine(int i)
void setColorSpace(const QColorSpace &colorSpace)
void setDotsPerMeterX(int x)
void setDotsPerMeterY(int y)
void setText(const QString &key, const QString &text)
QString text(const QString &key) const const
int width() const const
virtual int imageCount() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
typedef Capabilities
bool isOpen() const const
bool isReadable() const const
bool isWritable() const const
QByteArray peek(qint64 maxSize)
QByteArray read(qint64 maxSize)
qint64 write(const QByteArray &data)
T value(qsizetype i) const const
QLocale c()
float toFloat(QStringView s, bool *ok) const const
QRgba64 fromRgba64(quint16 r, quint16 g, quint16 b, quint16 a)
QString fromStdString(const std::string &str)
QString & insert(qsizetype position, QChar ch)
CaseInsensitive
int idealThreadCount()
QTimeZone fromSecondsAheadOfUtc(int offset)
QVariant fromValue(T &&value)
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:40 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.