mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-07-14 19:04:17 -04:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
df82311a10 | |||
1e47a751df | |||
09b9ff7bf9 | |||
ee77e349e3 | |||
4168c46964 | |||
f9f29304d8 | |||
ec0918d962 | |||
dadff2791c | |||
106279d32e | |||
f5962442ca | |||
7cc4cb8d0c | |||
2bf3a859fc | |||
3ce3ab2364 | |||
c09b88b7c6 | |||
8cc25c39b4 | |||
43c80793ac | |||
e90bca4924 | |||
bfc73ca260 |
@ -2,9 +2,10 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-static.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-static.yml
|
||||
- project: sysadmin/ci-utilities
|
||||
file:
|
||||
- /gitlab-templates/linux.yml
|
||||
- /gitlab-templates/linux-static.yml
|
||||
- /gitlab-templates/freebsd.yml
|
||||
- /gitlab-templates/windows.yml
|
||||
- /gitlab-templates/windows-static.yml
|
||||
|
@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16)
|
||||
project(KImageFormats)
|
||||
|
||||
include(FeatureSummary)
|
||||
find_package(ECM 5.111.0 NO_MODULE)
|
||||
find_package(ECM 5.116.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)
|
||||
|
||||
|
@ -99,6 +99,12 @@ if (LibHeif_FOUND)
|
||||
kimageformats_write_tests(FUZZ 1
|
||||
heif-nodatacheck-lossless
|
||||
)
|
||||
|
||||
if (LibHeif_VERSION VERSION_GREATER_EQUAL "1.17.0")
|
||||
kimageformats_read_tests(FUZZ 1
|
||||
hej2
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
|
BIN
autotests/read/hej2/rgb_kcolorchooser.hej2
Normal file
BIN
autotests/read/hej2/rgb_kcolorchooser.hej2
Normal file
Binary file not shown.
BIN
autotests/read/hej2/rgb_kcolorchooser.png
Normal file
BIN
autotests/read/hej2/rgb_kcolorchooser.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
autotests/read/hej2/rgba_kolourpaint.hej2
Normal file
BIN
autotests/read/hej2/rgba_kolourpaint.hej2
Normal file
Binary file not shown.
BIN
autotests/read/hej2/rgba_kolourpaint.png
Normal file
BIN
autotests/read/hej2/rgba_kolourpaint.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -16,9 +16,34 @@
|
||||
|
||||
#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()
|
||||
: m_parseState(ParseAvifNotParsed)
|
||||
, m_quality(52)
|
||||
, m_quality(KIMG_AVIF_DEFAULT_QUALITY)
|
||||
, m_container_width(0)
|
||||
, m_container_height(0)
|
||||
, 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 minQuantizer = 0;
|
||||
int maxQuantizerAlpha = 0;
|
||||
#endif
|
||||
avifResult res;
|
||||
|
||||
bool save_grayscale; // true - monochrome, false - colors
|
||||
@ -567,13 +600,15 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
break;
|
||||
}
|
||||
|
||||
// quality settings
|
||||
#if AVIF_VERSION < 1000000
|
||||
// deprecated quality settings
|
||||
if (maxQuantizer > 20) {
|
||||
minQuantizer = maxQuantizer - 20;
|
||||
if (maxQuantizer > 40) { // we decrease quality of alpha channel here
|
||||
maxQuantizerAlpha = maxQuantizer - 40;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (save_grayscale && !image.hasAlphaChannel()) { // we are going to save grayscale image without alpha channel
|
||||
if (save_depth > 8) {
|
||||
@ -584,7 +619,15 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
QImage tmpgrayimage = image.convertToFormat(tmpformat);
|
||||
|
||||
avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400);
|
||||
#if AVIF_VERSION >= 110000
|
||||
res = avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
|
||||
if (res != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR in avifImageAllocatePlanes: %s", avifResultToString(res));
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
|
||||
#endif
|
||||
|
||||
if (tmpgrayimage.colorSpace().isValid()) {
|
||||
avif->colorPrimaries = (avifColorPrimaries)1;
|
||||
@ -646,8 +689,8 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
QImage tmpcolorimage = image.convertToFormat(tmpformat);
|
||||
|
||||
avifPixelFormat pixel_format = AVIF_PIXEL_FORMAT_YUV420;
|
||||
if (maxQuantizer < 20) {
|
||||
if (maxQuantizer < 10) {
|
||||
if (m_quality >= KIMG_AVIF_QUALITY_HIGH) {
|
||||
if (m_quality >= KIMG_AVIF_QUALITY_BEST) {
|
||||
pixel_format = AVIF_PIXEL_FORMAT_YUV444; // best quality
|
||||
} else {
|
||||
pixel_format = AVIF_PIXEL_FORMAT_YUV422; // high quality
|
||||
@ -771,7 +814,15 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
avif->transferCharacteristics = transfer_to_save;
|
||||
|
||||
if (iccprofile.size() > 0) {
|
||||
#if AVIF_VERSION >= 1000000
|
||||
res = avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
|
||||
if (res != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR in avifImageSetProfileICC: %s", avifResultToString(res));
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
|
||||
#endif
|
||||
}
|
||||
|
||||
avifRGBImage rgb;
|
||||
@ -807,6 +858,8 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
avifRWData raw = AVIF_DATA_EMPTY;
|
||||
avifEncoder *encoder = avifEncoderCreate();
|
||||
encoder->maxThreads = qBound(1, QThread::idealThreadCount(), 64);
|
||||
|
||||
#if AVIF_VERSION < 1000000
|
||||
encoder->minQuantizer = minQuantizer;
|
||||
encoder->maxQuantizer = maxQuantizer;
|
||||
|
||||
@ -814,6 +867,17 @@ bool QAVIFHandler::write(const QImage &image)
|
||||
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
|
||||
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;
|
||||
|
||||
@ -870,7 +934,7 @@ void QAVIFHandler::setOption(ImageOption option, const QVariant &value)
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
} else if (m_quality < 0) {
|
||||
m_quality = 52;
|
||||
m_quality = KIMG_AVIF_DEFAULT_QUALITY;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
@ -923,6 +987,8 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
return false;
|
||||
}
|
||||
|
||||
avifResult decodeResult;
|
||||
|
||||
if (m_decoder->imageIndex >= 0) {
|
||||
if (m_decoder->imageCount < 2) {
|
||||
m_parseState = ParseAvifSuccess;
|
||||
@ -930,11 +996,16 @@ bool QAVIFHandler::jumpToNextImage()
|
||||
}
|
||||
|
||||
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning
|
||||
avifDecoderReset(m_decoder);
|
||||
decodeResult = avifDecoderReset(m_decoder);
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR in avifDecoderReset: %s", avifResultToString(decodeResult));
|
||||
m_parseState = ParseAvifError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avifResult decodeResult = avifDecoderNextImage(m_decoder);
|
||||
decodeResult = avifDecoderNextImage(m_decoder);
|
||||
|
||||
if (decodeResult != AVIF_RESULT_OK) {
|
||||
qWarning("ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));
|
||||
|
@ -68,8 +68,8 @@
|
||||
class K_IStream : public Imf::IStream
|
||||
{
|
||||
public:
|
||||
K_IStream(QIODevice *dev, const QByteArray &fileName)
|
||||
: IStream(fileName.data())
|
||||
K_IStream(QIODevice *dev)
|
||||
: IStream("K_IStream")
|
||||
, m_dev(dev)
|
||||
{
|
||||
}
|
||||
@ -159,7 +159,7 @@ bool EXRHandler::read(QImage *outImage)
|
||||
int width;
|
||||
int height;
|
||||
|
||||
K_IStream istr(device(), QByteArray());
|
||||
K_IStream istr(device());
|
||||
Imf::RgbaInputFile file(istr);
|
||||
Imath::Box2i dw = file.dataWindow();
|
||||
bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
|
||||
@ -271,6 +271,13 @@ bool EXRHandler::canRead(QIODevice *device)
|
||||
return false;
|
||||
}
|
||||
|
||||
#if OPENEXR_VERSION_MAJOR == 3 && OPENEXR_VERSION_MINOR > 2
|
||||
// openexpr >= 3.3 uses seek and tell extensively
|
||||
if (device->isSequential()) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
const QByteArray head = device->peek(4);
|
||||
|
||||
return Imf::isImfMagic(head.data());
|
||||
|
@ -22,6 +22,7 @@ size_t HEIFHandler::m_initialized_count = 0;
|
||||
bool HEIFHandler::m_plugins_queried = false;
|
||||
bool HEIFHandler::m_heif_decoder_available = false;
|
||||
bool HEIFHandler::m_heif_encoder_available = false;
|
||||
bool HEIFHandler::m_hej2_decoder_available = false;
|
||||
|
||||
extern "C" {
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (m_parseState != ParseHeicError) {
|
||||
setFormat("heif");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -300,17 +314,6 @@ bool HEIFHandler::write_helper(const QImage &image)
|
||||
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)
|
||||
{
|
||||
if (header.size() < 28) {
|
||||
@ -350,6 +353,22 @@ bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
|
||||
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
|
||||
{
|
||||
if (option == Quality) {
|
||||
@ -425,7 +444,7 @@ bool HEIFHandler::ensureDecoder()
|
||||
}
|
||||
|
||||
const QByteArray buffer = device()->readAll();
|
||||
if (!HEIFHandler::isSupportedBMFFType(buffer)) {
|
||||
if (!HEIFHandler::isSupportedBMFFType(buffer) && !HEIFHandler::isSupportedHEJ2(buffer)) {
|
||||
m_parseState = ParseHeicError;
|
||||
return false;
|
||||
}
|
||||
@ -457,8 +476,17 @@ bool HEIFHandler::ensureDecoder()
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool hasAlphaChannel = heif_image_handle_has_alpha_channel(handle);
|
||||
const int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle);
|
||||
|
||||
if (bit_depth < 8) {
|
||||
m_parseState = ParseHeicError;
|
||||
heif_image_handle_release(handle);
|
||||
heif_context_free(ctx);
|
||||
qWarning() << "HEIF image with undefined or unsupported bit depth.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool hasAlphaChannel = heif_image_handle_has_alpha_channel(handle);
|
||||
heif_chroma chroma;
|
||||
|
||||
QImage::Format target_image_format;
|
||||
@ -483,11 +511,7 @@ bool HEIFHandler::ensureDecoder()
|
||||
m_parseState = ParseHeicError;
|
||||
heif_image_handle_release(handle);
|
||||
heif_context_free(ctx);
|
||||
if (bit_depth > 0) {
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
} else {
|
||||
qWarning() << "Undefined bit depth.";
|
||||
}
|
||||
qWarning() << "Unsupported bit depth:" << bit_depth;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -500,6 +524,16 @@ bool HEIFHandler::ensureDecoder()
|
||||
struct heif_image *img = nullptr;
|
||||
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
|
||||
|
||||
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
|
||||
if (err.code == heif_error_Invalid_input && err.subcode == heif_suberror_Unknown_NCLX_matrix_coefficients && img == nullptr && buffer.contains("Xiaomi")) {
|
||||
qWarning() << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
|
||||
|
||||
// second try to decode with strict decoding disabled
|
||||
decoder_option->strict_decoding = 0;
|
||||
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (decoder_option) {
|
||||
heif_decoding_options_free(decoder_option);
|
||||
}
|
||||
@ -814,6 +848,9 @@ bool HEIFHandler::isHeifDecoderAvailable()
|
||||
}
|
||||
#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_decoder_available = heif_have_decoder_for_format(heif_compression_HEVC);
|
||||
m_plugins_queried = true;
|
||||
@ -839,6 +876,9 @@ bool HEIFHandler::isHeifEncoderAvailable()
|
||||
}
|
||||
#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_encoder_available = heif_have_encoder_for_format(heif_compression_HEVC);
|
||||
m_plugins_queried = true;
|
||||
@ -853,6 +893,34 @@ bool HEIFHandler::isHeifEncoderAvailable()
|
||||
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()
|
||||
{
|
||||
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
|
||||
@ -901,6 +969,15 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
|
||||
}
|
||||
return format_cap;
|
||||
}
|
||||
|
||||
if (format == "hej2") {
|
||||
Capabilities format_cap;
|
||||
if (HEIFHandler::isHej2DecoderAvailable()) {
|
||||
format_cap |= CanRead;
|
||||
}
|
||||
return format_cap;
|
||||
}
|
||||
|
||||
if (!format.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
@ -909,8 +986,16 @@ QImageIOPlugin::Capabilities HEIFPlugin::capabilities(QIODevice *device, const Q
|
||||
}
|
||||
|
||||
Capabilities cap;
|
||||
if (device->isReadable() && HEIFHandler::canRead(device) && HEIFHandler::isHeifDecoderAvailable()) {
|
||||
cap |= CanRead;
|
||||
if (device->isReadable()) {
|
||||
const QByteArray header = device->peek(28);
|
||||
|
||||
if (HEIFHandler::isSupportedBMFFType(header) && HEIFHandler::isHeifDecoderAvailable()) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
|
||||
if (HEIFHandler::isSupportedHEJ2(header) && HEIFHandler::isHej2DecoderAvailable()) {
|
||||
cap |= CanRead;
|
||||
}
|
||||
}
|
||||
|
||||
if (device->isWritable() && HEIFHandler::isHeifEncoderAvailable()) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"Keys": [ "heif", "heic" ],
|
||||
"MimeTypes": [ "image/heif", "image/heif" ]
|
||||
"Keys": [ "heif", "heic", "hej2" ],
|
||||
"MimeTypes": [ "image/heif", "image/heif", "image/hej2k" ]
|
||||
}
|
||||
|
@ -24,17 +24,18 @@ public:
|
||||
bool read(QImage *image) override;
|
||||
bool write(const QImage &image) override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
QVariant option(ImageOption option) const override;
|
||||
void setOption(ImageOption option, const QVariant &value) override;
|
||||
bool supportsOption(ImageOption option) const override;
|
||||
|
||||
static bool isHeifDecoderAvailable();
|
||||
static bool isHeifEncoderAvailable();
|
||||
static bool isHej2DecoderAvailable();
|
||||
|
||||
static bool isSupportedBMFFType(const QByteArray &header);
|
||||
static bool isSupportedHEJ2(const QByteArray &header);
|
||||
|
||||
private:
|
||||
static bool isSupportedBMFFType(const QByteArray &header);
|
||||
bool ensureParsed() const;
|
||||
bool ensureDecoder();
|
||||
|
||||
@ -57,6 +58,7 @@ private:
|
||||
static bool m_plugins_queried;
|
||||
static bool m_heif_decoder_available;
|
||||
static bool m_heif_encoder_available;
|
||||
static bool m_hej2_decoder_available;
|
||||
|
||||
static QMutex &getHEIFHandlerMutex();
|
||||
};
|
||||
|
@ -228,7 +228,7 @@ bool QJpegXLHandler::countALLFrames()
|
||||
}
|
||||
|
||||
JxlColorEncoding color_encoding;
|
||||
if (m_basicinfo.uses_original_profile == JXL_FALSE) {
|
||||
if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
|
||||
JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
|
||||
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
|
||||
}
|
||||
@ -960,13 +960,7 @@ bool QJpegXLHandler::rewind()
|
||||
|
||||
JxlDecoderCloseInput(m_decoder);
|
||||
|
||||
if (m_basicinfo.uses_original_profile) {
|
||||
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
|
||||
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
|
||||
m_parseState = ParseJpegXLError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
|
||||
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
|
||||
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
|
||||
m_parseState = ParseJpegXLError;
|
||||
@ -983,6 +977,12 @@ bool QJpegXLHandler::rewind()
|
||||
JxlColorEncoding color_encoding;
|
||||
JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
|
||||
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
|
||||
} else {
|
||||
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
|
||||
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
|
||||
m_parseState = ParseJpegXLError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -308,6 +308,11 @@ static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.BytesPerLine < (header.width() + 7) / 8) {
|
||||
qWarning() << "PCX image has invalid BytesPerLine value";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
return false;
|
||||
@ -418,6 +423,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsigned int bpl = std::min(header.BytesPerLine, static_cast<quint16>(header.width()));
|
||||
|
||||
for (int y = 0; y < header.height(); ++y) {
|
||||
if (s.atEnd()) {
|
||||
return false;
|
||||
@ -434,7 +441,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
|
||||
}
|
||||
|
||||
uint *p = (uint *)img.scanLine(y);
|
||||
for (int x = 0; x < header.width(); ++x) {
|
||||
|
||||
for (unsigned int x = 0; x < bpl; ++x) {
|
||||
p[x] = qRgb(r_buf[x], g_buf[x], b_buf[x]);
|
||||
}
|
||||
}
|
||||
|
@ -807,12 +807,10 @@ QVariant RAWHandler::option(ImageOption option) const
|
||||
rawProcessor->imgdata.rawparams.shot_select = currentImageNumber();
|
||||
#endif
|
||||
if (rawProcessor->open_datastream(&stream) == LIBRAW_SUCCESS) {
|
||||
if (rawProcessor->unpack() == LIBRAW_SUCCESS) {
|
||||
auto w = libraw_get_iwidth(&rawProcessor->imgdata);
|
||||
auto h = libraw_get_iheight(&rawProcessor->imgdata);
|
||||
// flip & 4: taken from LibRaw code
|
||||
v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
|
||||
}
|
||||
auto w = libraw_get_iwidth(&rawProcessor->imgdata);
|
||||
auto h = libraw_get_iheight(&rawProcessor->imgdata);
|
||||
// flip & 4: taken from LibRaw code
|
||||
v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
|
||||
}
|
||||
d->rollbackTransaction();
|
||||
}
|
||||
|
@ -88,10 +88,6 @@ static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
|
||||
s >> head.height;
|
||||
s >> head.pixel_size;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
// Create image.
|
||||
img = imageAlloc(tga.width, tga.height, QImage::Format_RGB32);
|
||||
img = imageAlloc(tga.width, tga.height, imageFormat(tga));
|
||||
if (img.isNull()) {
|
||||
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
|
||||
return false;
|
||||
@ -181,21 +228,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
||||
|
||||
TgaHeaderInfo info(tga);
|
||||
|
||||
// Bits 0-3 are the numbers of alpha bits (can be zero!)
|
||||
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);
|
||||
qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size;
|
||||
|
||||
@ -391,23 +424,25 @@ bool TGAHandler::read(QImage *outImage)
|
||||
{
|
||||
// qDebug() << "Loading TGA file!";
|
||||
|
||||
QDataStream s(device());
|
||||
s.setByteOrder(QDataStream::LittleEndian);
|
||||
|
||||
// Read image header.
|
||||
auto d = device();
|
||||
TgaHeader tga;
|
||||
s >> tga;
|
||||
s.device()->seek(TgaHeader::SIZE + tga.id_length);
|
||||
|
||||
// Check image file format.
|
||||
if (s.atEnd()) {
|
||||
if (!peekHeader(d, tga) || !IsSupported(tga)) {
|
||||
// qDebug() << "This TGA file is not valid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check supported file types.
|
||||
if (!IsSupported(tga)) {
|
||||
// qDebug() << "This TGA file is not supported.";
|
||||
if (d->isSequential()) {
|
||||
d->read(TgaHeader::SIZE + tga.id_length);
|
||||
} 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;
|
||||
}
|
||||
|
||||
@ -468,6 +503,42 @@ bool TGAHandler::write(const QImage &image)
|
||||
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)
|
||||
{
|
||||
if (!device) {
|
||||
@ -491,10 +562,12 @@ bool TGAHandler::canRead(QIODevice *device)
|
||||
return false;
|
||||
}
|
||||
|
||||
QDataStream stream(head);
|
||||
stream.setByteOrder(QDataStream::LittleEndian);
|
||||
TgaHeader tga;
|
||||
stream >> tga;
|
||||
if (!peekHeader(device, tga)) {
|
||||
qWarning("TGAHandler::canRead() error while reading the header");
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsSupported(tga);
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ public:
|
||||
bool read(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);
|
||||
};
|
||||
|
||||
|
@ -1093,7 +1093,9 @@ bool XCFImageFormat::loadProperty(QDataStream &xcf_io, PropType &type, QByteArra
|
||||
size = 0;
|
||||
} else {
|
||||
xcf_io >> size;
|
||||
if (size > 256000) {
|
||||
if (size > 256000 * 4) {
|
||||
// NOTE: I didn't find any reference to maximum property dimensions in the specs, so I assume it's just a sanity check.
|
||||
qCDebug(XCFPLUGIN) << "XCF: loadProperty skips" << type << "due to size being too large";
|
||||
return false;
|
||||
}
|
||||
data = new char[size];
|
||||
@ -1672,8 +1674,12 @@ bool XCFImageFormat::assignImageBytes(Layer &layer, uint i, uint j, const GimpPr
|
||||
for (int y = 0; y < height; y++) {
|
||||
uchar *dataPtr = bits + y * bytesPerLine;
|
||||
uchar *alphaPtr = nullptr;
|
||||
if (!layer.alpha_tiles.isEmpty())
|
||||
alphaPtr = layer.alpha_tiles[j][i].scanLine(y);
|
||||
if (layer.alpha_tiles.size() > j && layer.alpha_tiles.at(j).size() > i) {
|
||||
QImage &alphaTile = layer.alpha_tiles[j][i];
|
||||
if (alphaTile.width() >= width && alphaTile.height() > y) {
|
||||
alphaPtr = alphaTile.scanLine(y);
|
||||
}
|
||||
}
|
||||
if (bpc == 4) {
|
||||
#ifdef USE_FLOAT_IMAGES
|
||||
if (precision < GimpPrecision::GIMP_PRECISION_HALF_LINEAR) {
|
||||
@ -1970,6 +1976,12 @@ static bool convertFloatTo16Bit(uchar *output, quint64 outputSize, uchar *input)
|
||||
*/
|
||||
bool XCFImageFormat::loadLevel(QDataStream &xcf_io, Layer &layer, qint32 bpp, const GimpPrecision precision)
|
||||
{
|
||||
auto bpc = bytesPerChannel(precision);
|
||||
if ((bpc == 0) || (bpp % bpc)) {
|
||||
qCDebug(XCFPLUGIN) << "XCF: the stream seems corrupted";
|
||||
return false;
|
||||
}
|
||||
|
||||
qint32 width;
|
||||
qint32 height;
|
||||
|
||||
@ -2755,10 +2767,10 @@ void XCFImageFormat::copyLayerToImage(XCFImage &xcf_image)
|
||||
// For each tile...
|
||||
|
||||
for (uint j = 0; j < layer.nrows; j++) {
|
||||
uint y = j * TILE_HEIGHT;
|
||||
qint32 y = qint32(j * TILE_HEIGHT);
|
||||
|
||||
for (uint i = 0; i < layer.ncols; i++) {
|
||||
uint x = i * TILE_WIDTH;
|
||||
qint32 x = qint32(i * TILE_WIDTH);
|
||||
|
||||
// This seems the best place to apply the dissolve because it
|
||||
// depends on the global position of each tile's
|
||||
@ -3045,7 +3057,7 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
|
||||
merge = mergeRGBToRGB;
|
||||
break;
|
||||
case GRAY_GIMAGE:
|
||||
if (layer.opacity == OPAQUE_OPACITY) {
|
||||
if (layer.opacity == OPAQUE_OPACITY && xcf_image.image.depth() <= 8) {
|
||||
merge = mergeGrayToGray;
|
||||
} else {
|
||||
merge = mergeGrayToRGB;
|
||||
@ -3181,10 +3193,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
|
||||
qCDebug(XCFPLUGIN) << "Using QPainter for mode" << layer.mode;
|
||||
|
||||
for (uint j = 0; j < layer.nrows; j++) {
|
||||
uint y = j * TILE_HEIGHT;
|
||||
qint32 y = qint32(j * TILE_HEIGHT);
|
||||
|
||||
for (uint i = 0; i < layer.ncols; i++) {
|
||||
uint x = i * TILE_WIDTH;
|
||||
qint32 x = qint32(i * TILE_WIDTH);
|
||||
|
||||
QImage &tile = layer.image_tiles[j][i];
|
||||
if (x + layer.x_offset < MAX_IMAGE_WIDTH &&
|
||||
@ -3210,10 +3222,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
|
||||
#endif
|
||||
|
||||
for (uint j = 0; j < layer.nrows; j++) {
|
||||
uint y = j * TILE_HEIGHT;
|
||||
qint32 y = qint32(j * TILE_HEIGHT);
|
||||
|
||||
for (uint i = 0; i < layer.ncols; i++) {
|
||||
uint x = i * TILE_WIDTH;
|
||||
qint32 x = qint32(i * TILE_WIDTH);
|
||||
|
||||
// This seems the best place to apply the dissolve because it
|
||||
// depends on the global position of each tile's
|
||||
@ -3853,6 +3865,9 @@ bool XCFImageFormat::mergeGrayAToRGB(const Layer &layer, uint i, uint j, int k,
|
||||
}
|
||||
|
||||
switch (layer.mode) {
|
||||
case GIMP_LAYER_MODE_NORMAL:
|
||||
case GIMP_LAYER_MODE_NORMAL_LEGACY:
|
||||
break;
|
||||
case GIMP_LAYER_MODE_MULTIPLY:
|
||||
case GIMP_LAYER_MODE_MULTIPLY_LEGACY: {
|
||||
src = INT_MULT(src, dst);
|
||||
@ -4146,7 +4161,9 @@ bool XCFHandler::canRead() const
|
||||
bool XCFHandler::read(QImage *image)
|
||||
{
|
||||
XCFImageFormat xcfif;
|
||||
return xcfif.readXCF(device(), image);
|
||||
auto ok = xcfif.readXCF(device(), image);
|
||||
m_imageSize = image->size();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool XCFHandler::write(const QImage &)
|
||||
@ -4166,6 +4183,9 @@ QVariant XCFHandler::option(ImageOption option) const
|
||||
QVariant v;
|
||||
|
||||
if (option == QImageIOHandler::Size) {
|
||||
if (!m_imageSize.isEmpty()) {
|
||||
return m_imageSize;
|
||||
}
|
||||
/*
|
||||
* The image structure always starts at offset 0 in the XCF file.
|
||||
* byte[9] "gimp xcf " File type identification
|
||||
@ -4178,7 +4198,7 @@ QVariant XCFHandler::option(ImageOption option) const
|
||||
* uint32 width Width of canvas
|
||||
* uint32 height Height of canvas
|
||||
*/
|
||||
if (auto d = device()) {
|
||||
else if (auto d = device()) {
|
||||
// transactions works on both random and sequential devices
|
||||
d->startTransaction();
|
||||
auto ba9 = d->read(9); // "gimp xcf "
|
||||
|
@ -24,6 +24,13 @@ public:
|
||||
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||
|
||||
static bool canRead(QIODevice *device);
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief m_imageSize
|
||||
* Image size cache used by option()
|
||||
*/
|
||||
QSize m_imageSize;
|
||||
};
|
||||
|
||||
class XCFPlugin : public QImageIOPlugin
|
||||
|
Reference in New Issue
Block a user