Compare commits

..

13 Commits

Author SHA1 Message Date
0c25cb5b8c Add XCursor image format plug-in
Allows to read XCursor files, including ones with animation.

The cursor hotspot is set in QImage textKeys as "HotspotX" and "HotspotY".
The "Sizes" textKey contains a list of available sizes in this file.
2026-01-30 19:24:56 +01:00
2c8a1ad6ff GIT_SILENT anitest: Fix typo
text -> test
2026-01-30 17:36:12 +01:00
e0f1ba640a IFF: add uncompressed RGFX support 2026-01-27 13:58:01 +01:00
32773e5f0c IFF: add CD-i Rle7 support 2026-01-20 21:17:08 +00:00
2410e45614 Decode Atari ST VDAT chunks 2026-01-20 10:20:32 +01:00
99e4223393 IFF: add support for CD-i YUVS chunk (and minor code improvements) 2026-01-15 16:53:50 +01:00
8224c0099d Add support for CD-I IFF images 2026-01-14 16:53:19 +01:00
8d7fb2c3fd Add JXL testfile which previously triggered crash 2026-01-11 15:25:03 +01:00
3353809906 jxl: fix crash on lossy gray images
There was a rare crash during decoding of some lossy gray images.
Problem was reported in https://github.com/libjxl/libjxl/issues/4549
This is a workaround which avoids JxlDecoderSetCms call.
2026-01-11 15:03:06 +01:00
abf4d32858 Add gray AVIF files with various transfer functions 2026-01-10 20:41:52 +00:00
6b1c52c55c avif: Improved color profiles support 2026-01-10 20:41:52 +00:00
e644ab997f GIT_SILENT Upgrade CMake version requirement to 3.27.
See https://community.kde.org/Frameworks/Policies
2026-01-10 00:13:59 +01:00
1fb3363e7b Update version to 6.23.0 2026-01-02 18:59:54 +01:00
67 changed files with 2706 additions and 75 deletions

