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"