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 | namespace // 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 |
38 | static 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 | |
78 | template<class float_T> |
79 | void 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 | |
101 | QImage::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. |
111 | static 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 | |
214 | static QSize (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 | |
268 | bool 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 | |
290 | bool 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 | |
301 | QVariant 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 | |
324 | HDRHandler::HDRHandler() |
325 | { |
326 | } |
327 | |
328 | bool HDRHandler::canRead() const |
329 | { |
330 | if (canRead(device: device())) { |
331 | setFormat("hdr" ); |
332 | return true; |
333 | } |
334 | return false; |
335 | } |
336 | |
337 | bool 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 | |
360 | QImageIOPlugin::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 | |
379 | QImageIOHandler *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 | |