Compare commits

...

8 Commits

Author SHA1 Message Date
df82311a10 jxl: Disable color conversion for animations
Partial backport from master
2024-12-02 18:41:10 +01:00
1e47a751df heif: backport fixes from master
Avoid crash in heif_image_handle_has_alpha_channel
Try to load Xiaomi images with non-strict decoding second time.
2024-11-23 16:03:57 +01:00
09b9ff7bf9 exr: Fix read/write with openexr 3.3
It really wants to have a filename

Also it uses seek and tell a lot so sequential devices are for now not
supported

BUGS: 494571

(cherry picked from commit 3489806ae2568b4aba1167b29828dfb745231fb5)
2024-10-16 23:56:17 +02:00
ee77e349e3 Retire KF5 Android CI
See extra-cmake-modules!461.https://invent.kde.org/frameworks/extra-cmake-modules/-/merge_requests/461.

GIT_SILENT
2024-09-19 18:26:52 +02:00
4168c46964 RAW: Fixed unnecessary image unpack
Backport of MR !254 (According to the libraw documentation, the sizes are available directly after open_datastream.)

One benefit is that it doubles the speed of preview creation in Dolphin.
2024-09-15 10:31:40 +00:00
f9f29304d8 XCF: fix crash (KF5) 2024-08-27 21:52:25 +00:00
ec0918d962 PCX and XCF: Fixes backport
Backport quality and security bug fixes from master:
- MR !220
- MR !226
- MR !231 (XCF part only)
- MR !241
- MR !242
- MR !244
2024-08-15 16:04:35 +00:00
dadff2791c avif: check return values
Some libavif calls did not return values in the older versions.
Situation changed meanwhile;
we can check the return values for error conditions now.
2024-07-15 18:24:49 +02:00
9 changed files with 116 additions and 39 deletions

View File

@ -6,7 +6,6 @@ include:
file: file:
- /gitlab-templates/linux.yml - /gitlab-templates/linux.yml
- /gitlab-templates/linux-static.yml - /gitlab-templates/linux-static.yml
- /gitlab-templates/android.yml
- /gitlab-templates/freebsd.yml - /gitlab-templates/freebsd.yml
- /gitlab-templates/windows.yml - /gitlab-templates/windows.yml
- /gitlab-templates/windows-static.yml - /gitlab-templates/windows-static.yml

View File

@ -619,7 +619,15 @@ bool QAVIFHandler::write(const QImage &image)
QImage tmpgrayimage = image.convertToFormat(tmpformat); QImage tmpgrayimage = image.convertToFormat(tmpformat);
avif = avifImageCreate(tmpgrayimage.width(), tmpgrayimage.height(), save_depth, AVIF_PIXEL_FORMAT_YUV400); 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); avifImageAllocatePlanes(avif, AVIF_PLANES_YUV);
#endif
if (tmpgrayimage.colorSpace().isValid()) { if (tmpgrayimage.colorSpace().isValid()) {
avif->colorPrimaries = (avifColorPrimaries)1; avif->colorPrimaries = (avifColorPrimaries)1;
@ -806,7 +814,15 @@ bool QAVIFHandler::write(const QImage &image)
avif->transferCharacteristics = transfer_to_save; avif->transferCharacteristics = transfer_to_save;
if (iccprofile.size() > 0) { 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()); avifImageSetProfileICC(avif, reinterpret_cast<const uint8_t *>(iccprofile.constData()), iccprofile.size());
#endif
} }
avifRGBImage rgb; avifRGBImage rgb;
@ -971,6 +987,8 @@ bool QAVIFHandler::jumpToNextImage()
return false; return false;
} }
avifResult decodeResult;
if (m_decoder->imageIndex >= 0) { if (m_decoder->imageIndex >= 0) {
if (m_decoder->imageCount < 2) { if (m_decoder->imageCount < 2) {
m_parseState = ParseAvifSuccess; m_parseState = ParseAvifSuccess;
@ -978,11 +996,16 @@ bool QAVIFHandler::jumpToNextImage()
} }
if (m_decoder->imageIndex >= m_decoder->imageCount - 1) { // start from beginning 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) { if (decodeResult != AVIF_RESULT_OK) {
qWarning("ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult)); qWarning("ERROR: Failed to decode Next image in sequence: %s", avifResultToString(decodeResult));

View File

@ -68,8 +68,8 @@
class K_IStream : public Imf::IStream class K_IStream : public Imf::IStream
{ {
public: public:
K_IStream(QIODevice *dev, const QByteArray &fileName) K_IStream(QIODevice *dev)
: IStream(fileName.data()) : IStream("K_IStream")
, m_dev(dev) , m_dev(dev)
{ {
} }
@ -159,7 +159,7 @@ bool EXRHandler::read(QImage *outImage)
int width; int width;
int height; int height;
K_IStream istr(device(), QByteArray()); K_IStream istr(device());
Imf::RgbaInputFile file(istr); Imf::RgbaInputFile file(istr);
Imath::Box2i dw = file.dataWindow(); Imath::Box2i dw = file.dataWindow();
bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A; bool isRgba = file.channels() & Imf::RgbaChannels::WRITE_A;
@ -271,6 +271,13 @@ bool EXRHandler::canRead(QIODevice *device)
return false; 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); const QByteArray head = device->peek(4);
return Imf::isImfMagic(head.data()); return Imf::isImfMagic(head.data());

View File

@ -476,8 +476,17 @@ bool HEIFHandler::ensureDecoder()
return false; 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); 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; heif_chroma chroma;
QImage::Format target_image_format; QImage::Format target_image_format;
@ -502,11 +511,7 @@ bool HEIFHandler::ensureDecoder()
m_parseState = ParseHeicError; m_parseState = ParseHeicError;
heif_image_handle_release(handle); heif_image_handle_release(handle);
heif_context_free(ctx); heif_context_free(ctx);
if (bit_depth > 0) { qWarning() << "Unsupported bit depth:" << bit_depth;
qWarning() << "Unsupported bit depth:" << bit_depth;
} else {
qWarning() << "Undefined bit depth.";
}
return false; return false;
} }
@ -519,6 +524,16 @@ bool HEIFHandler::ensureDecoder()
struct heif_image *img = nullptr; struct heif_image *img = nullptr;
err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, decoder_option); 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) { if (decoder_option) {
heif_decoding_options_free(decoder_option); heif_decoding_options_free(decoder_option);
} }

