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 | |
26 | typedef unsigned char uchar; |
27 | |
28 | Q_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 | |
34 | class |
35 | { |
36 | public: |
37 | () |
38 | { |
39 | m_colorSpace = QColorSpace(QColorSpace::SRgbLinear); |
40 | m_transformation = QImageIOHandler::TransformationNone; |
41 | } |
42 | (const Header&) = default; |
43 | Header& (const Header&) = default; |
44 | |
45 | bool () const { return width() > 0 && height() > 0; } |
46 | qint32 () const { return(m_size.width()); } |
47 | qint32 () const { return(m_size.height()); } |
48 | QString () const { return(m_software); } |
49 | QImageIOHandler::Transformations () 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 () 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 () const { |
79 | float mul = 1; |
80 | for (auto&& v : m_exposure) |
81 | mul *= v; |
82 | return mul; |
83 | } |
84 | |
85 | QImageIOHandler::Transformations ; |
86 | QColorSpace ; |
87 | QString ; |
88 | QSize ; |
89 | QList<float> ; |
90 | }; |
91 | |
92 | class HDRHandlerPrivate |
93 | { |
94 | public: |
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(data: line.data(), MAXLINE); |
123 | |
124 | if (line.startsWith(bv: "FORMAT=" )) { |
125 | format = line.mid(index: 7, len: len - 7).trimmed(); |
126 | } |
127 | if (line.startsWith(bv: "SOFTWARE=" )) { |
128 | h.m_software = QString::fromUtf8(ba: line.mid(index: 9, len: len - 9)).trimmed(); |
129 | } |
130 | if (line.startsWith(bv: "EXPOSURE=" )) { |
131 | auto ok = false; |
132 | auto ex = QLocale::c().toFloat(s: QString::fromLatin1(ba: line.mid(index: 9, len: len - 9)).trimmed(), ok: &ok); |
133 | if (ok) |
134 | h.m_exposure << ex; |
135 | } |
136 | if (line.startsWith(bv: "PRIMARIES=" )) { |
137 | auto list = line.mid(index: 10, len: len - 10).trimmed().split(sep: ' '); |
138 | QList<double> primaries; |
139 | for (auto&& v : list) { |
140 | auto ok = false; |
141 | auto d = QLocale::c().toDouble(s: QString::fromLatin1(ba: v), ok: &ok); |
142 | if (ok) |
143 | primaries << d; |
144 | } |
145 | if (primaries.size() == 8) { |
146 | auto cs = QColorSpace(QPointF(primaries.at(i: 6), primaries.at(i: 7)), |
147 | QPointF(primaries.at(i: 0), primaries.at(i: 1)), |
148 | QPointF(primaries.at(i: 2), primaries.at(i: 3)), |
149 | QPointF(primaries.at(i: 4), primaries.at(i: 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(data: line.data(), MAXLINE); |
165 | line.resize(size: 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(subject: QString::fromLatin1(ba: 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(nth: 1); |
188 | auto c1 = match.captured(nth: 3); |
189 | if (c0.at(i: 1) == u'Y') { |
190 | if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'+') |
191 | h.m_transformation = QImageIOHandler::TransformationNone; |
192 | if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'-') |
193 | h.m_transformation = QImageIOHandler::TransformationMirror; |
194 | if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'+') |
195 | h.m_transformation = QImageIOHandler::TransformationFlip; |
196 | if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'-') |
197 | h.m_transformation = QImageIOHandler::TransformationRotate180; |
198 | } |
199 | else { |
200 | if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'+') |
201 | h.m_transformation = QImageIOHandler::TransformationRotate90; |
202 | if (c0.at(i: 0) == u'-' && c1.at(i: 0) == u'-') |
203 | h.m_transformation = QImageIOHandler::TransformationMirrorAndRotate90; |
204 | if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'+') |
205 | h.m_transformation = QImageIOHandler::TransformationFlipAndRotate90; |
206 | if (c0.at(i: 0) == u'+' && c1.at(i: 0) == u'-') |
207 | h.m_transformation = QImageIOHandler::TransformationRotate270; |
208 | } |
209 | |
210 | h.m_size = QSize(match.captured(nth: 4).toInt(), match.captured(nth: 2).toInt()); |
211 | return h; |
212 | } |
213 | |
214 | private: |
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 |
220 | static 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 | |
260 | template<class float_T> |
261 | void (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(min: -31, val: int(image[3]) - 128, max: 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 | |
288 | QImage::Format imageFormat() |
289 | { |
290 | #ifdef HDR_HALF_QUALITY |
291 | return QImage::Format_RGBX16FPx4; |
292 | #else |
293 | return QImage::Format_RGBX32FPx4; |
294 | #endif |
295 | } |
296 | |
297 | // Load the HDR image. |
298 | static bool (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, format: 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(size: 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(c: 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: image + 4, width: 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 | |
403 | bool HDRHandler::read(QImage *outImage) |
404 | { |
405 | QDataStream s(device()); |
406 | |
407 | const Header& h = d->header(device: 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), value: h.software()); |
424 | } |
425 | |
426 | *outImage = img; |
427 | return true; |
428 | } |
429 | |
430 | bool HDRHandler::supportsOption(ImageOption option) const |
431 | { |
432 | if (option == QImageIOHandler::Size) { |
433 | return true; |
434 | } |
435 | if (option == QImageIOHandler::ImageFormat) { |
436 | return true; |
437 | } |
438 | if (option == QImageIOHandler::ImageTransformation) { |
439 | return true; |
440 | } |
441 | return false; |
442 | } |
443 | |
444 | QVariant 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(device: dev); |
451 | if (h.isValid()) { |
452 | v = QVariant::fromValue(value: h.m_size); |
453 | } |
454 | } |
455 | } |
456 | |
457 | if (option == QImageIOHandler::ImageFormat) { |
458 | v = QVariant::fromValue(value: imageFormat()); |
459 | } |
460 | |
461 | if (option == QImageIOHandler::ImageTransformation) { |
462 | if (auto dev = device()) { |
463 | auto&& h = d->header(device: dev); |
464 | if (h.isValid()) { |
465 | v = QVariant::fromValue(value: h.transformation()); |
466 | } |
467 | } |
468 | } |
469 | |
470 | return v; |
471 | } |
472 | |
473 | HDRHandler::HDRHandler() |
474 | : QImageIOHandler() |
475 | , d(new HDRHandlerPrivate) |
476 | { |
477 | } |
478 | |
479 | bool HDRHandler::canRead() const |
480 | { |
481 | if (canRead(device: device())) { |
482 | setFormat("hdr" ); |
483 | return true; |
484 | } |
485 | return false; |
486 | } |
487 | |
488 | bool HDRHandler::canRead(QIODevice *device) |
489 | { |
490 | if (!device) { |
491 | qWarning(msg: "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(maxlen: 11) == "#?RADIANCE\n" || device->peek(maxlen: 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 | |
511 | QImageIOPlugin::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 | |
530 | QImageIOHandler *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" |
539 | |