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 | |
18 | namespace // 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 14 |
31 | #define QOI_END_STREAM_PAD 8 |
32 | |
33 | struct { |
34 | () |
35 | : MagicNumber(0) |
36 | , Width(0) |
37 | , Height(0) |
38 | , Channels(0) |
39 | , Colorspace(2) |
40 | { |
41 | } |
42 | |
43 | (const QoiHeader&) = default; |
44 | QoiHeader& (const QoiHeader&) = default; |
45 | |
46 | quint32 ; |
47 | quint32 ; |
48 | quint32 ; |
49 | quint8 ; |
50 | quint8 ; |
51 | }; |
52 | |
53 | struct 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 | |
64 | static QDataStream &(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 | |
74 | static QDataStream &(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 | |
84 | static bool (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 | |
101 | static int QoiHash(const Px &px) |
102 | { |
103 | return px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11; |
104 | } |
105 | |
106 | static QImage::Format (const QoiHeader &head) |
107 | { |
108 | if (IsSupported(head)) { |
109 | return (head.Channels == 3 ? QImage::Format_RGB32 : QImage::Format_ARGB32); |
110 | } |
111 | return QImage::Format_Invalid; |
112 | } |
113 | |
114 | static bool (QIODevice *device, const QoiHeader &qoi, QImage &img) |
115 | { |
116 | Px index[64] = {Px{.r: 0, .g: 0, .b: 0, .a: 0}}; |
117 | Px px = Px{.r: 0, .g: 0, .b: 0, .a: 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(a: quint64(1024), b: quint64(qoi.Width) * qoi.Channels * 3 / 2); |
125 | if (px_len > kMaxQVectorSize) { |
126 | return false; |
127 | } |
128 | |
129 | // Allocate image |
130 | img = imageAlloc(width: qoi.Width, height: qoi.Height, format: imageFormat(head: 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) { |
138 | img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); |
139 | } else { |
140 | img.setColorSpace(QColorSpace(QColorSpace::SRgb)); |
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(a: device->read(maxlen: 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(r: px.r, g: px.g, b: px.b, a: px.a); |
192 | } |
193 | |
194 | if (p) { |
195 | ba.remove(index: 0, len: 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(bv: QByteArray::fromRawData(data: "\x00\x00\x00\x00\x00\x00\x00\x01" , size: 8))); |
203 | } |
204 | |
205 | static bool (QIODevice *device, const QoiHeader &qoi, const QImage &img) |
206 | { |
207 | Px index[64] = {Px{.r: 0, .g: 0, .b: 0, .a: 0}}; |
208 | Px px = Px{.r: 0, .g: 0, .b: 0, .a: 255}; |
209 | Px px_prev = px; |
210 | |
211 | auto run = 0; |
212 | auto channels = qoi.Channels; |
213 | |
214 | QByteArray ba; |
215 | ba.reserve(asize: 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(image: 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(c: (vg_r + 8) << 4 | (vg_b + 8)); |
269 | } else { |
270 | ba.append(c: char(QOI_OP_RGB)); |
271 | ba.append(c: px.r); |
272 | ba.append(c: px.g); |
273 | ba.append(c: px.b); |
274 | } |
275 | } else { |
276 | ba.append(c: char(QOI_OP_RGBA)); |
277 | ba.append(c: px.r); |
278 | ba.append(c: px.g); |
279 | ba.append(c: px.b); |
280 | ba.append(c: px.a); |
281 | } |
282 | } |
283 | } |
284 | px_prev = px; |
285 | } |
286 | |
287 | auto written = device->write(data: ba); |
288 | if (written < 0) { |
289 | return false; |
290 | } |
291 | if (written) { |
292 | ba.remove(index: 0, len: written); |
293 | } |
294 | } |
295 | |
296 | // QOI end of stream |
297 | ba.append(a: QByteArray::fromRawData(data: "\x00\x00\x00\x00\x00\x00\x00\x01" , size: 8)); |
298 | |
299 | // write remaining data |
300 | for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) { |
301 | w = device->write(data: ba.constData() + write, len: size - write); |
302 | if (w < 0) { |
303 | return false; |
304 | } |
305 | } |
306 | |
307 | return true; |
308 | } |
309 | |
310 | } // namespace |
311 | |
312 | class QOIHandlerPrivate |
313 | { |
314 | public: |
315 | QOIHandlerPrivate() {} |
316 | ~QOIHandlerPrivate() {} |
317 | |
318 | QoiHeader m_header; |
319 | }; |
320 | |
321 | |
322 | QOIHandler::QOIHandler() |
323 | : QImageIOHandler() |
324 | , d(new QOIHandlerPrivate) |
325 | { |
326 | } |
327 | |
328 | bool QOIHandler::canRead() const |
329 | { |
330 | if (canRead(device: device())) { |
331 | setFormat("qoi" ); |
332 | return true; |
333 | } |
334 | return false; |
335 | } |
336 | |
337 | bool QOIHandler::canRead(QIODevice *device) |
338 | { |
339 | if (!device) { |
340 | qWarning(msg: "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(head: qoi); |
355 | } |
356 | |
357 | bool 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(head: qoi)) { |
368 | return false; |
369 | } |
370 | |
371 | QImage img; |
372 | bool result = LoadQOI(device: s.device(), qoi, img); |
373 | |
374 | if (result == false) { |
375 | return false; |
376 | } |
377 | |
378 | *image = img; |
379 | return true; |
380 | } |
381 | |
382 | bool 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(head: 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(device: s.device(), qoi, img: image); |
407 | } |
408 | |
409 | bool 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 | |
420 | QVariant QOIHandler::option(ImageOption option) const |
421 | { |
422 | QVariant v; |
423 | |
424 | if (option == QImageIOHandler::Size) { |
425 | auto&& = d->m_header; |
426 | if (IsSupported(head: header)) { |
427 | v = QVariant::fromValue(value: QSize(header.Width, header.Height)); |
428 | } else if (auto d = device()) { |
429 | QDataStream s(d->peek(maxlen: sizeof(QoiHeader))); |
430 | s.setByteOrder(QDataStream::BigEndian); |
431 | s >> header; |
432 | if (s.status() == QDataStream::Ok && IsSupported(head: header)) { |
433 | v = QVariant::fromValue(value: QSize(header.Width, header.Height)); |
434 | } |
435 | } |
436 | } |
437 | |
438 | if (option == QImageIOHandler::ImageFormat) { |
439 | auto&& = d->m_header; |
440 | if (IsSupported(head: header)) { |
441 | v = QVariant::fromValue(value: imageFormat(head: header)); |
442 | } else if (auto d = device()) { |
443 | QDataStream s(d->peek(maxlen: sizeof(QoiHeader))); |
444 | s.setByteOrder(QDataStream::BigEndian); |
445 | s >> header; |
446 | if (s.status() == QDataStream::Ok && IsSupported(head: header)) { |
447 | v = QVariant::fromValue(value: imageFormat(head: header)); |
448 | } |
449 | } |
450 | } |
451 | |
452 | return v; |
453 | } |
454 | |
455 | QImageIOPlugin::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 | |
477 | QImageIOHandler *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" |
486 | |