View File

@ -228,7 +228,7 @@ bool QJpegXLHandler::countALLFrames()
} }
JxlColorEncoding color_encoding; 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); JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding); JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding);
} }
@ -960,13 +960,7 @@ bool QJpegXLHandler::rewind()
JxlDecoderCloseInput(m_decoder); JxlDecoderCloseInput(m_decoder);
if (m_basicinfo.uses_original_profile) { if (m_basicinfo.uses_original_profile == JXL_FALSE && m_basicinfo.have_animation == JXL_FALSE) {
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError;
return false;
}
} else {
if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) { if (JxlDecoderSubscribeEvents(m_decoder, JXL_DEC_COLOR_ENCODING | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
qWarning("ERROR: JxlDecoderSubscribeEvents failed"); qWarning("ERROR: JxlDecoderSubscribeEvents failed");
m_parseState = ParseJpegXLError; m_parseState = ParseJpegXLError;
@ -983,6 +977,12 @@ bool QJpegXLHandler::rewind()
JxlColorEncoding color_encoding; JxlColorEncoding color_encoding;
JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE); JxlColorEncodingSetToSRGB(&color_encoding, JXL_FALSE);
JxlDecoderSetPreferredColorProfile(m_decoder, &color_encoding); 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; return true;

View File

@ -308,6 +308,11 @@ static bool readImage4(QImage &img, QDataStream &s, const PCXHEADER &header)
return false; 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) { for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) { if (s.atEnd()) {
return false; return false;
@ -418,6 +423,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
return false; return false;
} }
const unsigned int bpl = std::min(header.BytesPerLine, static_cast<quint16>(header.width()));
for (int y = 0; y < header.height(); ++y) { for (int y = 0; y < header.height(); ++y) {
if (s.atEnd()) { if (s.atEnd()) {
return false; return false;
@ -434,7 +441,8 @@ static bool readImage24(QImage &img, QDataStream &s, const PCXHEADER &header)
} }
uint *p = (uint *)img.scanLine(y); 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]); p[x] = qRgb(r_buf[x], g_buf[x], b_buf[x]);
} }
} }

View File

@ -807,12 +807,10 @@ QVariant RAWHandler::option(ImageOption option) const
rawProcessor->imgdata.rawparams.shot_select = currentImageNumber(); rawProcessor->imgdata.rawparams.shot_select = currentImageNumber();
#endif #endif
if (rawProcessor->open_datastream(&stream) == LIBRAW_SUCCESS) { if (rawProcessor->open_datastream(&stream) == LIBRAW_SUCCESS) {
if (rawProcessor->unpack() == LIBRAW_SUCCESS) { auto w = libraw_get_iwidth(&rawProcessor->imgdata);
auto w = libraw_get_iwidth(&rawProcessor->imgdata); auto h = libraw_get_iheight(&rawProcessor->imgdata);
auto h = libraw_get_iheight(&rawProcessor->imgdata); // flip & 4: taken from LibRaw code
// flip & 4: taken from LibRaw code v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
v = (rawProcessor->imgdata.sizes.flip & 4) ? QSize(h, w) : QSize(w, h);
}
} }
d->rollbackTransaction(); d->rollbackTransaction();
} }

View File

