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 | quint32 ; |
35 | quint32 ; |
36 | quint32 ; |
37 | quint8 ; |
38 | quint8 ; |
39 | }; |
40 | |
41 | struct 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 | |
52 | static QDataStream &(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 | |
62 | static QDataStream &(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 | |
72 | static bool (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 | |
89 | static int QoiHash(const Px &px) |
90 | { |
91 | return px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11; |
92 | } |
93 | |
94 | static QImage::Format (const QoiHeader &head) |
95 | { |
96 | if (IsSupported(head)) { |
97 | return (head.Channels == 3 ? QImage::Format_RGB32 : QImage::Format_ARGB32); |
98 | } |
99 | return QImage::Format_Invalid; |
100 | } |
101 | |
102 | static bool (QIODevice *device, const QoiHeader &qoi, QImage &img) |
103 | { |
104 | Px index[64] = {Px{.r: 0, .g: 0, .b: 0, .a: 0}}; |
105 | Px px = Px{.r: 0, .g: 0, .b: 0, .a: 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(a: quint64(1024), b: quint64(qoi.Width) * qoi.Channels * 3 / 2); |
113 | if (px_len > kMaxQVectorSize) { |
114 | return false; |
115 | } |
116 | |
117 | // Allocate image |
118 | img = imageAlloc(width: qoi.Width, height: qoi.Height, format: imageFormat(head: 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) { |
126 | img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); |
127 | } else { |
128 | img.setColorSpace(QColorSpace(QColorSpace::SRgb)); |
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(a: device->read(maxlen: 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(r: px.r, g: px.g, b: px.b, a: px.a); |
180 | } |
181 | |
182 | if (p) { |
183 | ba.remove(index: 0, len: 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(bv: QByteArray::fromRawData(data: "\x00\x00\x00\x00\x00\x00\x00\x01" , size: 8))); |
191 | } |
192 | |
193 | static bool (QIODevice *device, const QoiHeader &qoi, const QImage &img) |
194 | { |
195 | Px index[64] = {Px{.r: 0, .g: 0, .b: 0, .a: 0}}; |
196 | Px px = Px{.r: 0, .g: 0, .b: 0, .a: 255}; |
197 | Px px_prev = px; |
198 | |
199 | auto run = 0; |
200 | auto channels = qoi.Channels; |
201 | |
202 | QByteArray ba; |
203 | ba.reserve(asize: 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(image: 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(c: (vg_r + 8) << 4 | (vg_b + 8)); |
257 | } else { |
258 | ba.append(c: char(QOI_OP_RGB)); |
259 | ba.append(c: px.r); |
260 | ba.append(c: px.g); |
261 | ba.append(c: px.b); |
262 | } |
263 | } else { |
264 | ba.append(c: char(QOI_OP_RGBA)); |
265 | ba.append(c: px.r); |
266 | ba.append(c: px.g); |
267 | ba.append(c: px.b); |
268 | ba.append(c: px.a); |
269 | } |
270 | } |
271 | } |
272 | px_prev = px; |
273 | } |
274 | |
275 | auto written = device->write(data: ba); |
276 | if (written < 0) { |
277 | return false; |
278 | } |
279 | if (written) { |
280 | ba.remove(index: 0, len: written); |
281 | } |
282 | } |
283 | |
284 | // QOI end of stream |
285 | ba.append(a: QByteArray::fromRawData(data: "\x00\x00\x00\x00\x00\x00\x00\x01" , size: 8)); |
286 | |
287 | // write remaining data |
288 | for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) { |
289 | w = device->write(data: ba.constData() + write, len: size - write); |
290 | if (w < 0) { |
291 | return false; |
292 | } |
293 | } |
294 | |
295 | return true; |
296 | } |
297 | |
298 | } // namespace |
299 | |
300 | QOIHandler::QOIHandler() |
301 | { |
302 | } |
303 | |
304 | bool QOIHandler::canRead() const |
305 | { |
306 | if (canRead(device: device())) { |
307 | setFormat("qoi" ); |
308 | return true; |
309 | } |
310 | return false; |
311 | } |
312 | |
313 | bool QOIHandler::canRead(QIODevice *device) |
314 | { |
315 | if (!device) { |
316 | qWarning(msg: "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 = {.MagicNumber: 0, .Width: 0, .Height: 0, .Channels: 0, .Colorspace: 2}; |
332 | stream >> qoi; |
333 | |
334 | return IsSupported(head: qoi); |
335 | } |
336 | |
337 | bool QOIHandler::read(QImage *image) |
338 | { |
339 | QDataStream s(device()); |
340 | s.setByteOrder(QDataStream::BigEndian); |
341 | |
342 | // Read image header |
343 | QoiHeader qoi = {.MagicNumber: 0, .Width: 0, .Height: 0, .Channels: 0, .Colorspace: 2}; |
344 | s >> qoi; |
345 | |
346 | // Check if file is supported |
347 | if (!IsSupported(head: qoi)) { |
348 | return false; |
349 | } |
350 | |
351 | QImage img; |
352 | bool result = LoadQOI(device: s.device(), qoi, img); |
353 | |
354 | if (result == false) { |
355 | return false; |
356 | } |
357 | |
358 | *image = img; |
359 | return true; |
360 | } |
361 | |
362 | bool 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(head: 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(device: s.device(), qoi, img: image); |
387 | } |
388 | |
389 | bool 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 | |
400 | QVariant 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(maxlen: sizeof(QoiHeader)); |
409 | d->rollbackTransaction(); |
410 | |
411 | QDataStream s(ba); |
412 | s.setByteOrder(QDataStream::BigEndian); |
413 | |
414 | QoiHeader = {.MagicNumber: 0, .Width: 0, .Height: 0, .Channels: 0, .Colorspace: 2}; |
415 | s >> header; |
416 | |
417 | if (s.status() == QDataStream::Ok && IsSupported(head: header)) { |
418 | v = QVariant::fromValue(value: 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(maxlen: sizeof(QoiHeader)); |
428 | d->rollbackTransaction(); |
429 | |
430 | QDataStream s(ba); |
431 | s.setByteOrder(QDataStream::BigEndian); |
432 | |
433 | QoiHeader = {.MagicNumber: 0, .Width: 0, .Height: 0, .Channels: 0, .Colorspace: 2}; |
434 | s >> header; |
435 | |
436 | if (s.status() == QDataStream::Ok && IsSupported(head: header)) { |
437 | v = QVariant::fromValue(value: imageFormat(head: header)); |
438 | } |
439 | } |
440 | } |
441 | |
442 | return v; |
443 | } |
444 | |
445 | QImageIOPlugin::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 | |
467 | QImageIOHandler *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" |
476 | |