diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b94dd54..ea0dfdc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ All notable changes to this project will be documented in this file. -## [0.18.0 - 2024-0x-xx] +## [0.18.0 - 2024-07-27] + +### Added + +- `image.info["heif"]` dictionary with `camera_intrinsic_matrix` HEIF specific metadata. Currently only reading is supported. #234 ### Changed -- libheif updated from `1.17.6` to `1.18.1` version. +- libheif updated from `1.17.6` to `1.18.1` version. #249 ## [0.17.0 - 2024-07-02] diff --git a/LICENSES_bundled.txt b/LICENSES_bundled.txt index e4af284c..5c670b74 100644 --- a/LICENSES_bundled.txt +++ b/LICENSES_bundled.txt @@ -5,14 +5,14 @@ Binary wheels combine several license-compatible libraries. Here they are listed Name: libheif License: LGPLv3 Files: libheif.[dylib|so|dll] - For details, see https://github.com/strukturag/libheif/tree/v1.17.3/COPYING - Source code: https://github.com/strukturag/libheif/tree/v1.17.3 + For details, see https://github.com/strukturag/libheif/tree/v1.18.1/COPYING + Source code: https://github.com/strukturag/libheif/tree/v1.18.1 Name: libde265 License: LGPLv3 Files: libde265.[dylib|so|dll] - For details, see https://github.com/strukturag/libde265/tree/v1.0.12/COPYING - Source code: https://github.com/strukturag/libde265/tree/v1.0.12 + For details, see https://github.com/strukturag/libde265/tree/v1.0.15/COPYING + Source code: https://github.com/strukturag/libde265/tree/v1.0.15 Name: x265 License: GPLv2 diff --git a/libheif/heif.h b/libheif/heif.h index 2cdf17d8..382740bb 100644 --- a/libheif/heif.h +++ b/libheif/heif.h @@ -37,7 +37,7 @@ extern "C" { #include //#include -#define LIBHEIF_NUMERIC_VERSION ((1<<24) | (17<<16) | (6<<8) | 0) +#define LIBHEIF_NUMERIC_VERSION ((1<<24) | (18<<16) | (1<<8) | 0) // API versions table // @@ -232,6 +232,23 @@ enum heif_suberror_code // Invalid specification of region item heif_suberror_Invalid_region_data = 136, + // Image has no ispe property + heif_suberror_No_ispe_property = 137, + + heif_suberror_Camera_intrinsic_matrix_undefined = 138, + + heif_suberror_Camera_extrinsic_matrix_undefined = 139, + + // Invalid JPEG 2000 codestream - usually a missing marker + heif_suberror_Invalid_J2K_codestream = 140, + + heif_suberror_No_vvcC_box = 141, + + // icbr is only needed in some situations, this error is for those cases + heif_suberror_No_icbr_box = 142, + + // Decompressing generic compression or header compression data failed (e.g. bitstream corruption) + heif_suberror_Decompression_invalid_data = 150, // --- Memory_allocation_error --- @@ -240,6 +257,9 @@ enum heif_suberror_code // security limits further. heif_suberror_Security_limit_exceeded = 1000, + // There was an error from the underlying compression / decompression library. + // One possibility is lack of resources (e.g. memory). + heif_suberror_Compression_initialisation_error = 1001, // --- Usage_error --- @@ -288,6 +308,8 @@ enum heif_suberror_code heif_suberror_Unsupported_header_compression_method = 3005, + // Generically compressed data used an unsupported compression method + heif_suberror_Unsupported_generic_compression_method = 3006, // --- Encoder_plugin_error --- @@ -307,9 +329,10 @@ enum heif_suberror_code // --- Plugin loading error --- - heif_suberror_Plugin_loading_error = 6000, // a specific plugin file cannot be loaded - heif_suberror_Plugin_is_not_loaded = 6001, // trying to remove a plugin that is not loaded - heif_suberror_Cannot_read_plugin_directory = 6002 // error while scanning the directory for plugins + heif_suberror_Plugin_loading_error = 6000, // a specific plugin file cannot be loaded + heif_suberror_Plugin_is_not_loaded = 6001, // trying to remove a plugin that is not loaded + heif_suberror_Cannot_read_plugin_directory = 6002, // error while scanning the directory for plugins + heif_suberror_No_matching_decoder_installed = 6003 // no decoder found for that compression format }; @@ -411,7 +434,14 @@ enum heif_compression_format * * See ISO/IEC 23008-12:2022 Section 6.10.2 */ - heif_compression_mask = 9 + heif_compression_mask = 9, + /** + * High Throughput JPEG 2000 (HT-J2K) compression. + * + * The encapsulation of HT-J2K is specified in ISO/IEC 15444-16:2021. + * The core encoding is defined in ISO/IEC 15444-15, or ITU-T T.814. + */ + heif_compression_HTJ2K = 10 }; enum heif_chroma @@ -567,6 +597,18 @@ enum heif_filetype_result LIBHEIF_API enum heif_filetype_result heif_check_filetype(const uint8_t* data, int len); +/** + * Check the filetype box content for a supported file type. + * + *

