mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
qoi: write support
As a base I used the reference implementation found on the official site at https://qoiformat.org/ (MIT license). I added a class to convert scan lines in scanlineconverter.cpp. The class takes advantage of the QImage conversion and contrary to what one might expect, with large images it improves performance (compared to converting the whole image) 😄 In progressive mode, for each line, the following conversions (only if needed) are made before saving: 1. If the icc profile is set, the line is converted to sRGB or sRGB Linear. 2. The line is scaled to 8 bits with RGBA order.
This commit is contained in:
parent
4bd9d5baec
commit
8dc685df26
@ -22,7 +22,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:
|
||||
|
||||
@ -31,6 +30,7 @@ The following image formats have read and write support:
|
||||
- High Efficiency Image File Format (heif). Can be enabled with the KIMAGEFORMATS_HEIF build option.
|
||||
- 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
|
||||
|
@ -123,6 +123,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
|
||||
)
|
||||
|
BIN
autotests/write/rgb.qoi
Normal file
BIN
autotests/write/rgb.qoi
Normal file
Binary file not shown.
BIN
autotests/write/rgba.qoi
Normal file
BIN
autotests/write/rgba.qoi
Normal file
Binary file not shown.
@ -87,7 +87,7 @@ kimageformats_add_plugin(kimg_psd SOURCES psd.cpp)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_qoi SOURCES qoi.cpp)
|
||||
kimageformats_add_plugin(kimg_qoi SOURCES qoi.cpp scanlineconverter.cpp)
|
||||
|
||||
##################################
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2023 Ernest Gupik <ernestgupik@wp.pl>
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "qoi_p.h"
|
||||
#include "scanlineconverter_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QColorSpace>
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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) {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
100
src/imageformats/scanlineconverter.cpp
Normal file
100
src/imageformats/scanlineconverter.cpp
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "scanlineconverter_p.h"
|
||||
|
||||
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;
|
||||
}
|
79
src/imageformats/scanlineconverter_p.h
Normal file
79
src/imageformats/scanlineconverter_p.h
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2023 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SCANLINECONVERTER_P_H
|
||||
#define SCANLINECONVERTER_P_H
|
||||
|
||||
#include <QColorSpace>
|
||||
#include <QImage>
|
||||
|
||||
/*!
|
||||
* \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
|
Loading…
Reference in New Issue
Block a user