Compare commits

...

24 Commits

Author SHA1 Message Date
6c1a7ad339 update version for new release 2024-05-31 15:15:35 +02:00
c721fa481b Remove explicit maintainer from metainfo
All frameworks are maintained by the KDE community
2024-05-12 21:27:40 +02:00
ea15fed399 update version for new release 2024-05-12 14:06:57 +02:00
c2fabef501 Ensure dependencies are provided on Android 2024-05-06 00:02:54 +12:00
5b2c190823 update version for new release 2024-05-03 11:49:56 +02:00
1b94554323 update version for new release 2024-04-12 16:12:29 +01:00
c2c12b1d7e Fix build with Qt 6.7 on 32 bits
Qt changed the argument type again in 0ed34d1992
2024-04-11 08:30:45 +02:00
c169296fbf update version for new release 2024-04-05 11:45:58 +01:00
29aec82e67 Add KF_VERSION & KF_DEP_VERSION variables
For consistency with the other KF modules, but also to have some version
info with working copies/checkouts, as well as min required KF deps.
2024-03-18 16:24:41 +01:00
95ee381195 XCF: testcase update for fixed Qt
Updated testcases to work with Qt without alpha [bug](https://bugreports.qt.io/browse/QTBUG-120614.).

Needs Qt >= 6.6.2 for tests to pass
2024-03-14 21:49:09 +00:00
8e5951471d TGA: added options support
- Added Size and Format options support
- Fixed a double image allocation when reading RGBA images (RGB was always allocated and then replaced by RGBA one)
- Fixed the code for sequential devices

The Size option is used by the KIO 5 thumbnailer to avoid to use too memory. A backport to KF5 would serve CCBUG: 413801 and CCBUG: 479612
2024-03-04 23:47:59 +00:00
0710bc65f6 More header checks (CCBUG: 479612) 2024-02-29 15:55:57 +01:00
4be09ba419 update version for new release 2024-02-21 11:15:16 +00:00
6cbdf9cf54 Workaround QTBUG-120614 2024-02-09 07:55:28 +00:00
7d6de20e8c update version for new release 2024-02-01 09:26:20 +00:00
b37c991e39 Port to ECMFeatureSummary
This avoids a feature summary in the middle of the cmake configuration
when this module is used as a git submodule.

GIT_SILENT
2024-01-26 14:17:50 +01:00
249046f25d update version for new release 2024-01-10 11:27:02 +00:00
9f7b1b8dee ScanlineConverter: fix indexed conversion and support for source depth less than 24 bits 2024-01-06 09:04:06 +01:00
f065104b72 Less space used when saving a grayscale image 2024-01-06 08:20:14 +01:00
f34185197a Fix build with Qt 6.7 2023-12-20 18:32:28 +01:00
9f24023ca7 The maximum number of channels explained better
Just a clarification on why the maximum value of channels present in the specifications is not used.
2023-12-20 16:43:12 +00:00
8d1ef536be Code is qt6 only now. Remove unused check 2023-12-16 11:16:53 +00:00
da8ed31aec avif: new quality settings
Prior libavif 1.0, quality was set using two numbers
(minQuantizer and maxQuantizer).
New versions of libavif recommend using
new/single quality parameter instead.
Compression using the new settings is different
compared to the old.
For example, new quality 68 gives similar compression
like old default 52.
2023-12-13 17:40:21 +01:00
ce8383e5fd HEIF plug-in extended to support HEJ2 format
HEJ2 is JPEG 2000 codec encapsulated in HEIF container.
Only HEJ2 reading is implemented.
2023-12-07 13:26:43 +01:00
22 changed files with 301 additions and 76 deletions

View File

@ -1,5 +1,5 @@
Dependencies: Dependencies:
- 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows'] - 'on': ['Linux', 'FreeBSD', 'macOS', 'Windows', 'Android']
'require': 'require':
'frameworks/extra-cmake-modules': '@same' 'frameworks/extra-cmake-modules': '@same'
'frameworks/karchive' : '@same' 'frameworks/karchive' : '@same'

View File

@ -1,9 +1,11 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(KImageFormats) set(KF_VERSION "6.3.0") # handled by release scripts
set(KF_DEP_VERSION "6.3.0") # handled by release scripts
project(KImageFormats VERSION ${KF_VERSION})
include(FeatureSummary) include(FeatureSummary)
find_package(ECM 5.240.0 NO_MODULE) find_package(ECM 6.3.0 NO_MODULE)
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") 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) feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
@ -22,7 +24,7 @@ include(FindPkgConfig)
set(REQUIRED_QT_VERSION 6.5.0) set(REQUIRED_QT_VERSION 6.5.0)
find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) find_package(Qt6Gui ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
find_package(KF6Archive) find_package(KF6Archive ${KF_DEP_VERSION})
set_package_properties(KF6Archive PROPERTIES set_package_properties(KF6Archive PROPERTIES
TYPE OPTIONAL TYPE OPTIONAL
PURPOSE "Required for the QImage plugin for Krita and OpenRaster images" PURPOSE "Required for the QImage plugin for Krita and OpenRaster images"
@ -91,6 +93,7 @@ if (BUILD_TESTING)
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) include(ECMFeatureSummary)
ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)

View File

@ -97,6 +97,12 @@ if (LibHeif_FOUND)
kimageformats_write_tests(FUZZ 1 kimageformats_write_tests(FUZZ 1
heif-nodatacheck-lossless heif-nodatacheck-lossless
) )
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.17.0")
kimageformats_read_tests(FUZZ 1
hej2
)
endif()
endif() endif()
if (LibJXL_FOUND AND LibJXLThreads_FOUND) if (LibJXL_FOUND AND LibJXLThreads_FOUND)

BIN
autotests/read/exr/gray.exr Normal file

Binary file not shown.

BIN
autotests/read/exr/gray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -1,4 +1,3 @@
maintainer: alexmerry
description: Image format plugins for Qt description: Image format plugins for Qt
tier: 2 tier: 2
type: functional type: functional

View File

@ -16,9 +16,34 @@
#include <cfloat> #include <cfloat>
/*
Quality range - compression/subsampling
100 - lossless RGB compression
< KIMG_AVIF_QUALITY_BEST, 100 ) - YUV444 color subsampling
< KIMG_AVIF_QUALITY_HIGH, KIMG_AVIF_QUALITY_BEST ) - YUV422 color subsampling
< 0, KIMG_AVIF_QUALITY_HIGH ) - YUV420 color subsampling
< 0, KIMG_AVIF_QUALITY_LOW ) - lossy compression of alpha channel
*/
#ifndef KIMG_AVIF_DEFAULT_QUALITY
#define KIMG_AVIF_DEFAULT_QUALITY 68
#endif
#ifndef KIMG_AVIF_QUALITY_BEST
#define KIMG_AVIF_QUALITY_BEST 90
#endif
#ifndef KIMG_AVIF_QUALITY_HIGH
#define KIMG_AVIF_QUALITY_HIGH 80
#endif
#ifndef KIMG_AVIF_QUALITY_LOW
#define KIMG_AVIF_QUALITY_LOW 51
#endif
QAVIFHandler::QAVIFHandler() QAVIFHandler::QAVIFHandler()
: m_parseState(ParseAvifNotParsed) : m_parseState(ParseAvifNotParsed)
, m_quality(52) , m_quality(KIMG_AVIF_DEFAULT_QUALITY)
, m_container_width(0) , m_container_width(0)
, m_container_height(0) , m_container_height(0)
, m_rawAvifData(AVIF_DATA_EMPTY) , m_rawAvifData(AVIF_DATA_EMPTY)
@ -519,9 +544,17 @@ bool QAVIFHandler::write(const QImage &image)
} }
} }
if (m_quality > 100) {
m_quality = 100;
} else if (m_quality < 0) {
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
}
#if AVIF_VERSION < 1000000
int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100; int maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - qBound(0, m_quality, 100)) / 100;
int minQuantizer = 0; int minQuantizer = 0;
int maxQuantizerAlpha = 0; int maxQuantizerAlpha = 0;
#endif
avifResult res; avifResult res;
bool save_grayscale; // true - monochrome, false - colors bool save_grayscale; // true - monochrome, false - colors
@ -567,13 +600,15 @@ bool QAVIFHandler::write(const QImage &image)
break; break;
} }
// quality settings #if AVIF_VERSION < 1000000
// deprecated quality settings
if (maxQuantizer > 20) { if (maxQuantizer > 20) {
minQuantizer = maxQuantizer - 20; minQuantizer = maxQuantizer - 20;
if (maxQuantizer > 40) { // we decrease quality of alpha channel here if (maxQuantizer > 40) { // we decrease quality of alpha channel here
maxQuantizerAlpha = maxQuantizer - 40; maxQuantizerAlpha = maxQuantizer - 40;
} }
} }
#endif
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
if (save_depth > 8) { if (save_depth > 8) {
@ -646,8 +681,8 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpcolorimage = image.convertToFormat(tmpformat); QImage tmpcolorimage = image.convertToFormat(tmpformat);
avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420; avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420;
if (maxQuantizer < 20) { if (m_quality >= KIMG_AVIF_QUALITY_HIGH) {
if (maxQuantizer < 10) { if (m_quality >= KIMG_AVIF_QUALITY_BEST) {
pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality
} else { } else {
pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality
@ -807,6 +842,8 @@ bool QAVIFHandler::write(const QImage &image)
avifRWData raw = AVIF_DATA_EMPTY; avifRWData raw = AVIF_DATA_EMPTY;
avifEncoder *encoder = avifEncoderCreate(); avifEncoder *encoder = avifEncoderCreate();
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64); encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
#if AVIF_VERSION < 1000000
encoder->minQuantizer = minQuantizer; encoder->minQuantizer = minQuantizer;
encoder->maxQuantizer = maxQuantizer; encoder->maxQuantizer = maxQuantizer;
@ -814,6 +851,17 @@ bool QAVIFHandler::write(const QImage &image)
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS; encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = maxQuantizerAlpha; encoder->maxQuantizerAlpha = maxQuantizerAlpha;
} }
#else
encoder->quality = m_quality;
if (image.hasAlphaChannel()) {
if (m_quality >= KIMG_AVIF_QUALITY_LOW) {
encoder->qualityAlpha = 100;
} else {
encoder->qualityAlpha = 100 - (KIMG_AVIF_QUALITY_LOW - m_quality) / 2;
}
}
#endif
encoder->speed = 6; encoder->speed = 6;
@ -870,7 +918,7 @@ void QAVIFHandler::setOption(ImageOption option, const QVariant &value)
if (m_quality > 100) { if (m_quality > 100) {
m_quality = 100; m_quality = 100;
} else if (m_quality < 0) { } else if (m_quality < 0) {
m_quality = 52; m_quality = KIMG_AVIF_DEFAULT_QUALITY;
} }
return; return;
default: default:

View File

@ -94,7 +94,7 @@
// Allow the code to works on all QT versions supported by KDE // Allow the code to works on all QT versions supported by KDE
// project (Qt 5.15 and Qt 6.x) to easy backports fixes. // project (Qt 5.15 and Qt 6.x) to easy backports fixes.
#if (QT_VERSION_MAJOR >= 6) && !defined(EXR_USE_LEGACY_CONVERSIONS) #if !defined(EXR_USE_LEGACY_CONVERSIONS)
// If uncommented, the image is rendered in a float16 format, the result is very precise // If uncommented, the image is rendered in a float16 format, the result is very precise
#define EXR_USE_QT6_FLOAT_IMAGE // default uncommented #define EXR_USE_QT6_FLOAT_IMAGE // default uncommented
#endif #endif
@ -621,7 +621,14 @@ bool EXRHandler::write(const QImage &image)
// write the EXR // write the EXR
K_OStream ostr(device(), QByteArray()); K_OStream ostr(device(), QByteArray());
Imf::RgbaOutputFile file(ostr, header, image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB); auto channelsType = image.hasAlphaChannel() ? Imf::RgbaChannels::WRITE_RGBA : Imf::RgbaChannels::WRITE_RGB;
if (image.format() == QImage::Format_Mono ||
image.format() == QImage::Format_MonoLSB ||
image.format() == QImage::Format_Grayscale16 ||
image.format() == QImage::Format_Grayscale8) {
channelsType = Imf::RgbaChannels::WRITE_Y;
}
Imf::RgbaOutputFile file(ostr, header, channelsType);
Imf::Array2D<Imf::Rgba> pixels; Imf::Array2D<Imf::Rgba> pixels;
pixels.resizeErase(EXR_LINES_PER_BLOCK, width); pixels.resizeErase(EXR_LINES_PER_BLOCK, width);

View File

@ -22,6 +22,7 @@ size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false; bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false; bool HEIFHandler::m_heif_decoder_available = false;
bool HEIFHandler::m_heif_encoder_available = false; bool HEIFHandler::m_heif_encoder_available = false;
bool HEIFHandler::m_hej2_decoder_available = false;
extern "C" { extern "C" {
static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata) static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx */, const void *data, size_t size, void *userdata)
@ -59,12 +60,25 @@ HEIFHandler::HEIFHandler()
bool HEIFHandler::canRead() const bool HEIFHandler::canRead() const
{ {
if (m_parseState == ParseHeicNotParsed && !canRead(device())) { if (m_parseState == ParseHeicNotParsed) {
QIODevice *dev = device();
if (dev) {
const QByteArray header = dev->peek(28);
if (HEIFHandler::isSupportedBMFFType(header)) {
setFormat("heif");
return true;
}
if (HEIFHandler::isSupportedHEJ2(header)) {
setFormat("hej2");
return true;
}
}
return false; return false;
} }
if (m_parseState != ParseHeicError) { if (m_parseState != ParseHeicError) {
setFormat("heif");
return true; return true;
} }
return false; return false;
@ -300,17 +314,6 @@ bool HEIFHandler::write_helper(const QImage &image)
return true; return true;
} }
bool HEIFHandler::canRead(QIODevice *device)
{
if (!device) {
qWarning("HEIFHandler::canRead() called with no device");
return false;
}
const QByteArray header = device->peek(28);
return HEIFHandler::isSupportedBMFFType(header);
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header) bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{ {
if (header.size() < 28) { if (header.size() < 28) {
@ -350,6 +353,22 @@ bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
return false; return false;
} }
bool HEIFHandler::isSupportedHEJ2(const QByteArray &header)
{
if (header.size() < 28) {
return false;
}
const char *buffer = header.constData();
if (qstrncmp(buffer + 4, "ftyp", 4) == 0) {
if (qstrncmp(buffer + 8, "j2ki", 4) == 0) {
return true;
}
}
return false;
}
QVariant HEIFHandler::option(ImageOption option) const QVariant HEIFHandler::option(ImageOption option) const
{ {
if (option == Quality) { if (option == Quality) {
@ -425,7 +444,7 @@ bool HEIFHandler::ensureDecoder()
} }
const QByteArray buffer = device()->readAll(); const QByteArray buffer = device()->readAll();
if (!HEIFHandler::isSupportedBMFFType(buffer)) { if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer)) {
m_parseState = ParseHeicError; m_parseState = ParseHeicError;
return false; return false;
} }
@ -814,6 +833,9 @@ bool HEIFHandler::isHeifDecoderAvailable()
} }
#endif #endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC); m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC); m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_plugins_queried = true; m_plugins_queried = true;
@ -839,6 +861,9 @@ bool HEIFHandler::isHeifEncoderAvailable()
} }
#endif #endif
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC); m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC); m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_plugins_queried = true; m_plugins_queried = true;
@ -853,6 +878,34 @@ bool HEIFHandler::isHeifEncoderAvailable()
return m_heif_encoder_available; return m_heif_encoder_available;
} }
bool HEIFHandler::isHej2DecoderAvailable()
{
QMutexLocker locker(&getHEIFHandlerMutex());
if (!m_plugins_queried) {
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_init(nullptr);
}
#endif
m_heif_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
m_heif_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
m_hej2_decoder_available = heif_have_decoder_for_format(heif_compression_JPEG2000);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
return m_hej2_decoder_available;
}
void HEIFHandler::startHeifLib() void HEIFHandler::startHeifLib()
{ {
#if LIBHEIF_HAVE_VERSION(1, 13, 0) #if LIBHEIF_HAVE_VERSION(1, 13, 0)
@ -901,6 +954,15 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
} }
return format_cap; return format_cap;
} }
if (format == "hej2") {
Capabilities format_cap;
if (HEIFHandler::isHej2DecoderAvailable()) {
format_cap |= CanRead;
}
return format_cap;
}
if (!format.isEmpty()) { if (!format.isEmpty()) {
return {}; return {};
} }
@ -909,10 +971,18 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
} }
Capabilities cap; Capabilities cap;
if (device->isReadable() && HEIFHandler::canRead(device) && HEIFHandler::isHeifDecoderAvailable()) { if (device->isReadable()) {
const QByteArray header = device->peek(28);
if (HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable()) {
cap |= CanRead; cap |= CanRead;
} }
if (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable()) {
cap |= CanRead;
}
}
if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) { if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {
cap |= CanWrite; cap |= CanWrite;
} }