The data is assumed to start from the start of the `ftyp` box. + * + *

This function checks the compatible brands. + * + * @returns heif_error_ok if a supported brand is found, or other error if not. + */ +LIBHEIF_API +struct heif_error heif_has_compatible_filetype(const uint8_t* data, int len); + LIBHEIF_API int heif_check_jpeg_filetype(const uint8_t* data, int len); @@ -1371,6 +1413,44 @@ struct heif_error heif_image_get_nclx_color_profile(const struct heif_image* ima struct heif_color_profile_nclx** out_data); +// ------------------------- intrinsic and extrinsic matrices ------------------------- + +struct heif_camera_intrinsic_matrix +{ + double focal_length_x; + double focal_length_y; + double principal_point_x; + double principal_point_y; + double skew; +}; + + +LIBHEIF_API +int heif_image_handle_has_camera_intrinsic_matrix(const struct heif_image_handle* handle); + +LIBHEIF_API +struct heif_error heif_image_handle_get_camera_intrinsic_matrix(const struct heif_image_handle* handle, + struct heif_camera_intrinsic_matrix* out_matrix); + + +struct heif_camera_extrinsic_matrix; + +LIBHEIF_API +int heif_image_handle_has_camera_extrinsic_matrix(const struct heif_image_handle* handle); + +LIBHEIF_API +struct heif_error heif_image_handle_get_camera_extrinsic_matrix(const struct heif_image_handle* handle, + struct heif_camera_extrinsic_matrix** out_matrix); + +LIBHEIF_API +void heif_camera_extrinsic_matrix_release(struct heif_camera_extrinsic_matrix*); + +LIBHEIF_API +struct heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const struct heif_camera_extrinsic_matrix*, + double* out_matrix_row_major); + + + // ========================= heif_image ========================= // An heif_image contains a decoded pixel image in various colorspaces, chroma formats, @@ -1714,6 +1794,10 @@ struct heif_error heif_context_write(struct heif_context*, struct heif_writer* writer, void* userdata); +// Add a compatible brand that is now added automatically by libheif when encoding images (e.g. some application brands like 'geo1'). +LIBHEIF_API +void heif_context_add_compatible_brand(struct heif_context* ctx, + heif_brand2 compatible_brand); // ----- encoder ----- @@ -1904,7 +1988,7 @@ struct heif_error heif_encoder_get_parameter_integer(struct heif_encoder*, const char* parameter_name, int* value); -// TOD-O: name should be changed to heif_encoder_get_valid_integer_parameter_range +// TO-DO: name should be changed to heif_encoder_get_valid_integer_parameter_range LIBHEIF_API // DEPRECATED. struct heif_error heif_encoder_parameter_integer_valid_range(struct heif_encoder*, const char* parameter_name, @@ -2019,6 +2103,11 @@ struct heif_encoding_options // version 6 options struct heif_color_conversion_options color_conversion_options; + + // version 7 options + + // Set this to true to use compressed form of uncC where possible + uint8_t prefer_uncC_short_form; }; LIBHEIF_API @@ -2040,6 +2129,27 @@ struct heif_error heif_context_encode_image(struct heif_context*, const struct heif_encoding_options* options, struct heif_image_handle** out_image_handle); +/** + * @brief Encodes an array of images into a grid. + * + * @param ctx The file context + * @param tiles User allocated array of images that will form the grid. + * @param rows The number of rows in the grid. + * @param columns The number of columns in the grid. + * @param encoder Defines the encoder to use. See heif_context_get_encoder_for_format() + * @param input_options Optional, may be nullptr. + * @param out_image_handle Returns a handle to the grid. The caller is responsible for freeing it. + * @return Returns an error if ctx, tiles, or encoder is nullptr. If rows or columns is 0. + */ +LIBHEIF_API +struct heif_error heif_context_encode_grid(struct heif_context* ctx, + struct heif_image** tiles, + uint16_t rows, + uint16_t columns, + struct heif_encoder* encoder, + const struct heif_encoding_options* input_options, + struct heif_image_handle** out_image_handle); + LIBHEIF_API struct heif_error heif_context_set_primary_image(struct heif_context*, struct heif_image_handle* image_handle); @@ -2062,9 +2172,12 @@ struct heif_error heif_context_encode_thumbnail(struct heif_context*, enum heif_metadata_compression { - heif_metadata_compression_off, - heif_metadata_compression_auto, - heif_metadata_compression_deflate + heif_metadata_compression_off = 0, + heif_metadata_compression_auto = 1, + heif_metadata_compression_unknown = 2, // only used when reading unknown method from input file + heif_metadata_compression_deflate = 3, + heif_metadata_compression_zlib = 4, // do not use for header data + heif_metadata_compression_brotli = 5 }; // Assign 'thumbnail_image' as the thumbnail image of 'master_image'. @@ -2102,6 +2215,15 @@ struct heif_error heif_context_add_generic_metadata(struct heif_context* ctx, const void* data, int size, const char* item_type, const char* content_type); +// Add generic metadata with item_type "uri ". Items with this type do not have a content_type, but +// an item_uri_type and they have no content_encoding (they are always stored uncompressed). +LIBHEIF_API +struct heif_error heif_context_add_generic_uri_metadata(struct heif_context* ctx, + const struct heif_image_handle* image_handle, + const void* data, int size, + const char* item_uri_type, + heif_item_id* out_item_id); + // --- heif_image allocation /** diff --git a/pi-heif/LICENSES_bundled.txt b/pi-heif/LICENSES_bundled.txt index c838b6f5..dd62f532 100644 --- a/pi-heif/LICENSES_bundled.txt +++ b/pi-heif/LICENSES_bundled.txt @@ -5,11 +5,11 @@ Binary wheels combine several license-compatible libraries. Here they are listed Name: libheif License: LGPLv3 Files: libheif.[dylib|so|dll] - For details, see https://github.com/strukturag/libheif/tree/v1.17.3/COPYING - Source code: https://github.com/strukturag/libheif/tree/v1.17.3 + For details, see https://github.com/strukturag/libheif/tree/v1.18.1/COPYING + Source code: https://github.com/strukturag/libheif/tree/v1.18.1 Name: libde265 License: LGPLv3 Files: libde265.[dylib|so|dll] - For details, see https://github.com/strukturag/libde265/tree/v1.0.12/COPYING - Source code: https://github.com/strukturag/libde265/tree/v1.0.12 + For details, see https://github.com/strukturag/libde265/tree/v1.0.15/COPYING + Source code: https://github.com/strukturag/libde265/tree/v1.0.15 diff --git a/pillow_heif/_pillow_heif.c b/pillow_heif/_pillow_heif.c index 20c0720d..be80ef46 100644 --- a/pillow_heif/_pillow_heif.c +++ b/pillow_heif/_pillow_heif.c @@ -1183,6 +1183,56 @@ static PyObject* _CtxImage_depth_image_list(CtxImageObject* self, void* closure) return images_list; } +/* =========== CtxImage Experimental Part ======== */ + +static PyObject* _CtxImage_camera_intrinsic_matrix(CtxImageObject* self, void* closure) { + #if LIBHEIF_HAVE_VERSION(1,18,0) + struct heif_camera_intrinsic_matrix camera_intrinsic_matrix; + + if (!heif_image_handle_has_camera_intrinsic_matrix(self->handle)) { + Py_RETURN_NONE; + } + if (check_error(heif_image_handle_get_camera_intrinsic_matrix(self->handle, &camera_intrinsic_matrix))) { + Py_RETURN_NONE; + } + return Py_BuildValue( + "(ddddd)", + camera_intrinsic_matrix.focal_length_x, + camera_intrinsic_matrix.focal_length_y, + camera_intrinsic_matrix.principal_point_x, + camera_intrinsic_matrix.principal_point_y, + camera_intrinsic_matrix.skew + ); + #else + Py_RETURN_NONE; + #endif +} + +static PyObject* _CtxImage_camera_extrinsic_matrix_rot(CtxImageObject* self, void* closure) { + #if LIBHEIF_HAVE_VERSION(1,18,0) + struct heif_camera_extrinsic_matrix* camera_extrinsic_matrix; + double rot[9]; + struct heif_error error; + + if (!heif_image_handle_has_camera_extrinsic_matrix(self->handle)) { + Py_RETURN_NONE; + } + if (check_error(heif_image_handle_get_camera_extrinsic_matrix(self->handle, &camera_extrinsic_matrix))) { + Py_RETURN_NONE; + } + error = heif_camera_extrinsic_matrix_get_rotation_matrix(camera_extrinsic_matrix, rot); + heif_camera_extrinsic_matrix_release(camera_extrinsic_matrix); + if (check_error(error)) { + Py_RETURN_NONE; + } + return Py_BuildValue("(ddddddddd)", rot[0], rot[1], rot[2], rot[3], rot[4], rot[5], rot[6], rot[7], rot[8]); + #else + Py_RETURN_NONE; + #endif +} + +/* =========== CtxImage properties available to Python Part ======== */ + static struct PyGetSetDef _CtxImage_getseters[] = { {"size_mode", (getter)_CtxImage_size_mode, NULL, NULL, NULL}, {"primary", (getter)_CtxImage_primary, NULL, NULL, NULL}, @@ -1195,6 +1245,8 @@ static struct PyGetSetDef _CtxImage_getseters[] = { {"stride", (getter)_CtxImage_stride, NULL, NULL, NULL}, {"data", (getter)_CtxImage_data, NULL, NULL, NULL}, {"depth_image_list", (getter)_CtxImage_depth_image_list, NULL, NULL, NULL}, + {"camera_intrinsic_matrix", (getter)_CtxImage_camera_intrinsic_matrix, NULL, NULL, NULL}, + {"camera_extrinsic_matrix_rot", (getter)_CtxImage_camera_extrinsic_matrix_rot, NULL, NULL, NULL}, {NULL, NULL, NULL, NULL, NULL} }; diff --git a/pillow_heif/_version.py b/pillow_heif/_version.py index f1f6b416..76df4a07 100644 --- a/pillow_heif/_version.py +++ b/pillow_heif/_version.py @@ -1,3 +1,3 @@ """Version of pillow_heif/pi_heif.""" -__version__ = "0.17.0" +__version__ = "0.18.0.dev0" diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index fb58d679..19248054 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -14,6 +14,7 @@ MimCImage, _exif_from_pillow, _get_bytes, + _get_heif_meta, _get_orientation_for_encoder, _get_primary_index, _pil_to_supported_mode, @@ -151,6 +152,7 @@ def __init__(self, c_image): _depth_images: List[Optional[HeifDepthImage]] = ( [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] ) + _heif_meta = _get_heif_meta(c_image) self.info = { "primary": bool(c_image.primary), "bit_depth": int(c_image.bit_depth), @@ -161,6 +163,8 @@ def __init__(self, c_image): } if _xmp: self.info["xmp"] = _xmp + if _heif_meta: + self.info["heif"] = _heif_meta save_colorspace_chroma(c_image, self.info) _color_profile: Dict[str, Any] = c_image.color_profile if _color_profile: diff --git a/pillow_heif/misc.py b/pillow_heif/misc.py index 0267fda7..3165e5aa 100644 --- a/pillow_heif/misc.py +++ b/pillow_heif/misc.py @@ -336,6 +336,31 @@ def _get_primary_index(some_iterator, primary_index: Optional[int]) -> int: return primary_index +def __get_camera_intrinsic_matrix(values: Optional[tuple]): + return ( + { + "focal_length_x": values[0], + "focal_length_y": values[1], + "principal_point_x": values[2], + "principal_point_y": values[3], + "skew": values[4], + } + if values + else None + ) + + +def _get_heif_meta(c_image) -> dict: + r = {} + _camera_intrinsic_matrix = __get_camera_intrinsic_matrix(c_image.camera_intrinsic_matrix) + if _camera_intrinsic_matrix: + r["camera_intrinsic_matrix"] = _camera_intrinsic_matrix + _camera_extrinsic_matrix_rot = c_image.camera_extrinsic_matrix_rot + if _camera_extrinsic_matrix_rot: + r["camera_extrinsic_matrix_rot"] = _camera_extrinsic_matrix_rot + return r + + class CtxEncode: """Encoder bindings from python to python C module.""" @@ -455,6 +480,8 @@ def __init__(self, mode: str, size: tuple, data: bytes, **kwargs): self.primary = False self.chroma = HeifChroma.UNDEFINED.value self.colorspace = HeifColorspace.UNDEFINED.value + self.camera_intrinsic_matrix = None + self.camera_extrinsic_matrix_rot = None @property def size_mode(self): diff --git a/tests/images/heif_other/spatial_photo.heic b/tests/images/heif_other/spatial_photo.heic new file mode 100644 index 00000000..52e63f78 Binary files /dev/null and b/tests/images/heif_other/spatial_photo.heic differ diff --git a/tests/read_test.py b/tests/read_test.py index 7cdb76ac..0f6b6179 100644 --- a/tests/read_test.py +++ b/tests/read_test.py @@ -9,6 +9,7 @@ import dataset import helpers import pytest +from packaging.version import parse as parse_version from PIL import Image, ImageCms, ImageSequence, UnidentifiedImageError import pillow_heif @@ -272,6 +273,8 @@ def test_read_image(convert_hdr_to_8bit: bool) -> bool: assert image.info["bit_depth"] >= 8 assert image.stride >= minimal_stride assert len(image.data) == image.stride * image.size[1] + if str(image_path).find("spatial_photo.HEIC") == -1: + assert "heif" not in image.info return heif_file.info["bit_depth"] > 8 if str(image_path).find("zPug_3.heic") == -1: @@ -295,6 +298,8 @@ def test_pillow_read_images(image_path): collect() assert len(ImageSequence.Iterator(pillow_image)[i].tobytes()) assert isinstance(image.getxmp(), dict) + if str(image_path).find("spatial_photo.HEIC") == -1: + assert "heif" not in image.info assert getattr(pillow_image, "fp") is None if images_count > 1: assert getattr(pillow_image, "_heif_file") is not None @@ -491,6 +496,38 @@ def test_depth_image(): assert im_pil.info == depth_image.info +@pytest.mark.skipif( + parse_version(pillow_heif.libheif_version()) < parse_version("1.18.0"), reason="requires LibHeif 1.18+" +) +def test_read_heif_metadata(): + im = pillow_heif.open_heif("images/heif_other/spatial_photo.heic") + assert "heif" in im.info + assert im.info["heif"]["camera_intrinsic_matrix"] == { + "focal_length_x": 1525.444598197937, + "focal_length_y": 1525.444598197937, + "principal_point_x": 1280.0, + "principal_point_y": 1280.0, + "skew": 0.0, + } + assert im.info["heif"]["camera_extrinsic_matrix_rot"] == (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) + + +@pytest.mark.skipif( + parse_version(pillow_heif.libheif_version()) < parse_version("1.18.0"), reason="requires LibHeif 1.18+" +) +def test_pillow_read_heif_metadata(): + im = Image.open("images/heif_other/spatial_photo.heic") + assert "heif" in im.info + assert im.info["heif"]["camera_intrinsic_matrix"] == { + "focal_length_x": 1525.444598197937, + "focal_length_y": 1525.444598197937, + "principal_point_x": 1280.0, + "principal_point_y": 1280.0, + "skew": 0.0, + } + assert im.info["heif"]["camera_extrinsic_matrix_rot"] == (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0) + + def test_invalid_decoder(): try: pillow_heif.options.PREFERRED_DECODER["HEIF"] = "invalid_id"