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

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