View File

@ -1,4 +1,4 @@
{ {
"Keys": [ "heif", "heic" ], "Keys": [ "heif", "heic", "hej2" ],
"MimeTypes": [ "image/heif", "image/heif" ] "MimeTypes": [ "image/heif", "image/heif", "image/hej2k" ]
} }

View File

@ -24,17 +24,18 @@ public:
bool read(QImage *image) override; bool read(QImage *image) override;
bool write(const QImage &image) override; bool write(const QImage &image) override;
static bool canRead(QIODevice *device);
QVariant option(ImageOption option) const override; QVariant option(ImageOption option) const override;
void setOption(ImageOption option, const QVariant &value) override; void setOption(ImageOption option, const QVariant &value) override;
bool supportsOption(ImageOption option) const override; bool supportsOption(ImageOption option) const override;
static bool isHeifDecoderAvailable(); static bool isHeifDecoderAvailable();
static bool isHeifEncoderAvailable(); static bool isHeifEncoderAvailable();
static bool isHej2DecoderAvailable();
static bool isSupportedBMFFType(const QByteArray &header);
static bool isSupportedHEJ2(const QByteArray &header);
private: private:
static bool isSupportedBMFFType(const QByteArray &header);
bool ensureParsed() const; bool ensureParsed() const;
bool ensureDecoder(); bool ensureDecoder();
@ -57,6 +58,7 @@ private:
static bool m_plugins_queried; static bool m_plugins_queried;
static bool m_heif_decoder_available; static bool m_heif_decoder_available;
static bool m_heif_encoder_available; static bool m_heif_encoder_available;
static bool m_hej2_decoder_available;
static QMutex &getHEIFHandlerMutex(); static QMutex &getHEIFHandlerMutex();
}; };

