diff --git a/README.md b/README.md index ee08f9c..d27d796 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The following image formats have read-only support: - Animated Windows cursors (ani) - Camera RAW images (arw, cr2, cr3, dcs, dng, ...) +- Farbfeld (ff) - Gimp (xcf) - Interchange Format Files (iff, ilbm, lbm) - Krita (kra) @@ -256,6 +257,7 @@ limit depends on the format encoding). - DDS: 300,000 x 300,000 pixels - EXR: 300,000 x 300,000 pixels - EPS: same size as Qt's JPG plugin +- FF: 300,000 x 300,000 pixels - HDR: 300,000 x 300,000 pixels - HEIF: n/a - IFF: 65,535 x 65,535 pixels diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 3fe173e..1558c54 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -76,6 +76,7 @@ endmacro() # Loads each image in read//, and compares the # result against the data read from the corresponding png file kimageformats_read_tests( + ff hdr iff pcx diff --git a/autotests/ossfuzz/build_fuzzers.sh b/autotests/ossfuzz/build_fuzzers.sh index d9082a0..1bb0334 100755 --- a/autotests/ossfuzz/build_fuzzers.sh +++ b/autotests/ossfuzz/build_fuzzers.sh @@ -162,6 +162,7 @@ HANDLER_TYPES="ANIHandler ani QAVIFHandler avif QDDSHandler dds EXRHandler exr + FFHandler ff HDRHandler hdr HEIFHandler heif IFFHandler iff diff --git a/autotests/ossfuzz/kimgio_fuzzer.cc b/autotests/ossfuzz/kimgio_fuzzer.cc index e3408fa..a617571 100644 --- a/autotests/ossfuzz/kimgio_fuzzer.cc +++ b/autotests/ossfuzz/kimgio_fuzzer.cc @@ -23,7 +23,7 @@ Usage: python infra/helper.py build_image kimageformats python infra/helper.py build_fuzzers --sanitizer undefined|address|memory kimageformats - python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer + python infra/helper.py run_fuzzer kimageformats kimgio_[ani|avif|dds|exr|ff|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tim|tga|xcf]_fuzzer */ #include @@ -34,6 +34,7 @@ #include "avif_p.h" #include "dds_p.h" #include "exr_p.h" +#include "ff_p.h" #include "hdr_p.h" #include "heif_p.h" #include "iff_p.h" diff --git a/autotests/read/ff/testcard_rgba16.ff b/autotests/read/ff/testcard_rgba16.ff new file mode 100644 index 0000000..0f645d8 Binary files /dev/null and b/autotests/read/ff/testcard_rgba16.ff differ diff --git a/autotests/read/ff/testcard_rgba16.png b/autotests/read/ff/testcard_rgba16.png new file mode 100644 index 0000000..809bb0e Binary files /dev/null and b/autotests/read/ff/testcard_rgba16.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 12cc318..a8221e8 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -73,6 +73,10 @@ endif() ################################## +kimageformats_add_plugin(kimg_ff SOURCES ff.cpp) + +################################## + kimageformats_add_plugin(kimg_hdr SOURCES hdr.cpp) ################################## diff --git a/src/imageformats/ff.cpp b/src/imageformats/ff.cpp new file mode 100644 index 0000000..3aa4937 --- /dev/null +++ b/src/imageformats/ff.cpp @@ -0,0 +1,259 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2026 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +// Specs: https://tools.suckless.org/farbfeld/ + +#include "ff_p.h" +#include "util_p.h" + +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(LOG_FFPLUGIN) + +#ifdef QT_DEBUG +Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtDebugMsg) +#else +Q_LOGGING_CATEGORY(LOG_FFPLUGIN, "kf.imageformats.plugins.ff", QtWarningMsg) +#endif + +/* *** FF_MAX_IMAGE_WIDTH and FF_MAX_IMAGE_HEIGHT *** + * The maximum size in pixel allowed by the plugin. + */ +#ifndef FF_MAX_IMAGE_WIDTH +#define FF_MAX_IMAGE_WIDTH KIF_LARGE_IMAGE_PIXEL_LIMIT +#endif +#ifndef FF_MAX_IMAGE_HEIGHT +#define FF_MAX_IMAGE_HEIGHT FF_MAX_IMAGE_WIDTH +#endif + +#define HEADER_SIZE 16 + +class FFHeader +{ +private: + QByteArray m_rawHeader; + +public: + FFHeader() + { + + } + + bool isValid() const + { + if (m_rawHeader.size() < HEADER_SIZE) { + return false; + } + return m_rawHeader.startsWith(QByteArray::fromRawData("farbfeld", 8)); + } + + bool isSupported() const + { + auto w = width(); + auto h = height(); + if (w < 1 || w > FF_MAX_IMAGE_WIDTH || h < 1 || h > FF_MAX_IMAGE_HEIGHT) { + return false; + } + return format() != QImage::Format_Invalid; + } + + qint32 width() const + { + if (!isValid()) { + return 0; + } + return qFromBigEndian(m_rawHeader.data() + 8); + } + + qint32 height() const + { + if (!isValid()) { + return 0; + } + return qFromBigEndian(m_rawHeader.data() + 12); + } + + QSize size() const + { + return QSize(width(), height()); + } + + QImage::Format format() const + { + if (!isValid()) { + return QImage::Format_Invalid; + } + return QImage::Format_RGBA64; + } + + bool read(QIODevice *d) + { + m_rawHeader = d->read(HEADER_SIZE); + if (m_rawHeader.size() != HEADER_SIZE) { + return false; + } + return isValid(); + } + + bool peek(QIODevice *d) + { + m_rawHeader = d->peek(HEADER_SIZE); + if (m_rawHeader.size() != HEADER_SIZE) { + return false; + } + return isValid(); + } +}; + +class FFHandlerPrivate +{ +public: + FFHandlerPrivate() {} + ~FFHandlerPrivate() {} + + FFHeader m_header; +}; + +FFHandler::FFHandler() + : QImageIOHandler() + , d(new FFHandlerPrivate) +{ +} + +bool FFHandler::canRead() const +{ + if (canRead(device())) { + setFormat("ff"); + return true; + } + return false; +} + +bool FFHandler::canRead(QIODevice *device) +{ + if (!device) { + qCWarning(LOG_FFPLUGIN) << "FFHandler::canRead() called with no device"; + return false; + } + + FFHeader h; + if (!h.peek(device)) { + return false; + } + + return h.isSupported(); +} + +bool FFHandler::read(QImage *image) +{ + auto&& header = d->m_header; + + if (!header.read(device())) { + qCWarning(LOG_FFPLUGIN) << "FFHandler::read() invalid header"; + return false; + } + + auto img = imageAlloc(header.size(), header.format()); + if (img.isNull()) { + qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while allocating the image"; + return false; + } + + auto d = device(); + + auto size = img.bytesPerLine(); + for (auto y = 0, h = img.height(); y < h; ++y) { + auto line = reinterpret_cast(img.scanLine(y)); + if (d->read(line, size) != size) { + qCWarning(LOG_FFPLUGIN) << "FFHandler::read() error while reading image scanline"; + return false; + } +#if Q_LITTLE_ENDIAN + for (auto i = 0; i < size; i += 2) { + std::swap(line[i], line[i + 1]); + } +#endif + } + + img.setColorSpace(QColorSpace(QColorSpace::SRgb)); + + *image = img; + return true; +} + +bool FFHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) { + return true; + } + if (option == QImageIOHandler::ImageFormat) { + return true; + } + return false; +} + +QVariant FFHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + auto&& h = d->m_header; + if (h.isValid()) { + v = QVariant::fromValue(h.size()); + } else if (auto d = device()) { + if (h.peek(d)) { + v = QVariant::fromValue(h.size()); + } + } + } + + if (option == QImageIOHandler::ImageFormat) { + auto&& h = d->m_header; + if (h.isValid()) { + v = QVariant::fromValue(h.format()); + } else if (auto d = device()) { + if (h.peek(d)) { + v = QVariant::fromValue(h.format()); + } + } + } + + return v; +} + +QImageIOPlugin::Capabilities FFPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "ff") { + return Capabilities(CanRead); + } + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && FFHandler::canRead(device)) { + cap |= CanRead; + } + return cap; +} + +QImageIOHandler *FFPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new FFHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} + +#include "moc_ff_p.cpp" diff --git a/src/imageformats/ff.json b/src/imageformats/ff.json new file mode 100644 index 0000000..b4eced8 --- /dev/null +++ b/src/imageformats/ff.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "ff" ], + "MimeTypes": [ "image/x-farbfeld" ] +} diff --git a/src/imageformats/ff_p.h b/src/imageformats/ff_p.h new file mode 100644 index 0000000..5ae4f68 --- /dev/null +++ b/src/imageformats/ff_p.h @@ -0,0 +1,42 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2026 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KIMG_FF_P_H +#define KIMG_FF_P_H + +#include +#include + +class FFHandlerPrivate; +class FFHandler : public QImageIOHandler +{ +public: + FFHandler(); + + bool canRead() const override; + bool read(QImage *image) override; + + bool supportsOption(QImageIOHandler::ImageOption option) const override; + QVariant option(QImageIOHandler::ImageOption option) const override; + + static bool canRead(QIODevice *device); + +private: + const QScopedPointer d; +}; + +class FFPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "ff.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_FF_P_H