File indexing completed on 2024-04-28 15:25:43
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2023 Ernest Gupik <ernestgupik@wp.pl> 0004 SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "qoi_p.h" 0010 #include "scanlineconverter_p.h" 0011 #include "util_p.h" 0012 0013 #include <QColorSpace> 0014 #include <QFile> 0015 #include <QIODevice> 0016 #include <QImage> 0017 0018 namespace // Private 0019 { 0020 0021 #define QOI_OP_INDEX 0x00 /* 00xxxxxx */ 0022 #define QOI_OP_DIFF 0x40 /* 01xxxxxx */ 0023 #define QOI_OP_LUMA 0x80 /* 10xxxxxx */ 0024 #define QOI_OP_RUN 0xc0 /* 11xxxxxx */ 0025 #define QOI_OP_RGB 0xfe /* 11111110 */ 0026 #define QOI_OP_RGBA 0xff /* 11111111 */ 0027 #define QOI_MASK_2 0xc0 /* 11000000 */ 0028 0029 #define QOI_MAGIC (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | ((unsigned int)'i') << 8 | ((unsigned int)'f')) 0030 #define QOI_HEADER_SIZE 14 0031 #define QOI_END_STREAM_PAD 8 0032 0033 struct QoiHeader { 0034 quint32 MagicNumber; 0035 quint32 Width; 0036 quint32 Height; 0037 quint8 Channels; 0038 quint8 Colorspace; 0039 }; 0040 0041 struct Px { 0042 bool operator==(const Px &other) const 0043 { 0044 return r == other.r && g == other.g && b == other.b && a == other.a; 0045 } 0046 quint8 r; 0047 quint8 g; 0048 quint8 b; 0049 quint8 a; 0050 }; 0051 0052 static QDataStream &operator>>(QDataStream &s, QoiHeader &head) 0053 { 0054 s >> head.MagicNumber; 0055 s >> head.Width; 0056 s >> head.Height; 0057 s >> head.Channels; 0058 s >> head.Colorspace; 0059 return s; 0060 } 0061 0062 static QDataStream &operator<<(QDataStream &s, const QoiHeader &head) 0063 { 0064 s << head.MagicNumber; 0065 s << head.Width; 0066 s << head.Height; 0067 s << head.Channels; 0068 s << head.Colorspace; 0069 return s; 0070 } 0071 0072 static bool IsSupported(const QoiHeader &head) 0073 { 0074 // Check magic number 0075 if (head.MagicNumber != QOI_MAGIC) { 0076 return false; 0077 } 0078 // Check if the header is a valid QOI header 0079 if (head.Width == 0 || head.Height == 0 || head.Channels < 3 || head.Colorspace > 1) { 0080 return false; 0081 } 0082 // Set a reasonable upper limit 0083 if (head.Width > 300000 || head.Height > 300000) { 0084 return false; 0085 } 0086 return true; 0087 } 0088 0089 static int QoiHash(const Px &px) 0090 { 0091 return px.r * 3 + px.g * 5 + px.b * 7 + px.a * 11; 0092 } 0093 0094 static QImage::Format imageFormat(const QoiHeader &head) 0095 { 0096 if (IsSupported(head)) { 0097 return (head.Channels == 3 ? QImage::Format_RGB32 : QImage::Format_ARGB32); 0098 } 0099 return QImage::Format_Invalid; 0100 } 0101 0102 static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img) 0103 { 0104 Px index[64] = {Px{0, 0, 0, 0}}; 0105 Px px = Px{0, 0, 0, 255}; 0106 0107 // The px_len should be enough to read a complete "compressed" row: an uncompressible row can become 0108 // larger than the row itself. It should never be more than 1/3 (RGB) or 1/4 (RGBA) the length of the 0109 // row itself (see test bnm_rgb*.qoi) so I set the extra data to 1/2. 0110 // The minimum value is to ensure that enough bytes are read when the image is very small (e.g. 1x1px): 0111 // it can be set as large as you like. 0112 quint64 px_len = std::max(quint64(1024), quint64(qoi.Width) * qoi.Channels * 3 / 2); 0113 if (px_len > kMaxQVectorSize) { 0114 return false; 0115 } 0116 0117 // Allocate image 0118 img = imageAlloc(qoi.Width, qoi.Height, imageFormat(qoi)); 0119 if (img.isNull()) { 0120 return false; 0121 } 0122 0123 // Set the image colorspace based on the qoi.Colorspace value 0124 // As per specification: 0 = sRGB with linear alpha, 1 = all channels linear 0125 if (qoi.Colorspace) { 0126 img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); 0127 } else { 0128 img.setColorSpace(QColorSpace(QColorSpace::SRgb)); 0129 } 0130 0131 // Handle the byte stream 0132 QByteArray ba; 0133 for (quint32 y = 0, run = 0; y < qoi.Height; ++y) { 0134 if (quint64(ba.size()) < px_len) { 0135 ba.append(device->read(px_len)); 0136 } 0137 0138 if (ba.size() < QOI_END_STREAM_PAD) { 0139 return false; 0140 } 0141 0142 quint64 chunks_len = ba.size() - QOI_END_STREAM_PAD; 0143 quint64 p = 0; 0144 QRgb *scanline = reinterpret_cast<QRgb *>(img.scanLine(y)); 0145 const quint8 *input = reinterpret_cast<const quint8 *>(ba.constData()); 0146 for (quint32 x = 0; x < qoi.Width; ++x) { 0147 if (run > 0) { 0148 run--; 0149 } else if (p < chunks_len) { 0150 quint32 b1 = input[p++]; 0151 0152 if (b1 == QOI_OP_RGB) { 0153 px.r = input[p++]; 0154 px.g = input[p++]; 0155 px.b = input[p++]; 0156 } else if (b1 == QOI_OP_RGBA) { 0157 px.r = input[p++]; 0158 px.g = input[p++]; 0159 px.b = input[p++]; 0160 px.a = input[p++]; 0161 } else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) { 0162 px = index[b1]; 0163 } else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) { 0164 px.r += ((b1 >> 4) & 0x03) - 2; 0165 px.g += ((b1 >> 2) & 0x03) - 2; 0166 px.b += (b1 & 0x03) - 2; 0167 } else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) { 0168 quint32 b2 = input[p++]; 0169 quint32 vg = (b1 & 0x3f) - 32; 0170 px.r += vg - 8 + ((b2 >> 4) & 0x0f); 0171 px.g += vg; 0172 px.b += vg - 8 + (b2 & 0x0f); 0173 } else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) { 0174 run = (b1 & 0x3f); 0175 } 0176 index[QoiHash(px) & 0x3F] = px; 0177 } 0178 // Set the values for the pixel at (x, y) 0179 scanline[x] = qRgba(px.r, px.g, px.b, px.a); 0180 } 0181 0182 if (p) { 0183 ba.remove(0, p); 0184 } 0185 } 0186 0187 // From specs the byte stream's end is marked with 7 0x00 bytes followed by a single 0x01 byte. 0188 // NOTE: Instead of using "ba == QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)" 0189 // we preferred a generic check that allows data to exist after the end of the file. 0190 return (ba.startsWith(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8))); 0191 } 0192 0193 static bool SaveQOI(QIODevice *device, const QoiHeader &qoi, const QImage &img) 0194 { 0195 Px index[64] = {Px{0, 0, 0, 0}}; 0196 Px px = Px{0, 0, 0, 255}; 0197 Px px_prev = px; 0198 0199 auto run = 0; 0200 auto channels = qoi.Channels; 0201 0202 QByteArray ba; 0203 ba.reserve(img.width() * channels * 3 / 2); 0204 0205 ScanLineConverter converter(channels == 3 ? QImage::Format_RGB888 : QImage::Format_RGBA8888); 0206 converter.setTargetColorSpace(QColorSpace(qoi.Colorspace == 1 ? QColorSpace::SRgbLinear : QColorSpace::SRgb)); 0207 0208 for (auto h = img.height(), y = 0; y < h; ++y) { 0209 auto pixels = converter.convertedScanLine(img, y); 0210 if (pixels == nullptr) { 0211 return false; 0212 } 0213 0214 for (auto w = img.width() * channels, px_pos = 0; px_pos < w; px_pos += channels) { 0215 px.r = pixels[px_pos + 0]; 0216 px.g = pixels[px_pos + 1]; 0217 px.b = pixels[px_pos + 2]; 0218 0219 if (channels == 4) { 0220 px.a = pixels[px_pos + 3]; 0221 } 0222 0223 if (px == px_prev) { 0224 run++; 0225 if (run == 62 || (px_pos == w - channels && y == h - 1)) { 0226 ba.append(QOI_OP_RUN | (run - 1)); 0227 run = 0; 0228 } 0229 } else { 0230 int index_pos; 0231 0232 if (run > 0) { 0233 ba.append(QOI_OP_RUN | (run - 1)); 0234 run = 0; 0235 } 0236 0237 index_pos = QoiHash(px) & 0x3F; 0238 0239 if (index[index_pos] == px) { 0240 ba.append(QOI_OP_INDEX | index_pos); 0241 } else { 0242 index[index_pos] = px; 0243 0244 if (px.a == px_prev.a) { 0245 signed char vr = px.r - px_prev.r; 0246 signed char vg = px.g - px_prev.g; 0247 signed char vb = px.b - px_prev.b; 0248 0249 signed char vg_r = vr - vg; 0250 signed char vg_b = vb - vg; 0251 0252 if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) { 0253 ba.append(QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2)); 0254 } else if (vg_r > -9 && vg_r < 8 && vg > -33 && vg < 32 && vg_b > -9 && vg_b < 8) { 0255 ba.append(QOI_OP_LUMA | (vg + 32)); 0256 ba.append((vg_r + 8) << 4 | (vg_b + 8)); 0257 } else { 0258 ba.append(char(QOI_OP_RGB)); 0259 ba.append(px.r); 0260 ba.append(px.g); 0261 ba.append(px.b); 0262 } 0263 } else { 0264 ba.append(char(QOI_OP_RGBA)); 0265 ba.append(px.r); 0266 ba.append(px.g); 0267 ba.append(px.b); 0268 ba.append(px.a); 0269 } 0270 } 0271 } 0272 px_prev = px; 0273 } 0274 0275 auto written = device->write(ba); 0276 if (written < 0) { 0277 return false; 0278 } 0279 if (written) { 0280 ba.remove(0, written); 0281 } 0282 } 0283 0284 // QOI end of stream 0285 ba.append(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)); 0286 0287 // write remaining data 0288 for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) { 0289 w = device->write(ba.constData() + write, size - write); 0290 if (w < 0) { 0291 return false; 0292 } 0293 } 0294 0295 return true; 0296 } 0297 0298 } // namespace 0299 0300 QOIHandler::QOIHandler() 0301 { 0302 } 0303 0304 bool QOIHandler::canRead() const 0305 { 0306 if (canRead(device())) { 0307 setFormat("qoi"); 0308 return true; 0309 } 0310 return false; 0311 } 0312 0313 bool QOIHandler::canRead(QIODevice *device) 0314 { 0315 if (!device) { 0316 qWarning("QOIHandler::canRead() called with no device"); 0317 return false; 0318 } 0319 0320 device->startTransaction(); 0321 QByteArray head = device->read(QOI_HEADER_SIZE); 0322 qsizetype readBytes = head.size(); 0323 device->rollbackTransaction(); 0324 0325 if (readBytes < QOI_HEADER_SIZE) { 0326 return false; 0327 } 0328 0329 QDataStream stream(head); 0330 stream.setByteOrder(QDataStream::BigEndian); 0331 QoiHeader qoi = {0, 0, 0, 0, 2}; 0332 stream >> qoi; 0333 0334 return IsSupported(qoi); 0335 } 0336 0337 bool QOIHandler::read(QImage *image) 0338 { 0339 QDataStream s(device()); 0340 s.setByteOrder(QDataStream::BigEndian); 0341 0342 // Read image header 0343 QoiHeader qoi = {0, 0, 0, 0, 2}; 0344 s >> qoi; 0345 0346 // Check if file is supported 0347 if (!IsSupported(qoi)) { 0348 return false; 0349 } 0350 0351 QImage img; 0352 bool result = LoadQOI(s.device(), qoi, img); 0353 0354 if (result == false) { 0355 return false; 0356 } 0357 0358 *image = img; 0359 return true; 0360 } 0361 0362 bool QOIHandler::write(const QImage &image) 0363 { 0364 if (image.isNull()) { 0365 return false; 0366 } 0367 0368 QoiHeader qoi; 0369 qoi.MagicNumber = QOI_MAGIC; 0370 qoi.Width = image.width(); 0371 qoi.Height = image.height(); 0372 qoi.Channels = image.hasAlphaChannel() ? 4 : 3; 0373 qoi.Colorspace = image.colorSpace().transferFunction() == QColorSpace::TransferFunction::Linear ? 1 : 0; 0374 0375 if (!IsSupported(qoi)) { 0376 return false; 0377 } 0378 0379 QDataStream s(device()); 0380 s.setByteOrder(QDataStream::BigEndian); 0381 s << qoi; 0382 if (s.status() != QDataStream::Ok) { 0383 return false; 0384 } 0385 0386 return SaveQOI(s.device(), qoi, image); 0387 } 0388 0389 bool QOIHandler::supportsOption(ImageOption option) const 0390 { 0391 if (option == QImageIOHandler::Size) { 0392 return true; 0393 } 0394 if (option == QImageIOHandler::ImageFormat) { 0395 return true; 0396 } 0397 return false; 0398 } 0399 0400 QVariant QOIHandler::option(ImageOption option) const 0401 { 0402 QVariant v; 0403 0404 if (option == QImageIOHandler::Size) { 0405 if (auto d = device()) { 0406 // transactions works on both random and sequential devices 0407 d->startTransaction(); 0408 auto ba = d->read(sizeof(QoiHeader)); 0409 d->rollbackTransaction(); 0410 0411 QDataStream s(ba); 0412 s.setByteOrder(QDataStream::BigEndian); 0413 0414 QoiHeader header = {0, 0, 0, 0, 2}; 0415 s >> header; 0416 0417 if (s.status() == QDataStream::Ok && IsSupported(header)) { 0418 v = QVariant::fromValue(QSize(header.Width, header.Height)); 0419 } 0420 } 0421 } 0422 0423 if (option == QImageIOHandler::ImageFormat) { 0424 if (auto d = device()) { 0425 // transactions works on both random and sequential devices 0426 d->startTransaction(); 0427 auto ba = d->read(sizeof(QoiHeader)); 0428 d->rollbackTransaction(); 0429 0430 QDataStream s(ba); 0431 s.setByteOrder(QDataStream::BigEndian); 0432 0433 QoiHeader header = {0, 0, 0, 0, 2}; 0434 s >> header; 0435 0436 if (s.status() == QDataStream::Ok && IsSupported(header)) { 0437 v = QVariant::fromValue(imageFormat(header)); 0438 } 0439 } 0440 } 0441 0442 return v; 0443 } 0444 0445 QImageIOPlugin::Capabilities QOIPlugin::capabilities(QIODevice *device, const QByteArray &format) const 0446 { 0447 if (format == "qoi" || format == "QOI") { 0448 return Capabilities(CanRead | CanWrite); 0449 } 0450 if (!format.isEmpty()) { 0451 return {}; 0452 } 0453 if (!device->isOpen()) { 0454 return {}; 0455 } 0456 0457 Capabilities cap; 0458 if (device->isReadable() && QOIHandler::canRead(device)) { 0459 cap |= CanRead; 0460 } 0461 if (device->isWritable()) { 0462 cap |= CanWrite; 0463 } 0464 return cap; 0465 } 0466 0467 QImageIOHandler *QOIPlugin::create(QIODevice *device, const QByteArray &format) const 0468 { 0469 QImageIOHandler *handler = new QOIHandler; 0470 handler->setDevice(device); 0471 handler->setFormat(format); 0472 return handler; 0473 } 0474 0475 #include "moc_qoi_p.cpp"