View File

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.27)
set(KF_VERSION "6.22.0") # handled by release scripts
set(KF_VERSION "6.23.0") # handled by release scripts
set(KF_DEP_VERSION "6.22.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})

View File

@ -357,9 +357,16 @@ The plugin supports the following image data:
type 4.
- FORM PBM: PBM is a chunky version of IFF pictures. It supports 8-bit images
with color map only.
- FORM IMAG (Compact Disc-Interactive): It supports CLut4, CLut7, CLut8, Rle7
and DYuv formats.
- FORM RGFX: It supports uncompressed images only.
- FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit
RGBA images.
> [!note]
> The plugin only supports the IFF, ILBM, and LBM file extensions. You'll
> need to rename files with different extensions to open them.
### The JP2 plugin
**This plugin can be disabled by setting `KIMAGEFORMATS_JP2` to `OFF`

View File

@ -247,3 +247,8 @@ add_executable(anitest anitest.cpp)
target_link_libraries(anitest Qt6::Gui Qt6::Test)
ecm_mark_as_test(anitest)
add_test(NAME kimageformats-ani COMMAND anitest)
add_executable(xcursortest xcursortest.cpp)
target_link_libraries(xcursortest Qt6::Gui Qt6::Test)
ecm_mark_as_test(xcursortest)
add_test(NAME kimageformats-xcursortest COMMAND xcursortest)

View File

@ -48,7 +48,7 @@ private Q_SLOTS:
QCOMPARE(reader.text(QStringLiteral("Author")), QStringLiteral("KDE Community"));
}
void textRead()
void testRead()
{
QImageReader reader(QFINDTESTDATA("ani/test.ani"));
QVERIFY(reader.canRead());

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "profile_gray.png",
"colorSpace" : {
"colorModel" : "Gray",
"primaries" : "Custom",
"transferFunction" : "SRgb",
"gamma" : 2.31
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "profile_gray_gamma22.png",
"colorSpace" : {
"colorModel" : "Gray",
"primaries" : "Custom",
"transferFunction" : "Gamma",
"gamma" : 2.2
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "profile_gray_gamma28.png",
"colorSpace" : {
"colorModel" : "Gray",
"primaries" : "Custom",
"transferFunction" : "Gamma",
"gamma" : 2.8
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -0,0 +1,11 @@
[
{
"fileName" : "profile_gray_linear.png",
"colorSpace" : {
"colorModel" : "Gray",
"primaries" : "Custom",
"transferFunction" : "Linear",
"gamma" : 1
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View File

@ -0,0 +1,12 @@
[
{
"fileName" : "profile_gray_prophoto.png",
"colorSpace" : {
"description" : "grayscale D50 with ProPhoto TRC",
"colorModel" : "Gray",
"primaries" : "Custom",
"transferFunction" : "Custom",
"gamma" : 0
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

View File

@ -0,0 +1,9 @@
[
{
"fileName" : "cdi_dyuv_each.iff",
"resolution" : {
"dotsPerMeterX" : 3937,
"dotsPerMeterY" : 5249
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

View File

@ -0,0 +1,37 @@
[
{
"fileName" : "sv5_rgba32_rgx.png",
"colorSpace" : {
"description" : "TIFF ICC Profile",
"primaries" : "SRgb",
"transferFunction" : "SRgb",
"gamma" : 0
},
"metadata" : [
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"key" : "CreationDate",
"value" : "2025-01-14T10:34:51"
},
{
"key" : "Description",
"value" : "TV broadcast test image."
},
{
"key" : "Title",
"value" : "Test Card"
}
],
"resolution" : {
"dotsPerMeterX" : 2835,
"dotsPerMeterY" : 2835
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

View File

@ -0,0 +1,14 @@
[
{
"fileName" : "gray_linear_lossy.png",
"fuzziness" : 1,
"description" : "Minimum fuzziness value to pass the test on all architectures.",
"colorSpace" : {
"description" : "Gra_D65_Rel_SRG",
"primaries" : "Custom",
"transferFunction" : "SRgb",
"gamma" : 0,
"colorModel" : "Gray"
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
autotests/xcursor/wait Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

129
autotests/xcursortest.cpp Normal file
View File

@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <kde@broulik.de>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include <QImage>
#include <QImageReader>
#include <QTest>
using namespace Qt::StringLiterals;
static bool imgEquals(const QImage &im1, const QImage &im2)
{
const int height = im1.height();
const int width = im1.width();
for (int i = 0; i < height; ++i) {
const auto *line1 = reinterpret_cast<const quint8 *>(im1.scanLine(i));
const auto *line2 = reinterpret_cast<const quint8 *>(im2.scanLine(i));
for (int j = 0; j < width; ++j) {
if (line1[j] - line2[j] != 0) {
return false;
}
}
}
return true;
}
class XCursorTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase()
{
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
}
void testReadMetadata()
{
QImageReader reader(QFINDTESTDATA("xcursor/wait"));
QVERIFY(reader.canRead());
QCOMPARE(reader.imageCount(), 18);
// By default it chooses the largest size
QCOMPARE(reader.size(), QSize(72, 72));
QCOMPARE(reader.text(u"Sizes"_s), u"24,48,72"_s);
}
void testRead_data()
{
QTest::addColumn<int>("size");
QTest::addColumn<int>("reference");
// It prefers downsampling over upsampling.
QTest::newRow("12px") << 12 << 24;
QTest::newRow("24px") << 24 << 24;
QTest::newRow("48px") << 48 << 48;
QTest::newRow("50px") << 50 << 72;
QTest::newRow("72px") << 72 << 72;
QTest::newRow("default") << 0 << 72;
}
void testRead()
{
QFETCH(int, size);
QFETCH(int, reference);
QImageReader reader(QFINDTESTDATA("xcursor/wait"));
QVERIFY(reader.canRead());
QCOMPARE(reader.currentImageNumber(), 0);
if (size) {
reader.setScaledSize(QSize(size, size));
}
QCOMPARE(reader.size(), QSize(reference, reference));
QImage aniFrame;
QVERIFY(reader.read(&aniFrame));
QImage img1(QFINDTESTDATA(u"xcursor/wait_%1_1.png"_s.arg(reference)));
img1.convertTo(aniFrame.format());
QVERIFY(imgEquals(aniFrame, img1));
QCOMPARE(reader.nextImageDelay(), 40);
QCOMPARE(reader.text(u"HotspotX"_s), u"48"_s);
QCOMPARE(reader.text(u"HotspotY"_s), u"48"_s);
QVERIFY(reader.canRead());
// that read() above should have advanced us to the next frame
QCOMPARE(reader.currentImageNumber(), 1);
QVERIFY(reader.read(&aniFrame));
QImage img2(QFINDTESTDATA(u"xcursor/wait_%1_2.png"_s.arg(reference)));
img2.convertTo(aniFrame.format());
QVERIFY(imgEquals(aniFrame, img2));
// Would be nice to have a cursor with variable delay and hotspot :-)
QCOMPARE(reader.nextImageDelay(), 40);
QCOMPARE(reader.text(u"HotspotX"_s), u"48"_s);
QCOMPARE(reader.text(u"HotspotY"_s), u"48"_s);
QVERIFY(reader.canRead());
QCOMPARE(reader.currentImageNumber(), 2);
QVERIFY(reader.read(&aniFrame));
QImage img3(QFINDTESTDATA(u"xcursor/wait_%1_3.png"_s.arg(reference)));
img3.convertTo(aniFrame.format());
QVERIFY(imgEquals(aniFrame, img3));
QCOMPARE(reader.text(u"HotspotX"_s), u"48"_s);
QCOMPARE(reader.text(u"HotspotY"_s), u"48"_s);
QCOMPARE(reader.nextImageDelay(), 40);
QVERIFY(reader.canRead());
QCOMPARE(reader.currentImageNumber(), 3);
}
};
QTEST_MAIN(XCursorTests)
#include "xcursortest.moc"

View File

@ -145,6 +145,10 @@ kimageformats_add_plugin(kimg_xcf SOURCES xcf.cpp)
##################################
kimageformats_add_plugin(kimg_xcursor SOURCES xcursor.cpp)
##################################
if (LibRaw_FOUND)
kimageformats_add_plugin(kimg_raw SOURCES raw.cpp)
kde_enable_exceptions()

View File

@ -388,6 +388,9 @@ bool QAVIFHandler::decode_one_frame()
case 2: /* AVIF_COLOR_PRIMARIES_UNSPECIFIED */
colorspace = QColorSpace(QColorSpace::Primaries::SRgb, q_trc, q_trc_gamma);
break;
case 9:
colorspace = QColorSpace(QColorSpace::Primaries::Bt2020, q_trc, q_trc_gamma);
break;
/* AVIF_COLOR_PRIMARIES_SMPTE432 */
case 12:
colorspace = QColorSpace(QColorSpace::Primaries::DciP3D65, q_trc, q_trc_gamma);
@ -740,6 +743,15 @@ bool QAVIFHandler::write(const QImage &image)
/* AVIF_TRANSFER_CHARACTERISTICS_LINEAR */
avif->transferCharacteristics = (avifTransferCharacteristics)8;
break;
case QColorSpace::TransferFunction::Gamma:
if (qAbs(tmpgrayimage.colorSpace().gamma() - 2.2f) < 0.1f) {
/* AVIF_TRANSFER_CHARACTERISTICS_BT470M */
avif->transferCharacteristics = (avifTransferCharacteristics)4;
} else if (qAbs(tmpgrayimage.colorSpace().gamma() - 2.8f) < 0.1f) {
/* AVIF_TRANSFER_CHARACTERISTICS_BT470BG */
avif->transferCharacteristics = (avifTransferCharacteristics)5;
}
break;
case QColorSpace::TransferFunction::SRgb:
/* AVIF_TRANSFER_CHARACTERISTICS_SRGB */
avif->transferCharacteristics = (avifTransferCharacteristics)13;
@ -754,6 +766,42 @@ bool QAVIFHandler::write(const QImage &image)
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
break;
}
if (avif->transferCharacteristics == 2) { // in case TransferFunction was not identified yet
if (tmpgrayimage.colorSpace().colorModel() == QColorSpace::ColorModel::Gray && lossless) {
avif->colorPrimaries = (avifColorPrimaries)2;
avif->matrixCoefficients = (avifMatrixCoefficients)6;
QByteArray iccprofile_gray = tmpgrayimage.colorSpace().iccProfile();
if (iccprofile_gray.size() > 0) {
#if AVIF_VERSION >= 1000000
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
if (res != AVIF_RESULT_OK) {
qCWarning(LOG_AVIFPLUGIN, "ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
return false;
}
#else
avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile_gray.constData()), iccprofile_gray.size());
#endif
}
} else { // convert to grayscale with SRgb
tmpgrayimage.convertToColorSpace(QColorSpace(QPointF(0.3127f, 0.329f), QColorSpace::TransferFunction::SRgb), QImage::Format_Grayscale16);
switch (tmpgrayimage.format()) {
case QImage::Format_Grayscale8:
save_depth = 8;
break;
case QImage::Format_Grayscale16:
save_depth = 10;
avif->transferCharacteristics = (avifTransferCharacteristics)13;
break;
default:
qCWarning(LOG_AVIFPLUGIN, "Error saving Gray image");
return false;
break;
}
}
}
}
if (save_depth > 8) { // QImage::Format_Grayscale16
@ -838,6 +886,10 @@ bool QAVIFHandler::write(const QImage &image)
/* AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL */
matrix_to_save = (avifMatrixCoefficients)12;
break;
case QColorSpace::Primaries::Bt2020:
primaries_to_save = (avifColorPrimaries)9;
matrix_to_save = (avifMatrixCoefficients)12;
break;
default:
/* AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED */
primaries_to_save = (avifColorPrimaries)2;

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
SPDX-FileCopyrightText: 2025-2026 Mirco Miranda <mircomir@outlook.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
@ -10,6 +10,7 @@
* - https://wiki.amigaos.net/wiki/IFF_FORM_and_Chunk_Registry
* - https://www.fileformat.info/format/iff/egff.htm
* - https://download.autodesk.com/us/maya/2010help/index.html (Developer resources -> File formats -> Maya IFF)
* - https://aminet.net/package/dev/misc/IFF-RGFX
*/
#ifndef KIMG_CHUNKS_P_H
@ -55,7 +56,17 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define CMAP_CHUNK QByteArray("CMAP")
#define CMYK_CHUNK QByteArray("CMYK") // https://wiki.amigaos.net/wiki/ILBM_IFF_Interleaved_Bitmap#ILBM.CMYK
#define DPI__CHUNK QByteArray("DPI ")
#define IDAT_CHUNK QByteArray("IDAT")
#define IHDR_CHUNK QByteArray("IHDR")
#define IPAR_CHUNK QByteArray("IPAR")
#define PLTE_CHUNK QByteArray("PLTE")
#define RBOD_CHUNK QByteArray("RBOD")
#define RCOL_CHUNK QByteArray("RCOL")
#define RFLG_CHUNK QByteArray("RFLG")
#define RGHD_CHUNK QByteArray("RGHD")
#define RSCM_CHUNK QByteArray("RSCM")
#define XBMI_CHUNK QByteArray("XBMI")
#define YUVS_CHUNK QByteArray("YUVS")
// Different palette for scanline
#define BEAM_CHUNK QByteArray("BEAM")
@ -79,14 +90,18 @@ Q_DECLARE_LOGGING_CATEGORY(LOG_IFFPLUGIN)
#define FVER_CHUNK QByteArray("FVER")
#define HIST_CHUNK QByteArray("HIST")
#define NAME_CHUNK QByteArray("NAME")
#define VDAT_CHUNK QByteArray("VDAT")
#define VERS_CHUNK QByteArray("VERS")
#define XMP0_CHUNK QByteArray("XMP0") // https://aminet.net/package/docs/misc/IFF-metadata
// FORM types
#define ACBM_FORM_TYPE QByteArray("ACBM")
#define ILBM_FORM_TYPE QByteArray("ILBM")
#define IMAG_FORM_TYPE QByteArray("IMAG")
#define PBM__FORM_TYPE QByteArray("PBM ")
#define RGB8_FORM_TYPE QByteArray("RGB8")
#define RGBN_FORM_TYPE QByteArray("RGBN")
#define RGFX_FORM_TYPE QByteArray("RGFX")
#define CIMG_FOR4_TYPE QByteArray("CIMG")
#define TBMP_FOR4_TYPE QByteArray("TBMP")
@ -424,7 +439,8 @@ public:
enum Compression {
Uncompressed = 0, /**< Image data are uncompressed. */
Rle = 1, /**< Image data are RLE compressed. */
RgbN8 = 4 /**< RGB8/RGBN compresson. */
Vdat = 2, /**< Image data are VDAT compressed. */
RgbN8 = 4 /**< Image data are RGB8/RGBN compressed. */
};
enum Masking {
None = 0, /**< Designates an opaque rectangular image. */
@ -756,10 +772,11 @@ public:
/*!
* \brief readStride
* \param d The device.
* \param header The bitmap header.
* \param y The current scanline.
* \param header The bitmap header.
* \param camg The CAMG chunk (optional)
* \param cmap The CMAP chunk (optional)
* \param ipal The per-line palette chunk (optional)
* \param formType The type of the current form chunk.
* \return The scanline as requested for QImage.
* \warning Call resetStrideRead() once before this one.
@ -776,8 +793,6 @@ public:
* \brief resetStrideRead
* Reset the stride read set the position at the beginning of the data and reset all buffers.
* \param d The device.
* \param header The BMHDChunk chunk (mandatory)
* \param camg The CAMG chunk (optional)
* \return True on success, otherwise false.
* \sa strideRead
* \note Must be called once before strideRead().
@ -788,6 +803,7 @@ public:
* \brief safeModeId
* \param header The header.
* \param camg The CAMG chunk.
* \param cmap The CMAP chunk.
* \return The most likely ModeId if not explicitly specified.
*/
static CAMGChunk::ModeIds safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap = nullptr);
@ -808,6 +824,8 @@ protected:
QByteArray rgbN(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg = nullptr, const CMAPChunk *cmap = nullptr, const IPALChunk *ipal = nullptr) const;
virtual bool innerReadStructure(QIODevice *d) override;
private:
mutable QByteArray _readBuffer;
};
@ -921,6 +939,13 @@ public:
protected:
virtual bool innerReadStructure(QIODevice *d) override;
private:
QImage::Format iffFormat() const;
QImage::Format cdiFormat() const;
QImage::Format rgfxFormat() const;
};
@ -1357,6 +1382,32 @@ protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The VDATChunk class
*/
class VDATChunk : public IFFChunk
{
public:
virtual ~VDATChunk() override;
VDATChunk();
VDATChunk(const VDATChunk& other) = default;
VDATChunk& operator =(const VDATChunk& other) = default;
virtual bool isValid() const override;
CHUNKID_DEFINE(VDAT_CHUNK)
const QByteArray &uncompressedData(const BMHDChunk *header) const;
protected:
virtual bool innerReadStructure(QIODevice *d) override;
private:
mutable QByteArray uncompressed;
};
/*!
* \brief The VERSChunk class
*/
@ -1400,6 +1451,500 @@ protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* *** I-CD IFF CHUNKS ***
*/
/*!
* \brief The IHDRChunk class
* Image Header
*/
class IHDRChunk: public IFFChunk
{
public:
enum Model {
Invalid = 0, /**< Invalid model. */
Rgb888 = 1, /**< red, green, blue - 8 bits per color. */
Rgb555 = 2, /**< Green Book absolute RGB. */
DYuv = 3, /**< Green Book Delta YUV. */
CLut8 = 4, /**< Green Book 8 bit CLUT. */
CLut7 = 5, /**< Green Book 7 bit CLUT. */
CLut4 = 6, /**< Green Book 4 bit CLUT. */
CLut3 = 7, /**< Green Book 3 bit CLUT. */
Rle7 = 8, /**< Green Book runlength coded 7 bit CLUT. */
Rle3 = 9, /**< Green Book runlength coded 3 bit CLUT. */
PaletteOnly = 10 /**< color palette only. */
};
enum DYuvKind {
One = 0,
Each = 1
};
struct Yuv {
Yuv(quint8 y0 = 0, quint8 u0 = 0, quint8 v0 = 0) : y(y0), u(u0), v(v0) {}
quint8 y;
quint8 u;
quint8 v;
};
virtual ~IHDRChunk() override;
IHDRChunk();
IHDRChunk(const IHDRChunk& other) = default;
IHDRChunk& operator =(const IHDRChunk& other) = default;
virtual bool isValid() const override;
/*!
* \brief width
* \return Width of the bitmap in pixels.
*/
qint32 width() const;
/*!
* \brief height
* \return Height of the bitmap in pixels.
*/
qint32 height() const;
/*!
* \brief size
* \return Size in pixels.
*/
QSize size() const;
/*!
* \brief lineSize
* Physical width of image (number of bytes in each scan line, including any data required at
* the end of each scan line for padding [see description of each models IDAT chunk for padding
* rules]) This field is not used when model() = Rle7 or Rle3.
* When model() = Rgb555, this field defines the size of one scan line of the upper
* or lower portion of the pixel data, but not the size of them both together.
*/
qint32 lineSize() const;
/*!
* \brief model
* Image model (coding method)
*/
Model model() const;
/*!
* \brief depth
* Physical size of pixel (number of bits per pixel used in storing image data) When
* model() = Rle7 or Rle3, this value only represents the size of a
* single pixel; the size of a run of pixels is indeterminate.
*/
quint16 depth() const;
/*!
* \brief yuvKind
* if model() = DYuv, indicates whether there is one DYUV start value for all
* scan lines (in yuvStart()), or whether each scan line has its own start value in the
* YUVS chunk which follows.
*/
DYuvKind yuvKind() const;
/*!
* \brief yuvStart
* Start values for DYUV image if model() = DYuv and dYuvKind() = One
*/
Yuv yuvStart() const;
CHUNKID_DEFINE(IHDR_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The IHDRChunk class
*/
class IPARChunk: public IFFChunk
{
public:
struct Rgb {
Rgb(quint8 r0 = 0, quint8 g0 = 0, quint8 b0 = 0) : r(r0), g(g0), b(b0) {}
quint8 r;
quint8 g;
quint8 b;
};
virtual ~IPARChunk() override;
IPARChunk();
IPARChunk(const IPARChunk& other) = default;
IPARChunk& operator =(const IPARChunk& other) = default;
virtual bool isValid() const override;
/*!
* \brief xOffset
* X offset of origin in source image [0 < xOffset() < xPage()]
*/
qint32 xOffset() const;
/*!
* \brief yOffset
* \returnX offset of origin in source image [0 < yOffset() < yPage()]
*/
qint32 yOffset() const;
/*!
* \brief aspectRatio
* Aspect ratio of pixels in source image.
*/
double aspectRatio() const;
/*!
* \brief xPage
* X size of source image.
*/
qint32 xPage() const;
/*!
* \brief yPage
* Y size of source image.
*/
qint32 yPage() const;
/*!
* \brief xGrub
* X location of hot spot within image.
*/
qint32 xGrub() const;
/*!
* \brief yGrub
* Y location of hot spot within image.
*/
qint32 yGrub() const;
/*!
* \brief transparency
* Transparent color.
*/
Rgb transparency() const;
/*!
* \brief mask
* Mask color.
*/
Rgb mask() const;
CHUNKID_DEFINE(IPAR_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The PLTEChunk class
*/
class PLTEChunk : public CMAPChunk
{
public:
virtual ~PLTEChunk() override;
PLTEChunk();
PLTEChunk(const PLTEChunk& other) = default;
PLTEChunk& operator =(const PLTEChunk& other) = default;
virtual bool isValid() const override;
/*!
* \brief count
* \return The number of color in the palette.
*/
virtual qint32 count() const override;
CHUNKID_DEFINE(PLTE_CHUNK)
protected:
qint32 offset() const;
qint32 total() const;
virtual QList<QRgb> innerPalette() const override;
};
/*!
* \brief The YUVSChunk class
*/
class YUVSChunk : public IFFChunk
{
public:
virtual ~YUVSChunk() override;
YUVSChunk();
YUVSChunk(const YUVSChunk& other) = default;
YUVSChunk& operator =(const YUVSChunk& other) = default;
virtual bool isValid() const override;
qint32 count() const;
IHDRChunk::Yuv yuvStart(qint32 y) const;
CHUNKID_DEFINE(YUVS_CHUNK)
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The IDATChunk class
*/
class IDATChunk : public IFFChunk
{
public:
virtual ~IDATChunk() override;
IDATChunk();
IDATChunk(const IDATChunk& other) = default;
IDATChunk& operator =(const IDATChunk& other) = default;
virtual bool isValid() const override;
CHUNKID_DEFINE(IDAT_CHUNK)
/*!
* \brief readStride
* \param d The device.
* \param y The current scanline.
* \param header The bitmap header.
* \param params The additional parameters (optional)
* \return The scanline as requested for QImage.
* \warning Call resetStrideRead() once before this one.
*/
QByteArray strideRead(QIODevice *d,
qint32 y,
const IHDRChunk *header,
const IPARChunk *params = nullptr,
const YUVSChunk *yuvs = nullptr) const;
/*!
* \brief resetStrideRead
* Reset the stride read set the position at the beginning of the data and reset all buffers.
* \param d The device.
* \return True on success, otherwise false.
* \sa strideRead
* \note Must be called once before strideRead().
*/
bool resetStrideRead(QIODevice *d) const;
protected:
quint32 strideSize(const IHDRChunk *header) const;
};
/*!
* *** RGFX IFF CHUNKS ***
*/
/*!
* \brief The RGHDChunk class
*/
class RGHDChunk : public IFFChunk
{
public:
enum Compression {
Uncompressed = 0,
Xpk = 1, /**< any XPK-packer */
Zip = 2 /**< libzip (LZ77/ZIP) compression */
};
enum BitmapType {
Planar8 = 0x0000, /**< unaligned planar 8 bit bitmap */
Chunky8 = 0x0001, /**< unaligned chunky 8 bit bitmap */
Rgb24 = 0x0002, /**< 3-byte 24 bit RGB triples */
Rgb32 = 0x0004, /**< 4-byte 32 bit ARGB quadruples */
Rgb15 = 0x0010, /**< 2-byte 15 bit RGB (x+3x5 bit integer) */
Rgb16 = 0x0020, /**< 2-byte 16 bit ARGB (1+3x5 bit integer) */
Rgb48 = 0x0040, /**< 6-byte 48 bit RGB (3x 16 bit integer) */
Rgb64 = 0x0080, /**< 8-byte 64 bit ARGB (4x 16 bit integer) */
Rgb96 = 0x0100, /**< 12-byte 96 bit RGB (3x 32 bit float) */
Rgb128 = 0x0200, /**< 16-byte 128 bit ARGB (4x 32 bit float) */
HasAlpha = (1 << 30), /**< set if A is meaningful */
HasInvAlpha = (1 << 31) /**< set if A is meaningful but inversed (A = 255 - alpha) */
};
Q_DECLARE_FLAGS(BitmapTypes, BitmapType)
virtual ~RGHDChunk() override;
RGHDChunk();
RGHDChunk(const RGHDChunk&) = default;
RGHDChunk& operator=(const RGHDChunk&) = default;
CHUNKID_DEFINE(RGHD_CHUNK)
virtual bool isValid() const override;
QSize size() const;
qint32 leftEdge() const;
qint32 topEdge() const;
qint32 width() const;
qint32 height() const;
qint32 pageWidth() const;
qint32 pageHeight() const;
quint32 depth() const;
quint32 pixelBits() const;
quint32 bytesPerLine() const;
Compression compression() const;
quint32 xAspect() const;
quint32 yAspect() const;
BitmapTypes bitmapType() const;
double aspectRatio() const;
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The RCOLChunk class
*/
class RCOLChunk : public CMAPChunk
{
public:
virtual ~RCOLChunk() override;
RCOLChunk();
RCOLChunk(const RCOLChunk& other) = default;
RCOLChunk& operator =(const RCOLChunk& other) = default;
virtual bool isValid() const override;
virtual qint32 count() const override;
CHUNKID_DEFINE(RCOL_CHUNK)
protected:
virtual QList<QRgb> innerPalette() const override;
};
/*!
* \brief The RFLGChunk class
*/
class RFLGChunk : public IFFChunk
{
public:
enum class Flag : quint32 {
FromGray = 0x08, /**< created from 8/16 bit gray source so R==G==B */
From8Bit = 0x10, /**< created from 8 bit source, so (R,G,B)&0xFF00 == ... & 0x00FF */
From4Bit = 0x20, /**< created from 4 bit source, so (R,G,B)&0xF0 == ... & 0x0F */
From8BitAlpha = 0x40, /**< 16/32 bit alpha created from 8 bit alpha source */
From16BitAlpha = 0x80 /**< 32 bit alpha created from 16 bit alpha source */
};
Q_DECLARE_FLAGS(Flags, Flag)
virtual ~RFLGChunk() override;
RFLGChunk();
RFLGChunk(const RFLGChunk&) = default;
RFLGChunk& operator=(const RFLGChunk&) = default;
CHUNKID_DEFINE(RFLG_CHUNK)
virtual bool isValid() const override;
Flags flags() const;
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The RSCMChunk class
*/
class RSCMChunk : public IFFChunk
{
public:
virtual ~RSCMChunk() override;
RSCMChunk();
RSCMChunk(const RSCMChunk&) = default;
RSCMChunk& operator=(const RSCMChunk&) = default;
CHUNKID_DEFINE(RSCM_CHUNK)
virtual bool isValid() const override;
/*!
* \brief viewMode Default screenmode
*
* Since HAM modes only can be identified by their ID (or DIPF) you have to make sure,
* that rcsm_ViewMode is OR'ed with HAM_KEY for these (same for EHB and EXTRAHALFBRITE_KEY).
*
* Specific RTG ViewModes will lose their meaning, as soon as graphics are transferred between
* different systems, which is why the two LocalVM entries are considered obsolete.
*
* Always set the obsolete entries to 0xFFFFFFFF and avoid interpreting them.
* \return default screenmode
*/
quint32 viewMode() const;
/*!
* \brief localVM0
* \obsolete obsolete local RTG
*/
quint32 localVM0() const;
/*!
* \brief localVM1
* \obsolete obsolete local RTG
*/
quint32 localVM1() const;
protected:
virtual bool innerReadStructure(QIODevice *d) override;
};
/*!
* \brief The RBODChunk class
*/
class RBODChunk : public IFFChunk
{
public:
virtual ~RBODChunk() override;
RBODChunk();
RBODChunk(const RBODChunk&) = default;
RBODChunk& operator=(const RBODChunk&) = default;
CHUNKID_DEFINE(RBOD_CHUNK)
virtual bool isValid() const override;
QByteArray strideRead(QIODevice *d,
qint32 y,
const RGHDChunk *header,
const RSCMChunk *rcsm = nullptr,
const RCOLChunk *rcol = nullptr) const;
bool resetStrideRead(QIODevice *d) const;
private:
QByteArray deinterleave(const QByteArray &planes, qint32 y, const RGHDChunk *header, const RSCMChunk *rcsm = nullptr, const RCOLChunk *rcol = nullptr) const;
quint32 strideSize(const RGHDChunk *header) const;
};
/*!
* *** UNDOCUMENTED CHUNKS ***

View File

@ -262,14 +262,26 @@ static void addMetadata(QImage &img, const IFOR_Chunk *form)
// if no explicit resolution was found, apply the aspect ratio to the default one
if (!resChanged) {
auto headers = IFFChunk::searchT<BMHDChunk>(form);
if (!headers.isEmpty()) {
auto xr = headers.first()->xAspectRatio();
auto yr = headers.first()->yAspectRatio();
if (xr > 0 && yr > 0 && xr > yr) {
img.setDotsPerMeterX(img.dotsPerMeterX() * yr / xr);
} else if (xr > 0 && yr > 0 && xr < yr) {
img.setDotsPerMeterY(img.dotsPerMeterY() * xr / yr);
if (form->formType() == IMAG_FORM_TYPE) {
auto params = IFFChunk::searchT<IPARChunk>(form);
if (!params.isEmpty()) {
img.setDotsPerMeterY(img.dotsPerMeterY() * params.first()->aspectRatio());
}
} else if (form->formType() == RGFX_FORM_TYPE) {
auto headers = IFFChunk::searchT<RGHDChunk>(form);
if (!headers.isEmpty()) {
img.setDotsPerMeterY(img.dotsPerMeterY() * headers.first()->aspectRatio());
}
} else {
auto headers = IFFChunk::searchT<BMHDChunk>(form);
if (!headers.isEmpty()) {
auto xr = headers.first()->xAspectRatio();
auto yr = headers.first()->yAspectRatio();
if (xr > 0 && yr > 0 && xr > yr) {
img.setDotsPerMeterX(img.dotsPerMeterX() * yr / xr);
} else if (xr > 0 && yr > 0 && xr < yr) {
img.setDotsPerMeterY(img.dotsPerMeterY() * xr / yr);
}
}
}
}
@ -327,7 +339,6 @@ bool IFFHandler::readStandardImage(QImage *image)
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<BMHDChunk>(form);
if (headers.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage(): no supported image found";
return false;
}
@ -426,7 +437,6 @@ bool IFFHandler::readMayaImage(QImage *image)
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<TBHDChunk>(form);
if (headers.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage(): no supported image found";
return false;
}
@ -475,6 +485,132 @@ bool IFFHandler::readMayaImage(QImage *image)
return true;
}
bool IFFHandler::readCDIImage(QImage *image)
{
auto forms = d->searchForms<FORMChunk>();
if (forms.isEmpty()) {
return false;
}
auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1));
auto &&form = forms.at(cin);
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<IHDRChunk>(form);
if (headers.isEmpty()) {
return false;
}
// create the image
auto &&header = headers.first();
auto img = imageAlloc(header->size(), form->format());
if (img.isNull()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while allocating the image";
return false;
}
// set the palette
if (img.format() == QImage::Format_Indexed8) {
auto pltes = IFFChunk::searchT<PLTEChunk>(form);
if (!pltes.isEmpty()) {
img.setColorTable(pltes.first()->palette());
}
}
// decoding the image
auto bodies = IFFChunk::searchT<IDATChunk>(form);
if (bodies.isEmpty()) {
img.fill(0);
} else {
auto &&body = bodies.first();
if (!body->resetStrideRead(device())) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while reading image data";
return false;
}
auto pars = IFFChunk::searchT<IPARChunk>(form);
auto yuvs = IFFChunk::searchT<YUVSChunk>(form);
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
auto ba = body->strideRead(device(), y, header,
pars.isEmpty() ? nullptr : pars.first(),
yuvs.isEmpty() ? nullptr : yuvs.first());
if (ba.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readCDIImage(): error while reading image scanline";
return false;
}
memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size()));
}
}
// set metadata (including image resolution)
addMetadata(img, form);
*image = img;
return true;
}
bool IFFHandler::readRGFXImage(QImage *image)
{
auto forms = d->searchForms<FORMChunk>();
if (forms.isEmpty()) {
return false;
}
auto cin = qBound(0, currentImageNumber(), int(forms.size() - 1));
auto &&form = forms.at(cin);
// show the first one (I don't have a sample with many images)
auto headers = IFFChunk::searchT<RGHDChunk>(form);
if (headers.isEmpty()) {
return false;
}
// create the image
auto &&header = headers.first();
auto img = imageAlloc(header->size(), form->format());
if (img.isNull()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readRGFXImage(): error while allocating the image";
return false;
}
// set the palette
if (img.format() == QImage::Format_Indexed8) {
auto pltes = IFFChunk::searchT<RCOLChunk>(form);
if (!pltes.isEmpty()) {
img.setColorTable(pltes.first()->palette());
}
}
// decoding the image
auto bodies = IFFChunk::searchT<RBODChunk>(form);
if (bodies.isEmpty()) {
img.fill(0);
} else {
auto &&body = bodies.first();
if (!body->resetStrideRead(device())) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readRGFXImage(): error while reading image data";
return false;
}
auto rcsms = IFFChunk::searchT<RSCMChunk>(form);
auto rcols = IFFChunk::searchT<RCOLChunk>(form);
for (auto y = 0, h = img.height(); y < h; ++y) {
auto line = reinterpret_cast<char*>(img.scanLine(y));
auto ba = body->strideRead(device(), y, header,
rcsms.isEmpty() ? nullptr : rcsms.first(),
rcols.isEmpty() ? nullptr : rcols.first());
if (ba.isEmpty()) {
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readRGFXImage(): error while reading image scanline";
return false;
}
memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size()));
}
}
// set metadata (including image resolution)
addMetadata(img, form);
*image = img;
return true;
}
bool IFFHandler::read(QImage *image)
{
if (!d->readStructure(device())) {
@ -490,6 +626,14 @@ bool IFFHandler::read(QImage *image)
return true;
}
if (readCDIImage(image)) {
return true;
}
if (readRGFXImage(image)) {
return true;
}
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read(): no supported image found";
return false;
}

View File

@ -35,6 +35,10 @@ private:
bool readMayaImage(QImage *image);
bool readCDIImage(QImage *image);
bool readRGFXImage(QImage *image);
private:
const QScopedPointer<IFFHandlerPrivate> d;
};

View File

@ -258,14 +258,16 @@ bool QJpegXLHandler::countALLFrames()
bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
JxlColorEncoding color_encoding;
if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
if (jxlcms) {
status = JxlDecoderSetCms(m_decoder, *jxlcms);
if (status != JXL_DEC_SUCCESS) {
qCWarning(LOG_JXLPLUGIN, "JxlDecoderSetCms ERROR");
if (!is_gray) {
const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
if (jxlcms) {
status = JxlDecoderSetCms(m_decoder, *jxlcms);
if (status != JXL_DEC_SUCCESS) {
qCWarning(LOG_JXLPLUGIN, "JxlDecoderSetCms ERROR");
}
} else {
qCWarning(LOG_JXLPLUGIN, "No JPEG XL CMS Interface");
}
} else {
qCWarning(LOG_JXLPLUGIN, "No JPEG XL CMS Interface");
}
JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
@ -853,7 +855,11 @@ bool QJpegXLHandler::write(const QImage &image)
size_t pixel_count = size_t(image.width()) * image.height();
if (MAX_IMAGE_PIXELS && pixel_count > MAX_IMAGE_PIXELS) {
qCWarning(LOG_JXLPLUGIN, "Image (%dx%d) will not be saved because it has more than %d megapixels!", image.width(), image.height(), MAX_IMAGE_PIXELS / 1024 / 1024);
qCWarning(LOG_JXLPLUGIN,
"Image (%dx%d) will not be saved because it has more than %d megapixels!",
image.width(),
image.height(),
MAX_IMAGE_PIXELS / 1024 / 1024);
return false;
}
@ -1838,17 +1844,20 @@ bool QJpegXLHandler::rewind()
return false;
}
const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
if (jxlcms) {
status = JxlDecoderSetCms(m_decoder, *jxlcms);
if (status != JXL_DEC_SUCCESS) {
qCWarning(LOG_JXLPLUGIN, "JxlDecoderSetCms ERROR");
bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
if (!is_gray) {
const JxlCmsInterface *jxlcms = JxlGetDefaultCms();
if (jxlcms) {
status = JxlDecoderSetCms(m_decoder, *jxlcms);
if (status != JXL_DEC_SUCCESS) {
qCWarning(LOG_JXLPLUGIN, "JxlDecoderSetCms ERROR");
}
} else {
qCWarning(LOG_JXLPLUGIN, "No JPEG XL CMS Interface");
}
} else {
qCWarning(LOG_JXLPLUGIN, "No JPEG XL CMS Interface");
}
bool is_gray = m_basicinfo.num_color_channels == 1 && m_basicinfo.alpha_bits == 0;
JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, is_gray ? JXL_TRUE : JXL_FALSE);
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);

View File

@ -0,0 +1,349 @@
/*
* SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <kde@broulik.de>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "xcursor_p.h"
#include <QImage>
#include <QLoggingCategory>
#include <QScopeGuard>
#include <QVariant>
#include <algorithm>
#include "util_p.h"
#ifdef QT_DEBUG
Q_LOGGING_CATEGORY(LOG_XCURSORPLUGIN, "kf.imageformats.plugins.xcursor", QtDebugMsg)
#else
Q_LOGGING_CATEGORY(LOG_XCURSORPLUGIN, "kf.imageformats.plugins.xcursor", QtWarningMsg)
#endif
using namespace Qt::StringLiterals;
static constexpr quint32 XCURSOR_MAGIC = 0x72756358; // "Xcur"
static constexpr quint32 XCURSOR_IMAGE_TYPE = 0xfffd0002;
XCursorHandler::XCursorHandler() = default;
bool XCursorHandler::canRead() const
{
if (canRead(device())) {
setFormat("xcursor");
return true;
}
// Check if there's another frame coming.
QDataStream stream(device());
stream.setByteOrder(QDataStream::LittleEndian);
// no peek on QDataStream...
const auto oldPos = device()->pos();
auto cleanup = qScopeGuard([this, oldPos] {
device()->seek(oldPos);
});
quint32 headerSize, type, subtype, version, width, height, xhot, yhot, delay;
stream >> headerSize >> type >> subtype >> version >> width >> height >> xhot >> yhot >> delay;
if (type != XCURSOR_IMAGE_TYPE || width == 0 || height == 0) {
return false;
}
return true;
}
bool XCursorHandler::read(QImage *outImage)
{
if (!ensureScanned()) {
return false;
}
const auto firstFrameOffset = m_images.value(m_currentSize).first();
if (device()->pos() < firstFrameOffset) {
device()->seek(firstFrameOffset);
}
QDataStream stream(device());
stream.setByteOrder(QDataStream::LittleEndian);
quint32 headerSize, type, subtype, version, width, height, xhot, yhot, delay;
stream >> headerSize >> type >> subtype >> version >> width >> height >> xhot >> yhot >> delay;
if (type != XCURSOR_IMAGE_TYPE || width == 0 || height == 0) {
return false;
}
QImage image = imageAlloc(width, height, QImage::Format_ARGB32);
if (image.isNull()) {
return false;
}
const qsizetype byteCount = width * height * sizeof(quint32);
if (stream.readRawData(reinterpret_cast<char *>(image.bits()), byteCount) != byteCount) {
return false;
}
*outImage = image;
++m_currentImageNumber;
m_nextImageDelay = delay;
m_hotspot = QPoint(xhot, yhot);
return !image.isNull();
}
int XCursorHandler::currentImageNumber() const
{
if (!ensureScanned()) {
return 0;
}
return m_currentImageNumber;
}
int XCursorHandler::imageCount() const
{
if (!ensureScanned()) {
return 0;
}
return m_images.value(m_currentSize).count();
}
bool XCursorHandler::jumpToImage(int imageNumber)
{
if (!ensureScanned()) {
return false;
}
if (imageNumber < 0) {
return false;
}
if (imageNumber == m_currentImageNumber) {
return true;
}
if (imageNumber >= imageCount()) {
return false;
}
if (!device()->seek(m_images.value(m_currentSize).at(imageNumber))) {
return false;
}
return true;
}
bool XCursorHandler::jumpToNextImage()
{
if (!ensureScanned()) {
return false;
}
return jumpToImage(m_currentImageNumber + 1);
}
int XCursorHandler::loopCount() const
{
if (!ensureScanned()) {
return 0;
}
return -1;
}
int XCursorHandler::nextImageDelay() const
{
if (!ensureScanned()) {
return 0;
}
return m_nextImageDelay;
}
bool XCursorHandler::supportsOption(ImageOption option) const
{
return option == Size || option == ScaledSize || option == Description || option == Animation;
}
QVariant XCursorHandler::option(ImageOption option) const
{
if (!supportsOption(option) || !ensureScanned()) {
return QVariant();
}
switch (option) {
case QImageIOHandler::Size:
return QSize(m_currentSize, m_currentSize);
case QImageIOHandler::Description: {
QString description;
if (m_hotspot.has_value()) {
description.append(u"HotspotX: %1\n\n"_s.arg(m_hotspot->x()));
description.append(u"HotspotY: %1\n\n"_s.arg(m_hotspot->y()));
}
// TODO std::transform...
QStringList stringSizes;
stringSizes.reserve(m_images.size());
for (auto it = m_images.keyBegin(); it != m_images.keyEnd(); ++it) {
stringSizes.append(QString::number(*it));
}
description.append(u"Sizes: %1\n\n"_s.arg(stringSizes.join(','_L1)));
return description;
}
case QImageIOHandler::Animation:
return imageCount() > 1;
default:
break;
}
return QVariant();
}
void XCursorHandler::setOption(ImageOption option, const QVariant &value)
{
switch (option) {
case QImageIOHandler::ScaledSize:
m_scaledSize = value.toSize();
pickSize();
break;
default:
break;
}
}
bool XCursorHandler::ensureScanned() const
{
if (m_scanned) {
return true;
}
if (device()->isSequential()) {
return false;
}
auto *mutableThis = const_cast<XCursorHandler *>(this);
const auto oldPos = device()->pos();
auto cleanup = qScopeGuard([this, oldPos] {
device()->seek(oldPos);
});
device()->seek(0);
const QByteArray intro = device()->read(4);
if (intro != "Xcur") {
return false;
}
QDataStream stream(device());
stream.setByteOrder(QDataStream::LittleEndian);
quint32 headerSize, version, ntoc;
stream >> headerSize >> version >> ntoc;
// TODO headerSize
// TODO version
if (!ntoc) {
return false;
}
mutableThis->m_images.clear();
for (quint32 i = 0; i < ntoc; ++i) {
quint32 type, size, position;
stream >> type >> size >> position;
if (type != XCURSOR_IMAGE_TYPE) {
continue;
}
mutableThis->m_images[size].append(position);
}
mutableThis->pickSize();
return !m_images.isEmpty();
}
void XCursorHandler::pickSize()
{
if (m_images.isEmpty()) {
return;
}
// If a scaled size was requested, find the closest match.
const auto sizes = m_images.keys();
// If no scaled size is specified, use the biggest one available.
m_currentSize = sizes.last();
if (!m_scaledSize.isEmpty()) {
// TODO Use some clever algo iterator thing instead of keys()...
const int wantedSize = std::max(m_scaledSize.width(), m_scaledSize.height());
// Prefer downsampling over upsampling.
for (int i = sizes.size() - 1; i >= 0; --i) {
const int size = sizes.at(i);
if (size < wantedSize) {
break;
}
m_currentSize = size;
}
}
}
bool XCursorHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(LOG_XCURSORPLUGIN) << "XCurosorHandler::canRead() called with no device";
return false;
}
if (device->isSequential()) {
return false;
}
const QByteArray intro = device->peek(4 * 4);
if (intro.length() != 4 * 4) {
return false;
}
if (!intro.startsWith("Xcur")) {
return false;
}
// TODO sanity check sizes?
return true;
}
QImageIOPlugin::Capabilities XCursorPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
if (format == "xcursor") {
return Capabilities(CanRead);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && XCursorHandler::canRead(device)) {
cap |= CanRead;
}
return cap;
}
QImageIOHandler *XCursorPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new XCursorHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_xcursor_p.cpp"

View File

@ -0,0 +1,8 @@
{
"Keys": [
"xcursor"
],
"MimeTypes": [
"image/x-xcursor"
]
}

View File

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <kde@broulik.de>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KIMG_XCURSOR_P_H
#define KIMG_XCURSOR_P_H
#include <QImageIOPlugin>
#include <QSize>
#include <optional>
struct XCursorImage {
qint64 offset;
quint32 delay;
};
class XCursorHandler : public QImageIOHandler
{
public:
XCursorHandler();
bool canRead() const override;
bool read(QImage *image) override;
int currentImageNumber() const override;
int imageCount() const override;
bool jumpToImage(int imageNumber) override;
bool jumpToNextImage() override;
int loopCount() const override;
int nextImageDelay() const override;
bool supportsOption(ImageOption option) const override;
QVariant option(ImageOption option) const override;
void setOption(ImageOption option, const QVariant &value) override;
static bool canRead(QIODevice *device);
private:
bool ensureScanned() const;
void pickSize();
bool m_scanned = false;
int m_currentImageNumber = 0;
QSize m_scaledSize;
int m_currentSize = 0;
QMap<int /*size*/, QVector<qint64 /*offset*/>> m_images;
int m_nextImageDelay = 0;
std::optional<QPoint> m_hotspot;
};
class XCursorPlugin : public QImageIOPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "xcursor.json")
public:
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
};
#endif // KIMG_XCURSOR_P_H