View File

@ -631,7 +631,7 @@ static bool IsValid(const PSDHeader &header)
qDebug() << "PSD header: invalid color mode" << header.color_mode; qDebug() << "PSD header: invalid color mode" << header.color_mode;
return false; return false;
} }
// Specs tells: "Supported range is 1 to 56" but the limit is 57: // Specs tells: "Supported range is 1 to 56" but when the alpha channel is present the limit is 57:
// Photoshop does not make you add more (see also 53alphas.psd test case). // Photoshop does not make you add more (see also 53alphas.psd test case).
if (header.channel_count < 1 || header.channel_count > 57) { if (header.channel_count < 1 || header.channel_count > 57) {
qDebug() << "PSD header: invalid number of channels" << header.channel_count; qDebug() << "PSD header: invalid number of channels" << header.channel_count;

View File

@ -60,20 +60,33 @@ const uchar *ScanLineConverter::convertedScanLine(const QImage &image, qint32 y)
} }
if (image.width() != _tmpBuffer.width() || image.format() != _tmpBuffer.format()) { if (image.width() != _tmpBuffer.width() || image.format() != _tmpBuffer.format()) {
_tmpBuffer = QImage(image.width(), 1, image.format()); _tmpBuffer = QImage(image.width(), 1, image.format());
_tmpBuffer.setColorTable(image.colorTable());
} }
if (_tmpBuffer.isNull()) { if (_tmpBuffer.isNull()) {
return nullptr; return nullptr;
} }
std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine())); std::memcpy(_tmpBuffer.bits(), image.constScanLine(y), std::min(_tmpBuffer.bytesPerLine(), image.bytesPerLine()));
auto tmp = _tmpBuffer;
if (colorSpaceConversion) { if (colorSpaceConversion) {
auto cs = image.colorSpace(); auto cs = image.colorSpace();
if (!cs.isValid()) { if (!cs.isValid()) {
cs = _defaultColorSpace; cs = _defaultColorSpace;
} }
_tmpBuffer.setColorSpace(cs); if (tmp.depth() < 24) {
_tmpBuffer.convertToColorSpace(_colorSpace); tmp.convertTo(tmp.hasAlphaChannel() ? QImage::Format_ARGB32 : QImage::Format_RGB32);
} }
_convBuffer = _tmpBuffer.convertToFormat(_targetFormat); tmp.setColorSpace(cs);
tmp.convertToColorSpace(_colorSpace);
}
/*
* Work Around for wrong RGBA64 -> 16FPx4/32FPx4 conversion on Intel architecture.
* Luckily convertTo() works fine with 16FPx4 images so I can use it instead convertToFormat().
* See also: https://bugreports.qt.io/browse/QTBUG-120614
*/
tmp.convertTo(_targetFormat);
_convBuffer = tmp;
if (_convBuffer.isNull()) { if (_convBuffer.isNull()) {
return nullptr; return nullptr;
} }
@ -90,9 +103,6 @@ qsizetype ScanLineConverter::bytesPerLine() const
bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace, const QColorSpace &defaultColorSpace) bool ScanLineConverter::isColorSpaceConversionNeeded(const QImage &image, const QColorSpace &targetColorSpace, const QColorSpace &defaultColorSpace)
{ {
if (image.depth() < 24) { // RGB 8 bit or grater only
return false;
}
auto sourceColorSpace = image.colorSpace(); auto sourceColorSpace = image.colorSpace();
if (!sourceColorSpace.isValid()) { if (!sourceColorSpace.isValid()) {
sourceColorSpace = defaultColorSpace; sourceColorSpace = defaultColorSpace;

View File

@ -88,10 +88,6 @@ static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
s >> head.height; s >> head.height;
s >> head.pixel_size; s >> head.pixel_size;
s >> head.flags; s >> head.flags;
/*qDebug() << "id_length: " << head.id_length << " - colormap_type: " << head.colormap_type << " - image_type: " << head.image_type;
qDebug() << "colormap_index: " << head.colormap_index << " - colormap_length: " << head.colormap_length << " - colormap_size: " << head.colormap_size;
qDebug() << "x_origin: " << head.x_origin << " - y_origin: " << head.y_origin << " - width:" << head.width << " - height:" << head.height << " - pixelsize:
" << head.pixel_size << " - flags: " << head.flags;*/
return s; return s;
} }
@ -117,6 +113,10 @@ static bool IsSupported(const TgaHeader &head)
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) { if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
return false; return false;
} }
// If the colormap_type field is set to zero, indicating that no color map exists, then colormap_size, colormap_index and colormap_length should be set to zero.
if (head.colormap_type == 0 && (head.colormap_size != 0 || head.colormap_index != 0 || head.colormap_length != 0)) {
return false;
}
return true; return true;
} }
@ -170,10 +170,57 @@ struct TgaHeaderInfo {
} }
}; };
static QImage::Format imageFormat(const TgaHeader &head)
{
auto format = QImage::Format_Invalid;
if (IsSupported(head)) {
// Bits 0-3 are the numbers of alpha bits (can be zero!)
const int numAlphaBits = head.flags & 0xf;
// However alpha exists only in the 32 bit format.
if ((head.pixel_size == 32) && (head.flags & 0xf)) {
if (numAlphaBits <= 8) {
format = QImage::Format_ARGB32;
}
}
else {
format = QImage::Format_RGB32;
}
}
return format;
}
/*!
* \brief peekHeader
* Reads the header but does not change the position in the device.
*/
static bool peekHeader(QIODevice *device, TgaHeader &header)
{
qint64 oldPos = device->pos();
QByteArray head = device->read(TgaHeader::SIZE);
int readBytes = head.size();
if (device->isSequential()) {
for (int pos = readBytes - 1; pos >= 0; --pos) {
device->ungetChar(head[pos]);
}
} else {
device->seek(oldPos);
}
if (readBytes < TgaHeader::SIZE) {
return false;
}
QDataStream stream(head);
stream.setByteOrder(QDataStream::LittleEndian);
stream >> header;
return true;
}
static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
{ {
// Create image. img = imageAlloc(tga.width, tga.height, imageFormat(tga));
img = imageAlloc(tga.width, tga.height, QImage::Format_RGB32);
if (img.isNull()) { if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height); qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
return false; return false;
@ -181,21 +228,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
TgaHeaderInfo info(tga); TgaHeaderInfo info(tga);
// Bits 0-3 are the numbers of alpha bits (can be zero!)
const int numAlphaBits = tga.flags & 0xf; const int numAlphaBits = tga.flags & 0xf;
// However alpha exists only in the 32 bit format.
if ((tga.pixel_size == 32) && (tga.flags & 0xf)) {
img = imageAlloc(tga.width, tga.height, QImage::Format_ARGB32);
if (img.isNull()) {
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
return false;
}
if (numAlphaBits > 8) {
return false;
}
}
uint pixel_size = (tga.pixel_size / 8); uint pixel_size = (tga.pixel_size / 8);
qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size; qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size;
@ -391,23 +424,25 @@ bool TGAHandler::read(QImage *outImage)
{ {
// qDebug() << "Loading TGA file!"; // qDebug() << "Loading TGA file!";
QDataStream s(device()); auto d = device();
s.setByteOrder(QDataStream::LittleEndian);
// Read image header.
TgaHeader tga; TgaHeader tga;
s >> tga; if (!peekHeader(d, tga) || !IsSupported(tga)) {
s.device()->seek(TgaHeader::SIZE + tga.id_length);
// Check image file format.
if (s.atEnd()) {
// qDebug() << "This TGA file is not valid."; // qDebug() << "This TGA file is not valid.";
return false; return false;
} }
// Check supported file types. if (d->isSequential()) {
if (!IsSupported(tga)) { d->read(TgaHeader::SIZE + tga.id_length);
// qDebug() << "This TGA file is not supported."; } else {
d->seek(TgaHeader::SIZE + tga.id_length);
}
QDataStream s(d);
s.setByteOrder(QDataStream::LittleEndian);
// Check image file format.
if (s.atEnd()) {
// qDebug() << "This TGA file is not valid.";
return false; return false;
} }
@ -468,6 +503,42 @@ bool TGAHandler::write(const QImage &image)
return true; return true;
} }
bool TGAHandler::supportsOption(ImageOption option) const
{
if (option == QImageIOHandler::Size) {
return true;
}
if (option == QImageIOHandler::ImageFormat) {
return true;
}
return false;
}
QVariant TGAHandler::option(ImageOption option) const
{
QVariant v;
if (option == QImageIOHandler::Size) {
if (auto d = device()) {
TgaHeader header;
if (peekHeader(d, header) && IsSupported(header)) {
v = QVariant::fromValue(QSize(header.width, header.height));
}
}
}
if (option == QImageIOHandler::ImageFormat) {
if (auto d = device()) {
TgaHeader header;
if (peekHeader(d, header) && IsSupported(header)) {
v = QVariant::fromValue(imageFormat(header));
}
}
}
return v;
}
bool TGAHandler::canRead(QIODevice *device) bool TGAHandler::canRead(QIODevice *device)
{ {
if (!device) { if (!device) {
@ -491,10 +562,12 @@ bool TGAHandler::canRead(QIODevice *device)
return false; return false;
} }
QDataStream stream(head);
stream.setByteOrder(QDataStream::LittleEndian);
TgaHeader tga; TgaHeader tga;
stream >> tga; if (!peekHeader(device, tga)) {
qWarning("TGAHandler::canRead() error while reading the header");
return false;
}
return IsSupported(tga); return IsSupported(tga);
} }

View File

@ -19,6 +19,9 @@ public:
bool read(QImage *image) override; bool read(QImage *image) override;
bool write(const QImage &image) override; bool write(const QImage &image) override;
bool supportsOption(QImageIOHandler::ImageOption option) const override;
QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device); static bool canRead(QIODevice *device);
}; };

View File

@ -960,7 +960,11 @@ bool XCFImageFormat::loadImageProperties(QDataStream &xcf_io, XCFImage &xcf_imag
case PROP_PARASITES: case PROP_PARASITES:
while (!property.atEnd()) { while (!property.atEnd()) {
char *tag; char *tag;
#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
quint32 size; quint32 size;
#else
qint64 size;
#endif
property.readBytes(tag, size); property.readBytes(tag, size);