From 906ecce500c7da97aa84d181e4385267da9a6dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Novomesk=C3=BD?= Date: Tue, 29 Aug 2023 20:43:02 +0200 Subject: [PATCH] qoi: write support backported from master --- README.md | 2 +- autotests/CMakeLists.txt | 1 + autotests/write/rgb.qoi | Bin 0 -> 1096 bytes autotests/write/rgba.qoi | Bin 0 -> 1390 bytes src/imageformats/CMakeLists.txt | 2 +- src/imageformats/qoi.cpp | 178 ++++++++++++++++++++++--- src/imageformats/qoi_p.h | 1 + src/imageformats/scanlineconverter.cpp | 101 ++++++++++++++ src/imageformats/scanlineconverter_p.h | 79 +++++++++++ 9 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 autotests/write/rgb.qoi create mode 100644 autotests/write/rgba.qoi create mode 100644 src/imageformats/scanlineconverter.cpp create mode 100644 src/imageformats/scanlineconverter_p.h diff --git a/README.md b/README.md index f84ce16..5673058 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ The following image formats have read-only support: - Photoshop documents (psd, psb, pdd, psdt) - Radiance HDR (hdr) - Sun Raster (ras) -- Quite OK Image format (qoi) The following image formats have read and write support: @@ -28,6 +27,7 @@ The following image formats have read and write support: - Encapsulated PostScript (eps) - JPEG XL (jxl) - Personal Computer Exchange (pcx) +- Quite OK Image format (qoi) - SGI images (rgb, rgba, sgi, bw) - Softimage PIC (pic) - Targa (tga): supports more formats than Qt's version diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index a6ea8ce..ff7bc20 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -125,6 +125,7 @@ kimageformats_read_tests(FUZZ 1 kimageformats_write_tests( pcx-lossless pic-lossless + qoi-lossless rgb-lossless tga # fixme: the alpha images appear not to be written properly ) diff --git a/autotests/write/rgb.qoi b/autotests/write/rgb.qoi new file mode 100644 index 0000000000000000000000000000000000000000..229070383c07f7d5c36760ffaa449fd8ffd60506 GIT binary patch literal 1096 zcmW-he@xV69LI5P{Zq5HY_>||kNmE!)uP<8Tz{l(E!N6rDq(4sODRv%iH1O^h|Kuq z5{ck|J2(`OScr-w#sS5T6AmtZ>_CqnQvr{=8=e6f~e?IT$ zv*&P1@^>*YF>%pY8M8goy+fULLuEF0^#M#uXHnp)rd%XS?G?%?NqRspv_{ZS!`mIAJo`OQ?QU!_as?iGX>!r zT(i?M*0=b9+%kN*h5Zm8@Cpr)AL&TAO{ z{4^OC4zbn$5eb85h^?I?yR(3^gW3FY{W#6$wS2LtVZ*eIvL!!aL_txMt`(~v%k63~ zalm(k4mRony_K zgvM1=-T>4|U&cxv<`Izu*zj|3%C1J0V@$w?7 zK0QO%iA+3JGen|9RI9kIu~73u2Zr}e%>1*69FArXcKDdjxx~WZJvj>De`eF!l235KuBt?G)9@!;MWQ~xR zdY+xXD!W`-d+W$SquU%OMtnPg;vMy90-IOu2~Q~Wp3Yg9#2mDaL`pCWMR*7DbuX= zmZ8)kn}sOmKO7Vb3|w=p@*Hl|xy?pjtG#Wkn5RdfJ69WqdDR&Of>ytxD^==Hwjd^zNrw^)Ez`^ zc2yAMea->c_3DG22y62F3M)VB`JC=y4h;r%P2nJfc%5VIX2S+8N^eA?-N$LO6}1LtB(6?hO3+o!(#x{=f3o zu%D>!g!Opy6kI1tYT@--W=HhJop+dUZ`QBJ#)`W-L}m<|;HA*ZvGn?&Ez!&UuQPE{ zc)HZ$@%0K(prSs zE6I$`^BMdDHeUB!Xx)P7 + SPDX-FileCopyrightText: 2023 Mirco Miranda SPDX-License-Identifier: LGPL-2.0-or-later */ #include "qoi_p.h" +#include "scanlineconverter_p.h" #include "util_p.h" #include @@ -37,6 +39,10 @@ struct QoiHeader { }; struct Px { + bool operator==(const Px &other) const + { + return r == other.r && g == other.g && b == other.b && a == other.a; + } quint8 r; quint8 g; quint8 b; @@ -53,6 +59,16 @@ static QDataStream &operator>>(QDataStream &s, QoiHeader &head) return s; } +static QDataStream &operator<<(QDataStream &s, const QoiHeader &head) +{ + s << head.MagicNumber; + s << head.Width; + s << head.Height; + s << head.Channels; + s << head.Colorspace; + return s; +} + static bool IsSupported(const QoiHeader &head) { // Check magic number @@ -85,19 +101,8 @@ static QImage::Format imageFormat(const QoiHeader &head) static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img) { - Px index[64] = {Px{ - 0, - 0, - 0, - 0, - }}; - - Px px = Px{ - 0, - 0, - 0, - 255, - }; + Px index[64] = {Px{0, 0, 0, 0}}; + Px px = Px{0, 0, 0, 255}; // The px_len should be enough to read a complete "compressed" row: an uncompressible row can become // larger than the row itself. It should never be more than 1/3 (RGB) or 1/4 (RGBA) the length of the @@ -136,7 +141,7 @@ static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img) quint64 chunks_len = ba.size() - QOI_END_STREAM_PAD; quint64 p = 0; - QRgb *scanline = (QRgb *)img.scanLine(y); + QRgb *scanline = reinterpret_cast(img.scanLine(y)); const quint8 *input = reinterpret_cast(ba.constData()); for (quint32 x = 0; x < qoi.Width; ++x) { if (run > 0) { @@ -185,6 +190,111 @@ static bool LoadQOI(QIODevice *device, const QoiHeader &qoi, QImage &img) return (ba.startsWith(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8))); } +static bool SaveQOI(QIODevice *device, const QoiHeader &qoi, const QImage &img) +{ + Px index[64] = {Px{0, 0, 0, 0}}; + Px px = Px{0, 0, 0, 255}; + Px px_prev = px; + + auto run = 0; + auto channels = qoi.Channels; + + QByteArray ba; + ba.reserve(img.width() * channels * 3 / 2); + + ScanLineConverter converter(channels == 3 ? QImage::Format_RGB888 : QImage::Format_RGBA8888); + converter.setTargetColorSpace(QColorSpace(qoi.Colorspace == 1 ? QColorSpace::SRgbLinear : QColorSpace::SRgb)); + + for (auto h = img.height(), y = 0; y < h; ++y) { + auto pixels = converter.convertedScanLine(img, y); + if (pixels == nullptr) { + return false; + } + + for (auto w = img.width() * channels, px_pos = 0; px_pos < w; px_pos += channels) { + px.r = pixels[px_pos + 0]; + px.g = pixels[px_pos + 1]; + px.b = pixels[px_pos + 2]; + + if (channels == 4) { + px.a = pixels[px_pos + 3]; + } + + if (px == px_prev) { + run++; + if (run == 62 || (px_pos == w - channels && y == h - 1)) { + ba.append(QOI_OP_RUN | (run - 1)); + run = 0; + } + } else { + int index_pos; + + if (run > 0) { + ba.append(QOI_OP_RUN | (run - 1)); + run = 0; + } + + index_pos = QoiHash(px) & 0x3F; + + if (index[index_pos] == px) { + ba.append(QOI_OP_INDEX | index_pos); + } else { + index[index_pos] = px; + + if (px.a == px_prev.a) { + signed char vr = px.r - px_prev.r; + signed char vg = px.g - px_prev.g; + signed char vb = px.b - px_prev.b; + + signed char vg_r = vr - vg; + signed char vg_b = vb - vg; + + if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) { + ba.append(QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2)); + } else if (vg_r > -9 && vg_r < 8 && vg > -33 && vg < 32 && vg_b > -9 && vg_b < 8) { + ba.append(QOI_OP_LUMA | (vg + 32)); + ba.append((vg_r + 8) << 4 | (vg_b + 8)); + } else { + ba.append(char(QOI_OP_RGB)); + ba.append(px.r); + ba.append(px.g); + ba.append(px.b); + } + } else { + ba.append(char(QOI_OP_RGBA)); + ba.append(px.r); + ba.append(px.g); + ba.append(px.b); + ba.append(px.a); + } + } + } + px_prev = px; + } + + auto written = device->write(ba); + if (written < 0) { + return false; + } + if (written) { + ba.remove(0, written); + } + } + + // QOI end of stream + ba.append(QByteArray::fromRawData("\x00\x00\x00\x00\x00\x00\x00\x01", 8)); + + // write remaining data + for (qint64 w = 0, write = 0, size = ba.size(); write < size; write += w) { + w = device->write(ba.constData() + write, size - write); + if (w < 0) { + return false; + } + } + + return true; +} + } // namespace QOIHandler::QOIHandler() @@ -218,7 +328,7 @@ bool QOIHandler::canRead(QIODevice *device) QDataStream stream(head); stream.setByteOrder(QDataStream::BigEndian); - QoiHeader qoi; + QoiHeader qoi = {0, 0, 0, 0, 2}; stream >> qoi; return IsSupported(qoi); @@ -230,7 +340,7 @@ bool QOIHandler::read(QImage *image) s.setByteOrder(QDataStream::BigEndian); // Read image header - QoiHeader qoi; + QoiHeader qoi = {0, 0, 0, 0, 2}; s >> qoi; // Check if file is supported @@ -249,6 +359,33 @@ bool QOIHandler::read(QImage *image) return true; } +bool QOIHandler::write(const QImage &image) +{ + if (image.isNull()) { + return false; + } + + QoiHeader qoi; + qoi.MagicNumber = QOI_MAGIC; + qoi.Width = image.width(); + qoi.Height = image.height(); + qoi.Channels = image.hasAlphaChannel() ? 4 : 3; + qoi.Colorspace = image.colorSpace().transferFunction() == QColorSpace::TransferFunction::Linear ? 1 : 0; + + if (!IsSupported(qoi)) { + return false; + } + + QDataStream s(device()); + s.setByteOrder(QDataStream::BigEndian); + s << qoi; + if (s.status() != QDataStream::Ok) { + return false; + } + + return SaveQOI(s.device(), qoi, image); +} + bool QOIHandler::supportsOption(ImageOption option) const { if (option == QImageIOHandler::Size) { @@ -274,7 +411,7 @@ QVariant QOIHandler::option(ImageOption option) const QDataStream s(ba); s.setByteOrder(QDataStream::BigEndian); - QoiHeader header; + QoiHeader header = {0, 0, 0, 0, 2}; s >> header; if (s.status() == QDataStream::Ok && IsSupported(header)) { @@ -293,7 +430,7 @@ QVariant QOIHandler::option(ImageOption option) const QDataStream s(ba); s.setByteOrder(QDataStream::BigEndian); - QoiHeader header; + QoiHeader header = {0, 0, 0, 0, 2}; s >> header; if (s.status() == QDataStream::Ok && IsSupported(header)) { @@ -308,7 +445,7 @@ QVariant QOIHandler::option(ImageOption option) const QImageIOPlugin::Capabilities QOIPlugin::capabilities(QIODevice *device, const QByteArray &format) const { if (format == "qoi" || format == "QOI") { - return Capabilities(CanRead); + return Capabilities(CanRead | CanWrite); } if (!format.isEmpty()) { return {}; @@ -321,6 +458,9 @@ QImageIOPlugin::Capabilities QOIPlugin::capabilities(QIODevice *device, const QB if (device->isReadable() && QOIHandler::canRead(device)) { cap |= CanRead; } + if (device->isWritable()) { + cap |= CanWrite; + } return cap; } diff --git a/src/imageformats/qoi_p.h b/src/imageformats/qoi_p.h index 216216b..b125e9f 100644 --- a/src/imageformats/qoi_p.h +++ b/src/imageformats/qoi_p.h @@ -17,6 +17,7 @@ public: bool canRead() const override; bool read(QImage *image) override; + bool write(const QImage &image) override; bool supportsOption(QImageIOHandler::ImageOption option) const override; QVariant option(QImageIOHandler::ImageOption option) const override; diff --git a/src/imageformats/scanlineconverter.cpp b/src/imageformats/scanlineconverter.cpp new file mode 100644 index 0000000..77a397a --- /dev/null +++ b/src/imageformats/scanlineconverter.cpp @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2023 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "scanlineconverter_p.h" +#include + +ScanLineConverter::ScanLineConverter(const QImage::Format &targetFormat) + : _targetFormat(targetFormat) +{ +} + +ScanLineConverter::ScanLineConverter(const ScanLineConverter &other) + : _targetFormat(other._targetFormat) + , _colorSpace(other._colorSpace) +{ +} + +ScanLineConverter &ScanLineConverter::operator=(const ScanLineConverter &other) +{ + this->_targetFormat = other._targetFormat; + this->_colorSpace = other._colorSpace; + return (*this); +} + +QImage::Format ScanLineConverter::targetFormat() const +{ + return _targetFormat; +} + +void ScanLineConverter::setTargetColorSpace(const QColorSpace &colorSpace) +{ + _colorSpace = colorSpace; +} + +QColorSpace ScanLineConverter::targetColorSpace() const +{ + return _colorSpace; +} + +const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y) +{ + auto colorSpaceConversion = isColorSpaceConversionNeeded(image, _colorSpace); + if (image.format() == _targetFormat && !colorSpaceConversion) { + return image.constScanLine(y); + } + if (image.width() != _tmpBuffer.width() || image.format() != _tmpBuffer.format()) { + _tmpBuffer = QImage(image.width(), 1, image.format()); + } + if (_tmpBuffer.isNull()) { + return nullptr; + } + std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine())); + if (colorSpaceConversion) { + _tmpBuffer.setColorSpace(image.colorSpace()); + _tmpBuffer.convertToColorSpace(_colorSpace); + } + _convBuffer = _tmpBuffer.convertToFormat(_targetFormat); + if (_convBuffer.isNull()) { + return nullptr; + } + return _convBuffer.constBits(); +} + +qsizetype ScanLineConverter::bytesPerLine() const +{ + if (_convBuffer.isNull()) { + return 0; + } + return _convBuffer.bytesPerLine(); +} + +bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const +{ + if (image.depth() < 24) { // RGB 8 bit or grater only + return false; + } + auto sourceColorSpace = image.colorSpace(); + if (!sourceColorSpace.isValid() || !targetColorSpace.isValid()) { + return false; + } + + auto stf = sourceColorSpace.transferFunction(); + auto spr = sourceColorSpace.primaries(); + auto ttf = targetColorSpace.transferFunction(); + auto tpr = targetColorSpace.primaries(); + // clang-format off + if (stf == QColorSpace::TransferFunction::Custom || + ttf == QColorSpace::TransferFunction::Custom || + spr == QColorSpace::Primaries::Custom || + tpr == QColorSpace::Primaries::Custom) { + return true; + } + // clang-format on + if (stf == ttf && spr == tpr) { + return false; + } + return true; +} diff --git a/src/imageformats/scanlineconverter_p.h b/src/imageformats/scanlineconverter_p.h new file mode 100644 index 0000000..82cf4aa --- /dev/null +++ b/src/imageformats/scanlineconverter_p.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2023 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef SCANLINECONVERTER_P_H +#define SCANLINECONVERTER_P_H + +#include +#include + +/*! + * \brief The scanlineFormatConversion class + * A class to convert an image scan line. It introduces some overhead on small images + * but performs better on large images. :) + */ +class ScanLineConverter +{ +public: + ScanLineConverter(const QImage::Format &targetFormat); + ScanLineConverter(const ScanLineConverter &other); + ScanLineConverter &operator=(const ScanLineConverter &other); + + /*! + * \brief targetFormat + * \return The target format set in the constructor. + */ + QImage::Format targetFormat() const; + + /*! + * \brief setTargetColorSpace + * Set the colorspace conversion. + * + * In addition to format conversion, it is also possible to convert the color + * space if the source image has a different one set. + * The conversion is done on the source format if and only if the image + * has a color depth greater than 24 bit and the color profile set is different + * from that of the image itself. + * \param colorSpace + */ + void setTargetColorSpace(const QColorSpace &colorSpace); + QColorSpace targetColorSpace() const; + + /*! + * \brief convertedScanLine + * Convert the scanline \a y. + * \note If the image format (and color space) is the same of converted format, it returns the image scan line. + * \return The scan line converted. + */ + const uchar *convertedScanLine(const QImage &image, qint32 y); + + /*! + * \brief bytesPerLine + * \return The size of the last converted scanline. + */ + qsizetype bytesPerLine() const; + + /*! + * \brief isColorSpaceConversionNeeded + * Calculates if a color space conversion is needed. + * \note Only 24 bit or grater images. + * \param image The source image. + * \param targetColorSpace The target color space. + * \return True if the conversion should be done otherwise false. + */ + bool isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace) const; + +private: + // data + QImage::Format _targetFormat; + QColorSpace _colorSpace; + + // internal buffers + QImage _tmpBuffer; + QImage _convBuffer; +}; + +#endif // SCANLINECONVERTER_P_H