1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2005 Christoph Hormann <chris_hormann@gmx.de>
4 SPDX-FileCopyrightText: 2005 Ignacio CastaƱo <castanyo@yahoo.es>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "hdr_p.h"
10#include "util_p.h"
11
12#include <QColorSpace>
13#include <QDataStream>
14#include <QFloat16>
15#include <QImage>
16#include <QLoggingCategory>
17#include <QRegularExpressionMatch>
18
19/* *** HDR_HALF_QUALITY ***
20 * If defined, a 16-bits float image is created, otherwise a 32-bits float ones (default).
21 */
22//#define HDR_HALF_QUALITY // default commented -> you should define it in your cmake file
23
24/* *** HDR_MAX_IMAGE_WIDTH and HDR_MAX_IMAGE_HEIGHT ***
25 * The maximum size in pixel allowed by the plugin.
26 */
27#ifndef HDR_MAX_IMAGE_WIDTH
28#define HDR_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT
29#endif
30#ifndef HDR_MAX_IMAGE_HEIGHT
31#define HDR_MAX_IMAGE_HEIGHT HDR_MAX_IMAGE_WIDTH
32#endif
33
34typedef unsigned char uchar;
35
36Q_LOGGING_CATEGORY(HDRPLUGIN, "kf.imageformats.plugins.hdr", QtWarningMsg)
37
38#define MAXLINE 1024
39#define MINELEN 8 // minimum scanline length for encoding
40#define MAXELEN 0x7fff // maximum scanline length for encoding
41
42class Header
43{
44public:
45 Header()
46 {
47 m_colorSpace = QColorSpace(QColorSpace::SRgbLinear);
48 m_transformation = QImageIOHandler::TransformationNone;
49 }
50 Header(const Header&) = default;
51 Header& operator=(const Header&) = default;
52
53 bool isValid() const
54 {
55 return width() > 0 && height() > 0 && width() <= HDR_MAX_IMAGE_WIDTH && height() <= HDR_MAX_IMAGE_HEIGHT;
56 }
57 qint32 width() const { return(m_size.width()); }
58 qint32 height() const { return(m_size.height()); }
59 QString software() const { return(m_software); }
60 QImageIOHandler::Transformations transformation() const { return(m_transformation); }
61
62 /*!
63 * \brief colorSpace
64 *
65 * The color space for the image.
66 *
67 * The CIE (x,y) chromaticity coordinates of the three (RGB)
68 * primaries and the white point used to standardize the picture's
69 * color system. This is used mainly by the ra_xyze program to
70 * convert between color systems. If no PRIMARIES line
71 * appears, we assume the standard primaries defined in
72 * src/common/color.h, namely "0.640 0.330 0.290
73 * 0.600 0.150 0.060 0.333 0.333" for red, green, blue
74 * and white, respectively.
75 */
76 QColorSpace colorSpace() const { return(m_colorSpace); }
77
78 /*!
79 * \brief exposure
80 *
81 * A single floating point number indicating a multiplier that has
82 * been applied to all the pixels in the file. EXPOSURE values are
83 * cumulative, so the original pixel values (i.e., radiances in
84 * watts/steradian/m^2) must be derived by taking the values in the
85 * file and dividing by all the EXPOSURE settings multiplied
86 * together. No EXPOSURE setting implies that no exposure
87 * changes have taken place.
88 */
89 float exposure() const {
90 float mul = 1;
91 for (auto&& v : m_exposure)
92 mul *= v;
93 return mul;
94 }
95
96 QImageIOHandler::Transformations m_transformation;
97 QColorSpace m_colorSpace;
98 QString m_software;
99 QSize m_size;
100 QList<float> m_exposure;
101};
102
103class HDRHandlerPrivate
104{
105public:
106 HDRHandlerPrivate()
107 {
108 }
109 ~HDRHandlerPrivate()
110 {
111 }
112
113 const Header& header(QIODevice *device)
114 {
115 auto&& h = m_header;
116 if (h.isValid()) {
117 return h;
118 }
119 h = readHeader(device);
120 return h;
121 }
122
123 static Header readHeader(QIODevice *device)
124 {
125 Header h;
126
127 int cnt = 0;
128 int len = 0;
129 QByteArray line(MAXLINE, char());
130 QByteArray format;
131
132 // Parse header
133 do {
134 len = device->readLine(data: line.data(), maxlen: line.size());
135 if (len < 0) {
136 break;
137 }
138 if (line.startsWith(bv: "FORMAT=")) {
139 format = line.mid(index: 7, len: len - 7).trimmed();
140 }
141 if (line.startsWith(bv: "SOFTWARE=")) {
142 h.m_software = QString::fromUtf8(ba: line.mid(index: 9, len: len - 9)).trimmed();
143 }
144 if (line.startsWith(bv: "EXPOSURE=")) {
145 auto ok = false;
146 auto ex = QLocale::c().toFloat(s: QString::fromLatin1(ba: line.mid(index: 9, len: len - 9)).trimmed(), ok: &ok);
147 if (ok)
148 h.m_exposure << ex;
149 }
150 if (line.startsWith(bv: "PRIMARIES=")) {
151 auto list = line.mid(index: 10, len: len - 10).trimmed().split(sep: ' ');
152 QList<double> primaries;
153 for (auto&& v : list) {
154 auto ok = false;
155 auto d = QLocale::c().toDouble(s: QString::fromLatin1(ba: v), ok: &ok);
156 if (ok)
157 primaries << d;
158 }
159 if (primaries.size() == 8) {
160 auto cs = QColorSpace(QPointF(primaries.at(i: 6), primaries.at(i: 7)),
161 QPointF(primaries.at(i: 0), primaries.at(i: 1)),
162 QPointF(primaries.at(i: 2), primaries.at(i: 3)),
163 QPointF(primaries.at(i: 4), primaries.at(i: 5)),
164 QColorSpace::TransferFunction::Linear);
165 cs.setDescription(QStringLiteral("Embedded RGB"));
166 if (cs.isValid())
167 h.m_colorSpace = cs;
168 }
169 }
170
171 } while ((len > 0) && (line[0] != '\n') && (cnt++ < 128));
172
173 if (format != "32-bit_rle_rgbe") {
174 qCDebug(HDRPLUGIN) << "Unknown HDR format:" << format;
175 return h;
176 }
177
178 len = device->readLine(data: line.data(), maxlen: line.size());
179 if (len < 0) {
180 qCDebug(HDRPLUGIN) << "Invalid HDR file, error while reading the first line after the header";
181 return h;
182 }
183 line.resize(size: len);
184
185 /*
186 * Handle flipping and rotation, as per the spec below.
187 * The single resolution line consists of 4 values, a X and Y label each followed by a numerical
188 * integer value. The X and Y are immediately preceded by a sign which can be used to indicate
189 * flipping, the order of the X and Y indicate rotation. The standard coordinate system for
190 * Radiance images would have the following resolution string -Y N +X N. This indicates that the
191 * vertical axis runs down the file and the X axis is to the right (imagining the image as a
192 * rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
193 * indicate a vertical flip. If the X value appears before the Y value then that indicates that
194 * the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
195 * The reader can convince themselves that the 8 combinations cover all the possible image orientations
196 * and rotations.
197 */
198 QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY])\\s+([0-9]+)\\s+([+\\-][XY])\\s+([0-9]+)\n"));
199 QRegularExpressionMatch match = resolutionRegExp.match(subject: QString::fromLatin1(ba: line));
200 if (!match.hasMatch()) {
201 qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
202 return h;
203 }
204
205 auto c0 = match.captured(nth: 1);
206 auto c1 = match.captured(nth: 3);
207 if (c0.at(i: 1) == u'Y') {
208 if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'+')
209 h.m_transformation = QImageIOHandler::TransformationNone;
210 if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'-')
211 h.m_transformation = QImageIOHandler::TransformationMirror;
212 if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'+')
213 h.m_transformation = QImageIOHandler::TransformationFlip;
214 if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'-')
215 h.m_transformation = QImageIOHandler::TransformationRotate180;
216 }
217 else {
218 if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'+')
219 h.m_transformation = QImageIOHandler::TransformationRotate90;
220 if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'-')
221 h.m_transformation = QImageIOHandler::TransformationMirrorAndRotate90;
222 if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'+')
223 h.m_transformation = QImageIOHandler::TransformationFlipAndRotate90;
224 if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'-')
225 h.m_transformation = QImageIOHandler::TransformationRotate270;
226 }
227
228 h.m_size = QSize(match.captured(nth: 4).toInt(), match.captured(nth: 2).toInt());
229 return h;
230 }
231
232private:
233 Header m_header;
234};
235
236// read an old style line from the hdr image file
237// if 'first' is true the first byte is already read
238static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
239{
240 int rshift = 0;
241 int i;
242
243 uchar *start = image;
244 while (width > 0) {
245 s >> image[0];
246 s >> image[1];
247 s >> image[2];
248 s >> image[3];
249
250 if (s.atEnd()) {
251 return false;
252 }
253
254 if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
255 // NOTE: we don't have an image sample that cover this code
256 if (rshift > 31) {
257 return false;
258 }
259 for (i = image[3] << rshift; i > 0 && width > 0; i--) {
260 if (image == start) {
261 return false; // you cannot be here at the first run
262 }
263 // memcpy(image, image-4, 4);
264 (uint &)image[0] = (uint &)image[0 - 4];
265 image += 4;
266 width--;
267 }
268 rshift += 8;
269 } else {
270 image += 4;
271 width--;
272 rshift = 0;
273 }
274 }
275 return true;
276}
277
278template<class float_T>
279void RGBE_To_QRgbLine(uchar *image, float_T *scanline, const Header& h)
280{
281 auto exposure = h.exposure();
282 for (int j = 0, width = h.width(); j < width; j++) {
283 // v = ldexp(1.0, int(image[3]) - 128);
284 float v;
285 int e = qBound(min: -31, val: int(image[3]) - 128, max: 31);
286 if (e > 0) {
287 v = float(1 << e);
288 } else {
289 v = 1.0f / float(1 << -e);
290 }
291
292 auto j4 = j * 4;
293 auto vn = v / 255.0f;
294 if (exposure > 0) {
295 vn /= exposure;
296 }
297
298 scanline[j4] = float_T(float(image[0]) * vn);
299 scanline[j4 + 1] = float_T(float(image[1]) * vn);
300 scanline[j4 + 2] = float_T(float(image[2]) * vn);
301 scanline[j4 + 3] = float_T(1.0f);
302 image += 4;
303 }
304}
305
306QImage::Format imageFormat()
307{
308#ifdef HDR_HALF_QUALITY
309 return QImage::Format_RGBX16FPx4;
310#else
311 return QImage::Format_RGBX32FPx4;
312#endif
313}
314
315// Load the HDR image.
316static bool LoadHDR(QDataStream &s, const Header& h, QImage &img)
317{
318 uchar val;
319 uchar code;
320
321 const int width = h.width();
322 const int height = h.height();
323
324 // Create dst image.
325 img = imageAlloc(width, height, format: imageFormat());
326 if (img.isNull()) {
327 qCDebug(HDRPLUGIN) << "Couldn't create image with size" << width << height << "and format RGB32";
328 return false;
329 }
330
331 QByteArray lineArray(4 * width, char());
332 uchar *image = reinterpret_cast<uchar *>(lineArray.data());
333
334 for (int cline = 0; cline < height; cline++) {
335#ifdef HDR_HALF_QUALITY
336 auto scanline = reinterpret_cast<qfloat16 *>(img.scanLine(cline));
337#else
338 auto scanline = reinterpret_cast<float *>(img.scanLine(cline));
339#endif
340
341 // determine scanline type
342 if ((width < MINELEN) || (MAXELEN < width)) {
343 Read_Old_Line(image, width, s);
344 RGBE_To_QRgbLine(image, scanline, h);
345 continue;
346 }
347
348 s >> val;
349
350 if (s.atEnd()) {
351 return true;
352 }
353
354 if (val != 2) {
355 s.device()->ungetChar(c: val);
356 Read_Old_Line(image, width, s);
357 RGBE_To_QRgbLine(image, scanline, h);
358 continue;
359 }
360
361 s >> image[1];
362 s >> image[2];
363 s >> image[3];
364
365 if (s.atEnd()) {
366 return true;
367 }
368
369 if ((image[1] != 2) || (image[2] & 128)) {
370 image[0] = 2;
371 Read_Old_Line(image: image + 4, width: width - 1, s);
372 RGBE_To_QRgbLine(image, scanline, h);
373 continue;
374 }
375
376 if ((image[2] << 8 | image[3]) != width) {
377 qCDebug(HDRPLUGIN) << "Line of pixels had width" << (image[2] << 8 | image[3]) << "instead of" << width;
378 return false;
379 }
380
381 // read each component
382 for (int i = 0, len = int(lineArray.size()); i < 4; i++) {
383 for (int j = 0; j < width;) {
384 s >> code;
385 if (s.atEnd()) {
386 qCDebug(HDRPLUGIN) << "Truncated HDR file";
387 return false;
388 }
389 if (code > 128) {
390 // run
391 code &= 127;
392 s >> val;
393 while (code != 0) {
394 auto idx = i + j * 4;
395 if (idx < len) {
396 image[idx] = val;
397 }
398 j++;
399 code--;
400 }
401 } else {
402 // non-run
403 while (code != 0) {
404 auto idx = i + j * 4;
405 if (idx < len) {
406 s >> image[idx];
407 }
408 j++;
409 code--;
410 }
411 }
412 }
413 }
414 RGBE_To_QRgbLine(image, scanline, h);
415 }
416
417 return true;
418}
419
420bool HDRHandler::read(QImage *outImage)
421{
422 QDataStream s(device());
423
424 const Header& h = d->header(device: s.device());
425 if (!h.isValid()) {
426 return false;
427 }
428
429 QImage img;
430 if (!LoadHDR(s, h, img)) {
431 // qCWarning(HDRPLUGIN) << "Error loading HDR file.";
432 return false;
433 }
434
435 // By setting the linear color space, programs that support profiles display HDR files as in GIMP and Photoshop.
436 img.setColorSpace(h.colorSpace());
437
438 // Metadata
439 if (!h.software().isEmpty()) {
440 img.setText(QStringLiteral(META_KEY_SOFTWARE), value: h.software());
441 }
442
443 *outImage = img;
444 return true;
445}
446
447bool HDRHandler::supportsOption(ImageOption option) const
448{
449 if (option == QImageIOHandler::Size) {
450 return true;
451 }
452 if (option == QImageIOHandler::ImageFormat) {
453 return true;
454 }
455 if (option == QImageIOHandler::ImageTransformation) {
456 return true;
457 }
458 return false;
459}
460
461QVariant HDRHandler::option(ImageOption option) const
462{
463 QVariant v;
464
465 if (option == QImageIOHandler::Size) {
466 if (auto dev = device()) {
467 auto&& h = d->header(device: dev);
468 if (h.isValid()) {
469 v = QVariant::fromValue(value: h.m_size);
470 }
471 }
472 }
473
474 if (option == QImageIOHandler::ImageFormat) {
475 v = QVariant::fromValue(value: imageFormat());
476 }
477
478 if (option == QImageIOHandler::ImageTransformation) {
479 if (auto dev = device()) {
480 auto&& h = d->header(device: dev);
481 if (h.isValid()) {
482 v = QVariant::fromValue(value: h.transformation());
483 }
484 }
485 }
486
487 return v;
488}
489
490HDRHandler::HDRHandler()
491 : QImageIOHandler()
492 , d(new HDRHandlerPrivate)
493{
494}
495
496bool HDRHandler::canRead() const
497{
498 if (canRead(device: device())) {
499 setFormat("hdr");
500 return true;
501 }
502 return false;
503}
504
505bool HDRHandler::canRead(QIODevice *device)
506{
507 if (!device) {
508 qCWarning(HDRPLUGIN) << "HDRHandler::canRead() called with no device";
509 return false;
510 }
511
512 // the .pic taken from official test cases does not start with this string but can be loaded.
513 if(device->peek(maxlen: 11) == "#?RADIANCE\n" || device->peek(maxlen: 7) == "#?RGBE\n") {
514 return true;
515 }
516
517 // allow to load official test cases: https://radsite.lbl.gov/radiance/framed.html
518 device->startTransaction();
519 auto h = HDRHandlerPrivate::readHeader(device);
520 device->rollbackTransaction();
521 if (h.isValid()) {
522 return true;
523 }
524
525 return false;
526}
527
528QImageIOPlugin::Capabilities HDRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
529{
530 if (format == "hdr") {
531 return Capabilities(CanRead);
532 }
533 if (!format.isEmpty()) {
534 return {};
535 }
536 if (!device->isOpen()) {
537 return {};
538 }
539
540 Capabilities cap;
541 if (device->isReadable() && HDRHandler::canRead(device)) {
542 cap |= CanRead;
543 }
544 return cap;
545}
546
547QImageIOHandler *HDRPlugin::create(QIODevice *device, const QByteArray &format) const
548{
549 QImageIOHandler *handler = new HDRHandler;
550 handler->setDevice(device);
551 handler->setFormat(format);
552 return handler;
553}
554
555#include "moc_hdr_p.cpp"
556

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