1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2024 Mirco Miranda <mircomir@outlook.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | /* |
9 | * See also: https://www.pauldebevec.com/Research/HDR/PFM/ |
10 | */ |
11 | |
12 | #include "pfm_p.h" |
13 | #include "util_p.h" |
14 | |
15 | #include <QColorSpace> |
16 | #include <QDataStream> |
17 | #include <QFloat16> |
18 | #include <QIODevice> |
19 | #include <QImage> |
20 | #include <QLoggingCategory> |
21 | |
22 | Q_DECLARE_LOGGING_CATEGORY(LOG_PFMPLUGIN) |
23 | Q_LOGGING_CATEGORY(LOG_PFMPLUGIN, "kf.imageformats.plugins.pfm" , QtWarningMsg) |
24 | |
25 | class |
26 | { |
27 | private: |
28 | /*! |
29 | * \brief m_bw True if grayscale. |
30 | */ |
31 | bool ; |
32 | |
33 | /*! |
34 | * \brief m_half True if half float. |
35 | */ |
36 | bool ; |
37 | |
38 | /*! |
39 | * \brief m_ps True if saved by Photoshop (Photoshop variant). |
40 | * |
41 | * When \a false the format of the header is the following (GIMP): |
42 | * [type] |
43 | * [xres] [yres] |
44 | * [byte_order] |
45 | * |
46 | * When \a true the format of the header is the following (Photoshop): |
47 | * [type] |
48 | * [xres] |
49 | * [yres] |
50 | * [byte_order] |
51 | */ |
52 | bool ; |
53 | |
54 | /*! |
55 | * \brief m_width The image width. |
56 | */ |
57 | qint32 ; |
58 | |
59 | /*! |
60 | * \brief m_height The image height. |
61 | */ |
62 | qint32 ; |
63 | |
64 | /*! |
65 | * \brief m_byteOrder The image byte orger. |
66 | */ |
67 | QDataStream::ByteOrder ; |
68 | |
69 | public: |
70 | () |
71 | : m_bw(false) |
72 | , m_half(false) |
73 | , m_ps(false) |
74 | , m_width(0) |
75 | , m_height(0) |
76 | , m_byteOrder(QDataStream::BigEndian) |
77 | { |
78 | |
79 | } |
80 | |
81 | bool () const |
82 | { |
83 | return (m_width > 0 && m_height > 0); |
84 | } |
85 | |
86 | bool isBlackAndWhite() const |
87 | { |
88 | return m_bw; |
89 | } |
90 | |
91 | bool () const |
92 | { |
93 | return m_half; |
94 | } |
95 | |
96 | bool () const |
97 | { |
98 | return m_ps; |
99 | } |
100 | |
101 | qint32 () const |
102 | { |
103 | return m_width; |
104 | } |
105 | |
106 | qint32 () const |
107 | { |
108 | return m_height; |
109 | } |
110 | |
111 | QSize () const |
112 | { |
113 | return QSize(m_width, m_height); |
114 | } |
115 | |
116 | QDataStream::ByteOrder () const |
117 | { |
118 | return m_byteOrder; |
119 | } |
120 | |
121 | QImage::Format () const |
122 | { |
123 | if (isValid()) { |
124 | return m_half ? QImage::Format_RGBX16FPx4 : QImage::Format_RGBX32FPx4; |
125 | } |
126 | return QImage::Format_Invalid; |
127 | } |
128 | |
129 | bool (QIODevice *d) |
130 | { |
131 | auto pf = d->read(maxlen: 3); |
132 | if (pf == QByteArray("PF\n" )) { |
133 | m_half = false; |
134 | m_bw = false; |
135 | } else if (pf == QByteArray("Pf\n" )) { |
136 | m_half = false; |
137 | m_bw = true; |
138 | } else if (pf == QByteArray("PH\n" )) { |
139 | m_half = true; |
140 | m_bw = false; |
141 | } else if (pf == QByteArray("Ph\n" )) { |
142 | m_half = true; |
143 | m_bw = true; |
144 | } else { |
145 | return false; |
146 | } |
147 | QString wh; |
148 | do { // read line and skip comments |
149 | wh = QString::fromLatin1(ba: d->readLine(maxlen: 128)); |
150 | } while (wh.startsWith(QStringLiteral("#" ))); |
151 | auto list = wh.split(QStringLiteral(" " )); |
152 | if (list.size() == 1) { |
153 | m_ps = true; // try for Photoshop version |
154 | list << QString::fromLatin1(ba: d->readLine(maxlen: 128)); |
155 | } |
156 | if (list.size() != 2) { |
157 | return false; |
158 | } |
159 | auto ok_o = false; |
160 | auto ok_w = false; |
161 | auto ok_h = false; |
162 | auto o = QString::fromLatin1(ba: d->readLine(maxlen: 128)).toDouble(ok: &ok_o); |
163 | auto w = list.first().toInt(ok: &ok_w); |
164 | auto h = list.last().toInt(ok: &ok_h); |
165 | if (!ok_o || !ok_w || !ok_h || o == 0) { |
166 | return false; |
167 | } |
168 | m_width = w; |
169 | m_height = h; |
170 | m_byteOrder = o > 0 ? QDataStream::BigEndian : QDataStream::LittleEndian; |
171 | return isValid(); |
172 | } |
173 | |
174 | bool (QIODevice *d) |
175 | { |
176 | d->startTransaction(); |
177 | auto ok = read(d); |
178 | d->rollbackTransaction(); |
179 | return ok; |
180 | } |
181 | }; |
182 | |
183 | class PFMHandlerPrivate |
184 | { |
185 | public: |
186 | PFMHandlerPrivate() {} |
187 | ~PFMHandlerPrivate() {} |
188 | |
189 | PFMHeader m_header; |
190 | }; |
191 | |
192 | PFMHandler::PFMHandler() |
193 | : QImageIOHandler() |
194 | , d(new PFMHandlerPrivate) |
195 | { |
196 | } |
197 | |
198 | bool PFMHandler::canRead() const |
199 | { |
200 | if (canRead(device: device())) { |
201 | setFormat("pfm" ); |
202 | return true; |
203 | } |
204 | return false; |
205 | } |
206 | |
207 | bool PFMHandler::canRead(QIODevice *device) |
208 | { |
209 | if (!device) { |
210 | qCWarning(LOG_PFMPLUGIN) << "PFMHandler::canRead() called with no device" ; |
211 | return false; |
212 | } |
213 | |
214 | PFMHeader h; |
215 | if (!h.peek(d: device)) { |
216 | return false; |
217 | } |
218 | |
219 | return h.isValid(); |
220 | } |
221 | |
222 | template<class T> |
223 | bool (qint32 y, QDataStream &s, QImage &img, const PFMHeader &) |
224 | { |
225 | auto bw = header.isBlackAndWhite(); |
226 | auto line = reinterpret_cast<T *>(img.scanLine(header.isPhotoshop() ? y : img.height() - y - 1)); |
227 | for (auto x = 0, n = img.width() * 4; x < n; x += 4) { |
228 | line[x + 3] = T(1); |
229 | s >> line[x]; |
230 | if (bw) { |
231 | line[x + 1] = line[x]; |
232 | line[x + 2] = line[x]; |
233 | } else { |
234 | s >> line[x + 1]; |
235 | s >> line[x + 2]; |
236 | } |
237 | if (s.status() != QDataStream::Ok) { |
238 | return false; |
239 | } |
240 | } |
241 | return true; |
242 | } |
243 | |
244 | bool PFMHandler::read(QImage *image) |
245 | { |
246 | auto&& = d->m_header; |
247 | if (!header.read(d: device())) { |
248 | qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() invalid header" ; |
249 | return false; |
250 | } |
251 | |
252 | QDataStream s(device()); |
253 | s.setFloatingPointPrecision(QDataStream::SinglePrecision); |
254 | s.setByteOrder(header.byteOrder()); |
255 | |
256 | auto img = imageAlloc(size: header.size(), format: header.format()); |
257 | if (img.isNull()) { |
258 | qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() error while allocating the image" ; |
259 | return false; |
260 | } |
261 | |
262 | for (auto y = 0, h = img.height(); y < h; ++y) { |
263 | auto ok = false; |
264 | if (header.isHalfFloat()) { |
265 | ok = readScanLine<qfloat16>(y, s, img, header); |
266 | } else { |
267 | ok = readScanLine<float>(y, s, img, header); |
268 | } |
269 | if (!ok) { |
270 | qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data" ; |
271 | return false; |
272 | } |
273 | } |
274 | |
275 | img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); |
276 | |
277 | *image = img; |
278 | return true; |
279 | } |
280 | |
281 | bool PFMHandler::supportsOption(ImageOption option) const |
282 | { |
283 | if (option == QImageIOHandler::Size) { |
284 | return true; |
285 | } |
286 | if (option == QImageIOHandler::ImageFormat) { |
287 | return true; |
288 | } |
289 | if (option == QImageIOHandler::Endianness) { |
290 | return true; |
291 | } |
292 | return false; |
293 | } |
294 | |
295 | QVariant PFMHandler::option(ImageOption option) const |
296 | { |
297 | QVariant v; |
298 | |
299 | if (option == QImageIOHandler::Size) { |
300 | auto&& h = d->m_header; |
301 | if (h.isValid()) { |
302 | v = QVariant::fromValue(value: h.size()); |
303 | } else if (auto dev = device()) { |
304 | if (h.peek(d: dev)) { |
305 | v = QVariant::fromValue(value: h.size()); |
306 | } |
307 | } |
308 | } |
309 | |
310 | if (option == QImageIOHandler::ImageFormat) { |
311 | auto&& h = d->m_header; |
312 | if (h.isValid()) { |
313 | v = QVariant::fromValue(value: h.format()); |
314 | } else if (auto dev = device()) { |
315 | if (h.peek(d: dev)) { |
316 | v = QVariant::fromValue(value: h.format()); |
317 | } |
318 | } |
319 | } |
320 | |
321 | if (option == QImageIOHandler::Endianness) { |
322 | auto&& h = d->m_header; |
323 | if (h.isValid()) { |
324 | v = QVariant::fromValue(value: h.byteOrder()); |
325 | } else if (auto dev = device()) { |
326 | if (h.peek(d: dev)) { |
327 | v = QVariant::fromValue(value: h.byteOrder()); |
328 | } |
329 | } |
330 | } |
331 | |
332 | return v; |
333 | } |
334 | |
335 | QImageIOPlugin::Capabilities PFMPlugin::capabilities(QIODevice *device, const QByteArray &format) const |
336 | { |
337 | if (format == "pfm" || format == "phm" ) { |
338 | return Capabilities(CanRead); |
339 | } |
340 | if (!format.isEmpty()) { |
341 | return {}; |
342 | } |
343 | if (!device->isOpen()) { |
344 | return {}; |
345 | } |
346 | |
347 | Capabilities cap; |
348 | if (device->isReadable() && PFMHandler::canRead(device)) { |
349 | cap |= CanRead; |
350 | } |
351 | return cap; |
352 | } |
353 | |
354 | QImageIOHandler *PFMPlugin::create(QIODevice *device, const QByteArray &format) const |
355 | { |
356 | QImageIOHandler *handler = new PFMHandler; |
357 | handler->setDevice(device); |
358 | handler->setFormat(format); |
359 | return handler; |
360 | } |
361 | |
362 | #include "moc_pfm_p.cpp" |
363 | |