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

KDE's Doxygen guidelines are available online.