@ -1093,7 +1093,9 @@ bool XCFImageFormat::loadProperty(QDataStream &xcf_io, PropType &type, QByteArra
size = 0; size = 0;
} else { } else {
xcf_io >> size; 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; return false;
} }
data = new char[size]; 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++) { for (int y = 0; y < height; y++) {
uchar *dataPtr = bits + y * bytesPerLine; uchar *dataPtr = bits + y * bytesPerLine;
uchar *alphaPtr = nullptr; uchar *alphaPtr = nullptr;
if (!layer.alpha_tiles.isEmpty()) if (layer.alpha_tiles.size() > j && layer.alpha_tiles.at(j).size() > i) {
alphaPtr = layer.alpha_tiles[j][i].scanLine(y); QImage &alphaTile = layer.alpha_tiles[j][i];
if (alphaTile.width() >= width && alphaTile.height() > y) {
alphaPtr = alphaTile.scanLine(y);
}
}
if (bpc == 4) { if (bpc == 4) {
#ifdef USE_FLOAT_IMAGES #ifdef USE_FLOAT_IMAGES
if (precision < GimpPrecision::GIMP_PRECISION_HALF_LINEAR) { 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) 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 width;
qint32 height; qint32 height;
@ -2755,10 +2767,10 @@ void XCFImageFormat::copyLayerToImage(XCFImage &xcf_image)
// For each tile... // For each tile...
for (uint j = 0; j < layer.nrows; j++) { 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++) { 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 // This seems the best place to apply the dissolve because it
// depends on the global position of each tile's // depends on the global position of each tile's
@ -3045,7 +3057,7 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
merge = mergeRGBToRGB; merge = mergeRGBToRGB;
break; break;
case GRAY_GIMAGE: case GRAY_GIMAGE:
if (layer.opacity == OPAQUE_OPACITY) { if (layer.opacity == OPAQUE_OPACITY && xcf_image.image.depth() <= 8) {
merge = mergeGrayToGray; merge = mergeGrayToGray;
} else { } else {
merge = mergeGrayToRGB; merge = mergeGrayToRGB;
@ -3181,10 +3193,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
qCDebug(XCFPLUGIN) << "Using QPainter for mode" << layer.mode; qCDebug(XCFPLUGIN) << "Using QPainter for mode" << layer.mode;
for (uint j = 0; j < layer.nrows; j++) { 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++) { 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]; QImage &tile = layer.image_tiles[j][i];
if (x + layer.x_offset < MAX_IMAGE_WIDTH && if (x + layer.x_offset < MAX_IMAGE_WIDTH &&
@ -3210,10 +3222,10 @@ void XCFImageFormat::mergeLayerIntoImage(XCFImage &xcf_image)
#endif #endif
for (uint j = 0; j < layer.nrows; j++) { 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++) { 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 // This seems the best place to apply the dissolve because it
// depends on the global position of each tile's // 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) { 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:
case GIMP_LAYER_MODE_MULTIPLY_LEGACY: { case GIMP_LAYER_MODE_MULTIPLY_LEGACY: {
src = INT_MULT(src, dst); src = INT_MULT(src, dst);
@ -4146,7 +4161,9 @@ bool XCFHandler::canRead() const
bool XCFHandler::read(QImage *image) bool XCFHandler::read(QImage *image)
{ {
XCFImageFormat xcfif; XCFImageFormat xcfif;
return xcfif.readXCF(device(), image); auto ok = xcfif.readXCF(device(), image);
m_imageSize = image->size();
return ok;
} }
bool XCFHandler::write(const QImage &) bool XCFHandler::write(const QImage &)
@ -4166,6 +4183,9 @@ QVariant XCFHandler::option(ImageOption option) const
QVariant v; QVariant v;
if (option == QImageIOHandler::Size) { if (option == QImageIOHandler::Size) {
if (!m_imageSize.isEmpty()) {
return m_imageSize;
}
/* /*
* The image structure always starts at offset 0 in the XCF file. * The image structure always starts at offset 0 in the XCF file.
* byte[9] "gimp xcf " File type identification * byte[9] "gimp xcf " File type identification
@ -4178,7 +4198,7 @@ QVariant XCFHandler::option(ImageOption option) const
* uint32 width Width of canvas * uint32 width Width of canvas
* uint32 height Height of canvas * uint32 height Height of canvas
*/ */
if (auto d = device()) { else if (auto d = device()) {
// transactions works on both random and sequential devices // transactions works on both random and sequential devices
d->startTransaction(); d->startTransaction();
auto ba9 = d->read(9); // "gimp xcf " auto ba9 = d->read(9); // "gimp xcf "

View File

@ -24,6 +24,13 @@ public:
QVariant option(QImageIOHandler::ImageOption option) const override; QVariant option(QImageIOHandler::ImageOption option) const override;
static bool canRead(QIODevice *device); static bool canRead(QIODevice *device);
private:
/*!
* \brief m_imageSize
* Image size cache used by option()
*/
QSize m_imageSize;
}; };
class XCFPlugin : public QImageIOPlugin class XCFPlugin : public QImageIOPlugin