HEIF: image transformation support

This commit is contained in:
Mirco Miranda
2026-04-21 08:32:39 +02:00
committed by Mirco Miranda
parent 18d0f93d60
commit 8048279473
39 changed files with 281 additions and 62 deletions

View File

@@ -72,7 +72,7 @@ set_property(CACHE KIMAGEFORMATS_HEJ2_TEST PROPERTY STRINGS "OFF" "READ_ONLY" "A
set(KIMAGEFORMATS_AVCI_TEST "ALL" CACHE STRING "Enable AVCI tests: OFF, ALL")
set_property(CACHE KIMAGEFORMATS_AVCI_TEST PROPERTY STRINGS "OFF" "ALL")
if(KIMAGEFORMATS_HEIF)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.10.0)
pkg_check_modules(LibHeif IMPORTED_TARGET libheif>=1.17.0)
endif()
add_feature_info(LibHeif LibHeif_FOUND "required for the QImage plugin for HEIF/HEIC images")

View File

@@ -333,6 +333,15 @@ distributions. In particular, it is necessary that the HEIF library has
support for HEVC codec. If HEVC codec is not available the plugin
will compile but will fail the tests.
The following defines can be defined in cmake to modify the behavior of the
plugin:
- `HEIF_DISABLE_QT_TRANSFORMATION`: HEIF transformations, in addition to
rotations and reflections, also support image cropping. Consequently, the
Qt plugin, must also honor the crop. This define is useful in case
of problems: activating it disables Qt's support for transformations,
delegating them to the HEIF libraries (which will therefore always apply
them regardless of what is requested from Qt).
**If you are interested in compiling the plugin without running the tests,
also use the following string options:**
- `KIMAGEFORMATS_HEIF_TEST` to change the behaviour of HEIF tests. Set to

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View File

