Compare commits
1 Commits
v6.25.0
...
work/kbrou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c25cb5b8c |
@@ -1,11 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.29)
|
||||
cmake_minimum_required(VERSION 3.27)
|
||||
|
||||
set(KF_VERSION "6.25.0") # handled by release scripts
|
||||
set(KF_DEP_VERSION "6.25.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})
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 6.25.0 NO_MODULE)
|
||||
find_package(ECM 6.22.0 NO_MODULE)
|
||||
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
|
||||
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
||||
@@ -21,7 +21,7 @@ include(ECMDeprecationSettings)
|
||||
include(CheckIncludeFiles)
|
||||
include(FindPkgConfig)
|
||||
|
||||
set(REQUIRED_QT_VERSION 6.9.0)
|
||||
set(REQUIRED_QT_VERSION 6.8.0)
|
||||
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
find_package(KF6Archive ${KF_DEP_VERSION})
|
||||
@@ -106,7 +106,7 @@ add_feature_info(LibJXR LibJXR_FOUND "required for the QImage plugin for JPEG XR
|
||||
|
||||
ecm_set_disabled_deprecation_versions(
|
||||
QT 6.11.0
|
||||
KF 6.23.0
|
||||
KF 6.21.0
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
@@ -21,7 +21,6 @@ The following image formats have read-only support:
|
||||
- Krita (kra)
|
||||
- OpenRaster (ora)
|
||||
- Pixar raster (pxr)
|
||||
- PlayStation graphics (tim)
|
||||
- Portable FloatMap/HalfMap (pfm, phm)
|
||||
- Photoshop documents (psd, psb, pdd, psdt)
|
||||
- Radiance HDR (hdr)
|
||||
@@ -251,7 +250,6 @@ limit depends on the format encoding).
|
||||
- RAW: 65,535 x 65,535 pixels
|
||||
- RGB: 65,535 x 65,535 pixels
|
||||
- SCT: 300,000 x 300,000 pixels
|
||||
- TIM: 65,535 x 65,535 pixels
|
||||
- TGA: 65,535 x 65,535 pixels
|
||||
- XCF: 300,000 x 300,000 pixels
|
||||
|
||||
|
||||
@@ -86,7 +86,6 @@ kimageformats_read_tests(
|
||||
ras
|
||||
rgb
|
||||
sct
|
||||
tim
|
||||
tga
|
||||
)
|
||||
|
||||
@@ -248,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)
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
################################################################################
|
||||
LDFLAGS=""
|
||||
if [[ $FUZZING_ENGINE == "afl" ]]; then
|
||||
LDFLAGS="-fuse-ld=lld"
|
||||
fi
|
||||
export LDFLAGS
|
||||
|
||||
# build zstd
|
||||
cd $SRC/zstd
|
||||
@@ -180,7 +175,6 @@ HANDLER_TYPES="ANIHandler ani
|
||||
RAWHandler raw
|
||||
RGBHandler rgb
|
||||
ScitexHandler sct
|
||||
TIMHandler tim
|
||||
TGAHandler tga
|
||||
XCFHandler xcf"
|
||||
|
||||
@@ -191,7 +185,7 @@ echo "$HANDLER_TYPES" | while read class format; do
|
||||
/usr/libexec/moc $SRC/kimageformats/src/imageformats/$format.cpp -o $format.moc
|
||||
header=`ls $SRC/kimageformats/src/imageformats/$format*.h`
|
||||
/usr/libexec/moc $header -o moc_`basename $header .h`.cpp
|
||||
$CXX $CXXFLAGS $LDFLAGS -fPIC -DHANDLER=$class -std=c++17 autotests/ossfuzz/kimgio_fuzzer.cc $SRC/kimageformats/src/imageformats/$format.cpp $SRC/kimageformats/src/imageformats/scanlineconverter.cpp $SRC/kimageformats/src/imageformats/microexif.cpp $SRC/kimageformats/src/imageformats/chunks.cpp -o $OUT/$fuzz_target_name -DJXL_STATIC_DEFINE -DJXL_THREADS_STATIC_DEFINE -DJXL_CMS_STATIC_DEFINE -DINITGUID -I $SRC/kimageformats/src/imageformats/ -I $SRC/libavif/include/ -I $SRC/libjxl/build/lib/include/ -I $SRC/libjxl/lib/include/ -I /usr/local/include/OpenEXR/ -I /usr/local/include/KF6/KArchive/ -I /usr/local/include/openjpeg-2.5 -I /usr/local/include/Imath -I $SRC/jxrlib/common/include -I $SRC/jxrlib/jxrgluelib -I $SRC/jxrlib/image/sys -I /usr/include/QtCore/ -I /usr/include/QtGui/ -I . $SRC/libavif/build/libavif.a /usr/local/lib/libheif.a /usr/local/lib/libde265.a /usr/local/lib/libopenh264.a $SRC/aom/build.libavif/libaom.a $SRC/libjxl/build/lib/libjxl_threads.a $SRC/libjxl/build/lib/libjxl.a $SRC/libjxl/build/lib/libjxl_cms.a $SRC/libjxl/build/third_party/highway/libhwy.a $SRC/libjxl/build/third_party/brotli/libbrotlidec.a $SRC/libjxl/build/third_party/brotli/libbrotlienc.a $SRC/libjxl/build/third_party/brotli/libbrotlicommon.a -lQt6Gui -lQt6Core -lQt6BundledLibpng -lQt6BundledHarfbuzz -lm -lQt6BundledPcre2 -ldl -lpthread $LIB_FUZZING_ENGINE /usr/local/lib/libz.a /usr/local/lib/x86_64-linux-gnu/libKF6Archive.a /usr/local/lib/libz.a /usr/local/lib/libraw.a /usr/local/lib/libOpenEXR-3_3.a /usr/local/lib/libIex-3_3.a /usr/local/lib/libImath-3_1.a /usr/local/lib/libIlmThread-3_3.a /usr/local/lib/libOpenEXRCore-3_3.a /usr/local/lib/libOpenEXRUtil-3_3.a /usr/local/lib/libopenjp2.a /usr/local/lib/libzstd.a $SRC/jxrlib/build/libjxrglue.a $SRC/jxrlib/build/libjpegxr.a /usr/local/lib/liblzma.a /usr/local/lib/libbz2.a -lclang_rt.builtins
|
||||
$CXX $CXXFLAGS -fPIC -DHANDLER=$class -std=c++17 autotests/ossfuzz/kimgio_fuzzer.cc $SRC/kimageformats/src/imageformats/$format.cpp $SRC/kimageformats/src/imageformats/scanlineconverter.cpp $SRC/kimageformats/src/imageformats/microexif.cpp $SRC/kimageformats/src/imageformats/chunks.cpp -o $OUT/$fuzz_target_name -DJXL_STATIC_DEFINE -DJXL_THREADS_STATIC_DEFINE -DJXL_CMS_STATIC_DEFINE -DINITGUID -I $SRC/kimageformats/src/imageformats/ -I $SRC/libavif/include/ -I $SRC/libjxl/build/lib/include/ -I $SRC/libjxl/lib/include/ -I /usr/local/include/OpenEXR/ -I /usr/local/include/KF6/KArchive/ -I /usr/local/include/openjpeg-2.5 -I /usr/local/include/Imath -I $SRC/jxrlib/common/include -I $SRC/jxrlib/jxrgluelib -I $SRC/jxrlib/image/sys -I /usr/include/QtCore/ -I /usr/include/QtGui/ -I . $SRC/libavif/build/libavif.a /usr/local/lib/libheif.a /usr/local/lib/libde265.a /usr/local/lib/libopenh264.a $SRC/aom/build.libavif/libaom.a $SRC/libjxl/build/lib/libjxl_threads.a $SRC/libjxl/build/lib/libjxl.a $SRC/libjxl/build/lib/libjxl_cms.a $SRC/libjxl/build/third_party/highway/libhwy.a $SRC/libjxl/build/third_party/brotli/libbrotlidec.a $SRC/libjxl/build/third_party/brotli/libbrotlienc.a $SRC/libjxl/build/third_party/brotli/libbrotlicommon.a -lQt6Gui -lQt6Core -lQt6BundledLibpng -lQt6BundledHarfbuzz -lm -lQt6BundledPcre2 -ldl -lpthread $LIB_FUZZING_ENGINE /usr/local/lib/libz.a -lKF6Archive /usr/local/lib/libz.a /usr/local/lib/libraw.a /usr/local/lib/libOpenEXR-3_3.a /usr/local/lib/libIex-3_3.a /usr/local/lib/libImath-3_1.a /usr/local/lib/libIlmThread-3_3.a /usr/local/lib/libOpenEXRCore-3_3.a /usr/local/lib/libOpenEXRUtil-3_3.a /usr/local/lib/libopenjp2.a /usr/local/lib/libzstd.a $SRC/jxrlib/build/libjxrglue.a $SRC/jxrlib/build/libjpegxr.a -llzma /usr/local/lib/libbz2.a -lclang_rt.builtins
|
||||
|
||||
# -lclang_rt.builtins in the previous line is a temporary workaround to avoid a linker error "undefined reference to __truncsfhf2". Investigate why this is needed here, but not anywhere else, and possibly remove it.
|
||||
|
||||
|
||||
@@ -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|hdr|heif|iff|jp2|jxl|jxr|kra|ora|pcx|pfm|pic|psd|pxr|qoi|ras|raw|rgb|sct|tga|xcf]_fuzzer
|
||||
*/
|
||||
|
||||
#include <QBuffer>
|
||||
@@ -52,7 +52,6 @@
|
||||
#include "raw_p.h"
|
||||
#include "rgb_p.h"
|
||||
#include "sct_p.h"
|
||||
#include "tim_p.h"
|
||||
#include "tga_p.h"
|
||||
#include "xcf_p.h"
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ git clone --depth 1 -b master https://invent.kde.org/frameworks/extra-cmake-modu
|
||||
git clone --depth 1 --branch=dev git://code.qt.io/qt/qtbase.git
|
||||
git clone --depth 1 --branch=dev git://code.qt.io/qt/qttools.git
|
||||
git clone --depth 1 -b master https://invent.kde.org/frameworks/karchive.git
|
||||
git clone --depth 1 -b v3.13.1 https://aomedia.googlesource.com/aom
|
||||
git clone --depth 1 -b v1.3.0 https://github.com/AOMediaCodec/libavif.git
|
||||
git clone --depth 1 -b v3.12.0 https://aomedia.googlesource.com/aom
|
||||
git clone --depth 1 -b v1.2.1 https://github.com/AOMediaCodec/libavif.git
|
||||
git clone --depth 1 https://github.com/strukturag/libde265.git
|
||||
git clone --depth 1 -b v2.5.4 https://github.com/uclouvain/openjpeg.git
|
||||
git clone --depth 1 -b v2.5.3 https://github.com/uclouvain/openjpeg.git
|
||||
git clone --depth 1 https://github.com/strukturag/libheif.git
|
||||
git clone --depth=1 --recursive --shallow-submodules https://github.com/libjxl/libjxl.git
|
||||
git clone --depth 1 https://github.com/LibRaw/LibRaw
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
BIN
autotests/xcursor/wait
Normal file
BIN
autotests/xcursor/wait_24_1.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
autotests/xcursor/wait_24_2.png
Normal file
|
After Width: | Height: | Size: 906 B |
BIN
autotests/xcursor/wait_24_3.png
Normal file
|
After Width: | Height: | Size: 943 B |
BIN
autotests/xcursor/wait_48_1.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
autotests/xcursor/wait_48_2.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
autotests/xcursor/wait_48_3.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
autotests/xcursor/wait_72_1.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
autotests/xcursor/wait_72_2.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
autotests/xcursor/wait_72_3.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
129
autotests/xcursortest.cpp
Normal 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"
|
||||
@@ -137,10 +137,6 @@ kimageformats_add_plugin(kimg_sct SOURCES sct.cpp)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_tim SOURCES tim.cpp)
|
||||
|
||||
##################################
|
||||
|
||||
kimageformats_add_plugin(kimg_tga SOURCES tga.cpp microexif.cpp scanlineconverter.cpp)
|
||||
|
||||
##################################
|
||||
@@ -149,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()
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
#include "ani_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QLoggingCategory>
|
||||
@@ -102,7 +101,7 @@ bool ANIHandler::read(QImage *outImage)
|
||||
}
|
||||
|
||||
const auto frameSize = *(reinterpret_cast<const quint32_le *>(frameSizeData.data()));
|
||||
if (!frameSize || frameSize > quint32(kMaxQVectorSize)) {
|
||||
if (!frameSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -418,9 +417,6 @@ bool ANIHandler::ensureScanned() const
|
||||
// IART and INAM are technically inside LIST->INFO but "INFO" is supposedly optional
|
||||
// so just handle those two attributes wherever we encounter them
|
||||
} else if (chunkId == "INAM" || chunkId == "IART") {
|
||||
if (chunkSize > kMaxQVectorSize) {
|
||||
return false;
|
||||
}
|
||||
const QByteArray value = device()->read(chunkSize);
|
||||
|
||||
if (static_cast<quint32_le>(value.size()) != chunkSize) {
|
||||
|
||||
@@ -513,12 +513,21 @@ bool QAVIFHandler::decode_one_frame()
|
||||
#else
|
||||
switch (m_decoder->image->imir.axis) {
|
||||
#endif
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
||||
case 0: // top-to-bottom
|
||||
result = result.mirrored(false, true);
|
||||
break;
|
||||
case 1: // left-to-right
|
||||
result = result.mirrored(true, false);
|
||||
break;
|
||||
#else
|
||||
case 0: // top-to-bottom
|
||||
result = result.flipped(Qt::Vertical);
|
||||
break;
|
||||
case 1: // left-to-right
|
||||
result = result.flipped(Qt::Horizontal);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1080,7 +1080,6 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
|
||||
// (red and green modify operations are unavailable)
|
||||
auto ctlbits = bitplanes > 5 ? 2 : 1;
|
||||
auto max = (1 << (bitplanes - ctlbits)) - 1;
|
||||
auto wrongIdx = false;
|
||||
quint8 prev[3] = {};
|
||||
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
|
||||
for (qint32 j = 0; j < 8; ++j, ++cnt) {
|
||||
@@ -1112,7 +1111,7 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
|
||||
prev[1] = qGreen(pal.at(idx));
|
||||
prev[2] = qBlue(pal.at(idx));
|
||||
} else {
|
||||
wrongIdx = true;
|
||||
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1122,9 +1121,6 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
|
||||
ba[cnt3 + 2] = char(prev[2]);
|
||||
}
|
||||
}
|
||||
if (wrongIdx) {
|
||||
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): HAM palette index out of range!";
|
||||
}
|
||||
} else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap) &&
|
||||
(bitplanes >= BITPLANES_HALFBRIDE_MIN && bitplanes <= BITPLANES_HALFBRIDE_MAX)) {
|
||||
// From A Quick Introduction to IFF.txt:
|
||||
@@ -1137,7 +1133,6 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
|
||||
// absolute colors.
|
||||
ba = QByteArray(rowLen * 8, char());
|
||||
auto palSize = cmap->count();
|
||||
auto wrongIdx = false;
|
||||
for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
|
||||
for (qint32 j = 0; j < 8; ++j, ++cnt) {
|
||||
quint8 idx = 0, ctl = 0;
|
||||
@@ -1152,13 +1147,10 @@ QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMH
|
||||
if (idx < palSize) {
|
||||
ba[cnt] = ctl ? idx + palSize : idx;
|
||||
} else {
|
||||
wrongIdx = true;
|
||||
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrongIdx) {
|
||||
qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): HalfBrite palette index out of range!";
|
||||
}
|
||||
} else {
|
||||
// From A Quick Introduction to IFF.txt:
|
||||
//
|
||||
@@ -2546,7 +2538,7 @@ static QByteArray decompressVdat(const QByteArray &comp)
|
||||
static QByteArray vdatToIlbmPlane(const QByteArray &vdatData, const BMHDChunk *header)
|
||||
{
|
||||
QByteArray ba(vdatData.size(), char());
|
||||
auto rowLen = qint32(header->rowLen());
|
||||
auto rowLen = header->rowLen();
|
||||
for (auto x = 0, n = 0; x < rowLen; x += 2) {
|
||||
for (auto y = 0, off = x, h = header->height(); y < h; y++, off += rowLen) {
|
||||
if ((off + 1 >= ba.size()) || n + 1 >= vdatData.size()) {
|
||||
@@ -2852,7 +2844,7 @@ bool PLTEChunk::isValid() const
|
||||
if (dataBytes() < 4) {
|
||||
return false;
|
||||
}
|
||||
if (dataBytes() - 4 < quint32(total()) * 3) {
|
||||
if (dataBytes() - 4 < total() * 3) {
|
||||
return false;
|
||||
}
|
||||
return chunkId() == PLTEChunk::defaultChunkId();
|
||||
@@ -3012,7 +3004,7 @@ QByteArray IDATChunk::strideRead(QIODevice *d, qint32 y, const IHDRChunk *header
|
||||
}
|
||||
|
||||
if (header->model() == IHDRChunk::CLut4) {
|
||||
if (rr.size() < (qint64(header->width()) + 1) / 2) {
|
||||
if (rr.size() < header->width() / 2) {
|
||||
return {};
|
||||
}
|
||||
QByteArray tmp(header->width(), char());
|
||||
@@ -3092,8 +3084,7 @@ quint32 IDATChunk::strideSize(const IHDRChunk *header) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
// width() and depth() are at most 65535
|
||||
auto rs = (quint32(header->width()) * header->depth() + 7) / 8;
|
||||
auto rs = (header->width() * header->depth() + 7) / 8;
|
||||
|
||||
// No padding bytes are inserted in the data.
|
||||
if (header->model() == IHDRChunk::Rgb888) {
|
||||
|
||||
@@ -474,7 +474,11 @@ bool IFFHandler::readMayaImage(QImage *image)
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
painter.drawImage(tp, ti);
|
||||
}
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
||||
img.mirror(false, true);
|
||||
#else
|
||||
img.flip(Qt::Orientation::Vertical);
|
||||
#endif
|
||||
addMetadata(img, form);
|
||||
|
||||
*image = img;
|
||||
|
||||
@@ -253,27 +253,20 @@ public:
|
||||
bool jp2ToImage(QImage *img) const
|
||||
{
|
||||
Q_ASSERT(img->depth() == 8 * sizeof(T) || img->depth() == 32 * sizeof(T));
|
||||
if (img->width() < 1 || img->height() < 1) {
|
||||
return false;
|
||||
}
|
||||
auto maxChannels = qint32(img->bytesPerLine() / sizeof(T) / img->width());
|
||||
for (qint32 c = 0, cc = std::min(qint32(m_jp2_image->numcomps), maxChannels); c < cc; ++c) {
|
||||
auto cs = std::min(cc == 1 ? 1 : 4, maxChannels);
|
||||
for (qint32 c = 0, cc = m_jp2_image->numcomps; c < cc; ++c) {
|
||||
auto cs = cc == 1 ? 1 : 4;
|
||||
auto &&jc = m_jp2_image->comps[c];
|
||||
if (jc.data == nullptr) {
|
||||
if (jc.data == nullptr)
|
||||
return false;
|
||||
}
|
||||
if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height()) {
|
||||
if (qint32(jc.w) != img->width() || qint32(jc.h) != img->height())
|
||||
return false;
|
||||
}
|
||||
|
||||
// discriminate between int and float (avoid complicating things by creating classes with template specializations)
|
||||
if (std::numeric_limits<T>::is_integer) {
|
||||
auto divisor = 1ull;
|
||||
auto prec = std::min(size_t(jc.prec), sizeof(*jc.data) * 8);
|
||||
if (prec > sizeof(T) * 8 && prec < 64) {
|
||||
auto divisor = 1;
|
||||
if (jc.prec > sizeof(T) * 8) {
|
||||
// convert to the wanted precision (e.g. 16-bit -> 8-bit: divisor = 65535 / 255 = 257)
|
||||
divisor = std::max(1ull, (((1ull << prec) - 1) / ((1ull << (sizeof(T) * 8)) - 1)));
|
||||
divisor = std::max(1, int(((1ll << jc.prec) - 1) / ((1ll << (sizeof(T) * 8)) - 1)));
|
||||
}
|
||||
for (qint32 y = 0, h = img->height(); y < h; ++y) {
|
||||
auto ptr = reinterpret_cast<T *>(img->scanLine(y));
|
||||
|
||||
@@ -2008,11 +2008,6 @@ bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rawboxsize > 8388608) { // 8MB limit
|
||||
qCWarning(LOG_JXLPLUGIN, "Skipped decoding of big JXL metadata box");
|
||||
return true;
|
||||
}
|
||||
|
||||
output.resize(rawboxsize);
|
||||
status = JxlDecoderSetBoxBuffer(m_decoder, reinterpret_cast<uint8_t *>(output.data()), output.size());
|
||||
if (status != JXL_DEC_SUCCESS) {
|
||||
@@ -2026,7 +2021,7 @@ bool QJpegXLHandler::extractBox(QByteArray &output, size_t container_size)
|
||||
if (status == JXL_DEC_BOX_NEED_MORE_OUTPUT) {
|
||||
size_t bytes_remains = JxlDecoderReleaseBoxBuffer(m_decoder);
|
||||
|
||||
if (output.size() > 33554432) { // approx. 32MB (4*8) limit for decompressed metadata box
|
||||
if (output.size() > 4194304) { // approx. 4MB limit for decompressed metadata box
|
||||
qCWarning(LOG_JXLPLUGIN, "JXL metadata box is too large");
|
||||
m_parseState = ParseJpegXLError;
|
||||
return false;
|
||||
|
||||
@@ -1017,7 +1017,7 @@ inline void rawChannelCopy(uchar *target, qint32 targetChannels, qint32 targetCh
|
||||
|
||||
|
||||
template<class T>
|
||||
inline bool cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
|
||||
inline void cmykToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
|
||||
{
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<T*>(target);
|
||||
@@ -1026,7 +1026,7 @@ inline bool cmykToRgb(uchar *target, qint32 targetChannels, const char *source,
|
||||
|
||||
if (sourceChannels < 2) {
|
||||
qCDebug(LOG_PSDPLUGIN) << "cmykToRgb: image is not a valid MCH/CMYK!";
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (qint32 w = 0; w < width; ++w) {
|
||||
@@ -1047,7 +1047,6 @@ inline bool cmykToRgb(uchar *target, qint32 targetChannels, const char *source,
|
||||
*(pt + 3) = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline double finv(double v)
|
||||
@@ -1067,7 +1066,7 @@ inline double gammaCorrection(double linear)
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline bool labToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
|
||||
inline void labToRgb(uchar *target, qint32 targetChannels, const char *source, qint32 sourceChannels, qint32 width, bool alpha = false)
|
||||
{
|
||||
auto s = reinterpret_cast<const T*>(source);
|
||||
auto t = reinterpret_cast<T*>(target);
|
||||
@@ -1076,7 +1075,7 @@ inline bool labToRgb(uchar *target, qint32 targetChannels, const char *source, q
|
||||
|
||||
if (sourceChannels < 3) {
|
||||
qCDebug(LOG_PSDPLUGIN) << "labToRgb: image is not a valid LAB!";
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (qint32 w = 0; w < width; ++w) {
|
||||
@@ -1111,7 +1110,6 @@ inline bool labToRgb(uchar *target, qint32 targetChannels, const char *source, q
|
||||
*(pt + 3) = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize, quint16 compression)
|
||||
@@ -1452,13 +1450,10 @@ bool PSDHandler::read(QImage *image)
|
||||
// Conversion to RGB
|
||||
if (header.color_mode == CM_CMYK || header.color_mode == CM_MULTICHANNEL) {
|
||||
if (tmpCmyk.isNull()) {
|
||||
if (header.depth == 8) {
|
||||
if (!cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
|
||||
return false;
|
||||
} else if (header.depth == 16) {
|
||||
if (!cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
|
||||
return false;
|
||||
}
|
||||
if (header.depth == 8)
|
||||
cmykToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
else if (header.depth == 16)
|
||||
cmykToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
} else if (header.depth == 8) {
|
||||
rawChannelsCopyToCMYK<quint8>(tmpCmyk.bits(), 4, psdScanline.data(), header.channel_count, header.width);
|
||||
if (auto rgbPtr = iccConv.convertedScanLine(tmpCmyk, 0))
|
||||
@@ -1474,13 +1469,10 @@ bool PSDHandler::read(QImage *image)
|
||||
}
|
||||
}
|
||||
if (header.color_mode == CM_LABCOLOR) {
|
||||
if (header.depth == 8) {
|
||||
if (!labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
|
||||
return false;
|
||||
} else if (header.depth == 16) {
|
||||
if (!labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha))
|
||||
return false;
|
||||
}
|
||||
if (header.depth == 8)
|
||||
labToRgb<quint8>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
else if (header.depth == 16)
|
||||
labToRgb<quint16>(img.scanLine(y), imgChannels, psdScanline.data(), header.channel_count, header.width, alpha);
|
||||
}
|
||||
if (header.color_mode == CM_RGB) {
|
||||
if (header.depth == 8)
|
||||
|
||||
@@ -319,10 +319,7 @@ bool SGIImagePrivate::readImage(QImage &img)
|
||||
|
||||
if (_rle) {
|
||||
uint l;
|
||||
_starttab = new (std::nothrow) quint32[_numrows];
|
||||
if (_starttab == nullptr) {
|
||||
return false;
|
||||
}
|
||||
_starttab = new quint32[_numrows];
|
||||
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
|
||||
_stream >> _starttab[l];
|
||||
_starttab[l] -= 512 + _numrows * 2 * sizeof(quint32);
|
||||
@@ -334,10 +331,7 @@ bool SGIImagePrivate::readImage(QImage &img)
|
||||
_starttab[l] = 0;
|
||||
}
|
||||
|
||||
_lengthtab = new (std::nothrow) quint32[_numrows];
|
||||
if (_lengthtab == nullptr) {
|
||||
return false;
|
||||
}
|
||||
_lengthtab = new quint32[_numrows];
|
||||
for (l = 0; !_stream.atEnd() && l < _numrows; l++) {
|
||||
_stream >> _lengthtab[l];
|
||||
if (_stream.status() != QDataStream::Ok) {
|
||||
@@ -800,10 +794,7 @@ bool SGIImagePrivate::writeImage(const QImage &image)
|
||||
_pixmax = 0;
|
||||
_colormap = NORMAL;
|
||||
_numrows = _ysize * _zsize;
|
||||
_starttab = new (std::nothrow) quint32[_numrows];
|
||||
if (_starttab == nullptr) {
|
||||
return false;
|
||||
}
|
||||
_starttab = new quint32[_numrows];
|
||||
_rlemap.setBaseOffset(512 + _numrows * 2 * sizeof(quint32));
|
||||
|
||||
if (!scanData(image, tfmt, tcs)) {
|
||||
|
||||
@@ -731,7 +731,7 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img)
|
||||
if (div == 0)
|
||||
hasAlpha = false;
|
||||
for (int x = x_start; x != x_end; x += x_step) {
|
||||
const int alpha = hasAlpha ? int(quint8(src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
|
||||
const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
|
||||
scanline[x] = qRgba(src[2], src[1], src[0], alpha);
|
||||
src += 4;
|
||||
}
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "tim_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QImage>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(LOG_TIMPLUGIN)
|
||||
Q_LOGGING_CATEGORY(LOG_TIMPLUGIN, "kf.imageformats.plugins.tim", QtWarningMsg)
|
||||
|
||||
#define TYPE_4BPP 0 // never seen
|
||||
#define TYPE_IDX_4BPP 8
|
||||
#define TYPE_8BPP 1 // never seen
|
||||
#define TYPE_IDX_8BPP 9
|
||||
#define TYPE_16BPP 2
|
||||
#define TYPE_24BPP 3
|
||||
|
||||
#define HEADER_SIZE 20
|
||||
|
||||
class TIMHeader
|
||||
{
|
||||
private:
|
||||
QByteArray m_rawHeader;
|
||||
|
||||
quint16 ui16(quint8 c1, quint8 c2) const {
|
||||
return (quint16(c2) << 8) | quint16(c1);
|
||||
}
|
||||
|
||||
quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const {
|
||||
return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1);
|
||||
}
|
||||
|
||||
public:
|
||||
TIMHeader()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
quint32 type() const
|
||||
{
|
||||
if (m_rawHeader.size() < HEADER_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
return ui32(m_rawHeader.at(4), m_rawHeader.at(5), m_rawHeader.at(6), m_rawHeader.at(7)) & 0xF;
|
||||
}
|
||||
|
||||
quint32 offset() const
|
||||
{
|
||||
if (m_rawHeader.size() < HEADER_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
auto o = quint32(HEADER_SIZE);
|
||||
auto t = type();
|
||||
if (t == TYPE_IDX_4BPP || t == TYPE_IDX_8BPP) { // indexed
|
||||
o += ui32(m_rawHeader.at(8), m_rawHeader.at(9), m_rawHeader.at(10), m_rawHeader.at(11));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
bool isValid(quint32 size = 0) const
|
||||
{
|
||||
if (m_rawHeader.size() < HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
if (size == 0) {
|
||||
size = offset();
|
||||
}
|
||||
if (m_rawHeader.size() < size) {
|
||||
return false;
|
||||
}
|
||||
return (m_rawHeader.startsWith(QByteArray::fromRawData("\x10\x00\x00\x00", 4)));
|
||||
}
|
||||
|
||||
bool isSupported() const
|
||||
{
|
||||
return format() != QImage::Format_Invalid;
|
||||
}
|
||||
|
||||
qint32 width() const
|
||||
{
|
||||
auto strideLen = strideSize();
|
||||
auto t = type();
|
||||
if (t == TYPE_4BPP || t == TYPE_IDX_4BPP) {
|
||||
return strideLen * 2;
|
||||
}
|
||||
if (t == TYPE_8BPP || t == TYPE_IDX_8BPP) {
|
||||
return strideLen;
|
||||
}
|
||||
if (t == TYPE_24BPP) {
|
||||
return strideLen / 3;
|
||||
}
|
||||
return strideLen / 2;
|
||||
}
|
||||
|
||||
qint32 height() const
|
||||
{
|
||||
auto o = offset();
|
||||
if (!isValid(o)) {
|
||||
return 0;
|
||||
}
|
||||
return qint32(ui16(m_rawHeader.at(o - 2), m_rawHeader.at(o - 1)));
|
||||
}
|
||||
|
||||
QSize size() const
|
||||
{
|
||||
return QSize(width(), height());
|
||||
}
|
||||
|
||||
QImage::Format format() const
|
||||
{
|
||||
auto t = type();
|
||||
if (t == TYPE_IDX_4BPP || t == TYPE_IDX_8BPP || t == TYPE_4BPP) {
|
||||
return QImage::Format_Indexed8;
|
||||
}
|
||||
if (t == TYPE_IDX_8BPP) {
|
||||
return QImage::Format_Grayscale8;
|
||||
}
|
||||
if (t == TYPE_16BPP) {
|
||||
return QImage::Format_RGB555;
|
||||
}
|
||||
if (t == TYPE_24BPP) {
|
||||
return QImage::Format_RGB888;
|
||||
}
|
||||
return QImage::Format_Invalid;
|
||||
}
|
||||
|
||||
quint32 strideSize() const
|
||||
{
|
||||
auto o = offset();
|
||||
if (!isValid(o)) {
|
||||
return 0;
|
||||
}
|
||||
return ui16(m_rawHeader.at(o - 4), m_rawHeader.at(o - 3)) * 2;
|
||||
}
|
||||
|
||||
qint32 paletteColors() const
|
||||
{
|
||||
if (this->format() != QImage::Format_Indexed8) {
|
||||
return 0;
|
||||
}
|
||||
return qint32(ui16(m_rawHeader.at(16), m_rawHeader.at(17)));
|
||||
}
|
||||
|
||||
qint32 paletteCount() const
|
||||
{
|
||||
if (this->format() != QImage::Format_Indexed8) {
|
||||
return 0;
|
||||
}
|
||||
return qint32(ui16(m_rawHeader.at(18), m_rawHeader.at(19)));
|
||||
}
|
||||
|
||||
QList<QRgb> palette() const
|
||||
{
|
||||
if (format() != QImage::Format_Indexed8) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 4bpp without CLUT is treated as indexed
|
||||
if (type() == TYPE_4BPP) {
|
||||
QList<QRgb> pal;
|
||||
for (auto i = 0; i < 16; ++i) {
|
||||
auto v = i * 17;
|
||||
pal << qRgb(v, v, v);
|
||||
}
|
||||
return pal;
|
||||
}
|
||||
|
||||
// read the first paette only
|
||||
auto len = paletteColors();
|
||||
if (!isValid(HEADER_SIZE + len * 2)) {
|
||||
return {};
|
||||
}
|
||||
QList<QRgb> clut;
|
||||
for (auto i = 0; i < len; ++i) {
|
||||
auto v = ui16(m_rawHeader.at(HEADER_SIZE + i * 2), m_rawHeader.at(HEADER_SIZE + i * 2 + 1));
|
||||
// in some specs, the bit 15 is the alpha but with the image sample used, transparencies appear
|
||||
// where there shouldn't be any (so, disabled for now)
|
||||
clut << qRgba((v & 0x1F) * 255 / 31, ((v >> 5) & 0x1F) * 255 / 31, ((v >> 10) & 0x1F) * 255 / 31, 255);
|
||||
}
|
||||
return clut;
|
||||
}
|
||||
|
||||
bool read(QIODevice *d)
|
||||
{
|
||||
m_rawHeader = d->read(HEADER_SIZE);
|
||||
if (m_rawHeader.size() != HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
auto o = offset() - HEADER_SIZE;
|
||||
if (o > kMaxQVectorSize - HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
m_rawHeader.append(d->read(o));
|
||||
return isValid();
|
||||
}
|
||||
|
||||
bool peek(QIODevice *d)
|
||||
{
|
||||
m_rawHeader = d->peek(HEADER_SIZE);
|
||||
if (m_rawHeader.size() != HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
auto o = offset();
|
||||
if (o > kMaxQVectorSize - HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
if (o > m_rawHeader.size()) {
|
||||
m_rawHeader = d->peek(o);
|
||||
}
|
||||
return isValid();
|
||||
}
|
||||
|
||||
bool jumpToImageData(QIODevice *d) const
|
||||
{
|
||||
if (d->isSequential()) {
|
||||
if (auto sz = std::max(offset() - quint32(m_rawHeader.size()), quint32())) {
|
||||
return d->read(sz).size() == sz;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return d->seek(offset());
|
||||
}
|
||||
};
|
||||
|
||||
class TIMHandlerPrivate
|
||||
{
|
||||
public:
|
||||
TIMHandlerPrivate() {}
|
||||
~TIMHandlerPrivate() {}
|
||||
|
||||
TIMHeader m_header;
|
||||
};
|
||||
|
||||
TIMHandler::TIMHandler()
|
||||
: QImageIOHandler()
|
||||
, d(new TIMHandlerPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
bool TIMHandler::canRead() const
|
||||
{
|
||||
if (canRead(device())) {
|
||||
setFormat("tim");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TIMHandler::canRead(QIODevice *device)
|
||||
{
|
||||
if (!device) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::canRead() called with no device";
|
||||
return false;
|
||||
}
|
||||
|
||||
TIMHeader h;
|
||||
if (!h.peek(device)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return h.isSupported();
|
||||
}
|
||||
|
||||
bool TIMHandler::read(QImage *image)
|
||||
{
|
||||
auto&& header = d->m_header;
|
||||
|
||||
if (!header.read(device())) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() invalid header";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto img = imageAlloc(header.size(), header.format());
|
||||
if (img.isNull()) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while allocating the image";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (img.format() == QImage::Format_Indexed8) {
|
||||
auto pal = header.palette();
|
||||
if (pal.isEmpty()) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while reading the palette";
|
||||
return false;
|
||||
}
|
||||
img.setColorTable(pal);
|
||||
}
|
||||
|
||||
auto d = device();
|
||||
if (!header.jumpToImageData(d)) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while seeking image data";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto size = std::min(img.bytesPerLine(), qsizetype(header.strideSize()));
|
||||
QByteArray tmpBuff;
|
||||
auto conv_4bpp = (header.type() == TYPE_4BPP || header.type() == TYPE_IDX_4BPP);
|
||||
if (conv_4bpp && size * 2 <= img.bytesPerLine()) {
|
||||
tmpBuff.resize(size);
|
||||
}
|
||||
for (auto y = 0, h = img.height(); y < h; ++y) {
|
||||
auto line = reinterpret_cast<char*>(img.scanLine(y));
|
||||
auto tbuf = tmpBuff.isEmpty() ? line : tmpBuff.data();
|
||||
if (d->read(tbuf, size) != size) {
|
||||
qCWarning(LOG_TIMPLUGIN) << "TIMHandler::read() error while reading image scanline";
|
||||
return false;
|
||||
}
|
||||
if (conv_4bpp) {
|
||||
for (auto x = 0, w = qint32(tmpBuff.size()); x < w; ++x) {
|
||||
auto &&v = tmpBuff.at(x);
|
||||
line[x * 2 + 1] = (v >> 4) & 0xF;
|
||||
line[x * 2] = v & 0xF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (img.format() == QImage::Format_RGB555) {
|
||||
img.rgbSwap();
|
||||
}
|
||||
|
||||
*image = img;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TIMHandler::supportsOption(ImageOption option) const
|
||||
{
|
||||
if (option == QImageIOHandler::Size) {
|
||||
return true;
|
||||
}
|
||||
if (option == QImageIOHandler::ImageFormat) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant TIMHandler::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 TIMPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
if (format == "tim") {
|
||||
return Capabilities(CanRead);
|
||||
}
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
if (!device->isOpen()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && TIMHandler::canRead(device)) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
QImageIOHandler *TIMPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||
{
|
||||
QImageIOHandler *handler = new TIMHandler;
|
||||
handler->setDevice(device);
|
||||
handler->setFormat(format);
|
||||
return handler;
|
||||
}
|
||||
|
||||
#include "moc_tim_p.cpp"
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"Keys": [ "tim" ],
|
||||
"MimeTypes": [ "image/x-tim" ]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2026 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KIMG_TIM_P_H
|
||||
#define KIMG_TIM_P_H
|
||||
|
||||
#include <QImageIOPlugin>
|
||||
#include <QScopedPointer>
|
||||
|
||||
class TIMHandlerPrivate;
|
||||
class TIMHandler : public QImageIOHandler
|
||||
{
|
||||
public:
|
||||
TIMHandler();
|
||||
|
||||
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<TIMHandlerPrivate> d;
|
||||
};
|
||||
|
||||
class TIMPlugin : public QImageIOPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "tim.json")
|
||||
|
||||
public:
|
||||
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||
};
|
||||
|
||||
#endif // KIMG_TIM_P_H
|
||||
349
src/imageformats/xcursor.cpp
Normal 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"
|
||||
8
src/imageformats/xcursor.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Keys": [
|
||||
"xcursor"
|
||||
],
|
||||
"MimeTypes": [
|
||||
"image/x-xcursor"
|
||||
]
|
||||
}
|
||||
69
src/imageformats/xcursor_p.h
Normal 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
|
||||