Compare commits

...

7 Commits

Author SHA1 Message Date
Nicolas Fella
eb896efea1 Update dependency version to 6.25.0 2026-04-03 18:34:25 +02:00
Mirco Miranda
142ec14c81 TIM: PlayStation graphics read only support 2026-03-24 08:38:08 +01:00
Albert Astals Cid
38b8b70304 Remove ifdef now that we depend on Qt >= 6.9.0 2026-03-23 22:57:17 +01:00
Albert Astals Cid
e28c48cfeb GIT_SILENT Upgrade CMake version requirement to 3.29.
See https://community.kde.org/Frameworks/Policies
2026-03-23 19:30:35 +01:00
Albert Astals Cid
3c08226aec GIT_SILENT Upgrade Qt6 version requirement to 6.9.0.
See https://community.kde.org/Frameworks/Policies
2026-03-23 19:13:47 +01:00
Mirco Miranda
7c86ccaefb IFF: fix Integer-overflow in IDATChunk::strideSize 2026-03-23 12:05:01 +01:00
Nicolas Fella
ec0610d5b0 Update version to 6.25.0 2026-03-07 22:30:10 +01:00
20 changed files with 461 additions and 20 deletions

View File

@@ -1,11 +1,11 @@
cmake_minimum_required(VERSION 3.27)
cmake_minimum_required(VERSION 3.29)
set(KF_VERSION "6.24.0") # handled by release scripts
set(KF_DEP_VERSION "6.24.0") # handled by release scripts
set(KF_VERSION "6.25.0") # handled by release scripts
set(KF_DEP_VERSION "6.25.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary)
find_package(ECM 6.24.0 NO_MODULE)
find_package(ECM 6.25.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.8.0)
set(REQUIRED_QT_VERSION 6.9.0)
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF6Archive ${KF_DEP_VERSION})

View File

@@ -21,6 +21,7 @@ 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)
@@ -250,6 +251,7 @@ 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

View File

@@ -86,6 +86,7 @@ kimageformats_read_tests(
ras
rgb
sct
tim
tga
)

View File

@@ -180,6 +180,7 @@ HANDLER_TYPES="ANIHandler ani
RAWHandler raw
RGBHandler rgb
ScitexHandler sct
TIMHandler tim
TGAHandler tga
XCFHandler xcf"

View File

@@ -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|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|tim|tga|xcf]_fuzzer
*/
#include <QBuffer>
@@ -52,6 +52,7 @@
#include "raw_p.h"
#include "rgb_p.h"
#include "sct_p.h"
#include "tim_p.h"
#include "tga_p.h"
#include "xcf_p.h"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

View File

@@ -137,6 +137,10 @@ 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)
##################################

View File

@@ -513,21 +513,12 @@ 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
}
}

View File

@@ -3092,7 +3092,8 @@ quint32 IDATChunk::strideSize(const IHDRChunk *header) const
return 0;
}
auto rs = (header->width() * header->depth() + 7) / 8;
// width() and depth() are at most 65535
auto rs = (quint32(header->width()) * header->depth() + 7) / 8;
// No padding bytes are inserted in the data.
if (header->model() == IHDRChunk::Rgb888) {

View File

@@ -474,11 +474,7 @@ 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;

398
src/imageformats/tim.cpp Normal file
View File

@@ -0,0 +1,398 @@
/*
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"

View File

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

42
src/imageformats/tim_p.h Normal file
View File

@@ -0,0 +1,42 @@
/*
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