@@ -0,0 +1,17 @@
[
{
"fileName" : "orientation_all.png",
"colorSpace" : {
"description" : "GIMP built-in sRGB",
"primaries" : "SRgb",
"transferFunction" : "SRgb",
"gamma" : 0
},
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

View File

@@ -0,0 +1,11 @@
[
{
"fileName" : "orientation_all.png",
"metadata" : [
{
"key" : "Software" ,
"value" : "LIFE Pro 2.20.35 (Linux)"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -11,6 +11,7 @@
#include "microexif_p.h"
#include "util_p.h"
#include <libheif/heif.h>
#include <libheif/heif_properties.h>
#include <QColorSpace>
#include <QLoggingCategory>
@@ -33,6 +34,18 @@ Q_LOGGING_CATEGORY(LOG_HEIFPLUGIN, "kf.imageformats.plugins.heif", QtWarningMsg)
#define HEIF_MAX_METADATA_SIZE (4 * 1024 * 1024)
#endif
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
/*!
* HEIF transformations, in addition to rotations and reflections,
* also support image cropping. Consequently, the Qt plugin, must
* also honor the crop. This define is useful in case of problems:
* activating it disables Qt's support for transformations,
* delegating them to the HEIF libraries (which will therefore
* always apply them regardless of what is requested from Qt).
*/
// #define HEIF_DISABLE_QT_TRANSFORMATION
#endif
size_t HEIFHandler::m_initialized_count = 0;
bool HEIFHandler::m_plugins_queried = false;
bool HEIFHandler::m_heif_decoder_available = false;
@@ -72,6 +85,7 @@ static struct heif_error heifhandler_write_callback(struct heif_context * /* ctx
HEIFHandler::HEIFHandler()
: m_parseState(ParseHeicNotParsed)
, m_quality(100)
, m_orientation(0)
{
}
@@ -123,15 +137,11 @@ bool HEIFHandler::write(const QImage &image)
return false;
}
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
startHeifLib();
#endif
bool success = write_helper(image);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -163,12 +173,10 @@ bool HEIFHandler::write_helper(const QImage &image)
}
heif_compression_format encoder_codec = heif_compression_HEVC;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (format() == "hej2") {
encoder_codec = heif_compression_JPEG2000;
save_depth = 8; // for compatibility reasons
}
#endif
heif_chroma chroma;
if (save_depth > 8) {
@@ -325,12 +333,21 @@ bool HEIFHandler::write_helper(const QImage &image)
}
}
if (m_orientation >= 1 && m_orientation <= 8) {
// Function available from HEIF v1.14
encoder_options->image_orientation = heif_orientation(m_orientation);
}
struct heif_image_handle *handle;
err = heif_context_encode_image(context, h_image, encoder, encoder_options, &handle);
// exif metadata
if (err.code == heif_error_Ok) {
auto exif = MicroExif::fromImage(tmpimage);
if (m_orientation >= 1 && m_orientation <= 8) {
// EXIF orientation must be coherent with HEIF orientation
exif.setOrientation(m_orientation);
}
if (!exif.isEmpty()) {
auto ba = exif.toByteArray();
err = heif_context_add_exif_metadata(context, handle, ba.constData(), ba.size());
@@ -376,6 +393,74 @@ bool HEIFHandler::write_helper(const QImage &image)
return true;
}
bool HEIFHandler::read_orientation_helper(void *heif_handle, const void *heif_ctx)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
// get the properties
heif_transform_mirror_direction mirror = heif_transform_mirror_direction::heif_transform_mirror_direction_invalid;
heif_property_id mir_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_mirror, &mir_id, 1) > 0) {
mirror = heif_item_get_property_transform_mirror(ctx, item_id, mir_id);
if (mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid)
return false;
}
int rotation_ccw = -1;
heif_property_id rot_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_rotation, &rot_id, 1) > 0) {
rotation_ccw = heif_item_get_property_transform_rotation_ccw(ctx, item_id, rot_id);
if (rotation_ccw == -1)
return false;
}
if (rotation_ccw == -1 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 0;
} else if (rotation_ccw == 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 1;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 2;
} else if (rotation_ccw == 180 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 3;
} else if (rotation_ccw <= 0 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 4;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_horizontal) {
m_orientation = 5;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 6;
} else if (rotation_ccw == 270 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_vertical) {
m_orientation = 7;
} else if (rotation_ccw == 90 && mirror == heif_transform_mirror_direction::heif_transform_mirror_direction_invalid) {
m_orientation = 8;
}
return true;
}
bool HEIFHandler::read_crop(void *heif_handle, const void *heif_ctx, const QSize &size, QRect &crop)
{
if (heif_handle == nullptr || heif_ctx == nullptr) {
return false;
}
auto handle = reinterpret_cast<heif_image_handle *>(heif_handle);
auto ctx = reinterpret_cast<const heif_context *>(heif_ctx);
auto item_id = heif_image_handle_get_item_id(handle);
heif_property_id crop_id;
if (heif_item_get_properties_of_type(ctx, item_id, heif_item_property_type_transform_crop, &crop_id, 1) > 0) {
int l = 0, t = 0, r = 0, b = 0;
heif_item_get_property_transform_crop_borders(ctx, item_id, crop_id, size.width(), size.height(), &l, &t, &r, &b);
crop = QRect(QPoint(t, l), size - QSize(b + t, r + l));
}
return crop.isValid();
}
bool HEIFHandler::isSupportedBMFFType(const QByteArray &header)
{
if (header.size() < 28) {
@@ -456,10 +541,10 @@ QVariant HEIFHandler::option(ImageOption option) const
switch (option) {
case Size:
return m_current_image.size();
break;
case ImageTransformation:
return int(MicroExif::orientationToTransformation(m_orientation));
default:
return QVariant();
break;
}
}
@@ -474,6 +559,9 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
m_quality = 100;
}
break;
case ImageTransformation:
m_orientation = MicroExif::transformationToOrientation(QImageIOHandler::Transformation(value.toUInt()));
break;
default:
QImageIOHandler::setOption(option, value);
break;
@@ -482,7 +570,11 @@ void HEIFHandler::setOption(ImageOption option, const QVariant &value)
bool HEIFHandler::supportsOption(ImageOption option) const
{
return option == Quality || option == Size;
auto ok = option == Quality || option == Size;
#ifndef HEIF_DISABLE_QT_TRANSFORMATION
ok = ok || option == ImageTransformation;
#endif
return ok;
}
bool HEIFHandler::ensureParsed() const
@@ -496,15 +588,12 @@ bool HEIFHandler::ensureParsed() const
HEIFHandler *that = const_cast<HEIFHandler *>(this);
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
startHeifLib();
#endif
bool success = that->ensureDecoder();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
finishHeifLib();
#endif
return success;
}
@@ -589,16 +678,19 @@ bool HEIFHandler::ensureDecoder()
return false;
}
bool ignore_transformations = false;
struct heif_decoding_options *decoder_option = heif_decoding_options_alloc();
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
decoder_option->strict_decoding = 1;
#ifdef HEIF_DISABLE_QT_TRANSFORMATION
decoder_option->ignore_transformations = ignore_transformations;
#else
decoder_option->ignore_transformations = ignore_transformations = read_orientation_helper(handle, ctx);
#endif
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")) {
qCWarning(LOG_HEIFPLUGIN) << "Non-standard HEIF image with invalid matrix_coefficients, probably made by a Xiaomi device!";
@@ -606,7 +698,6 @@ bool HEIFHandler::ensureDecoder()
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);
@@ -852,6 +943,12 @@ bool HEIFHandler::ensureDecoder()
break;
}
if (ignore_transformations) {
QRect crop_rect;
if (read_crop(handle, ctx, m_current_image.size(), crop_rect))
m_current_image = m_current_image.copy(crop_rect);
}
heif_color_profile_type profileType = heif_image_handle_get_color_profile_type(handle);
if (profileType == heif_color_profile_type_prof || profileType == heif_color_profile_type_rICC) {
size_t rawProfileSize = heif_image_handle_get_raw_color_profile_size(handle);
@@ -1045,34 +1142,27 @@ void HEIFHandler::queryHeifLib()
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);
m_hej2_encoder_available = heif_have_encoder_for_format(heif_compression_JPEG2000);
#endif
#if LIBHEIF_HAVE_VERSION(1, 19, 6)
m_avci_decoder_available = heif_have_decoder_for_format(heif_compression_AVC);
#endif
m_plugins_queried = true;
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
}
void HEIFHandler::startHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1080,12 +1170,10 @@ void HEIFHandler::startHeifLib()
}
m_initialized_count++;
#endif
}
void HEIFHandler::finishHeifLib()
{
#if LIBHEIF_HAVE_VERSION(1, 13, 0)
QMutexLocker locker(&getHEIFHandlerMutex());
if (m_initialized_count == 0) {
@@ -1096,8 +1184,6 @@ void HEIFHandler::finishHeifLib()
if (m_initialized_count == 0) {
heif_deinit();
}
#endif
}
QMutex &HEIFHandler::getHEIFHandlerMutex()

View File

@@ -51,9 +51,24 @@ private:
ParseHeicState m_parseState;
int m_quality;
QImage m_current_image;
quint16 m_orientation;
bool write_helper(const QImage &image);
/*!
* \brief heif_orientation_helper
* Read the transform_mirror and transform_rotation_ccw properties and set \a m_orientation
* \return True on success, otherwise false.
*/
bool read_orientation_helper(void *heif_handle, const void *heif_ctx);
/*!
* \brief read_crop
* Read the crop information.
* \return True on success, otherwise false.
*/
bool read_crop(void *heif_handle, const void *heif_ctx, const QSize& size, QRect &crop);
static void startHeifLib();
static void finishHeifLib();
static void queryHeifLib();

View File

@@ -744,7 +744,42 @@ void MicroExif::setOrientation(quint16 orient)
QImageIOHandler::Transformation MicroExif::transformation() const
{
switch (orientation()) {
return orientationToTransformation(orientation());
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
setOrientation(transformationToOrientation(t));
}
quint16 MicroExif::transformationToOrientation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
return 1;
case QImageIOHandler::TransformationMirror:
return 2;
case QImageIOHandler::TransformationRotate180:
return 3;
case QImageIOHandler::TransformationFlip:
return 4;
case QImageIOHandler::TransformationFlipAndRotate90:
return 5;
case QImageIOHandler::TransformationRotate90:
return 6;
case QImageIOHandler::TransformationMirrorAndRotate90:
return 7;
case QImageIOHandler::TransformationRotate270:
return 8;
default:
break;
}
return 0; // no orientation set
}
QImageIOHandler::Transformation MicroExif::orientationToTransformation(quint16 o)
{
switch (o) {
case 1:
return QImageIOHandler::TransformationNone;
case 2:
@@ -767,39 +802,6 @@ QImageIOHandler::Transformation MicroExif::transformation() const
return QImageIOHandler::TransformationNone;
}
void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
switch (t) {
case QImageIOHandler::TransformationNone:
setOrientation(1);
break;
case QImageIOHandler::TransformationMirror:
setOrientation(2);
break;
case QImageIOHandler::TransformationRotate180:
setOrientation(3);
break;
case QImageIOHandler::TransformationFlip:
setOrientation(4);
break;
case QImageIOHandler::TransformationFlipAndRotate90:
setOrientation(5);
break;
case QImageIOHandler::TransformationRotate90:
setOrientation(6);
break;
case QImageIOHandler::TransformationMirrorAndRotate90:
setOrientation(7);
break;
case QImageIOHandler::TransformationRotate270:
setOrientation(8);
break;
default:
break;
}
setOrientation(0); // no orientation set
}
QString MicroExif::software() const
{
return tiffString(TIFF_SOFTWARE);

View File

@@ -210,6 +210,19 @@ public:
QImageIOHandler::Transformation transformation() const;
void setTransformation(const QImageIOHandler::Transformation& t);
/*!
* \brief transformationToOrientation
* \param t The Qt transformation.
* \return The EXIF orientation value or 0 if none.
*/
static quint16 transformationToOrientation(const QImageIOHandler::Transformation& t);
/*!
* \brief orientationToTransformation
* \param o The EXIF orientation.
* \return The orientation converted in the equivalent Qt transformation.
*/
static QImageIOHandler::Transformation orientationToTransformation(quint16 o);
/*!
* \brief software
* \return Name and version number of the software package(s) used to create the image.