KImageFormats

qoi.cpp
1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2023 Ernest Gupik <ernestgupik@wp.pl>
4 SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "qoi_p.h"
10#include "scanlineconverter_p.h"
11#include "util_p.h"
12
13#include <QColorSpace>
14#include <QFile>
15#include <QIODevice>
16#include <QImage>
17
18namespace // Private
19{
20
21#define QOI_OP_INDEX 0x00 /* 00xxxxxx */
22#define QOI_OP_DIFF 0x40 /* 01xxxxxx */
23#define QOI_OP_LUMA 0x80 /* 10xxxxxx */
24#define QOI_OP_RUN 0xc0 /* 11xxxxxx */
25#define QOI_OP_RGB 0xfe /* 11111110 */
26#define QOI_OP_RGBA 0xff /* 11111111 */
27#define QOI_MASK_2 0xc0 /* 11000000 */
28
29#define QOI_MAGIC (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | ((unsigned int)'i') << 8 | ((unsigned int)'f'))
30#define QOI_HEADER_SIZE 14
31#define QOI_END_STREAM_PAD 8
32
33struct QoiHeader {
34 QoiHeader()
35 : MagicNumber(0)
36 , Width(0)
37 , Height(0)
38 , Channels(0)
39 , Colorspace(2)
40 {
41 }
42
43 QoiHeader(const QoiHeader&) = default;
44 QoiHeader& operator=(const QoiHeader&) = default;
45
46 quint32 MagicNumber;
47 quint32 Width;
48 quint32 Height;
49 quint8 Channels;
50 quint8 Colorspace;
51};
52
53struct Px {
54 bool operator==(const Px &other) const
55 {
56 return r == other.r && g == other.g && b == other.b && a == other.a;
57 }
58 quint8 r;
59 quint8 g;
60 quint8 b;
61 quint8 a;
62};
63
64static QDataStream &operator>>(QDataStream &s, QoiHeader &head)
65{
66 s >> head.MagicNumber;
67 s >> head.Width;
68 s >> head.Height;
69 s >> head.Channels;
70 s >> head.Colorspace;
71 return s;
72}
73
74static QDataStream &operator<<(QDataStream &s, const QoiHeader &head)
75{
76 s << head.MagicNumber;
77 s << head.Width;
78 s << head.Height;
79 s << head.Channels;
80 s << head.Colorspace;
81 return s;
82}
83
84static bool IsSupported(const QoiHeader &head)
85{
86 // Check magic number
87 if (head.MagicNumber != QOI_MAGIC) {
88 return false;
89 }
90 // Check if the header is a valid QOI header
91 if (head.Width == 0 || head.Height == 0 || head.Channels < 3 || head.Colorspace > 1) {
92 return false;
93 }
94 // Set a reasonable upper limit
95 if (head.Width > 300000 || head.Height > 300000) {
96 return false;
97 }
98 return true;
99}
100
101static int QoiHash(const Px &px)
102{
103 return px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11;
104}
105
106static QImage::Format imageFormat(const QoiHeader &head)
107{
108 if (IsSupported(head)) {
109 return (head.Channels == 3 ? QImage::Format_RGB32 : QImage::Format_ARGB32);
110 }
112}
113
114static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img)
115{
116 Px index[64] = {Px{0, 0, 0, 0}};
117 Px px = Px{0, 0, 0, 255};
118
119 // The px_len should be enough to read a complete "compressed" row: an uncompressible row can become
120 // larger than the row itself. It should never be more than 1/3 (RGB) or 1/4 (RGBA) the length of the
121 // row itself (see test bnm_rgb*.qoi) so I set the extra data to 1/2.
122 // The minimum value is to ensure that enough bytes are read when the image is very small (e.g. 1x1px):
123 // it can be set as large as you like.
124 quint64 px_len = std::max(quint64(1024), quint64(qoi.Width) * qoi.Channels * 3 / 2);
125 if (px_len > kMaxQVectorSize) {
126 return false;
127 }
128
129 // Allocate image
130 img = imageAlloc(qoi.Width, qoi.Height, imageFormat(qoi));
131 if (img.isNull()) {
132 return false;
133 }
134
135 // Set the image colorspace based on the qoi.Colorspace value
136 // As per specification: 0 = sRGB with linear alpha, 1 = all channels linear
137 if (qoi.Colorspace) {
139 } else {
141 }
142
143 // Handle the byte stream
144 QByteArray ba;
145 for (quint32 y = 0, run = 0; y < qoi.Height; ++y) {
146 if (quint64(ba.size()) < px_len) {
147 ba.append(device->read(px_len));
148 }
149
150 if (ba.size() < QOI_END_STREAM_PAD) {
151 return false;
152 }
153
154 quint64 chunks_len = ba.size() - QOI_END_STREAM_PAD;
155 quint64 p = 0;
156 QRgb *scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
157 const quint8 *input = reinterpret_cast<const quint8 *>(ba.constData());
158 for (quint32 x = 0; x < qoi.Width; ++x) {
159 if (run > 0) {
160 run--;
161 } else if (p < chunks_len) {
162 quint32 b1 = input[p++];
163
164 if (b1 == QOI_OP_RGB) {
165 px.r = input[p++];
166 px.g = input[p++];
167 px.b = input[p++];
168 } else if (b1 == QOI_OP_RGBA) {
169 px.r = input[p++];
170 px.g = input[p++];
171 px.b = input[p++];
172 px.a = input[p++];
173 } else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) {
174 px = index[b1];
175 } else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) {
176 px.r += ((b1 >> 4) & 0x03) - 2;
177 px.g += ((b1 >> 2) & 0x03) - 2;
178 px.b += (b1 & 0x03) - 2;
179 } else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) {
180 quint32 b2 = input[p++];
181 quint32 vg = (b1 & 0x3f) - 32;
182 px.r += vg - 8 + ((b2 >> 4) & 0x0f);
183 px.g += vg;
184 px.b += vg - 8 + (b2 & 0x0f);
185 } else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) {
186 run = (b1 & 0x3f);
187 }
188 index[QoiHash(px) & 0x3F] = px;
189 }
190 // Set the values for the pixel at (x, y)
191 scanline[x] = qRgba(px.r, px.g, px.b, px.a);
192 }
193
194 if (p) {
195 ba.remove(0, p);
196 }
197 }
198
199 // From specs the byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte.
200 // NOTE: Instead of using "ba == QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)"
201 // we preferred a generic check that allows data to exist after the end of the file.
202 return (ba.startsWith(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)));
203}
204
205static bool SaveQOI(QIODevice *device, const QoiHeader &qoi, const QImage &img)
206{
207 Px index[64] = {Px{0, 0, 0, 0}};
208 Px px = Px{0, 0, 0, 255};
209 Px px_prev = px;
210
211 auto run = 0;
212 auto channels = qoi.Channels;
213
214 QByteArray ba;
215 ba.reserve(img.width() * channels * 3 / 2);
216
217 ScanLineConverter converter(channels == 3 ? QImage::Format_RGB888 : QImage::Format_RGBA8888);
218 converter.setTargetColorSpace(QColorSpace(qoi.Colorspace == 1 ? QColorSpace::SRgbLinear : QColorSpace::SRgb));
219
220 for (auto h = img.height(), y = 0; y < h; ++y) {
221 auto pixels = converter.convertedScanLine(img, y);
222 if (pixels == nullptr) {
223 return false;
224 }
225
226 for (auto w = img.width() * channels, px_pos = 0; px_pos < w; px_pos += channels) {
227 px.r = pixels[px_pos + 0];
228 px.g = pixels[px_pos + 1];
229 px.b = pixels[px_pos + 2];
230
231 if (channels == 4) {
232 px.a = pixels[px_pos + 3];
233 }
234
235 if (px == px_prev) {
236 run++;
237 if (run == 62 || (px_pos == w - channels && y == h - 1)) {
238 ba.append(QOI_OP_RUN | (run - 1));
239 run = 0;
240 }
241 } else {
242 int index_pos;
243
244 if (run > 0) {
245 ba.append(QOI_OP_RUN | (run - 1));
246 run = 0;
247 }
248
249 index_pos = QoiHash(px) & 0x3F;
250
251 if (index[index_pos] == px) {
252 ba.append(QOI_OP_INDEX | index_pos);
253 } else {
254 index[index_pos] = px;
255
256 if (px.a == px_prev.a) {
257 signed char vr = px.r - px_prev.r;
258 signed char vg = px.g - px_prev.g;
259 signed char vb = px.b - px_prev.b;
260
261 signed char vg_r = vr - vg;
262 signed char vg_b = vb - vg;
263
264 if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) {
265 ba.append(QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2));
266 } else if (vg_r > -9 && vg_r < 8 && vg > -33 && vg < 32 && vg_b > -9 && vg_b < 8) {
267 ba.append(QOI_OP_LUMA | (vg + 32));
268 ba.append((vg_r + 8) << 4 | (vg_b + 8));
269 } else {
270 ba.append(char(QOI_OP_RGB));
271 ba.append(px.r);
272 ba.append(px.g);
273 ba.append(px.b);
274 }
275 } else {
276 ba.append(char(QOI_OP_RGBA));
277 ba.append(px.r);
278 ba.append(px.g);
279 ba.append(px.b);
280 ba.append(px.a);
281 }
282 }
283 }
284 px_prev = px;
285 }
286
287 auto written = device->write(ba);
288 if (written < 0) {
289 return false;
290 }
291 if (written) {
292 ba.remove(0, written);
293 }
294 }
295
296 // QOI end of stream
297 ba.append(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8));
298
299 // write remaining data
300 for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) {
301 w = device->write(ba.constData() + write, size - write);
302 if (w < 0) {
303 return false;
304 }
305 }
306
307 return true;
308}
309
310} // namespace
311
312class QOIHandlerPrivate
313{
314public:
315 QOIHandlerPrivate() {}
316 ~QOIHandlerPrivate() {}
317
318 QoiHeader m_header;
319};
320
321
322QOIHandler::QOIHandler()
324 , d(new QOIHandlerPrivate)
325{
326}
327
328bool QOIHandler::canRead() const
329{
330 if (canRead(device())) {
331 setFormat("qoi");
332 return true;
333 }
334 return false;
335}
336
337bool QOIHandler::canRead(QIODevice *device)
338{
339 if (!device) {
340 qWarning("QOIHandler::canRead() called with no device");
341 return false;
342 }
343
344 auto head = device->peek(QOI_HEADER_SIZE);
345 if (head.size() < QOI_HEADER_SIZE) {
346 return false;
347 }
348
349 QDataStream stream(head);
350 stream.setByteOrder(QDataStream::BigEndian);
351 QoiHeader qoi;
352 stream >> qoi;
353
354 return IsSupported(qoi);
355}
356
357bool QOIHandler::read(QImage *image)
358{
359 QDataStream s(device());
360 s.setByteOrder(QDataStream::BigEndian);
361
362 // Read image header
363 auto&& qoi = d->m_header;
364 s >> qoi;
365
366 // Check if file is supported
367 if (!IsSupported(qoi)) {
368 return false;
369 }
370
371 QImage img;
372 bool result = LoadQOI(s.device(), qoi, img);
373
374 if (result == false) {
375 return false;
376 }
377
378 *image = img;
379 return true;
380}
381
382bool QOIHandler::write(const QImage &image)
383{
384 if (image.isNull()) {
385 return false;
386 }
387
388 QoiHeader qoi;
389 qoi.MagicNumber = QOI_MAGIC;
390 qoi.Width = image.width();
391 qoi.Height = image.height();
392 qoi.Channels = image.hasAlphaChannel() ? 4 : 3;
393 qoi.Colorspace = image.colorSpace().transferFunction() == QColorSpace::TransferFunction::Linear ? 1 : 0;
394
395 if (!IsSupported(qoi)) {
396 return false;
397 }
398
399 QDataStream s(device());
400 s.setByteOrder(QDataStream::BigEndian);
401 s << qoi;
402 if (s.status() != QDataStream::Ok) {
403 return false;
404 }
405
406 return SaveQOI(s.device(), qoi, image);
407}
408
409bool QOIHandler::supportsOption(ImageOption option) const
410{
411 if (option == QImageIOHandler::Size) {
412 return true;
413 }
414 if (option == QImageIOHandler::ImageFormat) {
415 return true;
416 }
417 return false;
418}
419
420QVariant QOIHandler::option(ImageOption option) const
421{
422 QVariant v;
423
424 if (option == QImageIOHandler::Size) {
425 auto&& header = d->m_header;
426 if (IsSupported(header)) {
427 v = QVariant::fromValue(QSize(header.Width, header.Height));
428 } else if (auto d = device()) {
429 QDataStream s(d->peek(sizeof(QoiHeader)));
430 s.setByteOrder(QDataStream::BigEndian);
431 s >> header;
432 if (s.status() == QDataStream::Ok && IsSupported(header)) {
433 v = QVariant::fromValue(QSize(header.Width, header.Height));
434 }
435 }
436 }
437
438 if (option == QImageIOHandler::ImageFormat) {
439 auto&& header = d->m_header;
440 if (IsSupported(header)) {
441 v = QVariant::fromValue(imageFormat(header));
442 } else if (auto d = device()) {
443 QDataStream s(d->peek(sizeof(QoiHeader)));
444 s.setByteOrder(QDataStream::BigEndian);
445 s >> header;
446 if (s.status() == QDataStream::Ok && IsSupported(header)) {
447 v = QVariant::fromValue(imageFormat(header));
448 }
449 }
450 }
451
452 return v;
453}
454
455QImageIOPlugin::Capabilities QOIPlugin::capabilities(QIODevice *device, const QByteArray &format) const
456{
457 if (format == "qoi" || format == "QOI") {
458 return Capabilities(CanRead | CanWrite);
459 }
460 if (!format.isEmpty()) {
461 return {};
462 }
463 if (!device->isOpen()) {
464 return {};
465 }
466
467 Capabilities cap;
468 if (device->isReadable() && QOIHandler::canRead(device)) {
469 cap |= CanRead;
470 }
471 if (device->isWritable()) {
472 cap |= CanWrite;
473 }
474 return cap;
475}
476
477QImageIOHandler *QOIPlugin::create(QIODevice *device, const QByteArray &format) const
478{
479 QImageIOHandler *handler = new QOIHandler;
480 handler->setDevice(device);
481 handler->setFormat(format);
482 return handler;
483}
484
485#include "moc_qoi_p.cpp"
KCALENDARCORE_EXPORT QDataStream & operator>>(QDataStream &in, const KCalendarCore::Alarm::Ptr &)
QFlags< Capability > Capabilities
bool operator==(const StyleDelim &l, const StyleDelim &r)
QDebug operator<<(QDebug dbg, const PerceptualColor::MultiSpinBoxSection &value)
QByteArray & append(QByteArrayView data)
const char * constData() const const
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
QByteArray & remove(qsizetype pos, qsizetype len)
void reserve(qsizetype size)
qsizetype size() const const
bool startsWith(QByteArrayView bv) const const
TransferFunction transferFunction() const const
QColorSpace colorSpace() const const
bool hasAlphaChannel() const const
int height() const const
bool isNull() const const
uchar * scanLine(int i)
void setColorSpace(const QColorSpace &colorSpace)
int width() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
bool isOpen() const const
bool isReadable() const const
bool isWritable() const const
QByteArray peek(qint64 maxSize)
QByteArray read(qint64 maxSize)
qint64 write(const QByteArray &data)
QFuture< T > run(Function function,...)
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:07:21 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.