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
30namespace // Private.
31{
32#define MAXLINE 1024
33#define MINELEN 8 // minimum scanline length for encoding
34#define MAXELEN 0x7fff // maximum scanline length for encoding
35
36// read an old style line from the hdr image file
37// if 'first' is true the first byte is already read
38static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
39{
40 int rshift = 0;
41 int i;
42
43 uchar *start = image;
44 while (width > 0) {
45 s >> image[0];
46 s >> image[1];
47 s >> image[2];
48 s >> image[3];
49
50 if (s.atEnd()) {
51 return false;
52 }
53
54 if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
55 // NOTE: we don't have an image sample that cover this code
56 if (rshift > 31) {
57 return false;
58 }
59 for (i = image[3] << rshift; i > 0 && width > 0; i--) {
60 if (image == start) {
61 return false; // you cannot be here at the first run
62 }
63 // memcpy(image, image-4, 4);
64 (uint &)image[0] = (uint &)image[0 - 4];
65 image += 4;
66 width--;
67 }
68 rshift += 8;
69 } else {
70 image += 4;
71 width--;
72 rshift = 0;
73 }
74 }
75 return true;
76}
77
78template<class float_T>
79void RGBE_To_QRgbLine(uchar *image, float_T *scanline, int width)
80{
81 for (int j = 0; j < width; j++) {
82 // v = ldexp(1.0, int(image[3]) - 128);
83 float v;
84 int e = qBound(min: -31, val: int(image[3]) - 128, max: 31);
85 if (e > 0) {
86 v = float(1 << e);
87 } else {
88 v = 1.0f / float(1 << -e);
89 }
90
91 auto j4 = j * 4;
92 auto vn = v / 255.0f;
93 scanline[j4] = float_T(std::min(a: float(image[0]) * vn, b: 1.0f));
94 scanline[j4 + 1] = float_T(std::min(a: float(image[1]) * vn, b: 1.0f));
95 scanline[j4 + 2] = float_T(std::min(a: float(image[2]) * vn, b: 1.0f));
96 scanline[j4 + 3] = float_T(1.0f);
97 image += 4;
98 }
99}
100
101QImage::Format imageFormat()
102{
103#ifdef HDR_HALF_QUALITY
104 return QImage::Format_RGBX16FPx4;
105#else
106 return QImage::Format_RGBX32FPx4;
107#endif
108}
109
110// Load the HDR image.
111static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
112{
113 uchar val;
114 uchar code;
115
116 // Create dst image.
117 img = imageAlloc(width, height, format: imageFormat());
118 if (img.isNull()) {
119 qCDebug(HDRPLUGIN) << "Couldn't create image with size" << width << height << "and format RGB32";
120 return false;
121 }
122
123 QByteArray lineArray;
124 lineArray.resize(size: 4 * width);
125 uchar *image = reinterpret_cast<uchar *>(lineArray.data());
126
127 for (int cline = 0; cline < height; cline++) {
128#ifdef HDR_HALF_QUALITY
129 auto scanline = reinterpret_cast<qfloat16 *>(img.scanLine(cline));
130#else
131 auto scanline = reinterpret_cast<float *>(img.scanLine(cline));
132#endif
133
134 // determine scanline type
135 if ((width < MINELEN) || (MAXELEN < width)) {
136 Read_Old_Line(image, width, s);
137 RGBE_To_QRgbLine(image, scanline, width);
138 continue;
139 }
140
141 s >> val;
142
143 if (s.atEnd()) {
144 return true;
145 }
146
147 if (val != 2) {
148 s.device()->ungetChar(c: val);
149 Read_Old_Line(image, width, s);
150 RGBE_To_QRgbLine(image, scanline, width);
151 continue;
152 }
153
154 s >> image[1];
155 s >> image[2];
156 s >> image[3];
157
158 if (s.atEnd()) {
159 return true;
160 }
161
162 if ((image[1] != 2) || (image[2] & 128)) {
163 image[0] = 2;
164 Read_Old_Line(image: image + 4, width: width - 1, s);
165 RGBE_To_QRgbLine(image, scanline, width);
166 continue;
167 }
168
169 if ((image[2] << 8 | image[3]) != width) {
170 qCDebug(HDRPLUGIN) << "Line of pixels had width" << (image[2] << 8 | image[3]) << "instead of" << width;
171 return false;
172 }
173
174 // read each component
175 for (int i = 0, len = int(lineArray.size()); i < 4; i++) {
176 for (int j = 0; j < width;) {
177 s >> code;
178 if (s.atEnd()) {
179 qCDebug(HDRPLUGIN) << "Truncated HDR file";
180 return false;
181 }
182 if (code > 128) {
183 // run
184 code &= 127;
185 s >> val;
186 while (code != 0) {
187 auto idx = i + j * 4;
188 if (idx < len) {
189 image[idx] = val;
190 }
191 j++;
192 code--;
193 }
194 } else {
195 // non-run
196 while (code != 0) {
197 auto idx = i + j * 4;
198 if (idx < len) {
199 s >> image[idx];
200 }
201 j++;
202 code--;
203 }
204 }
205 }
206 }
207
208 RGBE_To_QRgbLine(image, scanline, width);
209 }
210
211 return true;
212}
213
214static QSize readHeaderSize(QIODevice *device)
215{
216 int len;
217 QByteArray line(MAXLINE + 1, Qt::Uninitialized);
218 QByteArray format;
219
220 // Parse header
221 do {
222 len = device->readLine(data: line.data(), MAXLINE);
223
224 if (line.startsWith(bv: "FORMAT=")) {
225 format = line.mid(index: 7, len: len - 7 - 1 /*\n*/);
226 }
227
228 } while ((len > 0) && (line[0] != '\n'));
229
230 if (format != "32-bit_rle_rgbe") {
231 qCDebug(HDRPLUGIN) << "Unknown HDR format:" << format;
232 return QSize();
233 }
234
235 len = device->readLine(data: line.data(), MAXLINE);
236 line.resize(size: len);
237
238 /*
239 TODO: handle flipping and rotation, as per the spec below
240 The single resolution line consists of 4 values, a X and Y label each followed by a numerical
241 integer value. The X and Y are immediately preceded by a sign which can be used to indicate
242 flipping, the order of the X and Y indicate rotation. The standard coordinate system for
243 Radiance images would have the following resolution string -Y N +X N. This indicates that the
244 vertical axis runs down the file and the X axis is to the right (imagining the image as a
245 rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
246 indicate a vertical flip. If the X value appears before the Y value then that indicates that
247 the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
248 The reader can convince themselves that the 8 combinations cover all the possible image orientations
249 and rotations.
250 */
251 QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY]) ([0-9]+) ([+\\-][XY]) ([0-9]+)\n"));
252 QRegularExpressionMatch match = resolutionRegExp.match(subject: QString::fromLatin1(ba: line));
253 if (!match.hasMatch()) {
254 qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
255 return QSize();
256 }
257
258 if ((match.captured(nth: 1).at(i: 1) != u'Y') || (match.captured(nth: 3).at(i: 1) != u'X')) {
259 qCDebug(HDRPLUGIN) << "Unsupported image orientation in HDR file.";
260 return QSize();
261 }
262
263 return QSize(match.captured(nth: 4).toInt(), match.captured(nth: 2).toInt());
264}
265
266} // namespace
267
268bool HDRHandler::read(QImage *outImage)
269{
270 QDataStream s(device());
271
272 QSize size = readHeaderSize(device: s.device());
273 if (!size.isValid()) {
274 return false;
275 }
276
277 QImage img;
278 if (!LoadHDR(s, width: size.width(), height: size.height(), img)) {
279 // qDebug() << "Error loading HDR file.";
280 return false;
281 }
282 // The images read by Gimp and Photoshop (including those of the tests) are interpreted with linear color space.
283 // By setting the linear color space, programs that support profiles display HDR files as in GIMP and Photoshop.
284 img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
285
286 *outImage = img;
287 return true;
288}
289
290bool HDRHandler::supportsOption(ImageOption option) const
291{
292 if (option == QImageIOHandler::Size) {
293 return true;
294 }
295 if (option == QImageIOHandler::ImageFormat) {
296 return true;
297 }
298 return false;
299}
300
301QVariant HDRHandler::option(ImageOption option) const
302{
303 QVariant v;
304
305 if (option == QImageIOHandler::Size) {
306 if (auto d = device()) {
307 // transactions works on both random and sequential devices
308 d->startTransaction();
309 auto size = readHeaderSize(device: d);
310 d->rollbackTransaction();
311 if (size.isValid()) {
312 v = QVariant::fromValue(value: size);
313 }
314 }
315 }
316
317 if (option == QImageIOHandler::ImageFormat) {
318 v = QVariant::fromValue(value: imageFormat());
319 }
320
321 return v;
322}
323
324HDRHandler::HDRHandler()
325{
326}
327
328bool HDRHandler::canRead() const
329{
330 if (canRead(device: device())) {
331 setFormat("hdr");
332 return true;
333 }
334 return false;
335}
336
337bool HDRHandler::canRead(QIODevice *device)
338{
339 if (!device) {
340 qWarning(msg: "HDRHandler::canRead() called with no device");
341 return false;
342 }
343
344 // the .pic taken from official test cases does not start with this string but can be loaded.
345 if(device->peek(maxlen: 11) == "#?RADIANCE\n" || device->peek(maxlen: 7) == "#?RGBE\n") {
346 return true;
347 }
348
349 // allow to load offical test cases: https://radsite.lbl.gov/radiance/framed.html
350 device->startTransaction();
351 QSize size = readHeaderSize(device);
352 device->rollbackTransaction();
353 if (size.isValid()) {
354 return true;
355 }
356
357 return false;
358}
359
360QImageIOPlugin::Capabilities HDRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
361{
362 if (format == "hdr") {
363 return Capabilities(CanRead);
364 }
365 if (!format.isEmpty()) {
366 return {};
367 }
368 if (!device->isOpen()) {
369 return {};
370 }
371
372 Capabilities cap;
373 if (device->isReadable() && HDRHandler::canRead(device)) {
374 cap |= CanRead;
375 }
376 return cap;
377}
378
379QImageIOHandler *HDRPlugin::create(QIODevice *device, const QByteArray &format) const
380{
381 QImageIOHandler *handler = new HDRHandler;
382 handler->setDevice(device);
383 handler->setFormat(format);
384 return handler;
385}
386
387#include "moc_hdr_p.cpp"
388

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