KImageFormats

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

KDE's Doxygen guidelines are available online.