Version in base suite: 1.19.8-1+deb13u1 Base version: libheif_1.19.8-1+deb13u1 Target version: libheif_1.23.4-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/libh/libheif/libheif_1.19.8-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/libh/libheif/libheif_1.23.4-1~deb13u1.dsc /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/av1-huge-frame-header-oom.heic |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avc32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avif32-mini.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avif32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/hevc32-mini.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/hevc32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/j2k32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/jpeg32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/unci32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/vvc32.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/sequence_corpus/seq_seed.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped.heic |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped_irot_imir.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_height.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_irot180.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_irot90.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_width.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/conformance_window_padding.heic |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/lightning_mini.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/meta_size_zero.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/mini_size_zero.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/moov_size_zero.heif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/rainbow-451x461.heic |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/simple_osm_tile_alpha.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/simple_osm_tile_meta.avif |binary /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/with-alpha-512x512.heic |binary libheif-1.23.4/CMakeLists.txt | 168 libheif-1.23.4/CMakePresets.json | 9 libheif-1.23.4/README.md | 185 libheif-1.23.4/SECURITY.md | 162 libheif-1.23.4/appveyor.yml | 34 libheif-1.23.4/build-emscripten.sh | 58 libheif-1.23.4/cmake/modules/FindAOM.cmake | 77 libheif-1.23.4/cmake/modules/FindBrotli.cmake | 2 libheif-1.23.4/cmake/modules/FindFFMPEG.cmake | 16 libheif-1.23.4/cmake/modules/FindOPENJPH.cmake | 72 libheif-1.23.4/cmake/modules/FindWEBP.cmake | 74 libheif-1.23.4/cmake/modules/FindX264.cmake | 38 libheif-1.23.4/cmake/modules/Findkvazaar.cmake | 5 libheif-1.23.4/debian/changelog | 172 libheif-1.23.4/debian/control | 20 libheif-1.23.4/debian/libheif1.symbols | 195 libheif-1.23.4/debian/patches/CVE-2025-68431.patch | 21 libheif-1.23.4/debian/patches/CVE-2026-32740.patch | 59 libheif-1.23.4/debian/patches/CVE-2026-32741.patch | 24 libheif-1.23.4/debian/patches/CVE-2026-32882.patch | 21 libheif-1.23.4/debian/patches/CVE-2026-47178.patch | 60 libheif-1.23.4/debian/patches/CVE-2026-47247.patch | 51 libheif-1.23.4/debian/patches/CVE-2026-47709-1_uncC-tile-count-overflow.patch | 29 libheif-1.23.4/debian/patches/CVE-2026-47709-2_missing-ispe-null-deref.patch | 36 libheif-1.23.4/debian/patches/CVE-2026-47714.patch | 222 libheif-1.23.4/debian/patches/CVE-2026-48029.patch | 120 libheif-1.23.4/debian/patches/CVE-2026-49271.patch | 26 libheif-1.23.4/debian/patches/CVE-2026-62289-clap-zero-size-tiling-underflow.patch | 97 libheif-1.23.4/debian/patches/CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch | 52 libheif-1.23.4/debian/patches/GHSA-5hqq-636x-r3cr.patch | 138 libheif-1.23.4/debian/patches/GHSA-73p7-m7gg-w2jv.patch | 52 libheif-1.23.4/debian/patches/overlay-simplify-overlap-area-computation.patch | 133 libheif-1.23.4/debian/patches/series | 16 libheif-1.23.4/debian/rules | 11 libheif-1.23.4/examples/CMakeLists.txt | 82 libheif-1.23.4/examples/SAI_datafile.cc | 238 libheif-1.23.4/examples/SAI_datafile.h | 54 libheif-1.23.4/examples/common.cc | 123 libheif-1.23.4/examples/common.h | 17 libheif-1.23.4/examples/heif_dec.cc | 434 - libheif-1.23.4/examples/heif_enc.cc | 1764 ++++ libheif-1.23.4/examples/heif_gen_bayer.cc | 664 + libheif-1.23.4/examples/heif_info.cc | 485 + libheif-1.23.4/examples/heif_test.cc | 6 libheif-1.23.4/examples/heif_thumbnailer.cc | 2 libheif-1.23.4/examples/heif_view.cc | 482 + libheif-1.23.4/examples/sdl.cc | 302 libheif-1.23.4/examples/sdl.hh | 82 libheif-1.23.4/examples/utf8.manifest | 9 libheif-1.23.4/examples/utf8.rc | 3 libheif-1.23.4/examples/vmt.cc | 314 libheif-1.23.4/examples/vmt.h | 38 libheif-1.23.4/funding.json | 123 libheif-1.23.4/fuzzing/CMakeLists.txt | 11 libheif-1.23.4/fuzzing/api_fuzzer.cc | 325 libheif-1.23.4/fuzzing/box_fuzzer.cc | 10 libheif-1.23.4/fuzzing/color_conversion_fuzzer.cc | 18 libheif-1.23.4/fuzzing/encoder_fuzzer.cc | 19 libheif-1.23.4/fuzzing/encoder_fuzzer.options | 4 libheif-1.23.4/fuzzing/encoder_fuzzer_lsan_suppressions.txt | 1 libheif-1.23.4/fuzzing/file_fuzzer.cc | 14 libheif-1.23.4/fuzzing/sequence_fuzzer.cc | 78 libheif-1.23.4/fuzzing/tile_fuzzer.cc | 174 libheif-1.23.4/gdk-pixbuf/pixbufloader-heif.c | 17 libheif-1.23.4/go/heif/heif.go | 82 libheif-1.23.4/heifio/CMakeLists.txt | 67 libheif-1.23.4/heifio/decoder_heif.cc | 140 libheif-1.23.4/heifio/decoder_heif.h | 35 libheif-1.23.4/heifio/decoder_jpeg.cc | 66 libheif-1.23.4/heifio/decoder_png.cc | 219 libheif-1.23.4/heifio/decoder_raw.cc | 220 libheif-1.23.4/heifio/decoder_raw.h | 51 libheif-1.23.4/heifio/decoder_tiff.cc | 1757 ++++ libheif-1.23.4/heifio/decoder_tiff.h | 63 libheif-1.23.4/heifio/decoder_webp.cc | 326 libheif-1.23.4/heifio/decoder_webp.h | 35 libheif-1.23.4/heifio/decoder_y4m.cc | 8 libheif-1.23.4/heifio/encoder.h | 16 libheif-1.23.4/heifio/encoder_jpeg.cc | 187 libheif-1.23.4/heifio/encoder_jpeg.h | 10 libheif-1.23.4/heifio/encoder_png.cc | 162 libheif-1.23.4/heifio/encoder_png.h | 9 libheif-1.23.4/heifio/encoder_tiff.cc | 6 libheif-1.23.4/heifio/encoder_tiff.h | 6 libheif-1.23.4/heifio/encoder_webp.cc | 246 libheif-1.23.4/heifio/encoder_webp.h | 64 libheif-1.23.4/heifio/encoder_y4m.cc | 8 libheif-1.23.4/heifio/encoder_y4m.h | 10 libheif-1.23.4/heifio/exif.cc | 45 libheif-1.23.4/heifio/exif.h | 4 libheif-1.23.4/libheif/CMakeLists.txt | 142 libheif-1.23.4/libheif/Doxyfile.in | 8 libheif-1.23.4/libheif/api/libheif/api_structs.h | 82 libheif-1.23.4/libheif/api/libheif/heif.cc | 3824 ---------- libheif-1.23.4/libheif/api/libheif/heif.h | 2621 ------ libheif-1.23.4/libheif/api/libheif/heif_aux_images.cc | 351 libheif-1.23.4/libheif/api/libheif/heif_aux_images.h | 182 libheif-1.23.4/libheif/api/libheif/heif_brands.cc | 469 + libheif-1.23.4/libheif/api/libheif/heif_brands.h | 385 + libheif-1.23.4/libheif/api/libheif/heif_color.cc | 684 + libheif-1.23.4/libheif/api/libheif/heif_color.h | 479 + libheif-1.23.4/libheif/api/libheif/heif_components.cc | 279 libheif-1.23.4/libheif/api/libheif/heif_components.h | 264 libheif-1.23.4/libheif/api/libheif/heif_context.cc | 312 libheif-1.23.4/libheif/api/libheif/heif_context.h | 343 libheif-1.23.4/libheif/api/libheif/heif_cxx.h | 160 libheif-1.23.4/libheif/api/libheif/heif_decoding.cc | 277 libheif-1.23.4/libheif/api/libheif/heif_decoding.h | 219 libheif-1.23.4/libheif/api/libheif/heif_emscripten.h | 225 libheif-1.23.4/libheif/api/libheif/heif_encoding.cc | 845 ++ libheif-1.23.4/libheif/api/libheif/heif_encoding.h | 425 + libheif-1.23.4/libheif/api/libheif/heif_entity_groups.cc | 100 libheif-1.23.4/libheif/api/libheif/heif_entity_groups.h | 221 libheif-1.23.4/libheif/api/libheif/heif_error.h | 311 libheif-1.23.4/libheif/api/libheif/heif_experimental.cc | 263 libheif-1.23.4/libheif/api/libheif/heif_experimental.h | 383 - libheif-1.23.4/libheif/api/libheif/heif_export.h | 49 libheif-1.23.4/libheif/api/libheif/heif_image.cc | 410 + libheif-1.23.4/libheif/api/libheif/heif_image.h | 421 + libheif-1.23.4/libheif/api/libheif/heif_image_handle.cc | 354 libheif-1.23.4/libheif/api/libheif/heif_image_handle.h | 196 libheif-1.23.4/libheif/api/libheif/heif_items.cc | 299 libheif-1.23.4/libheif/api/libheif/heif_items.h | 197 libheif-1.23.4/libheif/api/libheif/heif_library.cc | 107 libheif-1.23.4/libheif/api/libheif/heif_library.h | 211 libheif-1.23.4/libheif/api/libheif/heif_metadata.cc | 252 libheif-1.23.4/libheif/api/libheif/heif_metadata.h | 136 libheif-1.23.4/libheif/api/libheif/heif_omaf.cc | 47 libheif-1.23.4/libheif/api/libheif/heif_omaf.h | 104 libheif-1.23.4/libheif/api/libheif/heif_plugin.cc | 6 libheif-1.23.4/libheif/api/libheif/heif_plugin.h | 185 libheif-1.23.4/libheif/api/libheif/heif_properties.cc | 967 +- libheif-1.23.4/libheif/api/libheif/heif_properties.h | 375 libheif-1.23.4/libheif/api/libheif/heif_regions.cc | 778 +- libheif-1.23.4/libheif/api/libheif/heif_regions.h | 226 libheif-1.23.4/libheif/api/libheif/heif_security.cc | 67 libheif-1.23.4/libheif/api/libheif/heif_security.h | 124 libheif-1.23.4/libheif/api/libheif/heif_sequences.cc | 917 ++ libheif-1.23.4/libheif/api/libheif/heif_sequences.h | 726 + libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.cc | 279 libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.h | 202 libheif-1.23.4/libheif/api/libheif/heif_text.cc | 162 libheif-1.23.4/libheif/api/libheif/heif_text.h | 161 libheif-1.23.4/libheif/api/libheif/heif_tiling.cc | 321 libheif-1.23.4/libheif/api/libheif/heif_tiling.h | 137 libheif-1.23.4/libheif/api/libheif/heif_uncompressed.cc | 134 libheif-1.23.4/libheif/api/libheif/heif_uncompressed.h | 133 libheif-1.23.4/libheif/api_structs.h | 147 libheif-1.23.4/libheif/bitstream.cc | 261 libheif-1.23.4/libheif/bitstream.h | 118 libheif-1.23.4/libheif/box.cc | 1268 ++- libheif-1.23.4/libheif/box.h | 463 + libheif-1.23.4/libheif/brands.cc | 183 libheif-1.23.4/libheif/brands.h | 32 libheif-1.23.4/libheif/codecs/avc_boxes.cc | 313 libheif-1.23.4/libheif/codecs/avc_boxes.h | 35 libheif-1.23.4/libheif/codecs/avc_dec.cc | 23 libheif-1.23.4/libheif/codecs/avc_dec.h | 2 libheif-1.23.4/libheif/codecs/avc_enc.cc | 303 libheif-1.23.4/libheif/codecs/avc_enc.h | 78 libheif-1.23.4/libheif/codecs/avif_boxes.cc | 6 libheif-1.23.4/libheif/codecs/avif_boxes.h | 10 libheif-1.23.4/libheif/codecs/avif_dec.cc | 2 libheif-1.23.4/libheif/codecs/avif_enc.cc | 222 libheif-1.23.4/libheif/codecs/avif_enc.h | 75 libheif-1.23.4/libheif/codecs/decoder.cc | 494 + libheif-1.23.4/libheif/codecs/decoder.h | 81 libheif-1.23.4/libheif/codecs/encoder.cc | 168 libheif-1.23.4/libheif/codecs/encoder.h | 93 libheif-1.23.4/libheif/codecs/hevc_boxes.cc | 398 - libheif-1.23.4/libheif/codecs/hevc_boxes.h | 129 libheif-1.23.4/libheif/codecs/hevc_dec.cc | 29 libheif-1.23.4/libheif/codecs/hevc_dec.h | 2 libheif-1.23.4/libheif/codecs/hevc_enc.cc | 309 libheif-1.23.4/libheif/codecs/hevc_enc.h | 78 libheif-1.23.4/libheif/codecs/jpeg2000_boxes.cc | 40 libheif-1.23.4/libheif/codecs/jpeg2000_boxes.h | 17 libheif-1.23.4/libheif/codecs/jpeg2000_dec.cc | 8 libheif-1.23.4/libheif/codecs/jpeg2000_enc.cc | 112 libheif-1.23.4/libheif/codecs/jpeg2000_enc.h | 90 libheif-1.23.4/libheif/codecs/jpeg_boxes.cc | 1 libheif-1.23.4/libheif/codecs/jpeg_boxes.h | 12 libheif-1.23.4/libheif/codecs/jpeg_dec.cc | 10 libheif-1.23.4/libheif/codecs/jpeg_enc.cc | 134 libheif-1.23.4/libheif/codecs/jpeg_enc.h | 87 libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.cc | 303 libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.h | 211 libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.cc | 96 libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.h | 45 libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.cc | 130 libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.h | 46 libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.cc | 122 libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.h | 65 libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.cc | 115 libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.h | 48 libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.cc | 131 libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.h | 44 libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.cc | 969 ++ libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.h | 214 libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.cc | 958 -- libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.h | 47 libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.cc | 107 libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.h | 61 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.cc | 513 + libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.h | 123 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.cc | 250 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.h | 57 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.cc | 270 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.h | 57 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc | 301 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.h | 57 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc | 197 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.h | 59 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.cc | 180 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.h | 213 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc | 177 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.h | 58 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc | 144 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.h | 58 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.cc | 150 libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.h | 61 libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.cc | 83 libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.h | 86 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.cc | 431 + libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.h | 139 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc | 259 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.h | 68 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc | 124 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h | 58 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc | 136 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h | 58 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc | 113 libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h | 56 libheif-1.23.4/libheif/codecs/uncompressed/unc_types.h | 115 libheif-1.23.4/libheif/codecs/vvc_boxes.cc | 129 libheif-1.23.4/libheif/codecs/vvc_boxes.h | 57 libheif-1.23.4/libheif/codecs/vvc_dec.cc | 21 libheif-1.23.4/libheif/codecs/vvc_dec.h | 2 libheif-1.23.4/libheif/codecs/vvc_enc.cc | 297 libheif-1.23.4/libheif/codecs/vvc_enc.h | 80 libheif-1.23.4/libheif/color-conversion/alpha.cc | 436 + libheif-1.23.4/libheif/color-conversion/alpha.h | 42 libheif-1.23.4/libheif/color-conversion/bayer_bilinear.cc | 213 libheif-1.23.4/libheif/color-conversion/bayer_bilinear.h | 47 libheif-1.23.4/libheif/color-conversion/chroma_sampling.cc | 164 libheif-1.23.4/libheif/color-conversion/chroma_sampling.h | 16 libheif-1.23.4/libheif/color-conversion/colorconversion.cc | 182 libheif-1.23.4/libheif/color-conversion/colorconversion.h | 45 libheif-1.23.4/libheif/color-conversion/hdr_sdr.cc | 57 libheif-1.23.4/libheif/color-conversion/hdr_sdr.h | 8 libheif-1.23.4/libheif/color-conversion/monochrome.cc | 53 libheif-1.23.4/libheif/color-conversion/monochrome.h | 8 libheif-1.23.4/libheif/color-conversion/rgb2rgb.cc | 144 libheif-1.23.4/libheif/color-conversion/rgb2rgb.h | 24 libheif-1.23.4/libheif/color-conversion/rgb2yuv.cc | 195 libheif-1.23.4/libheif/color-conversion/rgb2yuv.h | 16 libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.cc | 69 libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.h | 4 libheif-1.23.4/libheif/color-conversion/yuv2rgb.cc | 252 libheif-1.23.4/libheif/color-conversion/yuv2rgb.h | 16 libheif-1.23.4/libheif/common_utils.cc | 39 libheif-1.23.4/libheif/common_utils.h | 43 libheif-1.23.4/libheif/compression.cc | 61 libheif-1.23.4/libheif/compression.h | 31 libheif-1.23.4/libheif/compression_brotli.cc | 25 libheif-1.23.4/libheif/compression_zlib.cc | 80 libheif-1.23.4/libheif/context.cc | 1066 ++ libheif-1.23.4/libheif/context.h | 170 libheif-1.23.4/libheif/error.cc | 67 libheif-1.23.4/libheif/error.h | 77 libheif-1.23.4/libheif/file.cc | 972 +- libheif-1.23.4/libheif/file.h | 121 libheif-1.23.4/libheif/file_layout.cc | 156 libheif-1.23.4/libheif/file_layout.h | 12 libheif-1.23.4/libheif/id_creator.cc | 111 libheif-1.23.4/libheif/id_creator.h | 55 libheif-1.23.4/libheif/image-items/avc.cc | 115 libheif-1.23.4/libheif/image-items/avc.h | 21 libheif-1.23.4/libheif/image-items/avif.cc | 79 libheif-1.23.4/libheif/image-items/avif.h | 22 libheif-1.23.4/libheif/image-items/grid.cc | 385 - libheif-1.23.4/libheif/image-items/grid.h | 52 libheif-1.23.4/libheif/image-items/hevc.cc | 129 libheif-1.23.4/libheif/image-items/hevc.h | 21 libheif-1.23.4/libheif/image-items/iden.cc | 73 libheif-1.23.4/libheif/image-items/iden.h | 23 libheif-1.23.4/libheif/image-items/image_item.cc | 1149 ++- libheif-1.23.4/libheif/image-items/image_item.h | 271 libheif-1.23.4/libheif/image-items/jpeg.cc | 109 libheif-1.23.4/libheif/image-items/jpeg.h | 19 libheif-1.23.4/libheif/image-items/jpeg2000.cc | 95 libheif-1.23.4/libheif/image-items/jpeg2000.h | 23 libheif-1.23.4/libheif/image-items/mask_image.cc | 64 libheif-1.23.4/libheif/image-items/mask_image.h | 20 libheif-1.23.4/libheif/image-items/overlay.cc | 161 libheif-1.23.4/libheif/image-items/overlay.h | 27 libheif-1.23.4/libheif/image-items/tiled.cc | 392 - libheif-1.23.4/libheif/image-items/tiled.h | 50 libheif-1.23.4/libheif/image-items/unc_image.cc | 518 - libheif-1.23.4/libheif/image-items/unc_image.h | 56 libheif-1.23.4/libheif/image-items/vvc.cc | 89 libheif-1.23.4/libheif/image-items/vvc.h | 18 libheif-1.23.4/libheif/image/image_description.cc | 369 libheif-1.23.4/libheif/image/image_description.h | 519 + libheif-1.23.4/libheif/image/pixelimage.cc | 2583 ++++++ libheif-1.23.4/libheif/image/pixelimage.h | 409 + libheif-1.23.4/libheif/init.cc | 47 libheif-1.23.4/libheif/init.h | 4 libheif-1.23.4/libheif/mdat_data.h | 65 libheif-1.23.4/libheif/mini.cc | 1388 +++ libheif-1.23.4/libheif/mini.h | 117 libheif-1.23.4/libheif/nclx.cc | 123 libheif-1.23.4/libheif/nclx.h | 67 libheif-1.23.4/libheif/omaf_boxes.cc | 88 libheif-1.23.4/libheif/omaf_boxes.h | 60 libheif-1.23.4/libheif/pixelimage.cc | 1618 ---- libheif-1.23.4/libheif/pixelimage.h | 313 libheif-1.23.4/libheif/plugin_registry.cc | 56 libheif-1.23.4/libheif/plugin_registry.h | 22 libheif-1.23.4/libheif/plugins/CMakeLists.txt | 19 libheif-1.23.4/libheif/plugins/decoder_aom.cc | 250 libheif-1.23.4/libheif/plugins/decoder_dav1d.cc | 270 libheif-1.23.4/libheif/plugins/decoder_ffmpeg.cc | 966 +- libheif-1.23.4/libheif/plugins/decoder_jpeg.cc | 236 libheif-1.23.4/libheif/plugins/decoder_libde265.cc | 245 libheif-1.23.4/libheif/plugins/decoder_openh264.cc | 406 - libheif-1.23.4/libheif/plugins/decoder_openjpeg.cc | 193 libheif-1.23.4/libheif/plugins/decoder_uncompressed.cc | 11 libheif-1.23.4/libheif/plugins/decoder_vvdec.cc | 212 libheif-1.23.4/libheif/plugins/decoder_webcodecs.cc | 959 ++ libheif-1.23.4/libheif/plugins/decoder_webcodecs.h | 34 libheif-1.23.4/libheif/plugins/encoder_aom.cc | 678 + libheif-1.23.4/libheif/plugins/encoder_input_check.h | 163 libheif-1.23.4/libheif/plugins/encoder_jpeg.cc | 185 libheif-1.23.4/libheif/plugins/encoder_kvazaar.cc | 692 + libheif-1.23.4/libheif/plugins/encoder_mask.cc | 58 libheif-1.23.4/libheif/plugins/encoder_openjpeg.cc | 140 libheif-1.23.4/libheif/plugins/encoder_openjph.cc | 178 libheif-1.23.4/libheif/plugins/encoder_rav1e.cc | 361 libheif-1.23.4/libheif/plugins/encoder_svt.cc | 687 + libheif-1.23.4/libheif/plugins/encoder_uncompressed.cc | 96 libheif-1.23.4/libheif/plugins/encoder_uvg266.cc | 640 + libheif-1.23.4/libheif/plugins/encoder_vvenc.cc | 553 - libheif-1.23.4/libheif/plugins/encoder_x264.cc | 1288 +++ libheif-1.23.4/libheif/plugins/encoder_x264.h | 34 libheif-1.23.4/libheif/plugins/encoder_x265.cc | 557 + libheif-1.23.4/libheif/plugins/nalu_utils.h | 23 libheif-1.23.4/libheif/plugins_unix.cc | 8 libheif-1.23.4/libheif/plugins_windows.cc | 8 libheif-1.23.4/libheif/region.cc | 143 libheif-1.23.4/libheif/region.h | 35 libheif-1.23.4/libheif/security_limits.cc | 278 libheif-1.23.4/libheif/security_limits.h | 157 libheif-1.23.4/libheif/sequences/chunk.cc | 155 libheif-1.23.4/libheif/sequences/chunk.h | 101 libheif-1.23.4/libheif/sequences/seq_boxes.cc | 2596 ++++++ libheif-1.23.4/libheif/sequences/seq_boxes.h | 1060 ++ libheif-1.23.4/libheif/sequences/track.cc | 1298 +++ libheif-1.23.4/libheif/sequences/track.h | 309 libheif-1.23.4/libheif/sequences/track_metadata.cc | 189 libheif-1.23.4/libheif/sequences/track_metadata.h | 49 libheif-1.23.4/libheif/sequences/track_visual.cc | 780 ++ libheif-1.23.4/libheif/sequences/track_visual.h | 104 libheif-1.23.4/libheif/text.cc | 39 libheif-1.23.4/libheif/text.h | 57 libheif-1.23.4/post.js | 14 libheif-1.23.4/scripts/build-oss-fuzz.sh | 374 libheif-1.23.4/scripts/check-api-symbols.sh | 128 libheif-1.23.4/scripts/check-c-headers.sh | 122 libheif-1.23.4/scripts/check-emscripten-enums.sh | 6 libheif-1.23.4/scripts/check-go-enums.sh | 6 libheif-1.23.4/scripts/check-licenses.sh | 2 libheif-1.23.4/scripts/encode-all-sequences.sh | 81 libheif-1.23.4/scripts/heif-modify-colr.py | 301 libheif-1.23.4/scripts/install-ci-linux.sh | 23 libheif-1.23.4/scripts/install-clang.sh | 13 libheif-1.23.4/scripts/run-ci.sh | 18 libheif-1.23.4/tests/CMakeLists.txt | 86 libheif-1.23.4/tests/alpha_cycle_deadlock.cc | 304 libheif-1.23.4/tests/avc_box.cc | 105 libheif-1.23.4/tests/bitstream_tests.cc | 155 libheif-1.23.4/tests/catch_amalgamated.cpp | 2 libheif-1.23.4/tests/catch_amalgamated.hpp | 4 libheif-1.23.4/tests/clap_decode.cc | 140 libheif-1.23.4/tests/clap_zero_size.cc | 142 libheif-1.23.4/tests/component_descriptions.cc | 377 libheif-1.23.4/tests/conversion.cc | 472 + libheif-1.23.4/tests/crop_plane_checks.cc | 107 libheif-1.23.4/tests/cxx_wrapper.cc | 46 libheif-1.23.4/tests/decompression_bomb.cc | 226 libheif-1.23.4/tests/duplicate_alpha_channel.cc | 109 libheif-1.23.4/tests/encode.cc | 63 libheif-1.23.4/tests/encode_grid.cc | 487 + libheif-1.23.4/tests/encode_htj2k.cc | 8 libheif-1.23.4/tests/encode_jpeg2000.cc | 8 libheif-1.23.4/tests/entity_groups.cc | 117 libheif-1.23.4/tests/error_item_decode.cc | 232 libheif-1.23.4/tests/exception_guard.cc | 85 libheif-1.23.4/tests/extended_type.cc | 6 libheif-1.23.4/tests/extract_area_plane_checks.cc | 121 libheif-1.23.4/tests/ffmpeg_decode_bitdepth.cc | 236 libheif-1.23.4/tests/file_layout.cc | 106 libheif-1.23.4/tests/fraction.cc | 71 libheif-1.23.4/tests/grid_tile_missing.cc | 606 + libheif-1.23.4/tests/hevc_sps.cc | 84 libheif-1.23.4/tests/id_creator.cc | 138 libheif-1.23.4/tests/iden_declared_size.cc | 226 libheif-1.23.4/tests/image_description_metadata.cc | 285 libheif-1.23.4/tests/item_properties.cc | 213 libheif-1.23.4/tests/item_writing.cc | 397 + libheif-1.23.4/tests/mini_box.cc | 425 + libheif-1.23.4/tests/mini_decode.cc | 26 libheif-1.23.4/tests/omaf.cc | 162 libheif-1.23.4/tests/omaf_boxes.cc | 62 libheif-1.23.4/tests/overlay_amplification.cc | 316 libheif-1.23.4/tests/parallel_grid_deadlock.cc | 468 + libheif-1.23.4/tests/pixel_data_types.cc | 191 libheif-1.23.4/tests/region.cc | 489 + libheif-1.23.4/tests/scale_plane_checks.cc | 160 libheif-1.23.4/tests/sequence_file_builder.h | 348 libheif-1.23.4/tests/sequence_mixed_bit_depth.cc | 237 libheif-1.23.4/tests/sequence_no_track.cc | 108 libheif-1.23.4/tests/sequence_null_options.cc | 184 libheif-1.23.4/tests/sequence_raw_sample_errors.cc | 238 libheif-1.23.4/tests/sequence_timing_overflow.cc | 257 libheif-1.23.4/tests/tai.cc | 140 libheif-1.23.4/tests/test_utils.cc | 126 libheif-1.23.4/tests/test_utils.h | 49 libheif-1.23.4/tests/text.cc | 166 libheif-1.23.4/tests/tiffdecode.cc | 24 libheif-1.23.4/tests/uncompressed_block_pixel_overpacked.cc | 239 libheif-1.23.4/tests/uncompressed_box.cc | 283 libheif-1.23.4/tests/uncompressed_decode.cc | 71 libheif-1.23.4/tests/uncompressed_decode_generic_compression.cc | 2 libheif-1.23.4/tests/uncompressed_decode_mono.cc | 2 libheif-1.23.4/tests/uncompressed_decode_rgb.cc | 6 libheif-1.23.4/tests/uncompressed_decode_rgb16.cc | 6 libheif-1.23.4/tests/uncompressed_decode_rgb565.cc | 2 libheif-1.23.4/tests/uncompressed_decode_rgb7.cc | 2 libheif-1.23.4/tests/uncompressed_decode_ycbcr.cc | 2 libheif-1.23.4/tests/uncompressed_decode_ycbcr420.cc | 69 libheif-1.23.4/tests/uncompressed_decode_ycbcr422.cc | 60 libheif-1.23.4/tests/uncompressed_encode.cc | 582 + libheif-1.23.4/tests/uncompressed_encode_multicomponent.cc | 557 + libheif-1.23.4/tests/uncompressed_idat_tiled.cc | 230 libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_colorconv.cc | 257 libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_encode.cc | 351 libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_overflow.cc | 272 libheif-1.23.4/tests/uncompressed_sequence_odd_chroma.cc | 347 libheif-1.23.4/tests/uncompressed_tile_range_overflow.cc | 239 libheif-1.23.4/tests/uncompressed_wide_component_colorconv.cc | 256 libheif-1.23.4/third-party/aom.cmd | 4 libheif-1.23.4/third-party/dav1d.cmd | 2 libheif-1.23.4/third-party/libpng.cmd | 13 libheif-1.23.4/third-party/svt.cmd | 4 482 files changed, 78811 insertions(+), 19990 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpb0hqs6oq/libheif_1.19.8-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpb0hqs6oq/libheif_1.23.4-1~deb13u1.dsc: no acceptable signature found diff -Nru libheif-1.19.8/CMakeLists.txt libheif-1.23.4/CMakeLists.txt --- libheif-1.19.8/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,6 @@ cmake_minimum_required (VERSION 3.16.3) # Oldest Ubuntu LTS (20.04 currently) -project(libheif LANGUAGES C CXX VERSION 1.19.8) +project(libheif LANGUAGES C CXX VERSION 1.23.4) # compatibility_version is never allowed to be decreased for any specific SONAME. # Libtool in the libheif-1.15.1 release had set it to 17.0.0, so we have to use this for the v1.x.y versions. @@ -15,12 +15,15 @@ # The version number. set (PACKAGE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}) -# Check for unistd.h +# Check for header files. +include (${CMAKE_ROOT}/Modules/CheckIncludeFileCXX.cmake) -include (${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake) - -CHECK_INCLUDE_FILE(unistd.h HAVE_UNISTD_H) +CHECK_INCLUDE_FILE_CXX(bit HAVE_BIT) +if (HAVE_BIT) + add_definitions(-DHAVE_BIT) +endif() +CHECK_INCLUDE_FILE_CXX(unistd.h HAVE_UNISTD_H) if (HAVE_UNISTD_H) add_definitions(-DHAVE_UNISTD_H) endif() @@ -32,19 +35,29 @@ if(NOT MSVC) # cmake 3.24 introduces this variable, but for backward compatibility, we set it explicitly if (CMAKE_COMPILE_WARNING_AS_ERROR) - add_definitions(-Werror) + add_compile_options(-Werror) endif () - add_definitions(-Wall) - add_definitions(-Wsign-compare) - add_definitions(-Wconversion) - add_definitions(-Wno-sign-conversion) - add_definitions(-Wno-error=conversion) - add_definitions(-Wno-error=unused-parameter) - add_definitions(-Wno-error=deprecated-declarations) + add_compile_options(-Wall) + add_compile_options(-Wsign-compare) + add_compile_options(-Wconversion) + add_compile_options(-Wno-sign-conversion) + add_compile_options(-Wno-error=conversion) + add_compile_options(-Wno-error=unused-parameter) + add_compile_options(-Wno-error=deprecated-declarations) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16) + # GCC 16 speculatively devirtualizes a virtual call to several candidate targets and then runs + # -Warray-bounds on the inlined body of a candidate whose type is larger than the object that was + # actually allocated, even though that branch can never be taken at runtime. Examples are the + # std::shared_ptr control block destructor and virtual calls through a base class pointer: + # "array subscript 'Box_meta[0]' is partly outside array bounds of 'unsigned char [56]'" + # This is a compiler false positive (observed with GCC 16.0 snapshots, 16.1 and 16.2), so do not + # let it fail the build. + add_compile_options(-Wno-error=array-bounds) + endif () if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_definitions(-Wno-error=tautological-compare) - add_definitions(-Wno-error=tautological-constant-out-of-range-compare) + add_compile_options(-Wno-error=tautological-compare) + add_compile_options(-Wno-error=tautological-constant-out-of-range-compare) endif () endif() @@ -64,7 +77,7 @@ include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG(-Wno-error=potentially-evaluated-expression has_potentially_evaluated_expression) if (has_potentially_evaluated_expression) - add_definitions(-Wno-error=potentially-evaluated-expression) + add_compile_options(-Wno-error=potentially-evaluated-expression) endif() LIST (APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/modules") @@ -112,7 +125,7 @@ endif () string(LENGTH "${displayName}" len) - math(EXPR fill "32 - ${len}") + math(EXPR fill "38 - ${len}") string(SUBSTRING " " 0 ${fill} filler) message("${displayName}${filler}: ${msg}") unset(msg) @@ -142,11 +155,16 @@ else () add_definitions(-DHAVE_KVAZAAR_ENABLE_LOGGING=0) endif () + if (HAVE_KVAZAAR_VERSION_STRING) + add_definitions(-DHAVE_KVAZAAR_VERSION_STRING=1) + else () + add_definitions(-DHAVE_KVAZAAR_VERSION_STRING=0) + endif () endif () # uvg266 -plugin_option(UVG266 "uvg266 VVC encoder (experimental)" OFF OFF) +plugin_option(UVG266 "uvg266 VVC encoder" OFF OFF) if (WITH_UVG266) find_package(UVG266) if ("${HAVE_UVG266_ENABLE_LOGGING}") @@ -158,11 +176,11 @@ # vvdec -plugin_option(VVDEC "vvdec VVC decoder (experimental)" OFF OFF) +plugin_option(VVDEC "vvdec VVC decoder" OFF OFF) if (WITH_VVDEC) # TODO: how to do configure vvdec cleanly? find_package(Threads REQUIRED) - find_package(vvdec 2.3.0) + find_package(vvdec 3.0.0) if (vvdec_FOUND) set(vvdec_LIBRARIES vvdec::vvdec) endif() @@ -170,7 +188,7 @@ # vvenc -plugin_option(VVENC "vvenc VVC encoder (experimental)" OFF OFF) +plugin_option(VVENC "vvenc VVC encoder" OFF OFF) if (WITH_VVENC) # TODO: how to do configure vvenc cleanly? find_package(Threads REQUIRED) @@ -180,6 +198,13 @@ endif() endif () +# x264 + +plugin_option(X264 "x264 AVC encoder" ON OFF) +if (WITH_X264) + find_package(X264) +endif() + # openh264 decoder plugin_option(OpenH264_DECODER "OpenH264 decoder" ON OFF) @@ -267,6 +292,26 @@ plugin_option(FFMPEG_DECODER "FFMPEG HEVC decoder (HW accelerated)" OFF OFF) if (WITH_FFMPEG_DECODER) find_package(FFMPEG COMPONENTS avcodec avutil) + + if (FFMPEG_avcodec_FOUND) + # The set of formats the FFmpeg decoder plugin can handle depends on the + # FFmpeg version. HT-J2K (High-Throughput JPEG 2000) decoding was added in + # FFmpeg 7.0 (libavcodec 61) and a native VVC decoder in FFmpeg 7.1 + # (libavcodec 61.19); earlier versions only provide the formats listed below. + message(STATUS "FFmpeg decoder (libavcodec ${FFMPEG_avcodec_VERSION}) can decode: HEVC, AVC, AV1, JPEG, JPEG 2000") + if (FFMPEG_avcodec_VERSION VERSION_GREATER_EQUAL "61.19") + message(STATUS " FFmpeg also supports VVC and HT-J2K (High-Throughput JPEG 2000) decoding") + elseif (FFMPEG_avcodec_VERSION VERSION_GREATER_EQUAL "61") + message(STATUS " FFmpeg also supports HT-J2K (High-Throughput JPEG 2000) decoding") + # VVC is detected at runtime (avcodec_find_decoder), not at compile time, and + # 7.1 is ABI-compatible with this 7.0 build, so VVC works once the FFmpeg + # loaded at runtime is 7.1+ -- no rebuild required. + message(STATUS " VVC decoding additionally needs the FFmpeg at runtime to be 7.1 (libavcodec 61.19) or later") + else () + message(STATUS " HT-J2K decoding needs FFmpeg 7.0 (libavcodec 61) or later") + message(STATUS " VVC decoding needs FFmpeg 7.1 (libavcodec 61.19) or later") + endif () + endif () endif () # openjph @@ -279,14 +324,22 @@ # uncompressed -option(WITH_UNCOMPRESSED_CODEC " Support internal ISO/IEC 23001-17 uncompressed codec (experimental) " OFF) +option(WITH_UNCOMPRESSED_CODEC "Support internal ISO/IEC 23001-17 uncompressed codec" OFF) + +# Web codecs +option(WITH_WEBCODECS "Support HEVC decoding through browser web codecs API (experimental)" OFF) + +if (WITH_WEBCODECS) + set(WEBCODECS_FOUND TRUE) # There is no library for web-codecs. Always mark as found. +endif() # --- show codec compilation summary message("\n=== Summary of compiled codecs ===") plugin_compilation_info(LIBDE265 LIBDE265 "libde265 HEVC decoder") plugin_compilation_info(FFMPEG_DECODER FFMPEG_avcodec "FFMPEG HEVC decoder (HW acc)") +plugin_compilation_info(WEBCODECS WEBCODECS "WebCodecs HEVC decoder (experimental)") plugin_compilation_info(X265 X265 "x265 HEVC encoder") plugin_compilation_info(KVAZAAR KVAZAAR "Kvazaar HEVC encoder") plugin_compilation_info(AOM_DECODER AOM_DECODER "AOM AV1 decoder") @@ -296,15 +349,16 @@ plugin_compilation_info(RAV1E RAV1E "Rav1e AV1 encoder") plugin_compilation_info(JPEG_DECODER JPEG "JPEG decoder") plugin_compilation_info(JPEG_ENCODER JPEG "JPEG encoder") +plugin_compilation_info(X264 X264 "x264 AVC encoder") plugin_compilation_info(OpenH264_DECODER OpenH264_DECODER "OpenH264 decoder") # plugin_compilation_info(OpenH264_ENCODER OpenH264_ENCODER "OpenH264 encoder") plugin_compilation_info(OpenJPEG_DECODER OpenJPEG "OpenJPEG J2K decoder") plugin_compilation_info(OpenJPEG_ENCODER OpenJPEG "OpenJPEG J2K encoder") # plugin_compilation_info(OPENJPH_DECODER OPENJPH "OpenJPH HT-J2K decoder") plugin_compilation_info(OPENJPH_ENCODER OPENJPH "OpenJPH HT-J2K encoder") -plugin_compilation_info(UVG266_ENCODER UVG266 "uvg266 VVC enc. (experimental)") -plugin_compilation_info(VVENC vvenc "vvenc VVC enc. (experimental)") -plugin_compilation_info(VVDEC vvdec "vvdec VVC dec. (experimental)") +plugin_compilation_info(UVG266_ENCODER UVG266 "uvg266 VVC encoder") +plugin_compilation_info(VVENC vvenc "vvenc VVC encoder") +plugin_compilation_info(VVDEC vvdec "vvdec VVC decoder") # --- show summary which formats are supported @@ -329,7 +383,7 @@ endmacro() -if (LIBDE265_FOUND OR FFMPEG_avcodec_FOUND) +if (LIBDE265_FOUND OR FFMPEG_avcodec_FOUND OR WITH_WEBCODECS) set(SUPPORTS_HEIC_DECODING TRUE) endif() if (X265_FOUND OR KVAZAAR_FOUND) @@ -369,7 +423,7 @@ if (OpenH264_DECODER_FOUND) set(SUPPORTS_AVC_DECODING TRUE) endif() -if (OpenH264_ENCODER_FOUND) +if (X264_FOUND) set(SUPPORTS_AVC_ENCODING TRUE) endif() @@ -378,6 +432,24 @@ set(SUPPORTS_UNCOMPRESSED_ENCODING TRUE) endif() +# The FFmpeg decoder plugin can decode several formats (in addition to the HEIC +# decoding accounted for above). HEVC/AVC/AV1/JPEG/JPEG2000 work with any supported +# FFmpeg; HT-J2K needs FFmpeg 7.0 (libavcodec 61) and the native VVC decoder +# FFmpeg 7.1 (libavcodec 61.19). FFmpeg's SONAME is per-major, so the build-time +# major version is also the runtime major version. +if (FFMPEG_avcodec_FOUND) + set(SUPPORTS_AVC_DECODING TRUE) + set(SUPPORTS_AVIF_DECODING TRUE) + set(SUPPORTS_JPEG_DECODING TRUE) + set(SUPPORTS_J2K_DECODING TRUE) + if (FFMPEG_avcodec_VERSION VERSION_GREATER_EQUAL "61") + set(SUPPORTS_J2K_HT_DECODING TRUE) + endif() + if (FFMPEG_avcodec_VERSION VERSION_GREATER_EQUAL "61.19") + set(SUPPORTS_VVC_DECODING TRUE) + endif() +endif() + message("\n=== Supported formats ===") message("format decoding encoding") format_compilation_info("AVC" SUPPORTS_AVC_DECODING SUPPORTS_AVC_ENCODING) @@ -394,8 +466,15 @@ # --- Libsharpyuv color space transforms option(WITH_LIBSHARPYUV "Build libsharpyuv" ON) +option(WITH_LIBSHARPYUV_INTERNAL "Use the libsharpyuv build in the 'third-party' directory" OFF) if (WITH_LIBSHARPYUV) - find_package(libsharpyuv) + if (WITH_LIBSHARPYUV_INTERNAL) + set(LIBSHARPYUV_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/third-party/libwebp) + set(LIBSHARPYUV_LIBRARIES ${PROJECT_SOURCE_DIR}/third-party/libwebp/build/libsharpyuv.a) + set(LIBSHARPYUV_FOUND YES) + else() + find_package(libsharpyuv) + endif() endif () if (LIBSHARPYUV_FOUND) message("libsharpyuv: found") @@ -431,6 +510,9 @@ if (OpenH264_FOUND AND (WITH_OpenH264_DECODER AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_OpenH264_DECODER_PLUGIN))) list(APPEND REQUIRES_PRIVATE "openh264") endif() +if (X264_FOUND AND (WITH_X264 AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_X264_PLUGIN))) + list(APPEND REQUIRES_PRIVATE "x264") +endif() if ((AOM_DECODER_FOUND AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_AOM_DECODER_PLUGIN)) OR (AOM_ENCODER_FOUND AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_AOM_ENCODER_PLUGIN))) list(APPEND REQUIRES_PRIVATE "aom") @@ -471,11 +553,20 @@ find_package(Brotli) if (Brotli_FOUND) message("Brotli found") - list(APPEND REQUIRES_PRIVATE "libbrotlidec") + list(APPEND REQUIRES_PRIVATE "libbrotlidec" "libbrotlienc") else() message("Brotli not found") endif() endif() +if (UVG266_FOUND AND (WITH_UVG266 AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_UVG266_PLUGIN))) + list(APPEND REQUIRES_PRIVATE "uvg266") +endif() +if (VVDEC_FOUND AND (WITH_VVDEC AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_VVDEC_PLUGIN))) + list(APPEND REQUIRES_PRIVATE "libvvdec") +endif() +if (VVENC_FOUND AND (WITH_VVENC AND NOT (PLUGIN_LOADING_SUPPORTED_AND_ENABLED AND WITH_VVENC_PLUGIN))) + list(APPEND REQUIRES_PRIVATE "libvvenc") +endif() list(JOIN REQUIRES_PRIVATE " " REQUIRES_PRIVATE) @@ -495,16 +586,18 @@ # --- option(WITH_EXAMPLES "Build examples" ON) +option(WITH_EXAMPLE_HEIF_THUMB "Build heif-thumbnailer tool" ON) +option(WITH_EXAMPLE_HEIF_VIEW "Build heif-view tool" ON) option(WITH_GDK_PIXBUF "Build gdk-pixbuf plugin" ON) +option(BUILD_DEVELOPMENT_TOOLS "Build development tools (heif-gen-bayer, etc.)" OFF) + option(WITH_REDUCED_VISIBILITY "Reduced symbol visibility in library" ON) option(WITH_HEADER_COMPRESSION OFF) option(ENABLE_MULTITHREADING_SUPPORT "Switch off for platforms without multithreading support" ON) option(ENABLE_PARALLEL_TILE_DECODING "Will launch multiple decoders to decode tiles in parallel (requires ENABLE_MULTITHREADING_SUPPORT)" ON) -option(ENABLE_EXPERIMENTAL_MINI_FORMAT "Enable experimental (draft) low-overhead box format (likely reduced interoperability)." OFF) - if (WITH_REDUCED_VISIBILITY) set(CMAKE_CXX_VISIBILITY_PRESET hidden) else () @@ -520,8 +613,10 @@ # --- API documentation # check if Doxygen is installed -find_package(Doxygen) -if (DOXYGEN_FOUND) +option(BUILD_DOCUMENTATION "Build doxygen documentation" ON) +if (BUILD_DOCUMENTATION) + find_package(Doxygen) + if (DOXYGEN_FOUND) # set input and output files set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/libheif/Doxyfile.in) set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) @@ -536,9 +631,10 @@ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen" VERBATIM ) -else (DOXYGEN_FOUND) - message("Doxygen tool needs to be installed to generate the API documentation") -endif (DOXYGEN_FOUND) + else (DOXYGEN_FOUND) + message("Doxygen tool needs to be installed to generate the API documentation") + endif (DOXYGEN_FOUND) +endif(BUILD_DOCUMENTATION) # --- Testing diff -Nru libheif-1.19.8/CMakePresets.json libheif-1.23.4/CMakePresets.json --- libheif-1.19.8/CMakePresets.json 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/CMakePresets.json 2026-09-06 14:45:17.000000000 +0000 @@ -24,7 +24,6 @@ "WITH_AOM_ENCODER_PLUGIN" : "OFF", "WITH_DAV1D" : "ON", "WITH_DAV1D_PLUGIN" : "OFF", - "ENABLE_EXPERIMENTAL_MINI_FORMAT" : "ON", "WITH_LIBDE265" : "ON", "WITH_LIBDE265_PLUGIN" : "OFF", "WITH_RAV1E" : "ON", @@ -33,6 +32,8 @@ "WITH_SvtEnc_PLUGIN" : "OFF", "WITH_X265" : "ON", "WITH_X265_PLUGIN" : "OFF", + "WITH_X264" : "ON", + "WITH_X264_PLUGIN" : "OFF", "WITH_JPEG_DECODER" : "ON", "WITH_JPEG_DECODER_PLUGIN" : "OFF", "WITH_JPEG_ENCODER" : "ON", @@ -44,6 +45,8 @@ "WITH_OpenJPEG_DECODER_PLUGIN" : "OFF", "WITH_OpenJPEG_ENCODER" : "ON", "WITH_OpenJPEG_ENCODER_PLUGIN" : "OFF", + "WITH_OPENJPH_ENCODER" : "ON", + "WITH_OPENJPH_ENCODER_PLUGIN" : "OFF", "WITH_FFMPEG_DECODER" : "ON", "WITH_FFMPEG_DECODER_PLUGIN" : "OFF", "WITH_OpenH264_DECODER" : "ON", @@ -58,8 +61,10 @@ "WITH_REDUCED_VISIBILITY" : "OFF", "WITH_HEADER_COMPRESSION" : "ON", "WITH_LIBSHARPYUV" : "ON", + "WITH_GEOTIFF" : "ON", "WITH_EXAMPLES": "ON", - "WITH_FUZZERS": "OFF" + "WITH_FUZZERS": "OFF", + "BUILD_DEVELOPMENT_TOOLS": "ON" } }, { diff -Nru libheif-1.19.8/README.md libheif-1.23.4/README.md --- libheif-1.19.8/README.md 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/README.md 2026-09-06 14:45:17.000000000 +0000 @@ -2,14 +2,23 @@ [![Build Status](https://github.com/strukturag/libheif/workflows/build/badge.svg)](https://github.com/strukturag/libheif/actions) [![Build Status](https://ci.appveyor.com/api/projects/status/github/strukturag/libheif?svg=true)](https://ci.appveyor.com/project/strukturag/libheif) [![Coverity Scan Build Status](https://scan.coverity.com/projects/16641/badge.svg)](https://scan.coverity.com/projects/strukturag-libheif) -libheif is an ISO/IEC 23008-12:2017 HEIF and AVIF (AV1 Image File Format) file format decoder and encoder. -There is partial support for ISO/IEC 23008-12:2022 (2nd Edition) capabilities. - -HEIF and AVIF are new image file formats employing HEVC (H.265) or AV1 image coding, respectively, for the +libheif is an ISO/IEC 23008-12 HEIF and AVIF (AV1 Image File Format) file format decoder and encoder. +HEIC and AVIF are new image file formats employing HEVC (H.265) or AV1 image coding, respectively, for the best compression ratios currently possible. -libheif makes use of [libde265](https://github.com/strukturag/libde265) for HEIF image decoding and x265 for encoding. +On top of HEIC and AVIF, libheif also supports HEIF images coded with VVC, AVC, JPEG, JPEG-2000, and ISO/IEC 23001-17. +The ISO/IEC 23001-17 codec is built-in to libheif and allows to store lossless images and video in many different formats. + +libheif makes use of various codec libraries for implementing each compression format. +For HEIC, [libde265](https://github.com/strukturag/libde265) is used by default for decoding and x265 for encoding. For AVIF, libaom, dav1d, svt-av1, or rav1e are used as codecs. +libheif can be built with a subset of the supported codecs to keep the size and the number of dependencies low. +Alternatively, the libheif codecs can also be built as separate plugins that can be installed and loaded dynamically when used. + +> **Project status (August 2026).** libheif and libde265 are maintained by a single independent developer with almost +> no recurring funding, while 37 security advisories had to be investigated, fixed and released in 2026 alone. +> If libheif is part of your product or service, please read [Funding](#funding) and [Commercial support](#commercial-support). +> Security issues are reported as described in [SECURITY.md](SECURITY.md). ## Supported features @@ -18,27 +27,28 @@ * HEIC, AVIF, VVC, AVC, JPEG-in-HEIF, JPEG2000, uncompressed (ISO/IEC 23001-17:2024) codecs * alpha channels, depth maps, thumbnails, auxiliary images * multiple images in a file +* HEIF image sequences and MP4 video, including alpha channels * tiled images with decoding individual tiles and encoding tiled images by adding tiles one after another * HDR images, correct color transform according to embedded color profiles * image transformations (crop, mirror, rotate), overlay images * plugin interface to add alternative codecs * reading EXIF and XMP metadata * region annotations and mask images -* decoding of files while downloading (e.g. extract image size before file has been completely downloaded) +* streaming of images and video by requesting data from the network through a data-reader interface Supported codecs: -| Format | Decoders | Encoders | -|:-------------|:-------------------:|:----------------------------:| -| HEIC | libde265, ffmpeg | x265, kvazaar | -| AVIF | AOM, dav1d | AOM, rav1e, svt-av1 | -| VVC | vvdec | vvenc, uvg266 | -| AVC | openh264 | - | -| JPEG | libjpeg(-turbo) | libjpeg(-turbo) | -| JPEG2000 | OpenJPEG | OpenJPEG | -| HTJ2K | OpenJPEG | OpenJPH | -| uncompressed | built-in | built-in | +| Format | Decoders | Encoders | +|:-------------|:-----------------------:|:----------------------------:| +| HEIC | libde265, ffmpeg | x265, kvazaar | +| AVIF | libaom, dav1d, ffmpeg | libaom, rav1e, svt-av1 | +| VVC | vvdec, ffmpeg | vvenc, uvg266 | +| AVC | openh264, ffmpeg | x264 | +| JPEG | libjpeg(-turbo), ffmpeg | libjpeg(-turbo) | +| JPEG2000 | OpenJPEG, ffmpeg | OpenJPEG | +| HTJ2K | OpenJPEG, ffmpeg | OpenJPH | +| uncompressed | built-in | built-in | -## API +## Programming API The library has a C API for easy integration and wide language support. @@ -46,7 +56,7 @@ The encoder can be switched between HEIF and AVIF simply by setting `heif_compression_HEVC` or `heif_compression_AV1` to `heif_context_get_encoder_for_format()`, or using any of the other compression formats. -Loading the primary image in an HEIF file is as easy as this: +### Loading the primary image from a HEIF file ```C heif_context* ctx = heif_context_alloc(); @@ -71,7 +81,7 @@ heif_context_free(ctx); ``` -Writing an HEIF file can be done like this: +### Writing a HEIF file ```C heif_context* ctx = heif_context_alloc(); @@ -94,7 +104,7 @@ heif_context_free(ctx); ``` -Get the EXIF data from an HEIF file: +### Get the EXIF data from a HEIF file ```C heif_item_id exif_id; @@ -107,18 +117,26 @@ } ``` -See the header file `heif.h` for the complete C API. +### Image sequences, MP4 video -There is also a C++ API which is a header-only wrapper to the C API. -Hence, you can use the C++ API and still be binary compatible. -Code using the C++ API is much less verbose than using the C API directly. +See the [image sequences API documentation](https://github.com/strukturag/libheif/wiki/Reading-and-Writing-Sequences). +Since HEIF image sequences are very similar to MP4 video, libheif can also read and write MP4 video (without audio) +with all supported codecs. -### Reading and Writing Tiled Images +### High-resolution tiled images For very large resolution images, it is not always feasible to process the whole image. In this case, `libheif` can process the image tile by tile. -See [this tutorial](https://github.com/strukturag/libheif/wiki/Reading-and-Writing-Tiled-Images) on how to use the API for this. +See the [image tiling API documentation](https://github.com/strukturag/libheif/wiki/Reading-and-Writing-Tiled-Images). + +### More documenation + +See the header files for the complete C API. + +There is also a C++ API which is a header-only wrapper to the C API. +Hence, you can use the C++ API and still be binary compatible. +Code using the C++ API is much less verbose than using the C API directly. ## Compiling @@ -164,7 +182,7 @@ * `WITH_{codec}_PLUGIN`: when enabled, the codec is compiled as a separate plugin. In order to use dynamic plugins, also make sure that `ENABLE_PLUGIN_LOADING` is enabled. -The placeholder `{codec}` can have these values: `LIBDE265`, `X265`, `AOM_DECODER`, `AOM_ENCODER`, `SvtEnc`, `DAV1D`, `FFMPEG_DECODER`, `JPEG_DECODER`, `JPEG_ENCODER`, `KVAZAAR`, `OpenJPEG_DECODER`, `OpenJPEG_ENCODER`, `OPENJPH_ENCODER`, `VVDEC`, `VVENC`, `UVG266`. +The placeholder `{codec}` can have these values: `LIBDE265`, `X265`, `AOM_DECODER`, `AOM_ENCODER`, `SvtEnc`, `DAV1D`, `OpenH264`, `X264`, `FFMPEG_DECODER`, `JPEG_DECODER`, `JPEG_ENCODER`, `KVAZAAR`, `OpenJPEG_DECODER`, `OpenJPEG_ENCODER`, `OPENJPH_ENCODER`, `VVDEC`, `VVENC`, `UVG266`, `WEBCODECS`. Further options are: @@ -174,9 +192,9 @@ Note that header compression is not widely supported yet. * `WITH_LIBSHARPYUV`: enables high-quality YCbCr/RGB color space conversion algorithms (requires `libsharpyuv`, e.g. from the `third-party` directory). -* `ENABLE_EXPERIMENTAL_FEATURES`: enables functions that are currently in development and for which the API is not stable yet. - When this is enabled, a header `heif_experimental.h` will be installed that contains this unstable API. - Distributions that rely on a stable API should not enable this. +* `ENABLE_EXPERIMENTAL_FEATURES`: enables functions that are currently in development and for which the API is not stable yet + and may contain security risks. When this is enabled, a header `heif_experimental.h` will be installed that contains this + unstable API. Distributions should not enable this in release or production builds. * `ENABLE_MULTITHREADING_SUPPORT`: can be used to disable any multithreading support, e.g. for embedded platforms. * `ENABLE_PARALLEL_TILE_DECODING`: when enabled, libheif will decode tiled images in parallel to speed up compilation. * `PLUGIN_DIRECTORY`: the directory where libheif will search for dynamic plugins when the environment @@ -256,16 +274,16 @@ * Run the `svt.cmd` script in the `third-party` directory to download SVT-AV1 and compile it. -When running `cmake` or `configure`, make sure that the environment variable +You have to enable SVT-AV1 with CMake. + +When running `cmake`, make sure that the environment variable `PKG_CONFIG_PATH` includes the absolute path to `third-party/SVT-AV1/Build/linux/install/lib/pkgconfig`. You may have to replace `linux` in this path with your system's identifier. -You have to enable SVT-AV1 with CMake. - ## Codec plugins Starting with v1.14.0, each codec backend can be compiled statically into libheif or as a dynamically loaded plugin. -You can choose this individually for each codec backend in the CMake settings. +You can choose this individually for each codec backend in the CMake settings using `WITH_{codec}_PLUGIN` options. Compiling a codec backend as dynamic plugin will generate a shared library that is installed in the system together with libheif. The advantage is that only the required plugins have to be installed and libheif has fewer dependencies. @@ -275,9 +293,38 @@ ### Codec specific notes -* the FFMPEG decoding plugin can make use of h265 hardware decoders. However, it currently (v1.17.0, ffmpeg v4.4.2) does not work +* The FFMPEG decoding plugin can make use of h265 hardware decoders. However, it currently (v1.17.0, ffmpeg v4.4.2) does not work correctly with all streams. Thus, libheif still prefers the libde265 decoder if it is available. +* The "webcodecs" HEVC decoder can only be used in emscripten builds since it uses the web-browser's API. For the same reason, it is not available as a plugin. + +## Usage + +libheif comes with a set of command line tools: +* `heif-dec` for decoding HEIF images to JPEG, PNG, TIFF, WebP, or Y4M. It can also decode image sequences or MP4 video. +* `heif-enc` for encoding JPEG, PNG, TIFF, WebP, Y4M, raw pixel data, or HEIF/AVIF images to HEIF/AVIF images, image sequences, or MP4 video. +* `heif-info` for getting some overview information about the HEIF file or (using the `-d` option) to dump the full box structure of the file. +* `heif-view` for displaying HEIF image sequences + +### `heif-enc` command line tool + +You can find more documentation for the `heif-enc` tool on the [wiki page](https://github.com/strukturag/libheif/wiki/heif%E2%80%90enc-Command-Line-Tool). + +### Security limits + +Libheif defines some security limits that prevent that very large images exceed the available memory or malicious input files can be used for a denial-of-service attack. +When you are programming against the libheif API and you need to process very large images, you can set the `heif_security_limits` individually. +When using `heif-dec`, there is the option to switch off security limits with `--disable-limits`. +In case a third-party software is using libheif, but does not give you a way to switch off the limits, you can set an environment variable `LIBHEIF_SECURITY_LIMITS=off` to switch it off globally. +Clearly, only do this if you know what you are doing and you are sure not to process malicious files. + +### Reporting security issues + +Please report vulnerabilities through +[GitHub private vulnerability reporting](https://github.com/strukturag/libheif/security/advisories/new), +not in public issues. [SECURITY.md](SECURITY.md) describes what a report must contain, what is in scope, +which versions receive fixes, and how fixes are released. + ## Encoder benchmark A current benchmark of the AVIF encoders (as of 14 Oct 2022) can be found on the Wiki page @@ -318,8 +365,8 @@ ## Example programs Some example programs are provided in the `examples` directory. -The program `heif-dec` converts all images stored in an HEIF/AVIF file to JPEG or PNG. -`heif-enc` lets you convert JPEG files to HEIF/AVIF. +The program `heif-dec` converts all images stored in an HEIF/AVIF file to JPEG, PNG, TIFF, WebP, or Y4M. +`heif-enc` lets you convert JPEG, PNG, TIFF, WebP, Y4M, raw pixel data (`.raw`, `.img`), and HEIF/AVIF files to HEIF/AVIF. The program `heif-info` is a simple, minimal decoder that dumps the file structure to the console. For example convert `example.heic` to JPEGs and one of the JPEGs back to HEIF: @@ -353,14 +400,20 @@ ## Software using libheif +libheif is the HEIF/AVIF implementation behind most open-source image software. Because ImageMagick, libvips, +GDAL and the desktop image loaders depend on it, it is also used indirectly by a large number of web services, +content management systems and image processing pipelines built on these libraries. + * [GIMP](https://www.gimp.org/) * [Krita](https://krita.org) * [ImageMagick](https://imagemagick.org/) * [GraphicsMagick](http://www.graphicsmagick.org/) * [darktable](https://www.darktable.org) * [digiKam 7.0.0](https://www.digikam.org/) -* [libvips](https://github.com/libvips/libvips) +* [libvips](https://github.com/libvips/libvips) and [sharp](https://sharp.pixelplumbing.com/) (Node.js) +* [pillow-heif](https://pypi.org/project/pillow-heif/) (Python / Pillow) * [kImageFormats](https://api.kde.org/frameworks/kimageformats/html/index.html) +* GNOME desktop (through the gdk-pixbuf loader included in libheif) * [libGD](https://libgd.github.io/) * [Kodi HEIF image decoder plugin](https://kodi.wiki/view/Add-on:HEIF_image_decoder) * [bimg](https://github.com/h2non/bimg) @@ -372,13 +425,59 @@ [![libheif packaging status](https://repology.org/badge/vertical-allrepos/libheif.svg?exclude_unsupported=1&columns=3&exclude_sources=modules,site&header=libheif%20packaging%20status)](https://repology.org/project/libheif/versions) -## Sponsors +## Funding + +libheif and libde265 are developed and maintained by me, Dirk Farin, as an independent developer. +The libraries are used by practically every open-source application and service that handles HEIC or AVIF +files (see [Software using libheif](#software-using-libheif)), but the maintenance work is almost entirely unfunded. + +From January to August 2026, 37 security advisories were published for libheif, most of them found with +automated tools by organizations that use libheif in their products, and six releases were made mainly to ship +security fixes. Each fix means reproducing, fixing, testing, fuzzing and releasing, currently done in evenings and +on weekends. Details are in [SECURITY.md](SECURITY.md). + +This is not a temporary situation. HEIF is built on a very flexible container format with wide value ranges and many +loosely specified features, so the space of feature combinations that has to be validated is huge, and improved automated +vulnerability discovery keeps finding new corners of it. +[SECURITY.md](SECURITY.md#why-heif-files-are-hard-to-parse-safely) explains this in more detail. + +Recurring funding is what allows this work to happen during regular working hours. +If libheif is part of your product or service, please consider one of these options: + +* **[GitHub Sponsors](https://github.com/sponsors/farindk)**, monthly or one-time. There are tiers for individuals, + small companies and larger commercial users. Organizations can pay by invoice through GitHub. +* **[Commercial support](#commercial-support)** if you need pre-notification of security advisories, a direct contact, + or dedicated engineering time. +* **Funding specific work.** New features, hardening of a subsystem your product relies on, or codec integrations can be + sponsored as scoped one-time projects. Several features of libheif were developed this way. + +A machine-readable description of the funding needs is in [funding.json](funding.json). + +### Commercial support + +For organizations that depend on libheif or libde265, commercial support is available directly from the author. +Invoicing and vendor onboarding are possible. A support agreement can include: + +* pre-release notification of embargoed security advisories, +* a direct contact for security and integration questions, +* an agreed number of engineering hours per month (triage of your reports, review of your patches, + backports to the version you ship, integration help), +* scoped projects (hardening, fuzzing, features, performance work). + +Contact: Dirk Farin + +Sponsorship through GitHub Sponsors funds the project as a whole and does not include these services. + +### Sponsors -Since I work as an independent developer, I need your support to be able to allocate time for libheif. -You can [sponsor](https://github.com/sponsors/farindk) the development using the link in the right hand column. +As of August 2026, the recurring sponsorship for libheif and libde265 amounts to **$41 per month**, against a goal of +$5,000 per month, which would fund about two days per week of maintenance (see [funding.json](funding.json)). +Thank you to everyone who sponsors the project. -A big thank you goes to these major sponsors for supporting the development of libheif: +Thank you also to these organizations, which funded the development of specific features in the past: +* AOMedia +* OGC (Open Geospatial Consortium) * Pinterest * Shopify shopify-logo * StrukturAG @@ -391,5 +490,5 @@ See COPYING for more details. Copyright (c) 2017-2020 Struktur AG
-Copyright (c) 2017-2025 Dirk Farin
+Copyright (c) 2017-2026 Dirk Farin
Contact: Dirk Farin diff -Nru libheif-1.19.8/SECURITY.md libheif-1.23.4/SECURITY.md --- libheif-1.19.8/SECURITY.md 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/SECURITY.md 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,162 @@ +# Security Policy + +libheif parses untrusted input files. Memory-safety bugs in the decoder are treated as +security vulnerabilities and are fixed with priority. This document describes how to report +them, what a useful report contains, and what you can expect from a project that is maintained +by a single, largely unpaid developer. + +## Why HEIF files are hard to parse safely + +HEIF is built on the ISO base media file format (ISOBMFF), a generic container that is +exceptionally hard to implement securely: + +* **Flexibility.** A file is a graph of boxes and items that reference each other: derived images + (grids, overlays, identity transforms), auxiliary images (alpha, depth), thumbnails, tiles, + metadata, sequences with sample tables, and properties attached to any of them. Almost every + feature can be combined with almost every other feature, references can be nested and shared, + and the decoder has to detect cycles, duplicated references and amplification through + reference chains, none of which the specification forbids explicitly. +* **Wide value ranges.** Sizes, offsets, counts and dimensions are 32- or 64-bit fields with no + meaningful upper bounds in the specification. Every field has to be checked not only against + the file, but against every other field it must be consistent with: declared image size versus + coded size, transformations (`clap`, `irot`, `imir`) versus tile grids, sample tables (`stsz`, + `stsc`, `stts`, `stco`) versus each other and versus the file length, and so on. An integer + overflow in any of these consistency checks becomes a memory-safety bug. +* **Vaguely defined features.** Many features are only loosely specified, or specify what a + valid file contains but not what a reader should do with an inconsistent one. Files produced + by real cameras and phones deviate from the specification in practice, so a decoder has to be + lenient enough to be useful and strict enough to be safe. The uncompressed codec + (ISO/IEC 23001-17) alone allows nearly arbitrary component layouts, interleaving modes, bit + depths, tilings and compression schemes. +* **Multiple codecs behind one container.** The container makes claims (dimensions, chroma + format, bit depth, color information) that the embedded HEVC, AV1, JPEG, JPEG 2000 or VVC + bitstream may contradict. libheif has to reconcile both before any pixel buffer is touched. +* **Many codec libraries.** libheif supports more than a dozen decoder and encoder backends + (libde265, x265, kvazaar, libaom, dav1d, rav1e, SVT-AV1, vvdec, vvenc, uvg266, openh264, + x264, ffmpeg, libjpeg, OpenJPEG, OpenJPH), in whatever versions the distributions ship. + Bugs and undocumented limits in these libraries surface through libheif, because libheif is + the component that hands them the untrusted data and that is named in the bug report. + Typical cases are integer overflows inside a codec at very large image sizes, crashes on + inputs that are valid for the container but exceed codec-internal limits, and frame buffers + allocated inside the codec that are outside libheif's memory accounting. libheif has to + anticipate these and compensate with its own checks and limits before data is passed to a + codec and after results come back, for code it does not control. + +libheif validates all of this, is fuzzed continuously, and uses configurable security limits +to bound memory and CPU use. Still, the number of feature combinations is large, and as +automated vulnerability discovery has improved, the number of reports has grown accordingly +(see [Maintenance capacity](#maintenance-capacity)). + +## Reporting a vulnerability + +Please do **not** open a public issue for security bugs. + +* Preferred: [GitHub private vulnerability reporting](https://github.com/strukturag/libheif/security/advisories/new). + This creates a private advisory in which the issue can be discussed and through which a CVE + is assigned when the fix is published. +* Alternative: email dirk.farin@gmail.com with "libheif security" in the subject line. + +You will normally receive an acknowledgement within a few business days. Reports are handled +in the evenings and on weekends, see [Maintenance capacity](#maintenance-capacity). + +Vulnerabilities in [libde265](https://github.com/strukturag/libde265) are reported the same way +in the libde265 repository. Bugs in other codec libraries (libaom, dav1d, x265, OpenJPEG, ...) +should be reported to those projects. + +## What a report must contain + +Reports are only actionable when they can be reproduced. Please include: + +1. **A reproducer file**: the crafted HEIF/AVIF file, or a minimal program that triggers the + bug through the public API. A description of a suspected bug without a reproducer is + treated as a normal bug report, not as a security report. +2. **The libheif version** (release tag or commit hash) and the build configuration + (CMake preset, codecs, and whether the security limits were changed). +3. **The sanitizer output** (ASan/UBSan/MSan trace) or a debugger backtrace. +4. **Your assessment of the impact**: crash only, out-of-bounds read, out-of-bounds write, + information disclosure, or resource exhaustion. +5. One report per bug. If you believe that several bugs share a root cause, say so. + +Please check the [published advisories](https://github.com/strukturag/libheif/security/advisories) +and test against the **latest release** before reporting. + +### Reports produced with AI tools + +Most reports received today are found with automated or AI-assisted tools. This is welcome, +but these tools produce many false positives and duplicates, and every report costs real time +to evaluate. Therefore: + +* A human must have verified the finding, reproduced it against the current release, and + written the report. Do not forward raw tool output. +* The requirements above (reproducer file, version, sanitizer trace) apply strictly. Reports + without a working reproducer are closed without further investigation. +* If your organization runs such tools against libheif at scale, please read + [Maintenance capacity](#maintenance-capacity) first. + +## Scope + +**In scope**: memory-safety violations, disclosure of uninitialized memory, and unbounded +resource consumption that can be triggered by decoding or inspecting a crafted input file +through the public API with the **default security limits**. This includes the image, sequence, +metadata, region and uncompressed-codec (`unci`) code paths. + +**Handled as regular bugs** (lower priority, usually without an advisory): + +* Issues that only occur when the security limits have been disabled or raised + (`LIBHEIF_SECURITY_LIMITS=off`, `heif_security_limits`, `--disable-limits`). Disabling the + limits is documented as unsafe for untrusted input. +* Issues that require the caller to violate the documented API contract, for example passing + inconsistent plane sizes to the encoder. These are still fixed, because libheif tries to + validate its inputs, but they are not vulnerabilities in libheif. +* Issues in the example programs (`heif-enc`, `heif-dec`, `heif-info`, ...) that are not in the + library itself, for example in the JPEG/PNG/TIFF/Y4M input readers. +* Issues in APIs that are only available with `ENABLE_EXPERIMENTAL_FEATURES=ON`. These APIs + are documented as unstable and must not be enabled in production builds. +* High but bounded memory or CPU use for very large valid images. Use the security limits to + constrain this to what your application needs. + +libheif is continuously fuzzed on [OSS-Fuzz](https://google.github.io/oss-fuzz/). Crashes found +by OSS-Fuzz are already known to the maintainer and do not need to be reported again. + +## Supported versions + +Only the latest release receives security fixes. There are no maintained long-term branches. +Distributions that ship older versions have to backport fixes themselves. Advisories reference +the fixing commits to make this easier. + +## Fix and disclosure process + +The following are targets, not commitments (see [Maintenance capacity](#maintenance-capacity)): + +| Severity | Typical handling | +|----------|------------------| +| Critical: out-of-bounds write or other likely code execution, reachable from a crafted file | Fix and release as soon as possible, usually within days. | +| High: out-of-bounds read with information disclosure, unbounded memory or CPU consumption | Fix on the main branch promptly; released together with other pending fixes, usually within a few weeks. | +| Medium / Low: crash, assertion failure, bounded denial of service | Fix on the main branch; included in the next regular release. | + +* Advisories are published on GitHub when the fixed release is available. CVE IDs are requested + through GitHub. +* The default embargo is 90 days from the report, or shorter when the fix is released earlier. + Reporters are credited in the advisory unless they ask not to be. +* Pre-release notification of embargoed advisories is available on request to downstream + distributors and to commercial support customers. +* libheif does not run a bug bounty program and does not pay for reports. +* Priority is decided by severity, not by who reports. + +## Maintenance capacity + +libheif and libde265 are maintained by one independent developer, largely in unpaid evenings +and weekends. There is no security team. + +To make the workload concrete: from January to August 2026, 37 security advisories were +published for libheif (3 rated critical, 13 high), and six releases were made mainly to ship +security fixes. Most of the 2026 reports were found with automated or AI-assisted tools, +often run by organizations that use libheif in their products. +Reproducing, fixing, testing, fuzzing and releasing each fix takes hours to days. + +The response times above are therefore best-effort. If your organization depends on libheif and +needs response-time commitments, advisory pre-notification, or dedicated engineering time, +please fund this work: + +* [GitHub Sponsors](https://github.com/sponsors/farindk) (organizations can pay by invoice), or +* a commercial support agreement, see [Commercial support](README.md#commercial-support) in the README. diff -Nru libheif-1.19.8/appveyor.yml libheif-1.23.4/appveyor.yml --- libheif-1.19.8/appveyor.yml 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/appveyor.yml 2026-09-06 14:45:17.000000000 +0000 @@ -7,18 +7,34 @@ environment: matrix: - arch: x64 + triplet: x64-windows + - arch: Win32 + triplet: x86-windows - arch: arm64 + triplet: arm64-windows install: - - vcpkg install aom:%arch%-windows - - vcpkg install dav1d:%arch%-windows - - vcpkg install ffmpeg[avcodec]:%arch%-windows - - vcpkg install libde265:%arch%-windows - - vcpkg install libjpeg-turbo:%arch%-windows - - vcpkg install libpng:%arch%-windows - - vcpkg install tiff:%arch%-windows - - ps: If (${env:arch} -eq "x64") { vcpkg install x265:${env:arch}-windows } - - vcpkg install zlib:%arch%-windows + # Build only Release vcpkg libraries (skip Debug) to halve install time + - ps: | + @('x64-windows', 'x86-windows', 'arm64-windows') | ForEach-Object { + $tripletDir = "C:\tools\vcpkg\triplets" + $content = (Get-Content "$tripletDir\$_.cmake" -Raw) + "`nset(VCPKG_BUILD_TYPE release)`n" + Set-Content "$tripletDir\$_.cmake" $content + } + # --- Phase 1: remove ffmpeg, upgrade everything else --- + #- vcpkg remove ffmpeg:%triplet% --recurse 2>NUL & ver>NUL + #- vcpkg upgrade --no-dry-run + # --- Phase 2: uncomment the two lines below to compile ffmpeg --- + - vcpkg upgrade --no-dry-run + - vcpkg install ffmpeg[avcodec]:%triplet% + - vcpkg install aom:%triplet% + - ps: If (${env:arch} -ne "Win32") { vcpkg install dav1d:${env:triplet} } + - vcpkg install libde265:%triplet% + - vcpkg install libjpeg-turbo:%triplet% + - vcpkg install libpng:%triplet% + - vcpkg install tiff:%triplet% + - vcpkg install x265:%triplet% + - vcpkg install zlib:%triplet% - cd c:\tools\vcpkg - vcpkg integrate install - cd %APPVEYOR_BUILD_FOLDER% diff -Nru libheif-1.19.8/build-emscripten.sh libheif-1.23.4/build-emscripten.sh --- libheif-1.19.8/build-emscripten.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/build-emscripten.sh 2026-09-06 14:45:17.000000000 +0000 @@ -19,10 +19,22 @@ SRCDIR=$1 CORES="${CORES:-`nproc --all`}" + ENABLE_LIBDE265="${ENABLE_LIBDE265:-1}" LIBDE265_VERSION="${LIBDE265_VERSION:-1.0.15}" + ENABLE_AOM="${ENABLE_AOM:-0}" AOM_VERSION="${AOM_VERSION:-3.6.1}" + +# Webcodecs is not on by default b/c asyncify increases the binary size considerably +ENABLE_WEBCODECS="${ENABLE_WEBCODECS:-0}" + +ENABLE_UNCOMPRESSED="${ENABLE_UNCOMPRESSED:-0}" + +# J2K still defunct. OpenJPEG compiles, but library is not picked up by libheif cmake. +ENABLE_OPENJPEG="${ENABLE_OPENJPEG:-0}" +OPENJPEG_VERSION="${OPENJPEG_VERSION:-2.5.4}" + STANDALONE="${STANDALONE:-0}" DEBUG="${DEBUG:-0}" USE_ES6="${USE_ES6:-0}" @@ -68,6 +80,7 @@ emcmake cmake aom-source \ -DENABLE_CCACHE=1 \ -DAOM_TARGET_CPU=generic \ + -DENABLE_APPS=0 \ -DENABLE_DOCS=0 \ -DENABLE_TESTS=0 \ -DENABLE_EXAMPLES=0 \ @@ -88,6 +101,42 @@ LIBRARY_LINKER_FLAGS="$LIBRARY_LINKER_FLAGS -L${AOM_DIR} -laom" fi +CONFIGURE_ARGS_OPENJPEG="" +if [ "$ENABLE_OPENJPEG" = "1" ]; then + [ -s "openjpeg-${OPENJPEG__VERSION}.tar.gz" ] || curl \ + -L \ + -o openjpeg-${OPENJPEG_VERSION}.tar.gz \ + "https://github.com/uclouvain/openjpeg/archive/refs/tags/v${OPENJPEG_VERSION}.tar.gz" + if [ ! -s "openjpeg-${OPENJPEG_VERSION}/bin/libopenjp2.a" ]; then + mkdir -p openjpeg-${OPENJPEG_VERSION}/openjpeg-source + tar xf openjpeg-${OPENJPEG_VERSION}.tar.gz -C openjpeg-${OPENJPEG_VERSION}/openjpeg-source + cd openjpeg-${OPENJPEG_VERSION} + emcmake cmake openjpeg-source/openjpeg-${OPENJPEG_VERSION} \ + -DBUILD_SHARED_LIBS=0 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=openjpeg-install + + emmake make -j${CORES} + emmake make install -j${CORES} + + cd .. + fi + + J2K_DIR="$(pwd)/openjpeg-${OPENJPEG_VERSION}" + CONFIGURE_ARGS_OPENJPEG="-DWITH_OpenJPEG_DECODER=ON -DCMAKE_PREFIX_PATH=${J2K_DIR}/openjpeg-install -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH" + LIBRARY_LINKER_FLAGS="$LIBRARY_LINKER_FLAGS -L${J2K_DIR}/bin -lopenjp2" +fi + +CONFIGURE_ARGS_WEBCODECS="" +if [ "$ENABLE_WEBCODECS" = "1" ]; then + CONFIGURE_ARGS_WEBCODECS="-DWITH_WEBCODECS=ON" +fi + +CONFIGURE_ARGS_UNCOMPRESSED="" +if [ "$ENABLE_UNCOMPRESSED" = "1" ]; then + CONFIGURE_ARGS_UNCOMPRESSED="-DWITH_UNCOMPRESSED_CODEC=ON" +fi + EXTRA_EXE_LINKER_FLAGS="-lembind" EXTRA_COMPILER_FLAGS="" if [ "$STANDALONE" = "1" ]; then @@ -102,7 +151,10 @@ -DCMAKE_CXX_FLAGS="${EXTRA_COMPILER_FLAGS}" \ -DCMAKE_EXE_LINKER_FLAGS="${LIBRARY_LINKER_FLAGS} ${EXTRA_EXE_LINKER_FLAGS}" \ $CONFIGURE_ARGS_LIBDE265 \ - $CONFIGURE_ARGS_AOM + $CONFIGURE_ARGS_AOM \ + $CONFIGURE_ARGS_WEBCODECS \ + $CONFIGURE_ARGS_UNCOMPRESSED \ + $CONFIGURE_ARGS_OPENJPEG VERBOSE=1 emmake make -j${CORES} @@ -113,6 +165,10 @@ BUILD_FLAGS="-lembind -o libheif.js --post-js ${SRCDIR}/post.js -sWASM=$USE_WASM -sDYNAMIC_EXECUTION=$USE_UNSAFE_EVAL" +if [ "$ENABLE_WEBCODECS" = "1" ]; then + BUILD_FLAGS="$BUILD_FLAGS -sASYNCIFY -sASYNCIFY_IMPORTS=['decode_with_browser_hevc']" +fi + if [ "$USE_TYPESCRIPT" = "1" ]; then BUILD_FLAGS="$BUILD_FLAGS --emit-tsd libheif.d.ts" fi diff -Nru libheif-1.19.8/cmake/modules/FindAOM.cmake libheif-1.23.4/cmake/modules/FindAOM.cmake --- libheif-1.19.8/cmake/modules/FindAOM.cmake 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindAOM.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -1,42 +1,53 @@ -include(LibFindMacros) -include(CheckSymbolExists) +find_package(AOM QUIET CONFIG) -libfind_pkg_check_modules(AOM_PKGCONF aom) +if(TARGET AOM::aom) + if(NOT AOM_FIND_QUIETLY) + message(STATUS "Found AOM: ${AOM_DIR}") + endif() +else() + include(LibFindMacros) + + libfind_pkg_check_modules(AOM_PKGCONF aom) + + find_path(AOM_INCLUDE_DIR + NAMES aom/aom_decoder.h aom/aom_encoder.h + HINTS ${AOM_PKGCONF_INCLUDE_DIRS} ${AOM_PKGCONF_INCLUDEDIR} + PATH_SUFFIXES AOM + ) + + find_library(AOM_LIBRARY + NAMES libaom aom + HINTS ${AOM_PKGCONF_LIBRARY_DIRS} ${AOM_PKGCONF_LIBDIR} + ) + + set(AOM_PROCESS_LIBS AOM_LIBRARY) + set(AOM_PROCESS_INCLUDES AOM_INCLUDE_DIR) + libfind_process(AOM) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(AOM + REQUIRED_VARS + AOM_INCLUDE_DIR + AOM_LIBRARY + ) +endif() + +if(AOM_FOUND) + include(CheckSymbolExists) -find_path(AOM_INCLUDE_DIR - NAMES aom/aom_decoder.h aom/aom_encoder.h - HINTS ${AOM_PKGCONF_INCLUDE_DIRS} ${AOM_PKGCONF_INCLUDEDIR} - PATH_SUFFIXES AOM -) - -list(APPEND CMAKE_REQUIRED_INCLUDES ${AOM_INCLUDE_DIR}) -check_symbol_exists(AOM_USAGE_GOOD_QUALITY aom/aom_encoder.h aom_usage_flag_exists) -unset(CMAKE_REQUIRED_INCLUDES) - -find_library(AOM_LIBRARY - NAMES libaom aom - HINTS ${AOM_PKGCONF_LIBRARY_DIRS} ${AOM_PKGCONF_LIBDIR} -) + list(APPEND CMAKE_REQUIRED_INCLUDES ${AOM_INCLUDE_DIRS}) + check_symbol_exists(AOM_USAGE_GOOD_QUALITY aom/aom_encoder.h aom_usage_flag_exists) + unset(CMAKE_REQUIRED_INCLUDES) -if(EXISTS "${AOM_INCLUDE_DIR}/aom/aom_decoder.h") + if(EXISTS "${AOM_INCLUDE_DIRS}/aom/aom_decoder.h") set(AOM_DECODER_FOUND YES) -else() + else() set(AOM_DECODER_FOUND NO) -endif() + endif() -if((EXISTS "${AOM_INCLUDE_DIR}/aom/aom_encoder.h") AND "${aom_usage_flag_exists}") + if((EXISTS "${AOM_INCLUDE_DIRS}/aom/aom_encoder.h") AND "${aom_usage_flag_exists}") set(AOM_ENCODER_FOUND YES) -else() + else() set(AOM_ENCODER_FOUND NO) + endif() endif() - -set(AOM_PROCESS_LIBS AOM_LIBRARY) -set(AOM_PROCESS_INCLUDES AOM_INCLUDE_DIR) -libfind_process(AOM) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(AOM - REQUIRED_VARS - AOM_INCLUDE_DIR - AOM_LIBRARIES -) diff -Nru libheif-1.19.8/cmake/modules/FindBrotli.cmake libheif-1.23.4/cmake/modules/FindBrotli.cmake --- libheif-1.19.8/cmake/modules/FindBrotli.cmake 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindBrotli.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -23,4 +23,4 @@ set(HAVE_BROTLI ${Brotli_FOUND}) set(BROTLI_INCLUDE_DIRS ${BROTLI_DEC_INCLUDE_DIR} ${BROTLI_ENC_INCLUDE_DIR}) -set(BROTLI_LIBS "${BROTLICOMMON_LIBRARY}" "${BROTLI_DEC_LIB}" "${BROTLI_ENC_LIB}") \ No newline at end of file +set(BROTLI_LIBS "${BROTLI_DEC_LIB}" "${BROTLI_ENC_LIB}" "${BROTLI_COMMON_LIB}") \ No newline at end of file diff -Nru libheif-1.19.8/cmake/modules/FindFFMPEG.cmake libheif-1.23.4/cmake/modules/FindFFMPEG.cmake --- libheif-1.19.8/cmake/modules/FindFFMPEG.cmake 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindFFMPEG.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -103,9 +103,17 @@ string(TOUPPER "${component}" component_upper) file(STRINGS "${version_header_path}" version REGEX "#define *LIB${component_upper}_VERSION_(MAJOR|MINOR|MICRO) ") - string(REGEX REPLACE ".*_MAJOR *\([0-9]*\).*" "\\1" major "${version}") - string(REGEX REPLACE ".*_MINOR *\([0-9]*\).*" "\\1" minor "${version}") - string(REGEX REPLACE ".*_MICRO *\([0-9]*\).*" "\\1" micro "${version}") + # Recent FFmpeg moved LIB_VERSION_MAJOR into a separate + # version_major.h, so it may not be present in version.h above. + set(version_major_header_path "${FFMPEG_${component}_INCLUDE_DIR}/lib${component}/version_major.h") + if (EXISTS "${version_major_header_path}") + file(STRINGS "${version_major_header_path}" version_major + REGEX "#define *LIB${component_upper}_VERSION_MAJOR ") + list(PREPEND version "${version_major}") + endif () + string(REGEX REPLACE ".*_MAJOR *([0-9]+).*" "\\1" major "${version}") + string(REGEX REPLACE ".*_MINOR *([0-9]+).*" "\\1" minor "${version}") + string(REGEX REPLACE ".*_MICRO *([0-9]+).*" "\\1" micro "${version}") if (NOT major STREQUAL "" AND NOT minor STREQUAL "" AND NOT micro STREQUAL "") @@ -155,7 +163,7 @@ if (EXISTS "${_ffmpeg_version_header_path}") file(STRINGS "${_ffmpeg_version_header_path}" _ffmpeg_version REGEX "FFMPEG_VERSION") - string(REGEX REPLACE ".*\"n?\(.*\)\"" "\\1" FFMPEG_VERSION "${_ffmpeg_version}") + string(REGEX REPLACE ".*\"n?(.*)\"" "\\1" FFMPEG_VERSION "${_ffmpeg_version}") unset(_ffmpeg_version) else () set(FFMPEG_VERSION FFMPEG_VERSION-NOTFOUND) diff -Nru libheif-1.19.8/cmake/modules/FindOPENJPH.cmake libheif-1.23.4/cmake/modules/FindOPENJPH.cmake --- libheif-1.19.8/cmake/modules/FindOPENJPH.cmake 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindOPENJPH.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -1,24 +1,50 @@ -include(LibFindMacros) -libfind_pkg_check_modules(OPENJPH_PKGCONF openjph) +find_package(OPENJPH QUIET CONFIG NAMES openjph) -find_path(OPENJPH_INCLUDE_DIR - NAMES openjph/ojph_version.h - HINTS ${OPENJPH_PKGCONF_INCLUDE_DIRS} ${OPENJPH_PKGCONF_INCLUDEDIR} - PATH_SUFFIXES OPENJPH -) - -find_library(OPENJPH_LIBRARY - NAMES libopenjph openjph - HINTS ${OPENJPH_PKGCONF_LIBRARY_DIRS} ${OPENJPH_PKGCONF_LIBDIR} -) - -set(OPENJPH_PROCESS_LIBS OPENJPH_LIBRARY) -set(OPENJPH_PROCESS_INCLUDES OPENJPH_INCLUDE_DIR) -libfind_process(OPENJPH) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OPENJPH - REQUIRED_VARS - OPENJPH_INCLUDE_DIR - OPENJPH_LIBRARY -) +if(TARGET openjph) + if(NOT OPENJPH_FIND_QUIETLY) + message(STATUS "Found openjph: ${OPENJPH_DIR}") + endif() + set(OPENJPH_LIBRARIES "openjph") + + # OpenJPH's exported config sets the include directory to + # /include/openjph, but libheif's sources include the headers with + # the "openjph/" prefix (e.g. #include "openjph/ojph_mem.h"). Add the parent + # directory so those includes resolve with both header layouts. + get_target_property(_openjph_incdirs openjph INTERFACE_INCLUDE_DIRECTORIES) + if(_openjph_incdirs) + foreach(_dir IN LISTS _openjph_incdirs) + list(APPEND OPENJPH_INCLUDE_DIRS "${_dir}") + if(_dir MATCHES "/openjph$") + get_filename_component(_parent "${_dir}" DIRECTORY) + list(APPEND OPENJPH_INCLUDE_DIRS "${_parent}") + endif() + endforeach() + unset(_parent) + endif() + unset(_openjph_incdirs) +else() + include(LibFindMacros) + libfind_pkg_check_modules(OPENJPH_PKGCONF openjph) + + find_path(OPENJPH_INCLUDE_DIR + NAMES openjph/ojph_version.h + HINTS ${OPENJPH_PKGCONF_INCLUDE_DIRS} ${OPENJPH_PKGCONF_INCLUDEDIR} + PATH_SUFFIXES OPENJPH + ) + + find_library(OPENJPH_LIBRARY + NAMES libopenjph openjph + HINTS ${OPENJPH_PKGCONF_LIBRARY_DIRS} ${OPENJPH_PKGCONF_LIBDIR} + ) + + set(OPENJPH_PROCESS_LIBS OPENJPH_LIBRARY) + set(OPENJPH_PROCESS_INCLUDES OPENJPH_INCLUDE_DIR) + libfind_process(OPENJPH) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(OPENJPH + REQUIRED_VARS + OPENJPH_INCLUDE_DIR + OPENJPH_LIBRARY + ) +endif() diff -Nru libheif-1.19.8/cmake/modules/FindWEBP.cmake libheif-1.23.4/cmake/modules/FindWEBP.cmake --- libheif-1.19.8/cmake/modules/FindWEBP.cmake 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindWEBP.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,74 @@ +# Locates libwebp (decode + encode) and libwebpmux. +# +# Tries libwebp's upstream CMake config first (which defines the WebP::webp +# and WebP::libwebpmux imported targets). If that is not available, falls +# back to pkg-config / manual library search and defines the same imported +# targets so consumers can link uniformly. +# +# Sets: +# WEBP_FOUND +# WebP::webp - imported target for libwebp +# WebP::libwebpmux - imported target for libwebpmux (depends on WebP::webp) + +find_package(WebP QUIET CONFIG) + +if(TARGET WebP::webp AND TARGET WebP::libwebpmux) + set(WEBP_FOUND TRUE) + if(NOT WEBP_FIND_QUIETLY) + message(STATUS "Found WebP (CONFIG): ${WebP_DIR}") + endif() +else() + include(LibFindMacros) + + libfind_pkg_check_modules(WEBP_PKGCONF libwebp) + libfind_pkg_check_modules(WEBPMUX_PKGCONF libwebpmux) + + find_path(WEBP_INCLUDE_DIR + NAMES webp/decode.h webp/encode.h + HINTS ${WEBP_PKGCONF_INCLUDE_DIRS} ${WEBP_PKGCONF_INCLUDEDIR} + ) + + find_path(WEBPMUX_INCLUDE_DIR + NAMES webp/mux.h + HINTS ${WEBPMUX_PKGCONF_INCLUDE_DIRS} ${WEBPMUX_PKGCONF_INCLUDEDIR} ${WEBP_INCLUDE_DIR} + ) + + find_library(WEBP_LIBRARY + NAMES webp libwebp + HINTS ${WEBP_PKGCONF_LIBRARY_DIRS} ${WEBP_PKGCONF_LIBDIR} + ) + + find_library(WEBPMUX_LIBRARY + NAMES webpmux libwebpmux + HINTS ${WEBPMUX_PKGCONF_LIBRARY_DIRS} ${WEBPMUX_PKGCONF_LIBDIR} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(WEBP + REQUIRED_VARS + WEBP_INCLUDE_DIR + WEBP_LIBRARY + WEBPMUX_INCLUDE_DIR + WEBPMUX_LIBRARY + ) + + if(WEBP_FOUND) + if(NOT TARGET WebP::webp) + add_library(WebP::webp UNKNOWN IMPORTED) + set_target_properties(WebP::webp PROPERTIES + IMPORTED_LOCATION "${WEBP_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${WEBP_INCLUDE_DIR}" + ) + endif() + if(NOT TARGET WebP::libwebpmux) + add_library(WebP::libwebpmux UNKNOWN IMPORTED) + set_target_properties(WebP::libwebpmux PROPERTIES + IMPORTED_LOCATION "${WEBPMUX_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${WEBPMUX_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "WebP::webp" + ) + endif() + endif() + + mark_as_advanced(WEBP_INCLUDE_DIR WEBP_LIBRARY WEBPMUX_INCLUDE_DIR WEBPMUX_LIBRARY) +endif() diff -Nru libheif-1.19.8/cmake/modules/FindX264.cmake libheif-1.23.4/cmake/modules/FindX264.cmake --- libheif-1.19.8/cmake/modules/FindX264.cmake 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/cmake/modules/FindX264.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,38 @@ +include(LibFindMacros) +libfind_pkg_check_modules(X264_PKGCONF x264) + +find_path(X264_INCLUDE_DIR + NAMES x264.h + HINTS ${X264_PKGCONF_INCLUDE_DIRS} ${X264_PKGCONF_INCLUDEDIR} + PATH_SUFFIXES X264 +) + +find_library(X264_LIBRARY + NAMES libx264 x264 + HINTS ${X264_PKGCONF_LIBRARY_DIRS} ${X264_PKGCONF_LIBDIR} +) + +set(X264_PROCESS_LIBS X264_LIBRARY) +set(X264_PROCESS_INCLUDES X264_INCLUDE_DIR) +libfind_process(X264) + +if(X264_INCLUDE_DIR) + set(x264_config_file "${X264_INCLUDE_DIR}/x264_config.h") + if(EXISTS ${x264_config_file}) + file(STRINGS + ${x264_config_file} + TMP + REGEX "#define X264_BUILD .*$") + string(REGEX REPLACE "#define X264_BUILD" "" TMP "${TMP}") + string(REGEX MATCHALL "[0-9.]+" X264_BUILD "${TMP}") + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(X264 + REQUIRED_VARS + X264_INCLUDE_DIR + X264_LIBRARIES + VERSION_VAR + X264_BUILD +) diff -Nru libheif-1.19.8/cmake/modules/Findkvazaar.cmake libheif-1.23.4/cmake/modules/Findkvazaar.cmake --- libheif-1.19.8/cmake/modules/Findkvazaar.cmake 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/cmake/modules/Findkvazaar.cmake 2026-09-06 14:45:17.000000000 +0000 @@ -1,5 +1,6 @@ include(LibFindMacros) include(CheckStructHasMember) +include(CheckSymbolExists) libfind_pkg_check_modules(KVAZAAR_PKGCONF kvazaar) @@ -19,9 +20,13 @@ libfind_process(KVAZAAR) set(CMAKE_REQUIRED_INCLUDES ${KVAZAAR_INCLUDE_DIR}) +set(CMAKE_REQUIRED_LIBRARIES ${KVAZAAR_LIBRARY}) CHECK_STRUCT_HAS_MEMBER("struct kvz_config" enable_logging_output kvazaar.h HAVE_KVAZAAR_ENABLE_LOGGING LANGUAGE CXX) +check_symbol_exists(kvz_get_version_string kvazaar.h + HAVE_KVAZAAR_VERSION_STRING) unset(CMAKE_REQUIRED_INCLUDES) +unset(CMAKE_REQUIRED_LIBRARIES) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(kvazaar diff -Nru libheif-1.19.8/debian/changelog libheif-1.23.4/debian/changelog --- libheif-1.19.8/debian/changelog 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/changelog 2026-09-09 06:41:39.000000000 +0000 @@ -1,14 +1,170 @@ -libheif (1.19.8-1+deb13u1) trixie-security; urgency=high +libheif (1.23.4-1~deb13u1) trixie-security; urgency=high * Non-maintainer upload by the Security Team. - * Fixed issues: - CVE-2025-68431, CVE-2026-32882, CVE-2026-32740, CVE-2026-47247, - CVE-2026-32741, CVE-2026-47709, CVE-2026-49271, CVE-2026-62292, - CVE-2026-47714, CVE-2026-48029, CVE-2026-62289 and GHSA-2h34-fcv6-jqvh - * Mitigated CVE-2026-47178: rejected files affected by the issue with - heif_error_Unsupported_feature instead of being decoded. + * New upstream release 1.23.4: + - CVE-2026-84450: assertion failure in Fraction::Fraction when a "clap" + box is combined with an oversized "ispe"; incomplete fix for + GHSA-jc8f-p23p-5hjg. + - CVE-2026-84451: out-of-bounds read; incomplete fix for + GHSA-73p7-m7gg-w2jv. + - CVE-2026-3949: out-of-bounds read in code, not enabled in Debian build. + - GHSA-x8r2-mggj-j6wr: heap buffer overflow (write) in the uncompressed + (unci) mixed-interleave decoder when the two chroma components declare + different bit depths. + - GHSA-w7mc-p8jc-p853: heap out-of-bounds read in the YCbCr 4:2:0 to + 16-bit interleaved RGB colour conversion. + - GHSA-4jqm-2x34-6f6r: heap buffer overflow in the SVT-AV1 encoder plugin + when encoding a high-bit-depth alpha channel. + - GHSA-9rj8-5mp5-26c9: out-of-bounds read in the RGB to YCbCr + identity-matrix colour conversion. + - GHSA-vg7w-rp49-4fc2: the max_items security limit was not enforced on + "iinf" child boxes, "iref" reference entries were not capped, Box_iref + references are now indexed by from_item_ID, and the Emscripten item-ID + helpers no longer use alloca. + - GHSA-5w8x-856j-7x7c: add_property() returned a property position + belonging to another item. + - GHSA-fqpw-fj22-78w4: encoder plugins did not validate the input image + before encoding it. + - GHSA-prgh-72vc-3xmc: cyclic decode reference graphs were not rejected + before decoding. + - GHSA-xrp2-63fq-jm8q: the decode-graph cycle check recursed without + bound; it is now an explicit worklist. + - GHSA-4rv4-953r-p24q: raw sequence sample leak and an unguarded C entry + point. + - GHSA-rhgw-q5g8-xjh2: coded/visible rectangle mismatch in the WebCodecs + decoder plugin. + * Do not build the heif-view tool so that the set of binary packages + stays identical to the one trixie already ships. + * d/control: Build libheif-plugin-kvazaar on all architectures again. - -- Aron Xu Thu, 06 Aug 2026 18:48:46 +0800 + -- Aron Xu Wed, 09 Sep 2026 14:41:39 +0800 + +libheif (1.23.3-1) unstable; urgency=medium + + * New upstream version 1.23.3 + * Fixes CVE-2026-84450, CVE-2026-84451. + * Fixes heap buffer overflow (write) in the uncompressed (unci) mixed- + interleave decoder when the two chroma components declare different + bit depths. See upstream commit 9d3cd91 (GHSA-x8r2-mggj-j6wr). + * Fixes decoder deadlock (DoS) via alpha-aux reference cycle. + See upstream commit a83fd7b (GHSA-8fmq-r4pf-7m57). + * Fixes heap out-of-bounds read in the YCbCr 4:2:0 to 16-bit interleaved + RGB conversion. See upstream commit c37bf2b (GHSA-w7mc-p8jc-p853). + * Fixes heap buffer overflow in the SVT-AV1 encoder plugin when encoding + a high-bit-depth alpha channel. + See upstream commits 0839585 and 5c3ed28 (GHSA-4jqm-2x34-6f6r). + * Fixes heap out-of-bounds read when converting odd-height 4:2:0 frames + of an uncompressed (uncv) image sequence to RGB. + See upstream commit fe48abf (GHSA-4h82-g446-83fm). + * Fixes out-of-bounds read in the RGB to YCbCr identity-matrix color + conversion. See upstream commit 8008614 (GHSA-9rj8-5mp5-26c9). + + -- Joachim Bauch Thu, 03 Sep 2026 10:20:32 +0200 + +libheif (1.23.2-1) unstable; urgency=medium + + * New upstream version 1.23.2 + * Fixes heap buffer overflow in scale_nearest_neighbor() via duplicate + alpha planes from nested iden/auxl items. See upstream commit f4fb8bd + (CVE-2026-84383). + * Fixes out-of-bounds read and write in derived-item and pixel-plane + handling. Upstream commits f4fb8bd, cd365da, bc7cf37, c0a63aa, + 1fcada5, c664797, 328c4f9, 3d81d56, 578ab47 and 4dd71d7. + * Fixes missing memory limits when doing brotli/zlib decompression. + See upstream commits 21893e8 and 6b6757b (CVE-2026-84384). + * Fixes CPU and memory amplification when decoding derived-image + references (grid, iovl, iden). See upstream commit 30b52a4 + (CVE-2026-84447). + * Fixes non-terminating decode loops and unbounded memory when decoding + sequence images. See upstream commit 3a7a69a (CVE-2026-84446). + * Fixes out-of-bounds write in the unci encoder. + See upstream commit e65071f (CVE-2026-84444). + * Fixes heap out-of-bounds read in the inline-mask region API. + See upstream commit 646d85f (CVE-2026-84448). + + -- Joachim Bauch Thu, 27 Aug 2026 22:59:53 +0200 + +libheif (1.23.1-1) unstable; urgency=medium + + * New upstream version 1.23.1 + * Fixes CVE-2026-54240, CVE-2026-54241 + * Unpackaged upstream version 1.23.0 fixes the following CVEs: + CVE-2026-50142 + * Unpackaged upstream version 1.22.1 fixes the following CVEs: + CVE-2026-49271 (Closes: #1140479) + * Unpackaged upstream version 1.22.0 fixes the following CVEs: + CVE-2026-3950 (Closes: #1130640), CVE-2026-32738, CVE-2026-32739, + CVE-2026-32740, CVE-2026-32741, CVE-2026-32814, CVE-2026-32882, + CVE-2026-41069, CVE-2026-41071 (Closes: #1137524), + CVE-2026-47178 (Closes: #1140223), CVE-2026-47247, CVE-2026-47251, + CVE-2026-47254, CVE-2026-47709, CVE-2026-47714, CVE-2026-48029 + * d/control: Bump "Standards-Version" to 4.7.4 + * Update symbols for new upstream version. + * d/control: Build-depend on libtiff-dev for TIFF support in examples. + * d/control: Build-depend on libwebp-dev for WebP support in examples. + * Remove patches no longer necessary. + + -- Joachim Bauch Wed, 01 Jul 2026 10:14:01 +0200 + +libheif (1.21.2-4) unstable; urgency=medium + + * Add patch to fix compilation with SvtAv1 v4.0.0 / v4.1.0 + (Closes: #1131942). + + -- Joachim Bauch Thu, 02 Apr 2026 23:11:10 +0200 + +libheif (1.21.2-3) unstable; urgency=medium + + * Team upload + * Have libheif-dev depend on libbrotli-dev as required by the + pkgconfig file. + * Demote libheif-plugin-libde265 from Depends to Suggests on Ubuntu + to avoid possible patent concerns. + * d/rules: Fix substvar assignment syntax for libheif:Suggests and + libheif:Recommends (colon was used instead of equals sign, causing + the variables to expand to empty strings) + + -- Charles Sat, 21 Feb 2026 14:12:49 -0000 + +libheif (1.21.2-2) unstable; urgency=medium + + * Team upload + * Don't build kvazaar plugin on loong64 because kvazaar FTBFS there + + -- Jeremy Bícha Sat, 07 Feb 2026 11:24:57 -0500 + +libheif (1.21.2-1) unstable; urgency=medium + + * New upstream version 1.21.2 + - Fixes CVE-2025-68431 (Closes: #1124317). + * Remove patches applied upstream. + * Update symbols for new upstream version. + * d/control: Update build dependencies for cmake AOM detection. + * d/control: Update Standards-Version to 4.7.3 and remove optional priority. + + -- Joachim Bauch Mon, 19 Jan 2026 16:27:41 +0100 + +libheif (1.20.2-2) unstable; urgency=medium + + * Upload to unstable. + + -- Joachim Bauch Mon, 18 Aug 2025 13:26:19 +0200 + +libheif (1.20.2-1) experimental; urgency=medium + + * New upstream version 1.20.2 + + -- Joachim Bauch Tue, 05 Aug 2025 15:44:43 +0200 + +libheif (1.20.1-1) experimental; urgency=medium + + * New upstream version 1.20.1 + * Add patch to fix undefined symbol in svtenc plugin. + * Update symbols for new upstream version. + * Add "heif-view" binary package. + * Add patch to load (encoding) plugins earlier. + + -- Joachim Bauch Mon, 28 Jul 2025 11:41:29 +0200 libheif (1.19.8-1) unstable; urgency=medium diff -Nru libheif-1.19.8/debian/control libheif-1.23.4/debian/control --- libheif-1.19.8/debian/control 2025-04-29 08:32:16.000000000 +0000 +++ libheif-1.23.4/debian/control 2026-09-09 06:41:39.000000000 +0000 @@ -1,19 +1,20 @@ Source: libheif Section: libs -Priority: optional Maintainer: Debian Multimedia Maintainers Uploaders: Joachim Bauch Build-Depends: + aom-tools, cmake, debhelper-compat (= 13), libaom-dev, libavcodec-dev, libdav1d-dev, + libbrotli-dev, libde265-dev (>= 1.0.7), libgdk-pixbuf-2.0-dev, libjpeg-dev, - libkvazaar-dev, + libkvazaar-dev [!loong64], libopenjp2-7-dev, libpng-dev, # librav1e-dev is built by src:rust-rav1e which is not available on all platforms. @@ -21,11 +22,14 @@ librav1e-dev [amd64 arm64 armel armhf mips64el mipsel powerpc ppc64 ppc64el riscv64 s390x], libsharpyuv-dev, libsvtav1enc-dev, + libtiff-dev, + libwebp-dev, + libwebm-dev, libx265-dev, + libyuv-dev, libz-dev, pkgconf -Standards-Version: 4.7.2 -Rules-Requires-Root: no +Standards-Version: 4.7.4 Homepage: http://www.libheif.org Vcs-Git: https://salsa.debian.org/multimedia-team/libheif.git Vcs-Browser: https://salsa.debian.org/multimedia-team/libheif @@ -36,8 +40,7 @@ Depends: ${misc:Depends}, ${shlibs:Depends}, - ${libheif:Depends}, - libheif-plugin-libde265 (= ${binary:Version}) + ${libheif:Depends} Recommends: ${libheif:Recommends}, libheif-plugin-aomenc (= ${binary:Version}) @@ -48,7 +51,7 @@ libheif-plugin-jpegenc (= ${binary:Version}), libheif-plugin-j2kdec (= ${binary:Version}), libheif-plugin-j2kenc (= ${binary:Version}), - libheif-plugin-kvazaar (= ${binary:Version}), + libheif-plugin-kvazaar (= ${binary:Version}) [!loong64], libheif-plugin-rav1e (= ${binary:Version}) [amd64 arm64 armel armhf mips64el mipsel powerpc ppc64 ppc64el riscv64 s390x], libheif-plugin-svtenc (= ${binary:Version}) Description: HEIF and AVIF file format decoder and encoder - shared library @@ -369,7 +372,7 @@ libheif-plugin-j2kenc (>= ${source:Version}), libheif-plugin-jpegdec (>= ${source:Version}), libheif-plugin-jpegenc (>= ${source:Version}), - libheif-plugin-kvazaar (>= ${source:Version}), + libheif-plugin-kvazaar (>= ${source:Version}) [!loong64], libheif-plugin-libde265 (>= ${source:Version}), libheif-plugin-rav1e (>= ${source:Version}) [amd64 arm64 armel armhf mips64el mipsel powerpc ppc64 ppc64el riscv64 s390x], libheif-plugin-svtenc (>= ${source:Version}), @@ -394,6 +397,7 @@ Architecture: any Depends: libheif1 (= ${binary:Version}), + libbrotli-dev, libsharpyuv-dev, libz-dev, ${misc:Depends} diff -Nru libheif-1.19.8/debian/libheif1.symbols libheif-1.23.4/debian/libheif1.symbols --- libheif-1.19.8/debian/libheif1.symbols 2025-01-07 12:11:40.000000000 +0000 +++ libheif-1.23.4/debian/libheif1.symbols 2026-07-01 07:52:48.000000000 +0000 @@ -10,10 +10,14 @@ heif_camera_extrinsic_matrix_release@Base 1.18.1 heif_check_filetype@Base 1.4.0 heif_check_jpeg_filetype@Base 1.14.0 + heif_color_conversion_options_ext_alloc@Base 1.20.0 + heif_color_conversion_options_ext_copy@Base 1.20.0 + heif_color_conversion_options_ext_free@Base 1.20.0 heif_color_conversion_options_set_defaults@Base 1.19.1 heif_context_add_XMP_metadata2@Base 1.14.0 heif_context_add_XMP_metadata@Base 1.3.2 heif_context_add_compatible_brand@Base 1.18.1 + heif_context_add_empty_unci_image@Base 1.20.0 heif_context_add_exif_metadata@Base 1.3.2 heif_context_add_generic_metadata@Base 1.6.0 heif_context_add_generic_uri_metadata@Base 1.18.1 @@ -26,6 +30,8 @@ heif_context_add_overlay_image@Base 1.19.1 heif_context_add_precompressed_mime_item@Base 1.18.1 heif_context_add_uri_item@Base 1.18.1 + heif_context_add_uri_metadata_sequence_track@Base 1.20.0 + heif_context_add_visual_sequence_track@Base 1.20.0 heif_context_alloc@Base 1.0.0 heif_context_assign_thumbnail@Base 1.3.2 heif_context_debug_dump_boxes_to_file@Base 1.0.0 @@ -40,6 +46,7 @@ heif_context_get_image_handle@Base 1.0.0 heif_context_get_item_references@Base 1.18.1 heif_context_get_list_of_item_IDs@Base 1.18.1 + heif_context_get_max_decoding_threads@Base 1.23.1 heif_context_get_number_of_items@Base 1.18.1 heif_context_get_list_of_top_level_image_IDs@Base 1.0.0 heif_context_get_number_of_top_level_images@Base 1.0.0 @@ -47,21 +54,34 @@ heif_context_get_primary_image_handle@Base 1.0.0 heif_context_get_region_item@Base 1.16.2 heif_context_get_security_limits@Base 1.19.1 + heif_context_get_sequence_duration@Base 1.20.0 + heif_context_get_sequence_timescale@Base 1.20.0 + heif_context_get_text_item@Base 1.21.2 + heif_context_get_track@Base 1.20.0 + heif_context_get_track_ids@Base 1.20.0 + heif_context_has_sequence@Base 1.20.0 heif_context_is_top_level_image_ID@Base 1.0.0 + heif_context_number_of_sequence_tracks@Base 1.20.0 heif_context_read_from_file@Base 1.0.0 heif_context_read_from_memory@Base 1.0.0 heif_context_read_from_memory_without_copy@Base 1.3.2 heif_context_read_from_reader@Base 1.3.2 + heif_context_set_major_brand@Base 1.20.0 heif_context_set_max_decoding_threads@Base 1.13.0 heif_context_set_maximum_image_size_limit@Base 1.6.0 + heif_context_set_number_of_sequence_repetitions@Base 1.21.2 heif_context_set_primary_image@Base 1.3.2 heif_context_set_security_limits@Base 1.19.1 + heif_context_set_sequence_timescale@Base 1.20.0 + heif_context_set_unif@Base 1.23.1 + heif_context_set_write_mini_format@Base 1.23.1 heif_context_write@Base 1.1.0 heif_context_write_to_file@Base 1.1.0 heif_decode_image@Base 1.0.0 heif_decoder_descriptor_get_id_name@Base 1.15.1 heif_decoder_descriptor_get_name@Base 1.15.1 heif_decoding_options_alloc@Base 1.0.0 + heif_decoding_options_copy@Base 1.20.0 heif_decoding_options_free@Base 1.0.0 heif_deinit@Base 1.13.0 heif_depth_representation_info_free@Base 1.0.0 @@ -96,6 +116,7 @@ heif_encoder_set_parameter_integer@Base 1.1.0 heif_encoder_set_parameter_string@Base 1.1.0 heif_encoding_options_alloc@Base 1.3.2 + heif_encoding_options_copy@Base 1.20.0 heif_encoding_options_free@Base 1.3.2 heif_entity_groups_release@Base 1.19.1 heif_error_success@Base 1.17.0 @@ -117,43 +138,112 @@ heif_has_compatible_filetype@Base 1.18.1 heif_have_decoder_for_format@Base 1.3.2 heif_have_encoder_for_format@Base 1.3.2 + heif_image_add_bayer_component@Base 1.23.1 + heif_image_add_component@Base 1.23.1 heif_image_add_decoding_warning@Base 1.13.0 heif_image_add_plane@Base 1.0.0 + heif_image_add_plane_safe@Base 1.20.0 + heif_image_add_polarization_pattern@Base 1.23.1 + heif_image_add_sensor_bad_pixels_map@Base 1.23.1 + heif_image_add_sensor_nuc@Base 1.23.1 heif_image_create@Base 1.0.0 heif_image_crop@Base 1.9.1 heif_image_extend_padding_to_size@Base 1.15.1 heif_image_extend_to_size_fill_with_zero@Base 1.19.1 + heif_image_extract_area@Base 1.20.0 + heif_image_get_ambient_viewing_environment@Base 1.23.1 + heif_image_get_bayer_pattern@Base 1.23.1 + heif_image_get_bayer_pattern_size@Base 1.23.1 heif_image_get_bits_per_pixel@Base 1.0.0 heif_image_get_bits_per_pixel_range@Base 1.5.0 heif_image_get_chroma_format@Base 1.0.0 + heif_image_get_chroma_location@Base 1.23.1 heif_image_get_color_profile_type@Base 1.5.0 heif_image_get_colorspace@Base 1.0.0 + heif_image_get_component@Base 1.23.1 + heif_image_get_component_bits_per_pixel@Base 1.23.1 + heif_image_get_component_channel@Base 1.23.1 + heif_image_get_component_complex32@Base 1.23.1 + heif_image_get_component_complex32_readonly@Base 1.23.1 + heif_image_get_component_complex64@Base 1.23.1 + heif_image_get_component_complex64_readonly@Base 1.23.1 + heif_image_get_component_datatype@Base 1.23.1 + heif_image_get_component_float32@Base 1.23.1 + heif_image_get_component_float32_readonly@Base 1.23.1 + heif_image_get_component_float64@Base 1.23.1 + heif_image_get_component_float64_readonly@Base 1.23.1 + heif_image_get_component_height@Base 1.23.1 + heif_image_get_component_int16@Base 1.23.1 + heif_image_get_component_int16_readonly@Base 1.23.1 + heif_image_get_component_int32@Base 1.23.1 + heif_image_get_component_int32_readonly@Base 1.23.1 + heif_image_get_component_int64@Base 1.23.1 + heif_image_get_component_int64_readonly@Base 1.23.1 + heif_image_get_component_int8@Base 1.23.1 + heif_image_get_component_int8_readonly@Base 1.23.1 + heif_image_get_component_readonly@Base 1.23.1 + heif_image_get_component_type@Base 1.23.1 + heif_image_get_component_uint16@Base 1.23.1 + heif_image_get_component_uint16_readonly@Base 1.23.1 + heif_image_get_component_uint32@Base 1.23.1 + heif_image_get_component_uint32_readonly@Base 1.23.1 + heif_image_get_component_uint64@Base 1.23.1 + heif_image_get_component_uint64_readonly@Base 1.23.1 + heif_image_get_component_width@Base 1.23.1 heif_image_get_content_light_level@Base 1.15.1 heif_image_get_decoding_warnings@Base 1.13.0 + heif_image_get_duration@Base 1.20.0 + heif_image_get_gimi_sample_content_id@Base 1.20.0 heif_image_get_height@Base 1.0.0 heif_image_get_mastering_display_colour_volume@Base 1.15.1 heif_image_get_nclx_color_profile@Base 1.5.0 + heif_image_get_nominal_diffuse_white_luminance@Base 1.23.1 + heif_image_get_number_of_polarization_patterns@Base 1.23.1 + heif_image_get_number_of_sensor_bad_pixels_maps@Base 1.23.1 + heif_image_get_number_of_sensor_nucs@Base 1.23.1 + heif_image_get_number_of_used_components@Base 1.23.1 + heif_image_get_omaf_image_projection@Base 1.23.1 heif_image_get_pixel_aspect_ratio@Base 1.15.1 + heif_image_get_plane2@Base 1.20.0 heif_image_get_plane@Base 1.0.0 + heif_image_get_plane_readonly2@Base 1.20.0 heif_image_get_plane_readonly@Base 1.0.0 + heif_image_get_polarization_pattern_data@Base 1.23.1 + heif_image_get_polarization_pattern_index_for_component@Base 1.23.1 + heif_image_get_polarization_pattern_info@Base 1.23.1 heif_image_get_primary_height@Base 1.9.1 heif_image_get_primary_width@Base 1.9.1 heif_image_get_raw_color_profile@Base 1.5.0 heif_image_get_raw_color_profile_size@Base 1.5.0 + heif_image_get_sensor_bad_pixels_map_data@Base 1.23.1 + heif_image_get_sensor_bad_pixels_map_info@Base 1.23.1 + heif_image_get_sensor_nuc_data@Base 1.23.1 + heif_image_get_sensor_nuc_info@Base 1.23.1 + heif_image_get_tai_timestamp@Base 1.20.0 + heif_image_get_used_component_ids@Base 1.23.1 heif_image_get_width@Base 1.0.0 heif_image_handle_add_region_item@Base 1.16.2 + heif_image_handle_add_text_item@Base 1.21.2 heif_image_handle_decode_image_tile@Base 1.19.1 heif_image_handle_free_auxiliary_types@Base 1.11.0 + heif_image_handle_get_ambient_viewing_environment@Base 1.23.1 heif_image_handle_get_auxiliary_image_handle@Base 1.10.0 heif_image_handle_get_auxiliary_type@Base 1.10.0 heif_image_handle_get_camera_extrinsic_matrix@Base 1.18.1 heif_image_handle_get_camera_intrinsic_matrix@Base 1.18.1 heif_image_handle_get_chroma_bits_per_pixel@Base 1.4.0 + heif_image_handle_get_cmpd_component_type@Base 1.23.1 + heif_image_handle_get_cmpd_component_type_uri@Base 1.23.1 heif_image_handle_get_color_profile_type@Base 1.4.0 + heif_image_handle_get_component_bits_per_pixel@Base 1.23.1 + heif_image_handle_get_component_datatype@Base 1.23.1 + heif_image_handle_get_component_type@Base 1.23.1 heif_image_handle_get_content_light_level@Base 1.19.1 heif_image_handle_get_context@Base 1.17.0 heif_image_handle_get_depth_image_representation_info@Base 1.0.0 + heif_image_handle_get_gimi_component_content_id@Base 1.23.1 heif_image_handle_get_depth_image_handle@Base 1.0.0 + heif_image_handle_get_gimi_content_id@Base 1.21.2 heif_image_handle_get_grid_image_tile_id@Base 1.19.1 heif_image_handle_get_height@Base 1.0.0 heif_image_handle_get_image_tiling@Base 1.19.1 @@ -164,6 +254,7 @@ heif_image_handle_get_list_of_depth_image_IDs@Base 1.0.0 heif_image_handle_get_list_of_metadata_block_IDs@Base 1.0.0 heif_image_handle_get_list_of_region_item_ids@Base 1.16.2 + heif_image_handle_get_list_of_text_item_ids@Base 1.21.2 heif_image_handle_get_list_of_thumbnail_IDs@Base 1.0.0 heif_image_handle_get_luma_bits_per_pixel@Base 1.4.0 heif_image_handle_get_mastering_display_colour_volume@Base 1.19.1 @@ -173,37 +264,68 @@ heif_image_handle_get_metadata_size@Base 1.0.0 heif_image_handle_get_metadata_type@Base 1.0.0 heif_image_handle_get_nclx_color_profile@Base 1.4.0 + heif_image_handle_get_nominal_diffuse_white_luminance@Base 1.23.1 heif_image_handle_get_number_of_auxiliary_images@Base 1.10.0 + heif_image_handle_get_number_of_cmpd_components@Base 1.23.1 + heif_image_handle_get_number_of_components@Base 1.23.1 heif_image_handle_get_number_of_depth_images@Base 1.0.0 heif_image_handle_get_number_of_metadata_blocks@Base 1.0.0 heif_image_handle_get_number_of_region_items@Base 1.16.2 + heif_image_handle_get_number_of_text_items@Base 1.21.2 heif_image_handle_get_number_of_thumbnails@Base 1.0.0 + heif_image_handle_get_omaf_image_projection@Base 1.23.1 heif_image_handle_get_pixel_aspect_ratio@Base 1.19.1 heif_image_handle_get_preferred_decoding_colorspace@Base 1.17.0 heif_image_handle_get_raw_color_profile@Base 1.4.0 heif_image_handle_get_raw_color_profile_size@Base 1.4.0 heif_image_handle_get_thumbnail@Base 1.0.0 + heif_image_handle_get_used_component_ids@Base 1.23.1 heif_image_handle_get_width@Base 1.0.0 heif_image_handle_has_alpha_channel@Base 1.0.0 + heif_image_handle_has_ambient_viewing_environment@Base 1.23.1 heif_image_handle_has_camera_extrinsic_matrix@Base 1.18.1 heif_image_handle_has_camera_intrinsic_matrix@Base 1.18.1 + heif_image_handle_has_content_light_level@Base 1.23.1 heif_image_handle_has_depth_image@Base 1.0.0 + heif_image_handle_has_gimi_component_content_ids@Base 1.23.1 + heif_image_handle_has_mastering_display_colour_volume@Base 1.23.1 + heif_image_handle_has_nominal_diffuse_white_luminance@Base 1.23.1 heif_image_handle_is_premultiplied_alpha@Base 1.12.0 heif_image_handle_is_primary_image@Base 1.0.0 heif_image_handle_release@Base 1.0.0 heif_image_handle_release_auxiliary_type@Base 1.16.2 + heif_image_handle_set_ambient_viewing_environment@Base 1.23.1 + heif_image_handle_set_content_light_level@Base 1.21.2 + heif_image_handle_set_gimi_component_content_id@Base 1.23.1 + heif_image_handle_set_gimi_content_id@Base 1.23.1 + heif_image_handle_set_mastering_display_colour_volume@Base 1.21.2 + heif_image_handle_set_nominal_diffuse_white_luminance@Base 1.23.1 + heif_image_handle_set_omaf_image_projection@Base 1.23.1 + heif_image_handle_set_pixel_aspect_ratio@Base 1.21.2 + heif_image_has_ambient_viewing_environment@Base 1.23.1 heif_image_has_channel@Base 1.2.0 + heif_image_has_chroma_location@Base 1.23.1 heif_image_has_content_light_level@Base 1.15.1 heif_image_has_mastering_display_colour_volume@Base 1.15.1 + heif_image_has_nominal_diffuse_white_luminance@Base 1.23.1 heif_image_is_premultiplied_alpha@Base 1.12.0 heif_image_release@Base 1.0.0 heif_image_scale_image@Base 1.0.0 + heif_image_set_ambient_viewing_environment@Base 1.23.1 + heif_image_set_bayer_pattern@Base 1.23.1 + heif_image_set_chroma_location@Base 1.23.1 heif_image_set_content_light_level@Base 1.15.1 + heif_image_set_duration@Base 1.20.0 + heif_image_set_gimi_component_content_id@Base 1.23.1 + heif_image_set_gimi_sample_content_id@Base 1.20.0 heif_image_set_mastering_display_colour_volume@Base 1.15.1 heif_image_set_nclx_color_profile@Base 1.4.0 + heif_image_set_nominal_diffuse_white_luminance@Base 1.23.1 + heif_image_set_omaf_image_projection@Base 1.23.1 heif_image_set_pixel_aspect_ratio@Base 1.15.1 heif_image_set_premultiplied_alpha@Base 1.12.0 heif_image_set_raw_color_profile@Base 1.4.0 + heif_image_set_tai_timestamp@Base 1.20.0 heif_init@Base 1.13.0 heif_item_add_property_user_description@Base 1.16.2 heif_item_add_raw_property@Base 1.18.1 @@ -213,8 +335,11 @@ heif_item_get_mime_item_content_encoding@Base 1.19.1 heif_item_get_mime_item_content_type@Base 1.19.1 heif_item_get_properties_of_type@Base 1.16.2 + heif_item_get_property_extended_language@Base 1.21.2 heif_item_get_property_raw_data@Base 1.18.1 heif_item_get_property_raw_size@Base 1.18.1 + heif_item_get_property_tai_clock_info@Base 1.20.0 + heif_item_get_property_tai_timestamp@Base 1.20.0 heif_item_get_property_transform_crop_borders@Base 1.16.2 heif_item_get_property_transform_mirror@Base 1.16.2 heif_item_get_property_transform_rotation_ccw@Base 1.16.2 @@ -225,17 +350,36 @@ heif_item_get_uri_item_uri_type@Base 1.19.1 heif_item_is_item_hidden@Base 1.19.1 heif_item_set_item_name@Base 1.19.1 + heif_item_set_property_extended_language@Base 1.21.2 + heif_item_set_property_tai_clock_info@Base 1.20.0 + heif_item_set_property_tai_timestamp@Base 1.20.0 heif_list_compatible_brands@Base 1.11.0 heif_load_plugin@Base 1.14.0 heif_load_plugins@Base 1.14.0 heif_main_brand@Base 1.4.0 heif_mastering_display_colour_volume_decode@Base 1.15.1 + heif_metadata_compression_method_supported@Base 1.21.2 heif_nclx_color_profile_alloc@Base 1.9.1 heif_nclx_color_profile_free@Base 1.8.0 heif_nclx_color_profile_set_color_primaries@Base 1.13.0 heif_nclx_color_profile_set_matrix_coefficients@Base 1.13.0 heif_nclx_color_profile_set_transfer_characteristics@Base 1.13.0 + heif_orientation_concat@Base 1.23.1 + heif_polarization_angle_is_no_filter@Base 1.23.1 + heif_polarization_angle_no_filter@Base 1.23.1 heif_property_user_description_release@Base 1.16.2 + heif_raw_sequence_sample_alloc@Base 1.20.0 + heif_raw_sequence_sample_get_data@Base 1.20.0 + heif_raw_sequence_sample_get_data_size@Base 1.20.0 + heif_raw_sequence_sample_get_duration@Base 1.20.0 + heif_raw_sequence_sample_get_gimi_sample_content_id@Base 1.20.0 + heif_raw_sequence_sample_get_tai_timestamp@Base 1.20.0 + heif_raw_sequence_sample_has_tai_timestamp@Base 1.20.0 + heif_raw_sequence_sample_release@Base 1.20.0 + heif_raw_sequence_sample_set_data@Base 1.20.0 + heif_raw_sequence_sample_set_duration@Base 1.20.0 + heif_raw_sequence_sample_set_gimi_sample_content_id@Base 1.20.0 + heif_raw_sequence_sample_set_tai_timestamp@Base 1.20.0 heif_read_main_brand@Base 1.11.0 heif_read_minor_version_brand@Base 1.19.1 heif_region_get_ellipse@Base 1.16.2 @@ -275,4 +419,55 @@ heif_register_encoder_plugin@Base 1.1.0 heif_release_item_data@Base 1.18.1 heif_release_item_references@Base 1.18.1 + heif_sequence_encoding_options_alloc@Base 1.20.0 + heif_sequence_encoding_options_copy@Base 1.23.1 + heif_sequence_encoding_options_release@Base 1.20.0 + heif_string_release@Base 1.20.0 + heif_tai_clock_info_alloc@Base 1.20.0 + heif_tai_clock_info_copy@Base 1.20.0 + heif_tai_clock_info_release@Base 1.20.0 + heif_tai_timestamp_packet_alloc@Base 1.20.0 + heif_tai_timestamp_packet_copy@Base 1.20.0 + heif_tai_timestamp_packet_release@Base 1.20.0 + heif_text_item_get_content@Base 1.21.2 + heif_text_item_get_id@Base 1.21.2 + heif_text_item_get_property_extended_language@Base 1.21.2 + heif_text_item_release@Base 1.21.2 + heif_text_item_set_extended_language@Base 1.21.2 + heif_track_add_raw_sequence_sample@Base 1.20.0 + heif_track_add_reference_to_track@Base 1.20.0 + heif_track_decode_next_image@Base 1.20.0 + heif_track_encode_end_of_sequence@Base 1.21.2 + heif_track_encode_sequence_image@Base 1.20.0 + heif_track_find_referring_tracks@Base 1.20.0 + heif_track_get_auxiliary_info_type@Base 1.21.2 + heif_track_get_auxiliary_info_type_urn@Base 1.21.2 + heif_track_get_gimi_track_content_id@Base 1.20.0 + heif_track_get_id@Base 1.20.0 + heif_track_get_image_resolution@Base 1.20.0 + heif_track_get_next_raw_sequence_sample@Base 1.20.0 + heif_track_get_number_of_repetitions@Base 1.23.1 + heif_track_get_number_of_sample_aux_infos@Base 1.20.0 + heif_track_get_number_of_track_reference_of_type@Base 1.20.0 + heif_track_get_number_of_track_reference_types@Base 1.20.0 + heif_track_get_references_from_track@Base 1.20.0 + heif_track_get_sample_aux_info_types@Base 1.20.0 + heif_track_get_sample_entry_type_of_first_cluster@Base 1.20.0 + heif_track_get_tai_clock_info_of_first_cluster@Base 1.20.0 + heif_track_get_timescale@Base 1.20.0 + heif_track_get_track_handler_type@Base 1.20.0 + heif_track_get_track_reference_types@Base 1.20.0 + heif_track_get_urim_sample_entry_uri_of_first_cluster@Base 1.20.0 + heif_track_has_alpha_channel@Base 1.21.2 + heif_track_options_alloc@Base 1.20.0 + heif_track_options_enable_sample_gimi_content_ids@Base 1.20.0 + heif_track_options_enable_sample_tai_timestamps@Base 1.20.0 + heif_track_options_release@Base 1.20.0 + heif_track_options_set_gimi_track_id@Base 1.20.0 + heif_track_options_set_interleaved_sample_aux_infos@Base 1.20.0 + heif_track_options_set_timescale@Base 1.20.0 + heif_track_release@Base 1.20.0 + heif_unci_image_parameters_alloc@Base 1.20.0 + heif_unci_image_parameters_copy@Base 1.20.0 + heif_unci_image_parameters_release@Base 1.20.0 heif_unload_plugin@Base 1.14.0 diff -Nru libheif-1.19.8/debian/patches/CVE-2025-68431.patch libheif-1.23.4/debian/patches/CVE-2025-68431.patch --- libheif-1.19.8/debian/patches/CVE-2025-68431.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2025-68431.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -Description: CVE-2025-68431: fix wrong copy width in HeifPixelImage::overlay() - The non-alpha memcpy path subtracted in_x0 from the copy length a second - time, although in_x0 has already been subtracted from in_w when the - overlay was clipped against the left border. Upstream reports this as a - heap buffer over-read (the subtraction can underflow to a huge length). -Origin: upstream, https://github.com/strukturag/libheif/commit/b8c12a7b70f46c9516711a988483bed377b78d46 -Last-Update: 2026-08-06 - -diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc -index 7425ea11..960ec99d 100644 ---- a/libheif/pixelimage.cc -+++ b/libheif/pixelimage.cc -@@ -1324,7 +1324,7 @@ Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t - if (!has_alpha) { - memcpy(out_p + out_x0 + (out_y0 + y - in_y0) * out_stride, - in_p + in_x0 + y * in_stride, -- in_w - in_x0); -+ in_w); - } - else { - for (uint32_t x = in_x0; x < in_w; x++) { diff -Nru libheif-1.19.8/debian/patches/CVE-2026-32740.patch libheif-1.23.4/debian/patches/CVE-2026-32740.patch --- libheif-1.19.8/debian/patches/CVE-2026-32740.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-32740.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,59 +0,0 @@ -Description: CVE-2026-32740: fix heap buffer overflow from chroma rounding mismatch in tile/area copy - copy_image_to() and extract_image_area() computed the copy size by - rounding (w - x0) / (h - y0) into chroma channel dimensions, which - for 4:2:0 subsampled planes and odd offsets/sizes can round up to a - larger value than channel_width(w)/channel_height(h) minus the - already-rounded offset xs/ys, causing the copy to run past the end - of the destination (copy_image_to) or source (extract_image_area) - plane. Clamp the copy size to the actual plane bounds instead. -Origin: upstream, https://github.com/strukturag/libheif/commit/23903961e1237ecdb0779a65b423cb75f524d254 -Last-Update: 2026-08-06 - -diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc -index c984bd69..09ac476c 100644 ---- a/libheif/pixelimage.cc -+++ b/libheif/pixelimage.cc -@@ -776,13 +776,16 @@ Error HeifPixelImage::copy_image_to(const std::shared_ptr& - uint32_t src_width = source->get_width(channel); - uint32_t src_height = source->get_height(channel); - -- uint32_t copy_width = std::min(src_width, channel_width(w - x0, chroma, channel)); -- uint32_t copy_height = std::min(src_height, channel_height(h - y0, chroma, channel)); -- -- copy_width *= source->get_storage_bits_per_pixel(channel) / 8; -- - uint32_t xs = channel_width(x0, chroma, channel); - uint32_t ys = channel_height(y0, chroma, channel); -+ -+ // Compute copy size from actual plane bounds to avoid chroma rounding mismatch. -+ // channel_height(y0) + channel_height(h - y0) can exceed channel_height(h) with 4:2:0 -+ // due to ceiling division, so we use (plane_size - offset) instead. -+ uint32_t copy_width = std::min(src_width, channel_width(w, chroma, channel) - xs); -+ uint32_t copy_height = std::min(src_height, channel_height(h, chroma, channel) - ys); -+ -+ copy_width *= source->get_storage_bits_per_pixel(channel) / 8; - xs *= source->get_storage_bits_per_pixel(channel) / 8; - - for (uint32_t py = 0; py < copy_height; py++) { -@@ -1595,13 +1598,16 @@ HeifPixelImage::extract_image_area(uint32_t x0, uint32_t y0, uint32_t w, uint32_ - }; - } - -- uint32_t copy_width = channel_width(minW, chroma, channel); -- uint32_t copy_height = channel_height(minH, chroma, channel); -- -- copy_width *= get_storage_bits_per_pixel(channel) / 8; -- - uint32_t xs = channel_width(x0, chroma, channel); - uint32_t ys = channel_height(y0, chroma, channel); -+ -+ // Clamp copy size to source plane bounds to avoid chroma rounding mismatch OOB read. -+ uint32_t src_plane_h = channel_height(get_height(), chroma, channel); -+ uint32_t src_plane_w = channel_width(get_width(), chroma, channel); -+ uint32_t copy_width = std::min(channel_width(minW, chroma, channel), src_plane_w - xs); -+ uint32_t copy_height = std::min(channel_height(minH, chroma, channel), src_plane_h - ys); -+ -+ copy_width *= get_storage_bits_per_pixel(channel) / 8; - xs *= get_storage_bits_per_pixel(channel) / 8; - - for (uint32_t py = 0; py < copy_height; py++) { diff -Nru libheif-1.19.8/debian/patches/CVE-2026-32741.patch libheif-1.23.4/debian/patches/CVE-2026-32741.patch --- libheif-1.19.8/debian/patches/CVE-2026-32741.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-32741.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -Description: CVE-2026-32741: fix heap buffer overflow in decode_mask_image() - The fast-path memcpy used data.size() (the size of the encoded input - buffer) as the copy length instead of width * height (the size of - the destination plane), and the stride comparison had a truncating - cast that could mask a mismatch. If the compressed payload is larger - than the destination plane, this overflows the plane buffer. -Origin: upstream, https://github.com/strukturag/libheif/commit/123694271ac02f2de68a3ccdc5d483eb8a2ae593 -Last-Update: 2026-08-06 - -diff --git a/libheif/image-items/mask_image.cc b/libheif/image-items/mask_image.cc -index 3670cc3a..e99a5916 100644 ---- a/libheif/image-items/mask_image.cc -+++ b/libheif/image-items/mask_image.cc -@@ -113,8 +113,8 @@ Error MaskImageCodec::decode_mask_image(const HeifContext* context, - - size_t stride; - uint8_t* dst = img->get_plane(heif_channel_Y, &stride); -- if (((uint32_t)stride) == width) { -- memcpy(dst, data.data(), data.size()); -+ if (stride == static_cast(width)) { -+ memcpy(dst, data.data(), static_cast(width) * height); - } - else - { diff -Nru libheif-1.19.8/debian/patches/CVE-2026-32882.patch libheif-1.23.4/debian/patches/CVE-2026-32882.patch --- libheif-1.19.8/debian/patches/CVE-2026-32882.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-32882.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -Description: CVE-2026-32882: fix overlay compositing OOB read when alpha plane stride differs from color plane stride - The alpha-blending path in HeifPixelImage::overlay() indexed the - alpha plane using the color channel's stride (in_stride) instead of - the alpha plane's own stride, causing an out-of-bounds read whenever - the two planes are not identically strided. -Origin: upstream, https://github.com/strukturag/libheif/commit/22b6266bf03a9f016f84a9b57b80e1dd7c79d64a -Last-Update: 2026-08-06 - -diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc -index 960ec99d..ed314740 100644 ---- a/libheif/pixelimage.cc -+++ b/libheif/pixelimage.cc -@@ -1330,7 +1330,7 @@ Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t - for (uint32_t x = in_x0; x < in_w; x++) { - uint8_t* outptr = &out_p[out_x0 + (out_y0 + y - in_y0) * out_stride + x]; - uint8_t in_val = in_p[in_x0 + y * in_stride + x]; -- uint8_t alpha_val = alpha_p[in_x0 + y * in_stride + x]; -+ uint8_t alpha_val = alpha_p[in_x0 + y * alpha_stride + x]; - - *outptr = (uint8_t) ((in_val * alpha_val + *outptr * (255 - alpha_val)) / 255); - } diff -Nru libheif-1.19.8/debian/patches/CVE-2026-47178.patch libheif-1.23.4/debian/patches/CVE-2026-47178.patch --- libheif-1.19.8/debian/patches/CVE-2026-47178.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-47178.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,60 +0,0 @@ -Description: CVE-2026-47178: reject uncompressed configurations whose decoders write out of bounds - This is not a port of the upstream arithmetic fix but a mitigation. It - rejects the affected files at format-validation time because the upstream - fix does not fit naturally into the five decoder classes in current version. -Forwarded: not-needed -Author: Aron Xu -Last-Update: 2026-08-06 - -diff --git a/libheif/codecs/uncompressed/unc_codec.cc b/libheif/codecs/uncompressed/unc_codec.cc -index b3ec1fbb..c07a6d81 100644 ---- a/libheif/codecs/uncompressed/unc_codec.cc -+++ b/libheif/codecs/uncompressed/unc_codec.cc -@@ -194,6 +194,32 @@ static Error uncompressed_image_type_is_supported(const std::shared_ptrget_sampling_type() != sampling_mode_no_subsampling -+ && (uncC->get_interleave_type() == interleave_mode_row -+ || uncC->get_interleave_type() == interleave_mode_pixel)) { -+ return Error(heif_error_Unsupported_feature, -+ heif_suberror_Unsupported_data_version, -+ "Chroma subsampling is not supported with row or pixel interleave"); -+ } -+ -+ if ((uncC->get_sampling_type() == sampling_mode_420 || uncC->get_sampling_type() == sampling_mode_422) -+ && (uncC->get_number_of_tile_columns() > 1 || uncC->get_number_of_tile_rows() > 1) -+ && (uncC->get_interleave_type() == interleave_mode_component -+ || uncC->get_interleave_type() == interleave_mode_mixed -+ || uncC->get_interleave_type() == interleave_mode_tile_component)) { -+ return Error(heif_error_Unsupported_feature, -+ heif_suberror_Unsupported_data_version, -+ "Tiled YCbCr 4:2:0/4:2:2 uncompressed images with component, mixed, or tile-component interleave are not supported"); -+ } -+ - if (uncC->get_block_size() != 0) { - std::stringstream sstr; - sstr << "Uncompressed block_size of " << ((int) uncC->get_block_size()) << " is not implemented yet"; -@@ -530,6 +556,14 @@ Error UncompressedImageCodec::decode_uncompressed_image_tile(const HeifContext* - return error; - } - -+ // Same check as in decode_uncompressed_image(). Without it, the per-tile -+ // decode API would bypass every uncC/cmpd configuration check, including the -+ // ones above that reject configurations whose decoders write out of bounds. -+ error = uncompressed_image_type_is_supported(uncC, cmpd); -+ if (error) { -+ return error; -+ } -+ - uint32_t tile_width = ispe->get_width() / uncC->get_number_of_tile_columns(); - uint32_t tile_height = ispe->get_height() / uncC->get_number_of_tile_rows(); - diff -Nru libheif-1.19.8/debian/patches/CVE-2026-47247.patch libheif-1.23.4/debian/patches/CVE-2026-47247.patch --- libheif-1.19.8/debian/patches/CVE-2026-47247.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-47247.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,51 +0,0 @@ -Description: CVE-2026-47247: fix grid tile coverage check and zero-initialize pixel plane allocation - (a) ImageItem_Grid::decode_full_grid_image() checked tile coverage - with integer division (src_width < grid_width / columns), which - under-detects gaps: e.g. 9 tiles of 11px covering a 107px-wide grid - pass the check (107/9 == 11) even though 9*11 = 99 < 107, leaving an - 8-pixel-wide strip of the canvas never written by any tile. Compare - with a multiplication instead so any gap is rejected. - . - (b) HeifPixelImage::ImagePlane::alloc() used a plain `new uint8_t[]`, - leaving stride padding and alignment slack uninitialized; combined - with (a), a grid gap left uninitialized heap bytes in the padding - region readable via the decoded image, leaking heap contents. Zero- - initialize the allocation so no uninitialized memory can be exposed - through any codepath that doesn't fully overwrite the plane - (including the grid gap from (a), and other partial-write paths). -Origin: upstream, https://github.com/strukturag/libheif/commit/6aca89d76a5118bd47adcb5b83e7b01a32134985 -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 - -diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc -index 7243f93b..565c70be 100644 ---- a/libheif/image-items/grid.cc -+++ b/libheif/image-items/grid.cc -@@ -313,8 +313,10 @@ Result> ImageItem_Grid::decode_full_grid_image(c - return err; - } - -- if (src_width < grid.get_width() / grid.get_columns() || -- src_height < grid.get_height() / grid.get_rows()) { -+ // Integer division would let e.g. 9 tiles of 11px each "cover" a 107px canvas -+ // (107/9 == 11), leaving an 8-pixel gap inside the visible image area. -+ if (static_cast(src_width) * grid.get_columns() < grid.get_width() || -+ static_cast(src_height) * grid.get_rows() < grid.get_height()) { - return Error{heif_error_Invalid_input, - heif_suberror_Invalid_grid_data, - "Grid tiles do not cover whole image"}; -diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc -index 09ac476c..df25cefd 100644 ---- a/libheif/pixelimage.cc -+++ b/libheif/pixelimage.cc -@@ -276,7 +276,9 @@ Error HeifPixelImage::ImagePlane::alloc(uint32_t width, uint32_t height, heif_ch - } - - try { -- allocated_mem = new uint8_t[static_cast(m_mem_height) * stride + alignment - 1]; -+ // Must zero-initialize: padding regions (stride, alignment slack) are not written by -+ // decoders, so uninitialized contents would leak across decoded images. -+ allocated_mem = new uint8_t[static_cast(m_mem_height) * stride + alignment - 1](); - uint8_t* mem_8 = allocated_mem; - - // shift beginning of image data to aligned memory position diff -Nru libheif-1.19.8/debian/patches/CVE-2026-47709-1_uncC-tile-count-overflow.patch libheif-1.23.4/debian/patches/CVE-2026-47709-1_uncC-tile-count-overflow.patch --- libheif-1.19.8/debian/patches/CVE-2026-47709-1_uncC-tile-count-overflow.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-47709-1_uncC-tile-count-overflow.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ -Description: CVE-2026-47709: integer overflow when parsing uncC tile counts - num_tile_cols_minus_one / num_tile_rows_minus_one are stored as (count - 1); - a stored value of 0xFFFFFFFF represents 2^32 tiles, which cannot be - represented in the uint32_t m_num_tile_cols/m_num_tile_rows fields and - overflows the security-limit division below it. Reject this value - unconditionally before it reaches the (otherwise policy-only, - user-disableable) max_number_of_tiles check. -Origin: upstream, https://github.com/strukturag/libheif/commit/f36bd8cddd0883dbe2d793f48f62d7d5be2ad678 -Last-Update: 2026-08-06 - ---- a/libheif/codecs/uncompressed/unc_boxes.cc -+++ b/libheif/codecs/uncompressed/unc_boxes.cc -@@ -301,6 +301,16 @@ Error Box_uncC::parse(BitstreamRange& range, const heif_security_limits* limits) - uint32_t num_tile_cols_minus_one = range.read32(); - uint32_t num_tile_rows_minus_one = range.read32(); - -+ // The field is stored as `count - 1`, so 0xFFFFFFFF would mean 2^32 tiles, -+ // which we cannot represent in our uint32 m_num_tile_cols/rows. Reject this -+ // unconditionally; the security-limit check below is policy and may be -+ // disabled by the user, but this representation limit must always hold. -+ if (num_tile_cols_minus_one == 0xFFFFFFFF || num_tile_rows_minus_one == 0xFFFFFFFF) { -+ return {heif_error_Unsupported_feature, -+ heif_suberror_Invalid_parameter_value, -+ "uncC num_tile_cols/rows_minus_one of 0xFFFFFFFF (2^32 tiles) exceeds the supported range"}; -+ } -+ - if (limits->max_number_of_tiles && - static_cast(num_tile_cols_minus_one) + 1 > limits->max_number_of_tiles / (static_cast(num_tile_rows_minus_one) + 1)) { - std::stringstream sstr; diff -Nru libheif-1.19.8/debian/patches/CVE-2026-47709-2_missing-ispe-null-deref.patch libheif-1.23.4/debian/patches/CVE-2026-47709-2_missing-ispe-null-deref.patch --- libheif-1.19.8/debian/patches/CVE-2026-47709-2_missing-ispe-null-deref.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-47709-2_missing-ispe-null-deref.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -Description: CVE-2026-47709: reject unci images with no ispe box - Without an 'ispe' box, get_heif_image_tiling() and get_tile_size() would - later dereference a null Box_ispe pointer (get_heif_image_tiling() only - has an assert(), which is compiled out in release/NDEBUG builds). Reject - such images while loading the item, mirroring the existing "No 'uncC' box - found" check just above it. -Origin: upstream, https://github.com/strukturag/libheif/commit/294d9c09db838aba9a615a6b6aba4c7019b7bbfd -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 -Comment: Upstream's fix targets ImageItem_uncompressed::initialize_decoder(), - which does not exist at 1.19.8, this patch adds the equivalent check - directly in on_load_file(). - ---- a/libheif/image-items/unc_image.cc -+++ b/libheif/image-items/unc_image.cc -@@ -514,6 +514,7 @@ Error ImageItem_uncompressed::on_load_file() - { - std::shared_ptr cmpd = get_property(); - std::shared_ptr uncC = get_property(); -+ std::shared_ptr ispe = get_property(); - - if (!uncC) { - return Error{heif_error_Invalid_input, -@@ -521,6 +522,12 @@ Error ImageItem_uncompressed::on_load_file() - "No 'uncC' box found."}; - } - -+ if (!ispe) { -+ return Error{heif_error_Invalid_input, -+ heif_suberror_Unspecified, -+ "No 'ispe' box found for uncompressed image item."}; -+ } -+ - m_decoder = std::make_shared(uncC, cmpd); - - DataExtent extent; diff -Nru libheif-1.19.8/debian/patches/CVE-2026-47714.patch libheif-1.23.4/debian/patches/CVE-2026-47714.patch --- libheif-1.19.8/debian/patches/CVE-2026-47714.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-47714.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,222 +0,0 @@ -Description: CVE-2026-47714: check inline region mask allocation against integer overflow and security limits - Box_iref-referenced region items (rgan) with an inline mask geometry (heif_region_type_inline_mask) - computed the mask buffer size as "width * height / 8" using 32-bit unsigned arithmetic with no - overflow or zero-size check before resizing/copying into mask_data, allowing a crafted region item - to trigger an integer overflow (undersized allocation followed by an out-of-bounds copy) or a - divide-by-zero-adjacent zero-size mask. Ported from upstream's fix, which introduced a MemoryHandle - allocation-tracking class that does not exist in this version (see Comment below): plumb the - heif_security_limits pointer through RegionItem::parse()/RegionGeometry::parse() down to - RegionGeometry_InlineMask::parse(), reject width==0 || height==0, compute the mask size with 64-bit - arithmetic as (uint64_t(width) * height + 7) / 8, and reject sizes exceeding - limits->max_memory_block_size before touching mask_data. -Origin: upstream, https://github.com/strukturag/libheif/commit/e561521f2382cf8f49b581a6044882390b8cc7a5 - , https://github.com/strukturag/libheif/commit/b1bd1c000e596f09ecd7bae87f95b88c21a6dc4a - , https://github.com/strukturag/libheif/commit/271d6ca590c7e1f00cd2fd0414d91b68f83dd4b6 -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 - ---- a/libheif/context.cc -+++ b/libheif/context.cc -@@ -870,7 +870,7 @@ Error HeifContext::interpret_heif_file() - if (err) { - return err; - } -- region_item->parse(region_data); -+ region_item->parse(region_data, get_security_limits()); - if (iref_box) { - std::vector references = iref_box->get_references_from(id); - for (const auto& ref : references) { -diff --git a/libheif/region.cc b/libheif/region.cc -index afbf7c37..eb25a4b8 100644 ---- a/libheif/region.cc -+++ b/libheif/region.cc -@@ -27,7 +27,7 @@ - #include - - --Error RegionItem::parse(const std::vector& data) -+Error RegionItem::parse(const std::vector& data, const heif_security_limits* limits) - { - if (data.size() < 8) { - return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, -@@ -105,7 +105,7 @@ Error RegionItem::parse(const std::vector& data) - continue; - } - -- Error error = region->parse(data, field_size, &dataOffset); -+ Error error = region->parse(data, field_size, &dataOffset, limits); - if (error) { - return error; - } -@@ -202,7 +202,8 @@ int32_t RegionGeometry::parse_signed(const std::vector& data, - - Error RegionGeometry_Point::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - unsigned int bytesRequired = (field_size / 8) * 2; - if (data.size() - *dataOffset < bytesRequired) { -@@ -243,7 +244,8 @@ void RegionGeometry_Point::encode(StreamWriter& writer, int field_size_bytes) co - - Error RegionGeometry_Rectangle::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - unsigned int bytesRequired = (field_size / 8) * 4; - if (data.size() - *dataOffset < bytesRequired) { -@@ -275,7 +277,8 @@ void RegionGeometry_Rectangle::encode(StreamWriter& writer, int field_size_bytes - - Error RegionGeometry_Ellipse::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - unsigned int bytesRequired = (field_size / 8) * 4; - if (data.size() - *dataOffset < bytesRequired) { -@@ -308,7 +311,8 @@ void RegionGeometry_Ellipse::encode(StreamWriter& writer, int field_size_bytes) - - Error RegionGeometry_Polygon::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - uint32_t bytesRequired1 = (field_size / 8) * 1; - if (data.size() - *dataOffset < bytesRequired1) { -@@ -339,7 +343,8 @@ Error RegionGeometry_Polygon::parse(const std::vector& data, - - Error RegionGeometry_ReferencedMask::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - unsigned int bytesRequired = (field_size / 8) * 4; - if (data.size() - *dataOffset < bytesRequired) { -@@ -394,7 +399,8 @@ void RegionGeometry_Polygon::encode(StreamWriter& writer, int field_size_bytes) - - Error RegionGeometry_InlineMask::parse(const std::vector& data, - int field_size, -- unsigned int* dataOffset) -+ unsigned int* dataOffset, -+ const heif_security_limits* limits) - { - unsigned int bytesRequired = (field_size / 8) * 4 + 1; - if (data.size() - *dataOffset < bytesRequired) { -@@ -411,13 +417,24 @@ Error RegionGeometry_InlineMask::parse(const std::vector& data, - return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, - "Deflate compressed inline mask is not yet supported"); - } -- unsigned int additionalBytesRequired = width * height / 8; -- if (data.size() - *dataOffset < additionalBytesRequired) { -+ -+ if (width == 0 || height == 0) { -+ return Error(heif_error_Invalid_input, heif_suberror_Unspecified, -+ "Zero size mask image."); -+ } -+ -+ uint64_t bytesForMask = (uint64_t(width) * height + 7) / 8; -+ if (limits->max_memory_block_size && bytesForMask > limits->max_memory_block_size) { -+ return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, -+ "Inline mask exceeds maximum memory block size."); -+ } -+ -+ if (data.size() - *dataOffset < bytesForMask) { - return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, - "Insufficient data remaining for inline mask region data[]"); - } -- mask_data.resize(additionalBytesRequired); -- std::copy(data.begin() + *dataOffset, data.begin() + *dataOffset + additionalBytesRequired, mask_data.begin()); -+ mask_data.resize(bytesForMask); -+ std::copy(data.begin() + *dataOffset, data.begin() + *dataOffset + static_cast(bytesForMask), mask_data.begin()); - return Error::Ok; - } - -diff --git a/libheif/region.h b/libheif/region.h -index 7509c656..e29d747e 100644 ---- a/libheif/region.h -+++ b/libheif/region.h -@@ -38,7 +38,7 @@ public: - RegionItem(heif_item_id itemId, uint32_t ref_width, uint32_t ref_height) - : item_id(itemId), reference_width(ref_width), reference_height(ref_height) {} - -- Error parse(const std::vector& data); -+ Error parse(const std::vector& data, const heif_security_limits* limits); - - Error encode(std::vector& result) const; - -@@ -67,7 +67,8 @@ public: - - virtual heif_region_type getRegionType() = 0; - -- virtual Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) = 0; -+ virtual Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) = 0; - - virtual bool encode_needs_32bit() const { return false; } - -@@ -82,7 +83,8 @@ protected: - class RegionGeometry_Point : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - bool encode_needs_32bit() const override; - -@@ -96,7 +98,8 @@ public: - class RegionGeometry_Rectangle : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - bool encode_needs_32bit() const override; - -@@ -111,7 +114,8 @@ public: - class RegionGeometry_Ellipse : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - bool encode_needs_32bit() const override; - -@@ -126,7 +130,8 @@ public: - class RegionGeometry_Polygon : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - bool encode_needs_32bit() const override; - -@@ -149,7 +154,8 @@ public: - class RegionGeometry_ReferencedMask : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int *dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - void encode(StreamWriter&, int field_size_bytes) const override; - -@@ -163,7 +169,8 @@ public: - class RegionGeometry_InlineMask : public RegionGeometry - { - public: -- Error parse(const std::vector& data, int field_size, unsigned int *dataOffset) override; -+ Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, -+ const heif_security_limits* limits) override; - - void encode(StreamWriter&, int field_size_bytes) const override; - diff -Nru libheif-1.19.8/debian/patches/CVE-2026-48029.patch libheif-1.23.4/debian/patches/CVE-2026-48029.patch --- libheif-1.19.8/debian/patches/CVE-2026-48029.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-48029.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,120 +0,0 @@ -Description: CVE-2026-48029: fix tile coordinates validation in rotated grid images - ImageItem::transform_requested_tile_position_to_original_tile_position() validated the - caller-supplied (tile_x, tile_y) against the wrong (in-file) tile-grid dimensions, and - heif_image_handle_get_grid_image_tile_id() performed its own bounds check against the - in-file grid before applying the transform at all. For a grid image with a 90/270 degree - irot rotation, the displayed tile grid has its columns and rows swapped relative to the - file; a tile coordinate that was in-range for the file's grid but out-of-range for the - displayed grid was accepted, and the inverse-rotation arithmetic (e.g. - `num_rows - 1 - tile_x` with `tile_x >= num_rows`) underflowed, leading to an - out-of-bounds read via the grid tile array. Validate the requested tile position against - the displayed dimensions (via process_image_transformations_on_tiling()) before doing any - arithmetic, track the current tile-grid extent through the reversed property walk so each - inverse rotation uses the correct (possibly already-swapped) dimensions, and call the - transform before the bounds check in heif_image_handle_get_grid_image_tile_id() so its - Error is propagated instead of being bypassed. -Origin: upstream, https://github.com/strukturag/libheif/commit/e523ec0bf379110b7c33d4c159f8b1202d332157 -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 - ---- a/libheif/api/libheif/heif.cc -+++ b/libheif/api/libheif/heif.cc -@@ -976,16 +976,19 @@ struct heif_error heif_image_handle_get_grid_image_tile_id(const struct heif_ima - } - - const ImageGrid& gridspec = gridItem->get_grid_spec(); -- if (tile_x >= gridspec.get_columns() || tile_y >= gridspec.get_rows()) { -+ -+ if (process_image_transformations) { -+ Error err = gridItem->transform_requested_tile_position_to_original_tile_position(tile_x, tile_y); -+ if (err) { -+ return err.error_struct(handle->context.get()); -+ } -+ } -+ else if (tile_x >= gridspec.get_columns() || tile_y >= gridspec.get_rows()) { - return { heif_error_Usage_error, - heif_suberror_Unspecified, - "Grid tile index out of range" }; - } - -- if (process_image_transformations) { -- gridItem->transform_requested_tile_position_to_original_tile_position(tile_x, tile_y); -- } -- - *tile_item_id = gridItem->get_grid_tiles()[tile_y * gridspec.get_columns() + tile_x]; - - return heif_error_ok; -diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc -index 77ae43ed..cbcd5519 100644 ---- a/libheif/image-items/image_item.cc -+++ b/libheif/image-items/image_item.cc -@@ -716,29 +716,50 @@ Error ImageItem::transform_requested_tile_position_to_original_tile_position(uin - return propertiesResult.error; - } - -+ // The caller's (tile_x, tile_y) are in the *displayed* tile grid, so they -+ // must be validated against the displayed dimensions, not the in-file ones. -+ // For rotations of 90/270 degrees the displayed grid has its columns and -+ // rows swapped relative to the file. - heif_image_tiling tiling = get_heif_image_tiling(); -+ if (Error err = process_image_transformations_on_tiling(tiling)) { -+ return err; -+ } -+ -+ if (tile_x >= tiling.num_columns || tile_y >= tiling.num_rows) { -+ return {heif_error_Usage_error, -+ heif_suberror_Unspecified, -+ "Tile coordinate out of range for displayed image"}; -+ } -+ -+ // Walk the property chain in reverse, undoing each transformation as we go. -+ // Track the current (intermediate) tile-grid dimensions so each inverse uses -+ // the right extent and so 90/270 degree rotations swap dims for subsequent steps. -+ uint32_t cur_cols = tiling.num_columns; -+ uint32_t cur_rows = tiling.num_rows; - - //for (auto& prop : std::ranges::reverse_view(propertiesResult.value)) { - for (auto propIter = propertiesResult.value.rbegin(); propIter != propertiesResult.value.rend(); propIter++) { - if (auto irot = std::dynamic_pointer_cast(*propIter)) { - switch (irot->get_rotation_ccw()) { - case 90: { -- uint32_t tx0 = tiling.num_columns - 1 - tile_y; -+ uint32_t tx0 = cur_rows - 1 - tile_y; - uint32_t ty0 = tile_x; -- tile_y = ty0; - tile_x = tx0; -+ tile_y = ty0; -+ std::swap(cur_cols, cur_rows); - break; - } - case 270: { - uint32_t tx0 = tile_y; -- uint32_t ty0 = tiling.num_rows - 1 - tile_x; -- tile_y = ty0; -+ uint32_t ty0 = cur_cols - 1 - tile_x; - tile_x = tx0; -+ tile_y = ty0; -+ std::swap(cur_cols, cur_rows); - break; - } - case 180: { -- tile_x = tiling.num_columns - 1 - tile_x; -- tile_y = tiling.num_rows - 1 - tile_y; -+ tile_x = cur_cols - 1 - tile_x; -+ tile_y = cur_rows - 1 - tile_y; - break; - } - case 0: -@@ -752,10 +773,10 @@ Error ImageItem::transform_requested_tile_position_to_original_tile_position(uin - if (auto imir = std::dynamic_pointer_cast(*propIter)) { - switch (imir->get_mirror_direction()) { - case heif_transform_mirror_direction_horizontal: -- tile_x = tiling.num_columns - 1 - tile_x; -+ tile_x = cur_cols - 1 - tile_x; - break; - case heif_transform_mirror_direction_vertical: -- tile_y = tiling.num_rows - 1 - tile_y; -+ tile_y = cur_rows - 1 - tile_y; - break; - default: - assert(false); diff -Nru libheif-1.19.8/debian/patches/CVE-2026-49271.patch libheif-1.23.4/debian/patches/CVE-2026-49271.patch --- libheif-1.19.8/debian/patches/CVE-2026-49271.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-49271.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -Description: CVE-2026-49271: prevent integer overflow when reading unci compressed data units - icef per-unit unit_offset/unit_size values (attacker-controlled) were used - directly to construct iterators into compressed_bytes with no bounds - check, allowing an out-of-bounds read. Add a subtraction-form bounds - check (which avoids a uint64_t wraparound that an addition-form check - would be vulnerable to) before constructing the iterators. -Origin: upstream, https://github.com/strukturag/libheif/commit/5782bca04a70ebc01c59397205a3cfff22841311 -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 - ---- a/libheif/codecs/uncompressed/decoder_abstract.cc -+++ b/libheif/codecs/uncompressed/decoder_abstract.cc -@@ -219,6 +219,13 @@ const Error AbstractDecoder::get_compressed_image_data_uncompressed(const HeifCo - } - - for (Box_icef::CompressedUnitInfo unit_info : icef_box->get_units()) { -+ if (unit_info.unit_offset > compressed_bytes.size() || -+ unit_info.unit_size > compressed_bytes.size() - unit_info.unit_offset) { -+ return {heif_error_Invalid_input, -+ heif_suberror_Unspecified, -+ "compressed data range out of bounds"}; -+ } -+ - auto unit_start = compressed_bytes.begin() + unit_info.unit_offset; - auto unit_end = unit_start + unit_info.unit_size; - std::vector compressed_unit_data = std::vector(unit_start, unit_end); diff -Nru libheif-1.19.8/debian/patches/CVE-2026-62289-clap-zero-size-tiling-underflow.patch libheif-1.23.4/debian/patches/CVE-2026-62289-clap-zero-size-tiling-underflow.patch --- libheif-1.19.8/debian/patches/CVE-2026-62289-clap-zero-size-tiling-underflow.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-62289-clap-zero-size-tiling-underflow.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -Description: CVE-2026-62289: fix clap transform double-application in image tiling (GHSA-jc8f-p23p-5hjg) - The base ImageItem::get_heif_image_tiling() returned the already - clap/irot/imir-transformed m_width/m_height, but - process_image_transformations_on_tiling() applies those same transformative - properties itself, so they were applied twice. For a 'clap' (clean - aperture) box that rounds a dimension down to zero, the second application - passed image_width==0 into Box_clap::left_rounded()/top_rounded(), where - `image_width - 1U` underflows to UINT32_MAX and violates the - Fraction(uint32_t,uint32_t) constructor's `assert(num <= INT32_MAX)` - (reachable-assertion abort in debug builds; undefined/corrupt crop - rectangle in NDEBUG release builds). - . - Three layers, as upstream: - - image_item.cc: base get_heif_image_tiling() now reports the coded - (ispe) dimensions when available, matching the grid/unc/tiled overrides, - so the transformative properties are applied exactly once. - - context.cc: reject a clap that rounds a dimension to zero or less at - parse time, mirroring the existing ispe zero-size rejection in - check_for_valid_image_size(). - - box.cc: guard left_rounded()/top_rounded() against a zero image - dimension as defense in depth. -Origin: upstream, https://github.com/strukturag/libheif/commit/f01870c1d7323a3003796d58eba7fff502be994c -Last-Update: 2026-08-06 - ---- a/libheif/box.cc -+++ b/libheif/box.cc -@@ -3410,6 +3410,10 @@ int Box_clap::left_rounded(uint32_t image_width) const - - // left = horizOff + (width-1)/2 - (clapWidth-1)/2 - -+ if (image_width == 0) { -+ return 0; -+ } -+ - Fraction pcX = m_horizontal_offset + Fraction(image_width - 1U, 2U); - Fraction left = pcX - (m_clean_aperture_width - 1) / 2; - -@@ -3425,6 +3429,10 @@ int Box_clap::right_rounded(uint32_t image_width) const - - int Box_clap::top_rounded(uint32_t image_height) const - { -+ if (image_height == 0) { -+ return 0; -+ } -+ - Fraction pcY = m_vertical_offset + Fraction(image_height - 1U, 2U); - Fraction top = pcY - (m_clean_aperture_height - 1) / 2; - ---- a/libheif/context.cc -+++ b/libheif/context.cc -@@ -491,8 +491,16 @@ Error HeifContext::interpret_heif_file() - for (const auto& prop : properties) { - auto clap = std::dynamic_pointer_cast(prop); - if (clap) { -- image->set_resolution(clap->get_width_rounded(), -- clap->get_height_rounded()); -+ int clap_width = clap->get_width_rounded(); -+ int clap_height = clap->get_height_rounded(); -+ if (clap_width <= 0 || clap_height <= 0) { -+ return {heif_error_Invalid_input, -+ heif_suberror_Invalid_clean_aperture, -+ "Clean aperture (clap) reduces image to zero size"}; -+ } -+ -+ image->set_resolution(static_cast(clap_width), -+ static_cast(clap_height)); - - if (image->has_intrinsic_matrix()) { - image->get_intrinsic_matrix().apply_clap(clap.get(), image->get_width(), image->get_height()); ---- a/libheif/image-items/image_item.cc -+++ b/libheif/image-items/image_item.cc -@@ -1070,10 +1070,21 @@ heif_image_tiling ImageItem::get_heif_image_tiling() const - tiling.num_columns = 1; - tiling.num_rows = 1; - -- tiling.tile_width = m_width; -- tiling.tile_height = m_height; -- tiling.image_width = m_width; -- tiling.image_height = m_height; -+ // Report the coded (pre-transformation) dimensions here. The caller applies -+ // the transformative properties (irot, imir, clap) via -+ // process_image_transformations_on_tiling(), so handing it the already -+ // transformed m_width/m_height would apply them a second time. -+ uint32_t coded_width = m_width; -+ uint32_t coded_height = m_height; -+ if (has_ispe_resolution()) { -+ coded_width = get_ispe_width(); -+ coded_height = get_ispe_height(); -+ } -+ -+ tiling.tile_width = coded_width; -+ tiling.tile_height = coded_height; -+ tiling.image_width = coded_width; -+ tiling.image_height = coded_height; - - tiling.top_offset = 0; - tiling.left_offset = 0; diff -Nru libheif-1.19.8/debian/patches/CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch libheif-1.23.4/debian/patches/CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch --- libheif-1.19.8/debian/patches/CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -Description: Fix heap OOB read in tili offset table parsing (GHSA-2h34-fcv6-jqvh) - Box_iloc::read_data() discarded the Error returned by Box_idat::read_data() - for construction_method=1 (idat) extents, then unconditionally decremented - the requested size by the (possibly unfulfilled) extent length. By combining - one valid and one out-of-range extent whose lengths sum to the requested - size, the post-loop "limited_size && size > 0" shortfall guard could be - bypassed and read_data() would return Ok with a shorter-than-expected - buffer. TiledHeader::read_offset_table_range() then trusted that buffer's - length and indexed into it via readvec() without a bounds check, producing - a heap out-of-bounds read while parsing the per-tile offset/size table of a - tiled ('tili') image item. - . - Fixes the root cause (propagate the idat read error, matching the - construction_method 0 branch already a few lines above) and adds a - defense-in-depth bounds check before the offset-table parsing loop. -Origin: upstream, https://github.com/strukturag/libheif/commit/5e8fcffc16a3a2cf17584a2400f7386146f830f0 -Reviewed-by: Aron Xu -Last-Update: 2026-08-06 - ---- a/libheif/box.cc -+++ b/libheif/box.cc -@@ -1701,10 +1701,13 @@ Error Box_iloc::read_data(heif_item_id item_id, - "idat box referenced in iref box is not present in file"}; - } - -- idat->read_data(istr, -- extent.offset + item->base_offset, -- extent.length, -- *dest, limits); -+ Error err = idat->read_data(istr, -+ extent.offset + item->base_offset, -+ extent.length, -+ *dest, limits); -+ if (err) { -+ return err; -+ } - - size -= extent.length; - } ---- a/libheif/image-items/tiled.cc -+++ b/libheif/image-items/tiled.cc -@@ -361,6 +361,10 @@ Error TiledHeader::read_offset_table_range(const std::shared_ptr& file - return err; - } - -+ if (data.size() < size_to_read) { -+ return eofError; -+ } -+ - size_t idx = 0; - for (uint64_t i = start; i < end; i++) { - m_offsets[i].offset = readvec(data, idx, m_parameters.offset_field_length / 8); diff -Nru libheif-1.19.8/debian/patches/GHSA-5hqq-636x-r3cr.patch libheif-1.23.4/debian/patches/GHSA-5hqq-636x-r3cr.patch --- libheif-1.19.8/debian/patches/GHSA-5hqq-636x-r3cr.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/GHSA-5hqq-636x-r3cr.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,138 +0,0 @@ -Description: heif_region_item_add_region_inline_mask(): clamp copy to declared region size - heif_region_item_add_region_inline_mask() sized the destination mask buffer from the - caller-declared region (width x height) but copied pixels from the source mask heif_image - using that image's own (possibly larger) width/height and an unbounded running pixel_index, - so a source mask image larger than the declared region wrote past the end of the - heap-allocated mask_data buffer. Clamp the copy loop to min(declared, source) in each - dimension and derive the destination bit index from the declared width, so an oversized - source image is cropped to the region instead of overflowing it. -Origin: upstream, https://github.com/strukturag/libheif/commit/40f361a1e1c3b8f1a90d4bace1d93227c06448de -Bug: https://github.com/strukturag/libheif/security/advisories/GHSA-5hqq-636x-r3cr -Last-Update: 2026-08-06 -Comment: Context-adapted, not a clean cherry-pick. - Upstream's fix (written against a later tree) uses heif_image_get_plane2()/size_t stride; - v1.19.8 only has heif_image_get_plane()/int stride at this call site, so that accessor was - left unchanged and only the copy-bounds logic fix was applied (std::min-clamped copy_width/ - copy_height, and pixel_index recomputed per-pixel from the declared width instead of an - unbounded running counter). The regression test from upstream's tests/region.cc was ported - verbatim (no API differences needed). Note: in this minimal build configuration (no AV1 - encoder plugin enabled), the new test - like the three pre-existing "create ... mask region" - tests in this file - fails at heif_context_get_encoder_for_format(ctx, heif_compression_AV1, ...) - with err.code==3 before it ever reaches the patched code path; this is the same pre-existing, - unrelated missing-encoder artifact recorded in the ctest baseline for the "region" binary, not - a new regression introduced by this patch. The fix was independently verified against a - standalone AddressSanitizer driver using the JPEG encoder path, which confirmed a heap-buffer- - overflow before this patch and a clean run after. - ---- a/libheif/api/libheif/heif_regions.cc -+++ b/libheif/api/libheif/heif_regions.cc -@@ -338,14 +338,15 @@ struct heif_error heif_region_item_add_region_inline_mask(struct heif_region_ite - uint32_t mask_width = mask_image->image->get_width(); - int stride; - uint8_t* p = heif_image_get_plane(mask_image, heif_channel_Y, &stride); -- uint64_t pixel_index = 0; - -- for (uint32_t y = 0; y < mask_height; y++) { -- for (uint32_t x = 0; x < mask_width; x++) { -+ uint32_t copy_width = std::min(width, mask_width); -+ uint32_t copy_height = std::min(height, mask_height); -+ -+ for (uint32_t y = 0; y < copy_height; y++) { -+ for (uint32_t x = 0; x < copy_width; x++) { - uint8_t mask_bit = p[y * stride + x] & 0x80; // use high-order bit of the 8-bit mask value as binary mask value -+ uint64_t pixel_index = static_cast(y) * width + x; - region->mask_data.data()[pixel_index/8] |= uint8_t(mask_bit >> (pixel_index % 8)); -- -- pixel_index++; - } - } - -diff --git a/tests/region.cc b/tests/region.cc -index fb7ebca4..ebb635ba 100644 ---- a/tests/region.cc -+++ b/tests/region.cc -@@ -586,3 +586,84 @@ TEST_CASE("create inline mask region from image") { - heif_image_handle_release(readbackHandle); - heif_context_free(readbackCtx); - } -+ -+ -+TEST_CASE("inline mask region from oversized image is cropped") { -+ // Regression test for GHSA-5hqq-636x-r3cr: when the source mask image is -+ // larger than the declared region, the old code sized the destination mask -+ // buffer from the region dimensions but indexed writes by the source image -+ // dimensions, causing a heap out-of-bounds write. The source image is now -+ // cropped to the region instead. -+ struct heif_error err; -+ -+ heif_context* ctx = heif_context_alloc(); -+ heif_encoder* enc; -+ err = heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &enc); -+ REQUIRE(err.code == heif_error_Ok); -+ -+ uint32_t input_width = 64; -+ uint32_t input_height = 64; -+ heif_image* img; -+ heif_image_create(input_width, input_height, heif_colorspace_YCbCr, -+ heif_chroma_420, &img); -+ fill_new_plane(img, heif_channel_Y, input_width, input_height); -+ fill_new_plane(img, heif_channel_Cb, (input_width + 1) / 2, (input_height + 1) / 2); -+ fill_new_plane(img, heif_channel_Cr, (input_width + 1) / 2, (input_height + 1) / 2); -+ -+ heif_image_handle* handle; -+ err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); -+ REQUIRE(err.code == heif_error_Ok); -+ -+ struct heif_region_item* region_item; -+ err = heif_image_handle_add_region_item(handle, input_width, input_height, ®ion_item); -+ REQUIRE(err.code == heif_error_Ok); -+ -+ // Source mask image (64x64) much larger than the declared region (8x2). -+ // With the bug this writes far past the end of a 2-byte destination buffer. -+ heif_image* mask_image; -+ heif_image_create(64, 64, heif_colorspace_monochrome, heif_chroma_monochrome, &mask_image); -+ err = heif_image_add_plane(mask_image, heif_channel_Y, 64, 64, 8); -+ REQUIRE(err.code == heif_error_Ok); -+ int stride; -+ uint8_t* p = heif_image_get_plane(mask_image, heif_channel_Y, &stride); -+ memset(p, 0, 64 * stride); -+ p[0] = 0x80; // region pixel (0,0) -> set -+ p[7] = 0x80; // region pixel (7,0) -> set -+ p[stride + 0] = 0x80; // region pixel (0,1) -> set -+ // Pixels outside the 8x2 region must be cropped away. With the old code these -+ // would corrupt the in-bounds result or write out of bounds: -+ p[10] = 0x80; // (10,0) beyond declared width -+ p[2 * stride + 0] = 0x80; // (0,2) beyond declared height -+ -+ heif_region* out_region = nullptr; -+ err = heif_region_item_add_region_inline_mask(region_item, 20, 50, 8, 2, mask_image, &out_region); -+ REQUIRE(err.code == heif_error_Ok); -+ REQUIRE(out_region != nullptr); -+ -+ size_t data_len = heif_region_get_inline_mask_data_len(out_region); -+ REQUIRE(data_len == 2); // (8*2+7)/8 = 2 bytes -+ -+ std::vector mask_data_in(data_len); -+ int32_t x, y; -+ uint32_t width, height; -+ err = heif_region_get_inline_mask_data(out_region, &x, &y, &width, &height, mask_data_in.data()); -+ REQUIRE(err.code == heif_error_Ok); -+ REQUIRE(x == 20); -+ REQUIRE(y == 50); -+ REQUIRE(width == 8); -+ REQUIRE(height == 2); -+ // Destination bits, row-major over the 8x2 region: -+ // (0,0)=index0 -> 0x80, (7,0)=index7 -> 0x01 => byte0 = 0x81 -+ // (0,1)=index8 -> 0x80 => byte1 = 0x80 -+ // The cropped-away pixels (10,0) and (0,2) must not appear. -+ REQUIRE(mask_data_in[0] == 0x81); -+ REQUIRE(mask_data_in[1] == 0x80); -+ -+ heif_region_release(out_region); -+ heif_image_release(mask_image); -+ heif_region_item_release(region_item); -+ heif_image_handle_release(handle); -+ heif_encoder_release(enc); -+ heif_context_free(ctx); -+ heif_image_release(img); -+} diff -Nru libheif-1.19.8/debian/patches/GHSA-73p7-m7gg-w2jv.patch libheif-1.23.4/debian/patches/GHSA-73p7-m7gg-w2jv.patch --- libheif-1.19.8/debian/patches/GHSA-73p7-m7gg-w2jv.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/GHSA-73p7-m7gg-w2jv.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -Description: GHSA-73p7-m7gg-w2jv: bounds-check unci tile-range extraction before memcpy - The final "cut out the range that we actually need" memcpy() calls in the - generic-compression / uncompressed decode path used attacker-controlled - range_start_offset/range_size with no bounds validation at all, allowing a - read past the end of the decompressed data buffer. Add a subtraction-form - bounds check (avoiding a uint64_t addition overflow) before each memcpy. -Origin: upstream, https://github.com/strukturag/libheif/commit/089a809bf6bed1abae102d5e97b6bb8c4f53b515 -Bug: https://github.com/strukturag/libheif/security/advisories/GHSA-73p7-m7gg-w2jv -Last-Update: 2026-08-06 -Comment: No Bug-Debian field: no CVE has been assigned to this advisory and - it therefore has no entry in the Debian security tracker yet. - . - Upstream's fix is against libheif/codecs/uncompressed/unc_decoder.cc - (function unc_decoder::get_compressed_image_data_uncompressed), which does - not exist at 1.19.8 (created later by the >=1.20 uncompressed codec - reorganization). v1.19.8's equivalent code, - AbstractDecoder::get_compressed_image_data_uncompressed() in - libheif/codecs/uncompressed/decoder_abstract.cc, has no range check at all - before either of its two memcpy() call sites (it predates even the - overflow-prone addition-form check that upstream commit d7a43f98 first - introduced in September 2025, which itself post-dates v1.19.8). This patch - hand-adds the non-overflowing subtraction-form check - (range_start_offset > size || range_size > size - range_start_offset) - immediately before each of the two memcpy() calls, using the same error - code/suberror/message as upstream's fix. - ---- a/libheif/codecs/uncompressed/decoder_abstract.cc -+++ b/libheif/codecs/uncompressed/decoder_abstract.cc -@@ -238,6 +238,11 @@ const Error AbstractDecoder::get_compressed_image_data_uncompressed(const HeifCo - } - - // cut out the range that we actually need -+ if (range_start_offset > data->size() || range_size > data->size() - range_start_offset) { -+ return {heif_error_Invalid_input, -+ heif_suberror_Unspecified, -+ "Data range out of existing range"}; -+ } - memcpy(data->data(), data->data() + range_start_offset, range_size); - data->resize(range_size); - } -@@ -256,6 +261,11 @@ const Error AbstractDecoder::get_compressed_image_data_uncompressed(const HeifCo - } - - // cut out the range that we actually need -+ if (range_start_offset > data->size() || range_size > data->size() - range_start_offset) { -+ return {heif_error_Invalid_input, -+ heif_suberror_Unspecified, -+ "Data range out of existing range"}; -+ } - memcpy(data->data(), data->data() + range_start_offset, range_size); - data->resize(range_size); - } diff -Nru libheif-1.19.8/debian/patches/overlay-simplify-overlap-area-computation.patch libheif-1.23.4/debian/patches/overlay-simplify-overlap-area-computation.patch --- libheif-1.19.8/debian/patches/overlay-simplify-overlap-area-computation.patch 2026-08-06 10:48:46.000000000 +0000 +++ libheif-1.23.4/debian/patches/overlay-simplify-overlap-area-computation.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +0,0 @@ -Description: Fix out-of-bounds accesses in HeifPixelImage::overlay() for negative offsets - This patch is upstream commit 85e21ad4, which restructures the - computation so that the right/bottom clip is evaluated in signed 64-bit - arithmetic BEFORE the left/top clip mutates in_w/in_h. After it, - in_w/in_h hold the true visible extent for both signs of dx/dy and all - four accesses above are in bounds. - . - It is also a hard prerequisite for the CVE-2025-68431 one-line fix that - follows it in this series: that fix replaces the memcpy length - `in_w - in_x0` with `in_w`, which is only correct on top of this - reordering. Applied to unreordered 1.19.8 code it would instead turn the - existing over-read into an over-WRITE of in_x0 bytes per row. -Origin: upstream, https://github.com/strukturag/libheif/commit/85e21ad44eba931314337300a2376b8d28f085ae -Last-Update: 2026-08-06 - -diff --git a/libheif/image-items/overlay.cc b/libheif/image-items/overlay.cc -index c010fee8..64d233de 100644 ---- a/libheif/image-items/overlay.cc -+++ b/libheif/image-items/overlay.cc -@@ -38,7 +38,7 @@ void writevec(uint8_t* data, size_t& idx, I value, int len) - - static int32_t readvec_signed(const std::vector& data, int& ptr, int len) - { -- const uint32_t high_bit = 0x80 << ((len - 1) * 8); -+ const uint32_t high_bit = UINT32_C(0x80) << ((len - 1) * 8); - - uint32_t val = 0; - while (len--) { -diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc -index 04e81fe2..7425ea11 100644 ---- a/libheif/pixelimage.cc -+++ b/libheif/pixelimage.cc -@@ -1250,11 +1250,8 @@ Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t - uint32_t out_w = get_width(channel); - uint32_t out_h = get_height(channel); - -- // top-left points where to start copying in source and destination -- uint32_t in_x0; -- uint32_t in_y0; -- uint32_t out_x0; -- uint32_t out_y0; -+ -+ // --- check whether overlay image overlaps with current image - - if (dx > 0 && static_cast(dx) >= out_w) { - // the overlay image is completely outside the right border -> skip overlaying -@@ -1265,37 +1262,50 @@ Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t - return Error::Ok; - } - -- if (dx < 0) { -- // overlay image started partially outside of left border -- -- in_x0 = negate_negative_int32(dx); -- out_x0 = 0; -- in_w = in_w - in_x0; // in_x0 < in_w because in_w > -dx = in_x0 -+ if (dy > 0 && static_cast(dy) >= out_h) { -+ // the overlay image is completely outside the bottom border -> skip overlaying -+ return Error::Ok; - } -- else { -- in_x0 = 0; -- out_x0 = static_cast(dx); -+ else if (dy < 0 && in_h <= negate_negative_int32(dy)) { -+ // the overlay image is completely outside the top border -> skip overlaying -+ return Error::Ok; - } - -- // we know that dx >= 0 && dx < out_w - -- if (static_cast(dx) > UINT32_MAX - in_w || -- dx + in_w > out_w) { -+ // --- compute overlapping area -+ -+ // top-left points where to start copying in source and destination -+ uint32_t in_x0; -+ uint32_t in_y0; -+ uint32_t out_x0; -+ uint32_t out_y0; -+ -+ // right border -+ if (dx + static_cast(in_w) > out_w) { - // overlay image extends partially outside of right border -+ in_w = static_cast(static_cast(out_w) - dx); -+ } - -- in_w = out_w - static_cast(dx); // we know that dx < out_w from first condition -+ // bottom border -+ if (dy + static_cast(in_h) > out_h) { -+ // overlay image extends partially outside of bottom border -+ in_h = static_cast(static_cast(out_h) - dy); - } - -+ // left border -+ if (dx < 0) { -+ // overlay image starts partially outside of left border - -- if (dy > 0 && static_cast(dy) >= out_h) { -- // the overlay image is completely outside the bottom border -> skip overlaying -- return Error::Ok; -+ in_x0 = negate_negative_int32(dx); -+ out_x0 = 0; -+ in_w = in_w - in_x0; // in_x0 < in_w because in_w > -dx = in_x0 - } -- else if (dy < 0 && in_h <= negate_negative_int32(dy)) { -- // the overlay image is completely outside the top border -> skip overlaying -- return Error::Ok; -+ else { -+ in_x0 = 0; -+ out_x0 = static_cast(dx); - } - -+ // top border - if (dy < 0) { - // overlay image started partially outside of top border - -@@ -1308,15 +1318,7 @@ Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t - out_y0 = static_cast(dy); - } - -- // we know that dy >= 0 && dy < out_h -- -- if (static_cast(dy) > UINT32_MAX - in_h || -- dy + in_h > out_h) { -- // overlay image extends partially outside of bottom border -- -- in_h = out_h - static_cast(dy); // we know that dy < out_h from first condition -- } -- -+ // --- computer overlay in overlapping area - - for (uint32_t y = in_y0; y < in_h; y++) { - if (!has_alpha) { diff -Nru libheif-1.19.8/debian/patches/series libheif-1.23.4/debian/patches/series --- libheif-1.19.8/debian/patches/series 2026-08-06 10:45:38.000000000 +0000 +++ libheif-1.23.4/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -overlay-simplify-overlap-area-computation.patch -CVE-2025-68431.patch -CVE-2026-32882.patch -CVE-2026-32740.patch -CVE-2026-47247.patch -CVE-2026-32741.patch -CVE-2026-47709-1_uncC-tile-count-overflow.patch -CVE-2026-47709-2_missing-ispe-null-deref.patch -CVE-2026-49271.patch -GHSA-73p7-m7gg-w2jv.patch -CVE-2026-47178.patch -CVE-2026-47714.patch -GHSA-5hqq-636x-r3cr.patch -CVE-2026-48029.patch -CVE-2026-GHSA-2h34-fcv6-jqvh-tili-offset-oob-read.patch -CVE-2026-62289-clap-zero-size-tiling-underflow.patch diff -Nru libheif-1.19.8/debian/rules libheif-1.23.4/debian/rules --- libheif-1.19.8/debian/rules 2025-01-07 12:11:40.000000000 +0000 +++ libheif-1.23.4/debian/rules 2026-09-09 06:40:35.000000000 +0000 @@ -25,6 +25,7 @@ -DCMAKE_LIBRARY_ARCHITECTURE="$(DEB_HOST_MULTIARCH)" \ -DPLUGIN_DIRECTORY=/usr/lib/$(DEB_HOST_MULTIARCH)/libheif/plugins \ -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF \ + -DWITH_EXAMPLE_HEIF_VIEW=OFF \ -DWITH_AOM_DECODER_PLUGIN=ON \ -DWITH_AOM_ENCODER_PLUGIN=ON \ -DWITH_DAV1D=ON \ @@ -36,8 +37,8 @@ -DWITH_JPEG_DECODER_PLUGIN=ON \ -DWITH_JPEG_ENCODER=ON \ -DWITH_JPEG_ENCODER_PLUGIN=ON \ - -DWITH_KVAZAAR=ON \ - -DWITH_KVAZAAR_PLUGIN=ON \ + -DWITH_KVAZAAR=$(if $(filter loong64,$(DEB_HOST_ARCH)),OFF,ON) \ + -DWITH_KVAZAAR_PLUGIN=$(if $(filter loong64,$(DEB_HOST_ARCH)),OFF,ON) \ -DWITH_LIBDE265_PLUGIN=ON \ -DWITH_LIBSHARPYUV=ON \ -DWITH_OpenJPEG_DECODER=ON \ @@ -63,11 +64,11 @@ ifeq ($(shell dpkg-vendor --query vendor),Ubuntu) dh_gencontrol -- \ -Vlibheif:Depends='libheif-plugin-aomdec (= $${binary:Version}) | libheif-plugin-dav1d (= $${binary:Version})' \ - -Vlibheif:Suggests:'libheif-plugin-x265 (= $${binary:Version})' + -Vlibheif:Suggests='libheif-plugin-libde265 (= $${binary:Version}), libheif-plugin-x265 (= $${binary:Version})' else dh_gencontrol -- \ - -Vlibheif:Depends='libheif-plugin-dav1d (= $${binary:Version}) | libheif-plugin-aomdec (= $${binary:Version})' \ - -Vlibheif:Recommends:'libheif-plugin-x265 (= $${binary:Version})' + -Vlibheif:Depends='libheif-plugin-libde265 (= $${binary:Version}), libheif-plugin-dav1d (= $${binary:Version}) | libheif-plugin-aomdec (= $${binary:Version})' \ + -Vlibheif:Recommends='libheif-plugin-x265 (= $${binary:Version})' endif UURL = git://github.com/strukturag/libheif.git diff -Nru libheif-1.19.8/examples/CMakeLists.txt libheif-1.23.4/examples/CMakeLists.txt --- libheif-1.19.8/examples/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -14,6 +14,13 @@ heif_info.cc common.cc common.h) +if(WIN32) + if(MSVC) + target_sources(heif-info PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-info PRIVATE utf8.rc) + endif() +endif() target_link_libraries(heif-info heif) install(TARGETS heif-info RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES heif-info.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) @@ -23,6 +30,13 @@ heif_dec.cc common.cc common.h) +if(WIN32) + if(MSVC) + target_sources(heif-dec PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-dec PRIVATE utf8.rc) + endif() +endif() target_link_libraries(heif-dec PRIVATE heif heifio) target_include_directories(heif-dec PRIVATE ${libheif_SOURCE_DIR}) install(TARGETS heif-dec RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) @@ -41,22 +55,54 @@ benchmark.h benchmark.cc common.cc - common.h) + common.h + SAI_datafile.cc + SAI_datafile.h) +if(WIN32) + if(MSVC) + target_sources(heif-enc PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-enc PRIVATE utf8.rc) + endif() +endif() target_link_libraries(heif-enc PRIVATE heif heifio) target_include_directories(heif-enc PRIVATE ${libheif_SOURCE_DIR}) install(TARGETS heif-enc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES heif-enc.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) +if (LIBSHARPYUV_FOUND) + target_compile_definitions(heif-enc PRIVATE HAVE_LIBSHARPYUV=1) +endif() + if (WITH_HEADER_COMPRESSION) target_compile_definitions(heif-enc PRIVATE WITH_HEADER_COMPRESSION=1) endif () +if (ENABLE_EXPERIMENTAL_FEATURES) + target_sources(heif-enc PRIVATE vmt.cc vmt.h) +endif () + + +if (BUILD_DEVELOPMENT_TOOLS AND PNG_FOUND) + add_executable(heif-gen-bayer + heif_gen_bayer.cc) + target_link_libraries(heif-gen-bayer PRIVATE heif heifio) + target_include_directories(heif-gen-bayer PRIVATE ${libheif_SOURCE_DIR}) +endif() + -if (PNG_FOUND) +if (WITH_EXAMPLE_HEIF_THUMB AND PNG_FOUND) add_executable(heif-thumbnailer ${getopt_sources} heif_thumbnailer.cc common.cc common.h) + if(WIN32) + if(MSVC) + target_sources(heif-thumbnailer PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-thumbnailer PRIVATE utf8.rc) + endif() + endif() target_link_libraries(heif-thumbnailer heif heifio) target_include_directories(heif-thumbnailer PRIVATE ${libheif_SOURCE_DIR}) @@ -64,8 +110,40 @@ install(FILES heif-thumbnailer.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) endif() + + +if (WITH_EXAMPLE_HEIF_VIEW) + find_package(SDL2 NO_MODULE) + + if (SDL2_FOUND) + add_executable(heif-view ${getopt_sources} + heif_view.cc + sdl.cc + sdl.hh + common.cc + common.h) + if(WIN32) + if(MSVC) + target_sources(heif-view PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-view PRIVATE utf8.rc) + endif() + endif() + target_link_libraries(heif-view PRIVATE heif SDL2::SDL2main SDL2::SDL2) + install(TARGETS heif-view RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif () +endif () + + add_executable(heif-test ${getopt_sources} heif_test.cc common.cc common.h) +if(WIN32) + if(MSVC) + target_sources(heif-test PRIVATE utf8.manifest) + elseif(MINGW) + target_sources(heif-test PRIVATE utf8.rc) + endif() +endif() target_link_libraries(heif-test heif) diff -Nru libheif-1.19.8/examples/SAI_datafile.cc libheif-1.23.4/examples/SAI_datafile.cc --- libheif-1.19.8/examples/SAI_datafile.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/SAI_datafile.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,238 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "SAI_datafile.h" + +#include +#include +#include +#include + + +SAI_datafile::~SAI_datafile() +{ + heif_tai_clock_info_release(tai_clock_info); + + for (auto tai : tai_timestamps) { + heif_tai_timestamp_packet_release(tai); + } +} + + +void SAI_datafile::handleHeaderEntry(const std::string& code, + const std::vector& values) +{ + if (code == "suid") { + active_sais.push_back(code); + if (!values.empty()) { + std::cerr << "Invalid 'suid' header line. May not have additional parameters.\n"; + exit(5); + } + } + else if (code == "stai") { + active_sais.push_back(code); + if (values.size() > 4) { + std::cerr << "Invalid 'stai' header line. May not have more than 4 parameters.\n"; + exit(5); + } + + tai_clock_info = heif_tai_clock_info_alloc(); + + for (size_t i=0;i 0xffffffff)) { + std::cerr << "Invalid SAI tai clock info entry in header\n"; + exit(5); + } + + if (i==2 && (val < 0 || val > 0x7fffffff)) { + std::cerr << "Invalid SAI tai clock info entry in header\n"; + exit(5); + } + + if (i==3 && (val < 0 || val > 0xff)) { + std::cerr << "Invalid SAI tai clock info entry in header\n"; + exit(5); + } + + switch (i) { + case 0: + tai_clock_info->time_uncertainty = val; + break; + case 1: + tai_clock_info->clock_resolution = static_cast(val); + break; + case 2: + tai_clock_info->clock_drift_rate = static_cast(val); + break; + case 3: + tai_clock_info->clock_type = static_cast(val); + break; + } + } + } + else { + std::cerr << "Unknown code in SAI data file header: " << code << "\n"; + exit(5); + } +} + +void SAI_datafile::handleMainEntry(const std::vector& values, int line, int main_item_line) +{ + if (active_sais.empty()) { + std::cerr << "Invalid SAI data file: data received, but no SAIs defined."; + exit(5); + } + + size_t idx = main_item_line % active_sais.size(); + if (active_sais[idx] == "suid") { + if (values.size() > 1) { + std::cerr << "Invalid SAI content-id entry in line " << line << "\n"; + exit(5); + } + + if (values.empty()) { + gimi_content_ids.push_back({}); + } + else { + gimi_content_ids.push_back(values[0]); + } + } + else if (active_sais[idx] == "stai") { + if (values.size() > 4) { + std::cerr << "Invalid SAI timestamp entry in line " << line << "\n"; + exit(5); + } + + if (values.empty()) { + tai_timestamps.push_back(nullptr); + } + else { + heif_tai_timestamp_packet* tai = heif_tai_timestamp_packet_alloc(); + for (size_t i=0;i=1 && i<=3 && (val < 0 || val > 1)) { + std::cerr << "Invalid SAI timestamp entry in line " << line << "\n"; + exit(5); + } + + switch (i) { + case 0: + tai->tai_timestamp = val; + break; + case 1: + tai->synchronization_state = static_cast(val); + break; + case 2: + tai->timestamp_generation_failure = static_cast(val); + break; + case 3: + tai->timestamp_is_modified = static_cast(val); + break; + } + } + + tai_timestamps.push_back(tai); + } + } +} + +bool SAI_datafile::isSeparatorLine(const std::string& line) +{ + return line.starts_with("---"); +} + +std::vector SAI_datafile::splitCSV(const std::string& line) +{ + std::vector parts; + std::stringstream ss(line); + std::string item; + + while (std::getline(ss, item, ',')) { + // Trim whitespace + item.erase(item.begin(), + std::find_if(item.begin(), item.end(), [](unsigned char ch){ return !std::isspace(ch); })); + item.erase( + std::find_if(item.rbegin(), item.rend(), [](unsigned char ch){ return !std::isspace(ch); }).base(), + item.end() + ); + parts.push_back(item); + } + + return parts; +} + + +void SAI_datafile::load_sai_data_from_file(const char* sai_file) +{ + std::ifstream istr(sai_file); + if (!istr) { + std::cerr << "Could not open SAI data file\n"; + exit(5); + } + + // --- read header + + std::string line; + bool inHeader = true; + int line_counter = 0; + int main_item_line = 0; + + while (std::getline(istr, line)) { + line_counter++; + + if (inHeader && line.empty()) + continue; + + if (inHeader && isSeparatorLine(line)) { + // Switch to main part + inHeader = false; + continue; + } + + if (inHeader) { + // Header line: starts with 4-character code, optional CSV list + if (line.size() < 4) { + std::cerr << "Invalid header line: " << line << "\n"; + continue; + } + + std::string code = line.substr(0, 4); + + std::vector values; + if (line.size() > 4) { + // Skip the space after the code, if present + std::string rest = line.substr(4); + if (!rest.empty() && (rest[0] == ' ' || rest[0] == '\t')) + rest.erase(0, 1); + + if (!rest.empty()) + values = splitCSV(rest); + } + + handleHeaderEntry(code, values); + } + else { + // Main section: entire line is CSV + auto values = splitCSV(line); + handleMainEntry(values, line_counter, main_item_line++); + } + } +} diff -Nru libheif-1.19.8/examples/SAI_datafile.h libheif-1.23.4/examples/SAI_datafile.h --- libheif-1.19.8/examples/SAI_datafile.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/SAI_datafile.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,54 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef SAI_DATAFILE_H +#define SAI_DATAFILE_H +#include +#include + +#include "libheif/heif_tai_timestamps.h" + +// Helper class for heif-enc to read a SAI data file and provide the data for the track's SAI items. +struct SAI_datafile +{ + ~SAI_datafile(); + + void load_sai_data_from_file(const char* sai_file); + + heif_tai_clock_info* tai_clock_info = nullptr; + + std::vector tai_timestamps; + std::vector gimi_content_ids; + + std::vector active_sais; + +private: + void handleHeaderEntry(const std::string& code, + const std::vector& values); + + void handleMainEntry(const std::vector& values, int line, int main_item_line); + + bool isSeparatorLine(const std::string& line); + + std::vector splitCSV(const std::string& line); +}; + + +#endif //SAI_DATAFILE_H diff -Nru libheif-1.19.8/examples/common.cc libheif-1.23.4/examples/common.cc --- libheif-1.19.8/examples/common.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/common.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,22 +27,125 @@ #include "common.h" #include "libheif/heif.h" #include +#include +#include +#include +#include +#include "common_utils.h" -void show_version() -{ - std::cout << LIBHEIF_VERSION << '\n' - << "libheif: " << heif_get_version() << '\n'; +namespace heif_examples { + void show_version() { - auto paths = heif_get_plugin_directories(); - for (int i=0;paths[i];i++) { - std::cout << "plugin path: " << paths[i] << '\n'; + std::cout << LIBHEIF_VERSION << '\n' + << "libheif: " << heif_get_version() << '\n'; + { + auto paths = heif_get_plugin_directories(); + for (int i = 0; paths[i]; i++) { + std::cout << "plugin path: " << paths[i] << '\n'; + } + + if (paths[0] == nullptr) { + std::cout << "plugin path: plugins are disabled\n"; + } + + heif_free_plugin_directories(paths); } + } + + +#define MAX_DECODERS 20 - if (paths[0] == nullptr) { - std::cout << "plugin path: plugins are disabled\n"; + void list_decoders(heif_compression_format format) + { + const heif_decoder_descriptor* decoders[MAX_DECODERS]; + int n = heif_get_decoder_descriptors(format, decoders, MAX_DECODERS); + + for (int i = 0; i < n; i++) { + const char* id = heif_decoder_descriptor_get_id_name(decoders[i]); + if (id == nullptr) { + id = "---"; + } + + std::cout << "- " << id << " = " << heif_decoder_descriptor_get_name(decoders[i]) << "\n"; } + } + + + void list_all_decoders() + { + std::cout << "AVC decoders:\n"; + list_decoders(heif_compression_AVC); + + std::cout << "AVIF decoders:\n"; + list_decoders(heif_compression_AV1); + + std::cout << "HEIC decoders:\n"; + list_decoders(heif_compression_HEVC); + + std::cout << "JPEG decoders:\n"; + list_decoders(heif_compression_JPEG); + + std::cout << "JPEG 2000 decoders:\n"; + list_decoders(heif_compression_JPEG2000); + + std::cout << "JPEG 2000 (HT) decoders:\n"; + list_decoders(heif_compression_HTJ2K); + + std::cout << "uncompressed:\n"; + list_decoders(heif_compression_uncompressed); - heif_free_plugin_directories(paths); + std::cout << "VVIC decoders:\n"; + list_decoders(heif_compression_VVC); } + + + std::string fourcc_to_string(uint32_t fourcc) + { + char s[5]; + s[0] = static_cast((fourcc >> 24) & 0xFF); + s[1] = static_cast((fourcc >> 16) & 0xFF); + s[2] = static_cast((fourcc >> 8) & 0xFF); + s[3] = static_cast((fourcc) & 0xFF); + s[4] = 0; + + return s; + } + + + int check_for_valid_input_HEIF_file(const std::string& input_filename) + { + std::ifstream istr(input_filename.c_str(), std::ios_base::binary); + if (istr.fail()) { + fprintf(stderr, "Input file does not exist.\n"); + return 10; + } + + std::array length{}; + istr.read((char*) length.data(), length.size()); + uint32_t box_size = four_bytes_to_uint32(length[0], length[1], length[2], length[3]); + if ((box_size < 16) || (box_size > 512)) { + fprintf(stderr, "Input file does not appear to start with a valid box length."); + if ((box_size & 0xFFFFFFF0) == 0xFFD8FFE0) { + fprintf(stderr, " Possibly could be a JPEG file instead.\n"); + } + else { + fprintf(stderr, "\n"); + } + return 1; + } + + std::vector ftyp_bytes(box_size); + std::copy(length.begin(), length.end(), ftyp_bytes.begin()); + istr.read((char*) ftyp_bytes.data() + 4, ftyp_bytes.size() - 4); + + heif_error filetype_check = heif_has_compatible_filetype(ftyp_bytes.data(), (int) ftyp_bytes.size()); + if (filetype_check.code != heif_error_Ok) { + fprintf(stderr, "Input file is not a supported format. %s\n", filetype_check.message); + return 1; + } + + return 0; + } + } diff -Nru libheif-1.19.8/examples/common.h libheif-1.23.4/examples/common.h --- libheif-1.19.8/examples/common.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/common.h 2026-09-06 14:45:17.000000000 +0000 @@ -27,6 +27,21 @@ #ifndef LIBHEIF_COMMON_H #define LIBHEIF_COMMON_H -void show_version(); +#include +#include + +namespace heif_examples { +// Note: the same function is also exists in common_utils.h, but is not in the public API. + std::string fourcc_to_string(uint32_t fourcc); + + void show_version(); + + void list_all_decoders(); + + void list_decoders(heif_compression_format format); + +// returns 0 on success, or program exit code in case of warning/error + int check_for_valid_input_HEIF_file(const std::string& input_filename); +} #endif //LIBHEIF_COMMON_H diff -Nru libheif-1.19.8/examples/heif_dec.cc libheif-1.23.4/examples/heif_dec.cc --- libheif-1.19.8/examples/heif_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/heif_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,8 +27,11 @@ #include #include +#include #include #include "libheif/heif_items.h" +#include "libheif/heif_experimental.h" +#include "libheif/heif_sequences.h" #if defined(HAVE_UNISTD_H) @@ -47,6 +50,7 @@ #include #include +#include #include "heifio/encoder.h" @@ -62,8 +66,13 @@ #include "heifio/encoder_tiff.h" #endif +#if HAVE_LIBWEBP +#include "heifio/encoder_webp.h" +#endif + #include "../heifio/encoder_y4m.h" #include "common.h" +#include "common_utils.h" #if defined(_MSC_VER) #include "getopt.h" @@ -71,6 +80,7 @@ #define UNUSED(x) (void)x + static void show_help(const char* argv0) { std::filesystem::path p(argv0); @@ -81,12 +91,30 @@ std::string title = sstr.str(); + int default_tile_threads = heif_context_get_max_decoding_threads(nullptr); + std::cerr << title << "\n" << std::string(title.length() + 1, '-') << "\n" << "Usage: " << filename << " [options] [output-image]\n" << "\n" "The program determines the output file format from the output filename suffix.\n" - "These suffixes are recognized: jpg, jpeg, png, tif, tiff, y4m. If no output filename is specified, 'jpg' is used.\n" + "These suffixes are recognized: "; + +#if HAVE_LIBJPEG + std::cerr << "jpeg, jpg, "; +#endif +#if HAVE_LIBPNG + std::cerr << "png, "; +#endif +#if HAVE_LIBTIFF + std::cerr << "tif, tiff, "; +#endif +#if HAVE_LIBWEBP + std::cerr << "webp, "; +#endif + std::cerr << "y4m"; + + std::cerr << ". If no output filename is specified, 'jpg' is used.\n" "\n" "Options:\n" " -h, --help show help\n" @@ -102,16 +130,24 @@ " --list-decoders list all available decoders (built-in and plugins)\n" " --tiles output all image tiles as separate images\n" " --quiet do not output status messages to console\n" + " -S, --sequence decode image sequence instead of still image\n" + " --ignore-editlist show the raw media sequence timeline without repetitions\n" " -C, --chroma-upsampling ALGO Force chroma upsampling algorithm (nn = nearest-neighbor / bilinear)\n" " --png-compression-level # Set to integer between 0 (fastest) and 9 (best). Use -1 for default.\n" - " --disable-limits disable all security limits (do not use in production environment)\n"; + " --transparency-composition-mode MODE Controls how transparent images are rendered when the output format\n" + " support transparency. MODE must be one of: white, black, checkerboard.\n" + " --disable-limits disable all security limits (do not use in production environment)\n" + " --auto-correct work around known broken-input quirks (e.g. Sony HIF full-range flag mismatch)\n" + " --codec-threads # number of threads to use in the codec plugin (0 = default)\n" + << " --tile-threads # max number of tiles to decode in parallel (default = " << default_tile_threads << ")\n" + << " --extract-mime-item TYPE extract the MIME item with the given content type into a file (mime-item.data)\n"; } class ContextReleaser { public: - ContextReleaser(struct heif_context* ctx) : ctx_(ctx) + ContextReleaser(heif_context* ctx) : ctx_(ctx) {} ~ContextReleaser() @@ -120,7 +156,7 @@ } private: - struct heif_context* ctx_; + heif_context* ctx_; }; @@ -134,14 +170,24 @@ int option_png_compression_level = -1; // use zlib default int option_output_tiles = 0; int option_disable_limits = 0; +int option_auto_correct = 0; +int option_sequence = 0; +int option_ignore_editlist = 0; +int option_num_codec_threads = 0; +int option_num_tile_threads = -1; // -1 means "do not override library default" +std::string option_extract_mime_item_type; std::string output_filename; std::string chroma_upsampling; +std::string transparency_composition_mode = "checkerboard"; #define OPTION_PNG_COMPRESSION_LEVEL 1000 +#define OPTION_TRANSPARENCY_COMPOSITION_MODE 1001 +#define OPTION_CODEC_THREADS 1002 +#define OPTION_EXTRACT_MIME_ITEM 1003 +#define OPTION_TILE_THREADS 1004 - -static struct option long_options[] = { +static option long_options[] = { {(char* const) "quality", required_argument, 0, 'q'}, {(char* const) "strict", no_argument, 0, 's'}, {(char* const) "decoder", required_argument, 0, 'd'}, @@ -154,61 +200,24 @@ {(char* const) "no-colons", no_argument, &option_no_colons, 1}, {(char* const) "list-decoders", no_argument, &option_list_decoders, 1}, {(char* const) "tiles", no_argument, &option_output_tiles, 1}, + {(char* const) "sequence", no_argument, &option_sequence, 1}, {(char* const) "help", no_argument, 0, 'h'}, {(char* const) "chroma-upsampling", required_argument, 0, 'C'}, {(char* const) "png-compression-level", required_argument, 0, OPTION_PNG_COMPRESSION_LEVEL}, + {(char* const) "transparency-composition-mode", required_argument, 0, OPTION_TRANSPARENCY_COMPOSITION_MODE}, {(char* const) "version", no_argument, 0, 'v'}, {(char* const) "disable-limits", no_argument, &option_disable_limits, 1}, + {(char* const) "auto-correct", no_argument, &option_auto_correct, 1}, + {(char* const) "ignore-editlist", no_argument, &option_ignore_editlist, 1}, + {(char* const) "codec-threads", required_argument, 0, OPTION_CODEC_THREADS}, + {(char* const) "tile-threads", required_argument, 0, OPTION_TILE_THREADS}, + {(char* const) "extract-mime-item", required_argument, 0, OPTION_EXTRACT_MIME_ITEM}, {nullptr, no_argument, nullptr, 0} }; #define MAX_DECODERS 20 -void list_decoders(heif_compression_format format) -{ - const heif_decoder_descriptor* decoders[MAX_DECODERS]; - int n = heif_get_decoder_descriptors(format, decoders, MAX_DECODERS); - - for (int i=0;i invalidChars = {'\\','/','*','?','"','<','>','|', ':'}; + std::string sanitized; + sanitized.reserve(filename.size()); // avoid reallocations + + for (char c : filename) { + if (invalidChars.find(c) != invalidChars.end()) { + sanitized += '_'; + } else { + sanitized += c; + } + } + return sanitized; +} + int decode_single_image(heif_image_handle* handle, - std::string filename_stem, - std::string filename_suffix, + const std::string& filename_stem, + const std::string& filename_suffix, heif_decoding_options* decode_options, std::unique_ptr& encoder) { @@ -250,8 +274,8 @@ int has_alpha = heif_image_handle_has_alpha_channel(handle); - struct heif_image* image; - struct heif_error err; + heif_image* image; + heif_error err; err = heif_decode_image(handle, &image, encoder->colorspace(has_alpha), @@ -297,7 +321,7 @@ assert(nDepthImages == 1); (void) nDepthImages; - struct heif_image_handle* depth_handle = nullptr; + heif_image_handle* depth_handle = nullptr; err = heif_image_handle_get_depth_image_handle(handle, depth_id, &depth_handle); if (err.code) { std::cerr << "Could not read depth channel\n"; @@ -306,7 +330,7 @@ int depth_bit_depth = heif_image_handle_get_luma_bits_per_pixel(depth_handle); - struct heif_image* depth_image; + heif_image* depth_image; err = heif_decode_image(depth_handle, &depth_image, encoder->colorspace(false), @@ -352,7 +376,7 @@ for (heif_item_id auxId : auxIDs) { - struct heif_image_handle* aux_handle = nullptr; + heif_image_handle* aux_handle = nullptr; err = heif_image_handle_get_auxiliary_image_handle(handle, auxId, &aux_handle); if (err.code) { std::cerr << "Could not read auxiliary image\n"; @@ -361,7 +385,7 @@ int aux_bit_depth = heif_image_handle_get_luma_bits_per_pixel(aux_handle); - struct heif_image* aux_image = nullptr; + heif_image* aux_image = nullptr; err = heif_decode_image(aux_handle, &aux_image, encoder->colorspace(false), @@ -392,7 +416,7 @@ std::ostringstream s; s << filename_stem; - s << "-" + auxType + "."; + s << "-" + sanitizeFilename(auxType) + "."; s << filename_suffix; std::string auxFilename = s.str(); @@ -464,7 +488,7 @@ return 1; } - offset = (exif[0] << 24) | (exif[1] << 16) | (exif[2] << 8) | exif[3]; + offset = four_bytes_to_uint32(exif[0], exif[1], exif[2], exif[3]); offset += 4; if (offset >= exifSize) { @@ -502,8 +526,8 @@ int decode_image_tiles(heif_image_handle* handle, - std::string filename_stem, - std::string filename_suffix, + const std::string& filename_stem, + const std::string& filename_suffix, heif_decoding_options* decode_options, std::unique_ptr& encoder) { @@ -528,8 +552,8 @@ for (uint32_t ty = 0; ty < tiling.num_rows; ty++) for (uint32_t tx = 0; tx < tiling.num_columns; tx++) { - struct heif_image* image; - struct heif_error err; + heif_image* image; + heif_error err; err = heif_image_handle_decode_image_tile(handle, &image, encoder->colorspace(has_alpha), @@ -615,7 +639,7 @@ //while ((opt = getopt(argc, argv, "q:s")) != -1) { while (true) { int option_index = 0; - int c = getopt_long(argc, argv, "hq:sd:C:vo:", long_options, &option_index); + int c = getopt_long(argc, argv, "hq:sd:C:vo:S", long_options, &option_index); if (c == -1) { break; } @@ -659,17 +683,40 @@ exit(5); } break; + case OPTION_TRANSPARENCY_COMPOSITION_MODE: + if (strcmp(optarg, "white") == 0 || + strcmp(optarg, "black") == 0 || + strcmp(optarg, "checkerboard") == 0) { + transparency_composition_mode = optarg; + } + else { + fprintf(stderr, "Unknown transparency composition mode. Must be one of: white, black, checkerboard.\n"); + exit(5); + } + break; case 'v': - show_version(); + heif_examples::show_version(); return 0; case 'o': output_filename = optarg; break; + case 'S': + option_sequence = 1; + break; + case OPTION_CODEC_THREADS: + option_num_codec_threads = atoi(optarg); + break; + case OPTION_TILE_THREADS: + option_num_tile_threads = atoi(optarg); + break; + case OPTION_EXTRACT_MIME_ITEM: + option_extract_mime_item_type = optarg; + break; } } if (option_list_decoders) { - list_all_decoders(); + heif_examples::list_all_decoders(); return 0; } @@ -749,6 +796,19 @@ #endif // HAVE_LIBTIFF } + if (suffix_lowercase == "webp") { +#if HAVE_LIBWEBP + static const int kDefaultWebpQuality = 75; + if (quality == -1) { + quality = kDefaultWebpQuality; + } + encoder.reset(new WebPEncoder(quality)); +#else + fprintf(stderr, "WEBP support has not been compiled in.\n"); + return 1; +#endif // HAVE_LIBWEBP + } + if (suffix_lowercase == "y4m") { encoder.reset(new Y4MEncoder()); } @@ -766,41 +826,13 @@ // --- check whether input is a supported HEIF file - // TODO: when we are reading from named pipes, we probably should not consume any bytes - // just for file-type checking. - // TODO: check, whether reading from named pipes works at all. - - std::ifstream istr(input_filename.c_str(), std::ios_base::binary); - if (istr.fail()) { - fprintf(stderr, "Input file does not exist.\n"); - return 10; - } - std::array length{}; - istr.read((char*) length.data(), length.size()); - uint32_t box_size = (length[0] << 24) + (length[1] << 16) + (length[2] << 8) + (length[3]); - if ((box_size < 16) || (box_size > 512)) { - fprintf(stderr, "Input file does not appear to start with a valid box length."); - if ((box_size & 0xFFFFFFF0) == 0xFFD8FFE0) { - fprintf(stderr, " Possibly could be a JPEG file instead.\n"); - } else { - fprintf(stderr, "\n"); - } - return 1; - } - - std::vector ftyp_bytes(box_size); - std::copy(length.begin(), length.end(), ftyp_bytes.begin()); - istr.read((char*) ftyp_bytes.data() + 4, ftyp_bytes.size() - 4); - - heif_error filetype_check = heif_has_compatible_filetype(ftyp_bytes.data(), (int)ftyp_bytes.size()); - if (filetype_check.code != heif_error_Ok) { - fprintf(stderr, "Input file is not a supported format. %s\n", filetype_check.message); - return 1; + if (int ret = heif_examples::check_for_valid_input_HEIF_file(input_filename)) { + return ret; } // --- read the HEIF file - struct heif_context* ctx = heif_context_alloc(); + heif_context* ctx = heif_context_alloc(); if (!ctx) { fprintf(stderr, "Could not create context object\n"); return 1; @@ -810,8 +842,12 @@ heif_context_set_security_limits(ctx, heif_get_disabled_security_limits()); } + if (option_num_tile_threads >= 0) { + heif_context_set_max_decoding_threads(ctx, option_num_tile_threads); + } + ContextReleaser cr(ctx); - struct heif_error err; + heif_error err; err = heif_context_read_from_file(ctx, input_filename.c_str(), nullptr); if (err.code != 0) { std::cerr << "Could not read HEIF/AVIF file: " << err.message << "\n"; @@ -819,6 +855,150 @@ } + if (option_sequence) { + if (!heif_context_has_sequence(ctx)) { + std::cerr << "File contains no image sequence\n"; + return 1; + } + + int nTracks = heif_context_number_of_sequence_tracks(ctx); + std::cout << "number of tracks: " << nTracks << "\n"; + + std::vector track_ids(nTracks); + heif_context_get_track_ids(ctx, track_ids.data()); + + for (uint32_t id : track_ids) { + heif_track* track = heif_context_get_track(ctx, id); + std::cout << "#" << id << " : " << heif_examples::fourcc_to_string(heif_track_get_track_handler_type(track)); + + if (heif_track_get_track_handler_type(track) == heif_track_type_image_sequence || + heif_track_get_track_handler_type(track) == heif_track_type_video || + heif_track_get_track_handler_type(track) == heif_track_type_auxiliary) { + uint16_t w,h; + heif_track_get_image_resolution(track, &w, &h); + std::cout << " " << w << "x" << h; + } + else { + uint32_t sample_entry_type = heif_track_get_sample_entry_type_of_first_cluster(track); + std::cout << "\n sample entry type: " << heif_examples::fourcc_to_string(sample_entry_type); + if (sample_entry_type == heif_fourcc('u', 'r', 'i', 'm')) { + const char* uri; + err = heif_track_get_urim_sample_entry_uri_of_first_cluster(track, &uri); + if (err.code) { + std::cerr << "Cannot read 'urim'-track URI: " << err.message << "\n"; + } + std::cout << "\n URI: " << uri; + heif_string_release(uri); + } + + // get metadata track samples + + for (;;) { + heif_raw_sequence_sample* sample; + err = heif_track_get_next_raw_sequence_sample(track, &sample); + if (err.code != 0) { + break; + } + + size_t dataSize; + const uint8_t* data = heif_raw_sequence_sample_get_data(sample, &dataSize); + + std::cout << "\n raw sample: "; + for (uint32_t i = 0; i < dataSize; i++) { + std::cout << " " << std::hex << (int)data[i]; + } + + heif_raw_sequence_sample_release(sample); + } + } + + std::cout << "\n"; + + heif_track_release(track); + } + + std::unique_ptr decode_options(heif_decoding_options_alloc(), heif_decoding_options_free); + encoder->UpdateDecodingOptions(nullptr, decode_options.get()); + decode_options->ignore_sequence_editlist = option_ignore_editlist; + decode_options->strict_decoding = strict_decoding; + decode_options->decoder_id = decoder_id; + decode_options->num_codec_threads = option_num_codec_threads; + decode_options->autocorrect_broken_input = (option_auto_correct != 0); + + heif_track* track = heif_context_get_track(ctx, 0); + + const char* track_contentId = heif_track_get_gimi_track_content_id(track); + if (track_contentId) { + std::cout << "track content ID: " << track_contentId << "\n"; + heif_string_release(track_contentId); + } + + const heif_tai_clock_info* taic = heif_track_get_tai_clock_info_of_first_cluster(track); + if (taic) { + std::cout << "taic: " << taic->time_uncertainty << " / " << taic->clock_resolution << " / " + << taic->clock_drift_rate << " / " << int(taic->clock_type) << "\n"; + } + + int with_alpha = heif_track_has_alpha_channel(track); + + for (int i=0; ;i++) { + heif_image* out_image = nullptr; + int bit_depth = 8; // TODO + err = heif_track_decode_next_image(track, &out_image, + encoder->colorspace(false), + encoder->chroma(with_alpha, bit_depth), + decode_options.get()); + if (err.code == heif_error_End_of_sequence) { + break; + } + else if (err.code) { + std::cerr << err.message << "\n"; + return 1; + } + + std::cout << "sample duration " << heif_image_get_duration(out_image) << "\n"; + + const char* contentID = heif_image_get_gimi_sample_content_id(out_image); + if (contentID) { + std::cout << "content ID " << contentID << "\n"; + heif_string_release(contentID); + } + + heif_tai_timestamp_packet* timestamp; + err = heif_image_get_tai_timestamp(out_image, ×tamp); + if (err.code) { + std::cerr << err.message << "\n"; + return 10; + } + else if (timestamp) { + std::cout << "TAI timestamp: " << timestamp->tai_timestamp << "\n"; + heif_tai_timestamp_packet_release(timestamp); + } + + std::ostringstream s; + s << output_filename_stem; + s << "-" << i+1; + s << "." << output_filename_suffix; + std::string numbered_filename = s.str(); + + bool written = encoder->Encode(nullptr, out_image, numbered_filename); + if (!written) { + fprintf(stderr, "could not write image\n"); + } + else { + if (!option_quiet) { + std::cout << "Written to " << numbered_filename << "\n"; + } + } + heif_image_release(out_image); + } + + heif_track_release(track); + + return 0; + } + + int num_images = heif_context_get_number_of_top_level_images(ctx); if (num_images == 0) { fprintf(stderr, "File doesn't contain any images\n"); @@ -867,6 +1047,8 @@ decode_options->strict_decoding = strict_decoding; decode_options->decoder_id = decoder_id; + decode_options->num_codec_threads = option_num_codec_threads; + decode_options->autocorrect_broken_input = (option_auto_correct != 0); if (!option_quiet) { decode_options->start_progress = start_progress; @@ -885,12 +1067,39 @@ int ret; + auto* color_conversion_options_ext = heif_color_conversion_options_ext_alloc(); + decode_options->color_conversion_options_ext = color_conversion_options_ext; + + + // If output file format does not support transparency, render in the selected composition mode. + + if (!encoder->supports_alpha()) { + if (transparency_composition_mode == "white") { + color_conversion_options_ext->alpha_composition_mode = heif_alpha_composition_mode_solid_color; + color_conversion_options_ext->background_red = 0xFFFFU; + color_conversion_options_ext->background_green = 0xFFFFU; + color_conversion_options_ext->background_blue = 0xFFFFU; + } + else if (transparency_composition_mode == "black") { + color_conversion_options_ext->alpha_composition_mode = heif_alpha_composition_mode_solid_color; + color_conversion_options_ext->background_red = 0; + color_conversion_options_ext->background_green = 0; + color_conversion_options_ext->background_blue = 0; + } + else if (transparency_composition_mode == "checkerboard") { + color_conversion_options_ext->alpha_composition_mode = heif_alpha_composition_mode_checkerboard; + } + } + if (option_output_tiles) { ret = decode_image_tiles(handle, numbered_output_filename_stem, output_filename_suffix, decode_options.get(), encoder); } else { ret = decode_single_image(handle, numbered_output_filename_stem, output_filename_suffix, decode_options.get(), encoder); } + + heif_color_conversion_options_ext_free(color_conversion_options_ext); + if (ret) { heif_image_handle_release(handle); return ret; @@ -901,5 +1110,32 @@ image_index++; } + + // --- extract MIME item data + + if (!option_extract_mime_item_type.empty()) { + int nItems = heif_context_get_number_of_items(ctx); + if (nItems > 0) { + std::vector item_ids(nItems); + heif_context_get_list_of_item_IDs(ctx, item_ids.data(), nItems); + + for (auto id : item_ids) { + uint32_t type = heif_item_get_item_type(ctx, id); + if (type == heif_item_type_mime) { + const char* mimeType = heif_item_get_mime_item_content_type(ctx, id); + if (option_extract_mime_item_type == mimeType) { + uint8_t* data = nullptr; + size_t data_size; + heif_item_get_item_data(ctx, id, nullptr, &data, &data_size); + std::ofstream ostr("mime-item.data"); + ostr.write((const char*)data, data_size); + heif_release_item_data(ctx, &data); + } + } + } + } + + } + return 0; } diff -Nru libheif-1.19.8/examples/heif_enc.cc libheif-1.23.4/examples/heif_enc.cc --- libheif-1.19.8/examples/heif_enc.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/heif_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -40,20 +40,35 @@ #include #include #include +#include +#include +#include #include #include #include "libheif/heif_items.h" +#include "heifio/decoder_heif.h" #include "heifio/decoder_jpeg.h" #include "heifio/decoder_png.h" #include "heifio/decoder_tiff.h" +#include "heifio/decoder_raw.h" #include "heifio/decoder_y4m.h" +#include "heifio/decoder_webp.h" #include "benchmark.h" #include "common.h" +#include "SAI_datafile.h" #include "libheif/api_structs.h" #include "libheif/heif_experimental.h" +#include "libheif/heif_sequences.h" +#include "libheif/heif_uncompressed.h" + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +#include "vmt.h" +#endif + +// --- command line parameters int master_alpha = 1; int thumb_alpha = 1; @@ -61,7 +76,7 @@ int two_colr_boxes = 0; int premultiplied_alpha = 0; int run_benchmark = 0; -int metadata_compression = 0; +heif_metadata_compression metadata_compression_method = heif_metadata_compression_off; int tiled_input_x_y = 0; const char* encoderId = nullptr; std::string chroma_downsampling; @@ -69,7 +84,7 @@ int tiled_image_width = 0; int tiled_image_height = 0; std::string tiling_method = "grid"; -heif_unci_compression unci_compression = heif_unci_compression_brotli; +heif_unci_compression unci_compression = heif_unci_compression_zlib; int add_pyramid_group = 0; uint16_t nclx_colour_primaries = 1; @@ -77,8 +92,90 @@ uint16_t nclx_matrix_coefficients = 6; int nclx_full_range = true; +std::optional clli; +struct pixel_aspect_ratio +{ + uint32_t h,v; +}; +std::optional pasp; + +// default to 30 fps +uint32_t sequence_timebase = 30; +uint32_t sequence_durations = 1; +uint32_t sequence_repetitions = 1; +std::string vmt_metadata_file; +bool binary_metadata_track = false; +std::string metadata_track_uri = "vmt:metadata"; + +int quality = 50; +bool lossless = false; +std::string output_filename; +int logging_level = 0; +bool option_show_parameters = false; +int thumbnail_bbox_size = 0; +int output_bit_depth = 10; +bool force_enc_av1f = false; +bool force_enc_avc = false; +bool force_enc_hevc = false; +bool force_enc_vvc = false; +bool force_enc_uncompressed = false; +bool force_enc_jpeg = false; +bool force_enc_jpeg2000 = false; +bool force_enc_htj2k = false; +bool use_tiling = false; +bool encode_sequence = false; +bool option_unif = false; +bool option_mini = false; +bool use_video_handler = false; +bool option_component_content_ids = false; +heif_orientation transform = heif_orientation_normal; +std::string option_mime_item_type; +std::string option_mime_item_file; +std::string option_mime_item_name; +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +std::string option_turtle_file; +bool option_embed_turtle = false; +#endif + +enum heif_sequence_gop_structure sequence_gop_structure = heif_sequence_gop_structure_lowdelay; +int sequence_keyframe_distance_min = 0; +int sequence_keyframe_distance_max = 0; +int sequence_max_frames = 0; // 0 -> no maximum +std::string option_gimi_track_id; +std::string option_sai_data_file; + +std::optional omaf_image_projection; +std::vector additional_compatible_brands; + +enum heif_output_nclx_color_profile_preset +{ + heif_output_nclx_color_profile_preset_custom, // Default. Use the values provided by the user. + heif_output_nclx_color_profile_preset_automatic, // Choose the profile depending on the input. + heif_output_nclx_color_profile_preset_compatible, // Choose a profile that is decoded correctly by most systems. + heif_output_nclx_color_profile_preset_Rec_601, // SD / JPEG / MPEG + heif_output_nclx_color_profile_preset_Rec_709, // HDTV / (sRGB) + heif_output_nclx_color_profile_preset_Rec_2020 // UHDTV +}; + +heif_output_nclx_color_profile_preset output_color_profile_preset = heif_output_nclx_color_profile_preset_custom; + + std::string property_pitm_description; +RawImageParameters raw_input_params; +bool force_raw_input = false; + +std::optional parse_int(const char* s) +{ + char* end; + long val = strtol(s, &end, 10); + if (*end != '\0' || end == s) { + return {}; + } + return val; +} + + // for benchmarking #if !defined(_MSC_VER) @@ -87,8 +184,8 @@ #if HAVE_GETTIMEOFDAY #include -struct timeval time_encoding_start; -struct timeval time_encoding_end; +timeval time_encoding_start; +timeval time_encoding_end; #endif const int OPTION_NCLX_MATRIX_COEFFICIENTS = 1000; @@ -107,9 +204,145 @@ const int OPTION_TILING_METHOD = 1013; const int OPTION_UNCI_COMPRESSION = 1014; const int OPTION_CUT_TILES = 1015; +const int OPTION_SEQUENCES_TIMEBASE = 1016; +const int OPTION_SEQUENCES_DURATIONS = 1017; +const int OPTION_SEQUENCES_FPS = 1018; +const int OPTION_VMT_METADATA_FILE = 1019; +const int OPTION_SEQUENCES_REPETITIONS = 1020; +const int OPTION_COLOR_PROFILE_PRESET = 1021; +const int OPTION_SET_CLLI = 1022; +const int OPTION_SET_PASP = 1023; +const int OPTION_SEQUENCES_GOP_STRUCTURE = 1024; +const int OPTION_SEQUENCES_MIN_KEYFRAME_DISTANCE = 1025; +const int OPTION_SEQUENCES_MAX_KEYFRAME_DISTANCE = 1026; +const int OPTION_SEQUENCES_MAX_FRAMES = 1027; +const int OPTION_USE_AVC_COMPRESSION = 1028; +const int OPTION_BINARY_METADATA_TRACK = 1029; +const int OPTION_METADATA_TRACK_URI = 1030; +const int OPTION_ADD_MIME_ITEM = 1031; +const int OPTION_MIME_ITEM_FILE = 1032; +const int OPTION_MIME_ITEM_NAME = 1033; +const int OPTION_METADATA_COMPRESSION = 1034; +const int OPTION_SEQUENCES_GIMI_TRACK_ID = 1035; +const int OPTION_SEQUENCES_SAI_DATA_FILE = 1036; +const int OPTION_USE_HEVC_COMPRESSION = 1037; +const int OPTION_SET_OMAF_IMAGE_PROJECTION = 1038; +const int OPTION_ADD_COMPATIBLE_BRAND = 1039; +const int OPTION_UNIF = 1040; +const int OPTION_RAW_WIDTH = 1041; +const int OPTION_RAW_HEIGHT = 1042; +const int OPTION_RAW_TYPE = 1043; +const int OPTION_RAW_ENDIAN = 1044; +const int OPTION_RAW = 1045; +const int OPTION_DO_ROTATE = 1046; +const int OPTION_DO_FLIP_H = 1047; +const int OPTION_DO_FLIP_V = 1048; +const int OPTION_MINI = 1049; + + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +const int OPTION_TURTLE = 2000; +const int OPTION_EMBED_TURTLE = 2001; +#endif + +const int OPTION_COMPONENT_CONTENT_IDS = 2002; + + +static std::string generate_random_uuid_urn() +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(0, 0xFFFFFFFF); + + uint32_t data[4]; + for (auto& d : data) { + d = dist(gen); + } + + auto* bytes = reinterpret_cast(data); + + // Set version 4 (random) and variant 1 (RFC 4122) + bytes[6] = (bytes[6] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + char buf[64]; + snprintf(buf, sizeof(buf), + "urn:uuid:%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], + bytes[6], bytes[7], + bytes[8], bytes[9], + bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]); + return buf; +} + + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +struct TurtleContentIDs { + std::string image_content_id; + // key: {layer, tx, ty} + std::map, std::string> tile_content_ids; +}; + + +enum class TurtlePendingType { + none, + image, + tile +}; -static struct option long_options[] = { +TurtleContentIDs parse_turtle_content_ids(const std::string& filename) +{ + TurtleContentIDs result; + + std::ifstream file(filename); + if (!file) { + std::cerr << "Warning: could not open turtle file: " << filename << "\n"; + return result; + } + + std::string line; + TurtlePendingType pending_type = TurtlePendingType::none; + int pending_layer = 0, pending_tx = 0, pending_ty = 0; + + while (std::getline(file, line)) { + // trim leading whitespace + size_t start = line.find_first_not_of(" \t"); + if (start == std::string::npos) { + continue; // empty line + } + line = line.substr(start); + + if (line.rfind("# image", 0) == 0) { + pending_type = TurtlePendingType::image; + } + else if (line.rfind("# tile ", 0) == 0) { + if (sscanf(line.c_str(), "# tile %d %d %d", &pending_layer, &pending_tx, &pending_ty) == 3) { + pending_type = TurtlePendingType::tile; + } + } + else if (pending_type != TurtlePendingType::none && line[0] == '<') { + // extract URI between < and > + auto close = line.find('>'); + if (close != std::string::npos) { + std::string uri = line.substr(1, close - 1); + if (pending_type == TurtlePendingType::image) { + result.image_content_id = uri; + } + else if (pending_type == TurtlePendingType::tile) { + result.tile_content_ids[{pending_layer, pending_tx, pending_ty}] = uri; + } + } + pending_type = TurtlePendingType::none; + } + } + + return result; +} +#endif + +static option long_options[] = { {(char* const) "help", no_argument, 0, 'h'}, {(char* const) "version", no_argument, 0, 'v'}, {(char* const) "quality", required_argument, 0, 'q'}, @@ -125,35 +358,75 @@ {(char* const) "bit-depth", required_argument, 0, 'b'}, {(char* const) "even-size", no_argument, 0, 'E'}, {(char* const) "avif", no_argument, 0, 'A'}, + {(char* const) "hevc", no_argument, 0, OPTION_USE_HEVC_COMPRESSION}, {(char* const) "vvc", no_argument, 0, OPTION_USE_VVC_COMPRESSION}, + {(char* const) "avc", no_argument, 0, OPTION_USE_AVC_COMPRESSION}, {(char* const) "jpeg", no_argument, 0, OPTION_USE_JPEG_COMPRESSION}, {(char* const) "jpeg2000", no_argument, 0, OPTION_USE_JPEG2000_COMPRESSION}, {(char* const) "htj2k", no_argument, 0, OPTION_USE_HTJ2K_COMPRESSION}, #if WITH_UNCOMPRESSED_CODEC {(char* const) "uncompressed", no_argument, 0, 'U'}, {(char* const) "unci-compression-method", required_argument, nullptr, OPTION_UNCI_COMPRESSION}, +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + {(char* const) "component-content-ids", no_argument, nullptr, OPTION_COMPONENT_CONTENT_IDS}, +#endif #endif + {(char* const) "color-profile", required_argument, 0, OPTION_COLOR_PROFILE_PRESET}, {(char* const) "matrix_coefficients", required_argument, 0, OPTION_NCLX_MATRIX_COEFFICIENTS}, {(char* const) "colour_primaries", required_argument, 0, OPTION_NCLX_COLOUR_PRIMARIES}, {(char* const) "transfer_characteristic", required_argument, 0, OPTION_NCLX_TRANSFER_CHARACTERISTIC}, {(char* const) "full_range_flag", required_argument, 0, OPTION_NCLX_FULL_RANGE_FLAG}, {(char* const) "enable-two-colr-boxes", no_argument, &two_colr_boxes, 1}, + {(char* const) "clli", required_argument, 0, OPTION_SET_CLLI}, + {(char* const) "pasp", required_argument, 0, OPTION_SET_PASP}, {(char* const) "premultiplied-alpha", no_argument, &premultiplied_alpha, 1}, + {(char* const) "rotate-cw", required_argument, 0, OPTION_DO_ROTATE}, + {(char* const) "flip-h", no_argument, 0, OPTION_DO_FLIP_H}, + {(char* const) "flip-v", no_argument, 0, OPTION_DO_FLIP_V}, {(char* const) "plugin-directory", required_argument, 0, OPTION_PLUGIN_DIRECTORY}, {(char* const) "benchmark", no_argument, &run_benchmark, 1}, - {(char* const) "enable-metadata-compression", no_argument, &metadata_compression, 1}, + {(char* const) "enable-metadata-compression", required_argument, 0, OPTION_METADATA_COMPRESSION}, {(char* const) "pitm-description", required_argument, 0, OPTION_PITM_DESCRIPTION}, {(char* const) "chroma-downsampling", required_argument, 0, 'C'}, -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES {(char* const) "cut-tiles", required_argument, nullptr, OPTION_CUT_TILES}, -#endif {(char* const) "tiled-input", no_argument, 0, 'T'}, {(char* const) "tiled-image-width", required_argument, nullptr, OPTION_TILED_IMAGE_WIDTH}, {(char* const) "tiled-image-height", required_argument, nullptr, OPTION_TILED_IMAGE_HEIGHT}, {(char* const) "tiled-input-x-y", no_argument, &tiled_input_x_y, 1}, {(char* const) "tiling-method", required_argument, nullptr, OPTION_TILING_METHOD}, {(char* const) "add-pyramid-group", no_argument, &add_pyramid_group, 1}, - {0, 0, 0, 0}, + {(char* const) "sequence", no_argument, 0, 'S'}, + {(char* const) "video", no_argument, 0, 'V'}, + {(char* const) "timebase", required_argument, nullptr, OPTION_SEQUENCES_TIMEBASE}, + {(char* const) "duration", required_argument, nullptr, OPTION_SEQUENCES_DURATIONS}, + {(char* const) "fps", required_argument, nullptr, OPTION_SEQUENCES_FPS}, + {(char* const) "repetitions", required_argument, nullptr, OPTION_SEQUENCES_REPETITIONS}, + {(char* const) "max-frames", required_argument, nullptr, OPTION_SEQUENCES_MAX_FRAMES}, +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + {(char* const) "vmt-metadata", required_argument, nullptr, OPTION_VMT_METADATA_FILE}, + {(char* const) "binary-metadata-track", no_argument, nullptr, OPTION_BINARY_METADATA_TRACK}, + {(char* const) "metadata-track-uri", required_argument, nullptr, OPTION_METADATA_TRACK_URI}, + {(char* const) "add-mime-item", required_argument, nullptr, OPTION_ADD_MIME_ITEM}, + {(char* const) "mime-item-file", required_argument, nullptr, OPTION_MIME_ITEM_FILE}, + {(char* const) "mime-item-name", required_argument, nullptr, OPTION_MIME_ITEM_NAME}, + {(char* const) "turtle", required_argument, nullptr, OPTION_TURTLE}, + {(char* const) "embed-turtle", no_argument, nullptr, OPTION_EMBED_TURTLE}, +#endif + {(char* const) "gop-structure", required_argument, nullptr, OPTION_SEQUENCES_GOP_STRUCTURE}, + {(char* const) "min-keyframe-distance", required_argument, nullptr, OPTION_SEQUENCES_MIN_KEYFRAME_DISTANCE}, + {(char* const) "max-keyframe-distance", required_argument, nullptr, OPTION_SEQUENCES_MAX_KEYFRAME_DISTANCE}, + {(char* const) "set-gimi-track-id", required_argument, nullptr, OPTION_SEQUENCES_GIMI_TRACK_ID}, + {(char* const) "sai-data-file", required_argument, nullptr, OPTION_SEQUENCES_SAI_DATA_FILE}, + {(char* const) "omaf-image-projection", required_argument, nullptr, OPTION_SET_OMAF_IMAGE_PROJECTION}, + {(char* const) "add-compatible-brand", required_argument, nullptr, OPTION_ADD_COMPATIBLE_BRAND}, + {(char* const) "unif", no_argument, nullptr, OPTION_UNIF}, + {(char* const) "mini", no_argument, nullptr, OPTION_MINI}, + {(char* const) "raw", no_argument, nullptr, OPTION_RAW}, + {(char* const) "raw-width", required_argument, nullptr, OPTION_RAW_WIDTH}, + {(char* const) "raw-height", required_argument, nullptr, OPTION_RAW_HEIGHT}, + {(char* const) "raw-type", required_argument, nullptr, OPTION_RAW_TYPE}, + {(char* const) "raw-endian", required_argument, nullptr, OPTION_RAW_ENDIAN}, + {0, 0, 0, 0} }; @@ -179,58 +452,133 @@ << "Note that when using the prefix, libheif cannot tell you which parameters and values are supported.\n" << "\n" << "Options:\n" - << " -h, --help show help\n" - << " -v, --version show version\n" - << " -q, --quality set output quality (0-100) for lossy compression\n" - << " -L, --lossless generate lossless output (-q has no effect). Image will be encoded as RGB (matrix_coefficients=0).\n" - << " -t, --thumb # generate thumbnail with maximum size # (default: off)\n" - << " --no-alpha do not save alpha channel\n" - << " --no-thumb-alpha do not save alpha channel in thumbnail image\n" - << " -o, --output output filename (optional)\n" - << " --verbose enable logging output (more will increase logging level)\n" - << " -P, --params show all encoder parameters and exit, input file not required or used.\n" - << " -b, --bit-depth # bit-depth of generated HEIF/AVIF file when using 16-bit PNG input (default: 10 bit)\n" - << " -p set encoder parameter (NAME=VALUE)\n" - << " -A, --avif encode as AVIF (not needed if output filename with .avif suffix is provided)\n" - << " --vvc encode as VVC (experimental)\n" - << " --jpeg encode as JPEG\n" - << " --jpeg2000 encode as JPEG 2000 (experimental)\n" - << " --htj2k encode as High Throughput JPEG 2000 (experimental)\n" + << " -h, --help show help\n" + << " -v, --version show version\n" + << " -o, --output output filename (optional)\n" + << " -q, --quality set output quality (0-100) for lossy compression\n" + << " -L, --lossless generate lossless output (-q has no effect). Image will be encoded as RGB (matrix_coefficients=0).\n" + << " -t, --thumb # generate thumbnail with maximum size # (default: off)\n" + << " --no-alpha do not save alpha channel\n" + << " --no-thumb-alpha do not save alpha channel in thumbnail image\n" + << " --verbose enable logging output (more will increase logging level)\n" + << " -b, --bit-depth # number of bits to use from an 16-bit PNG input, valid range: 9-16 (default: 10 bit)\n" + << " --premultiplied-alpha input image has premultiplied alpha\n" +#if WITH_HEADER_COMPRESSION + << " --enable-metadata-compression ALGO enable metadata item compression (experimental)\n" + << " Choose algorithm from {off"; // TODO: add 'auto', but it currently equals 'off' + if (heif_metadata_compression_method_supported(heif_metadata_compression_deflate)) { + std::cerr << ",deflate,zlib"; + } + if (heif_metadata_compression_method_supported(heif_metadata_compression_brotli)) { + std::cerr << ",brotli"; + } + std::cerr << "}.\n" +#endif + << " -C, --chroma-downsampling ALGO force chroma downsampling algorithm (nn = nearest-neighbor / average / sharp-yuv)\n" + << " (sharp-yuv makes edges look sharper when using YUV420 with bilinear chroma upsampling)\n" + << " --benchmark measure encoding time, PSNR, and output file size\n" + << " --pitm-description TEXT set user description for primary image (experimental)\n" +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + << " --add-mime-item TYPE add a mime item of the specified content type (experimental)\n" + << " --mime-item-file FILE use the specified FILE as the data to put into the mime item (experimental)\n" + << " --mime-item-name NAME assign the name to the embedded item (experimental)\n" + << " --turtle FILENAME read Turtle (RDF) file and assign GIMI content IDs to tiles (experimental)\n" + << " --embed-turtle also embed the Turtle file as metadata item in the HEIF (experimental)\n" +#endif + << " --add-compatible-brand BRAND add a compatible brand to the output file (4 characters)\n" + << " --unif use unified ID namespace (adds 'unif' compatible brand)\n" + << " --mini use compact 'mini' box format\n" + << "\n" + << "codecs:\n" + << " -A, --avif encode as AVIF (not needed if output filename with .avif suffix is provided)\n" + << " --hevc encode as HEVC (default)\n" + << " --vvc encode as VVC (experimental)\n" + << " --avc encode as AVC (experimental)\n" + << " --jpeg encode as JPEG\n" + << " --jpeg2000 encode as JPEG 2000 (experimental)\n" + << " --htj2k encode as High Throughput JPEG 2000 (experimental)\n" #if WITH_UNCOMPRESSED_CODEC - << " -U, --uncompressed encode as uncompressed image (according to ISO 23001-17) (EXPERIMENTAL)\n" + << " -U, --uncompressed encode as uncompressed image (according to ISO 23001-17)\n" << " --unci-compression METHOD choose one of these methods: none, deflate, zlib, brotli.\n" #endif - << " --list-encoders list all available encoders for all compression formats\n" - << " -e, --encoder ID select encoder to use (the IDs can be listed with --list-encoders)\n" - << " --plugin-directory DIR load all codec plugins in the directory\n" - << " --matrix_coefficients nclx profile: color conversion matrix coefficients, default=6 (see h.273)\n" - << " --colour_primaries nclx profile: color primaries (see h.273)\n" - << " --transfer_characteristic nclx profile: transfer characteristics (see h.273)\n" - << " --full_range_flag nclx profile: full range flag, default: 1\n" - << " --enable-two-colr-boxes will write both an ICC and an nclx color profile if both are present\n" - << " --premultiplied-alpha input image has premultiplied alpha\n" -#if WITH_HEADER_COMPRESSION - << " --enable-metadata-compression enable XMP metadata compression (experimental)\n" +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + << " --component-content-ids assign random content IDs to the components of an ISO 23001-17 image.\n" #endif - << " -C,--chroma-downsampling ALGO force chroma downsampling algorithm (nn = nearest-neighbor / average / sharp-yuv)\n" - << " (sharp-yuv makes edges look sharper when using YUV420 with bilinear chroma upsampling)\n" - << " --benchmark measure encoding time, PSNR, and output file size\n" - << " --pitm-description TEXT (experimental) set user description for primary image\n" -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES - << " --cut-tiles # cuts the input image into tiles of the given width\n" -#endif - << " -T,--tiled-input input is a set of tile images (only provide one filename with two tile position numbers).\n" - << " For example, 'tile-01-05.jpg' would be a valid input filename.\n" - << " You only have to provide the filename of one tile as input, heif-enc will scan the directory\n" - << " for the other tiles and determine the range of tiles automatically.\n" - << " --tiled-image-width # override image width of tiled image\n" - << " --tiled-image-height # override image height of tiled image\n" - << " --tiled-input-x-y usually, the first number in the input tile filename should be the y position.\n" - << " With this option, this can be swapped so that the first number is x, the second number y.\n" + << " --list-encoders list all available encoders for all compression formats\n" + << " -e, --encoder ID select encoder to use (the IDs can be listed with --list-encoders)\n" + << " --plugin-directory DIR load all codec plugins in the directory\n" + << " -P, --params show all encoder parameters and exit, input file not required or used.\n" + << " -p NAME=VALUE set encoder parameter\n" + << "\n" + << "raw input:\n" + << " --raw force raw pixel data input (for files without .raw/.img suffix)\n" + << " --raw-width # width of raw input image (computed from file size if omitted)\n" + << " --raw-height # height of raw input image (computed from file size if omitted)\n" + << " --raw-type TYPE pixel data type: uint8, sint8, uint16, sint16, uint32, sint32, float32, float64\n" + << " --raw-endian ENDIAN byte order of input data: little (default), big\n" + << "\n" + << "color profile:\n" + << " --color-profile NAME use a color profile preset for the output. Valid values are:\n" + << " custom: (default) use the provided matrix_coefficients, colour_primaries, transfer_characteristic\n" + << " auto: automatically guess suitable values from the input image characteristics\n" + << " compatible: use a profile that is decoded correctly by most applications by avoiding incomplete implementations\n" + << " 601: use Rec.601 (SD), close to JPEG\n" + << " 709: use Rec.709 (HDTV), close to sRGB\n" + << " 2020: use Rec.2020 (UHDTV), transfer curve will be selected based on bits per pixel\n" + << " --matrix_coefficients nclx profile: color conversion matrix coefficients, default=6 (see h.273)\n" + << " --colour_primaries nclx profile: color primaries (see h.273)\n" + << " --transfer_characteristic nclx profile: transfer characteristics (see h.273)\n" + << " --full_range_flag nclx profile: full range flag, default: 1\n" + << " --enable-two-colr-boxes will write both an ICC and an nclx color profile if both are present\n" + << " --clli MaxCLL,MaxPALL add 'content light level information' property to all encoded images\n" + << " --pasp h,v set pixel aspect ratio property to all encoded images\n" + << " --rotate-cw # rotate input image by 0, 90, 180, 270 degrees (clock-wise)\n" + << " --flip-h / --flip-v flip input image horizontally or vertically\n" + << "\n" + << "tiling:\n" + << " --cut-tiles # cuts the input image into square tiles of the given width\n" + << " -T, --tiled-input input is a set of tile images (only provide one filename with two tile position numbers).\n" + << " For example, 'tile-01-05.jpg' would be a valid input filename.\n" + << " You only have to provide the filename of one tile as input, heif-enc will scan the directory\n" + << " for the other tiles and determine the range of tiles automatically.\n" + << " --tiled-image-width # override image width of tiled image\n" + << " --tiled-image-height # override image height of tiled image\n" + << " --tiled-input-x-y usually, the first number in the input tile filename should be the y position.\n" + << " With this option, this can be swapped so that the first number is x, the second number y.\n" +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES || WITH_UNCOMPRESSED_CODEC + << " --tiling-method METHOD choose one of these methods: grid" #if HEIF_ENABLE_EXPERIMENTAL_FEATURES - << " --tiling-method METHOD choose one of these methods: grid, tili, unci. The default is 'grid'.\n" - << " --add-pyramid-group when several images are given, put them into a multi-resolution pyramid group.\n" + ", tili (experimental)" #endif +#if WITH_UNCOMPRESSED_CODEC + ", unci" +#endif + ". The default is 'grid'.\n" +#endif +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + << " --add-pyramid-group when several images are given, put them into a multi-resolution pyramid group. (experimental)\n" +#endif + << "\n" + << "sequences:\n" + << " -S, --sequence encode input images as sequence (input filenames with a number will pull in all files with this pattern).\n" + << " -V, --video encode as video instead of image sequence\n" + << " --timebase # set clock ticks/second for sequence\n" + << " --duration # set frame duration (default: 1)\n" + << " --fps # set timebase and duration based on fps\n" + << " --repetitions # set how often the sequence should be played back (default=1), special value: 'infinite'\n" + << " --gop-structure GOP frame types to use in GOP (intra-only, low-delay, unrestricted)\n" + << " --min-keyframe-distance # minimum distance of keyframes in sequence (0 = undefined)\n" + << " --max-keyframe-distance # maximum distance of keyframes in sequence (0 = undefined)\n" + << " --max-frames # limit sequence length to maximum number of frames\n" +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + << " --vmt-metadata FILE encode metadata track from VMT file (experimental)\n" + << " --binary-metadata-track parses VMT data as hex values that are written as raw binary (experimental)\n" + << " --metadata-track-uri URI uses the URI identifier for the metadata track (experimental)\n" + << " --set-gimi-track-id ID set the GIMI track ID for the visual track (experimental)\n" + << " --sai-data-file FILE use the specified FILE as input data for the video frames SAI data\n" +#endif + << "omnidirectional imagery:\n" + << " --omaf-image-projection PROJ set the image projection (equirectangular, cube-map)\n" ; } @@ -239,7 +587,7 @@ { std::cerr << "Parameters for encoder `" << heif_encoder_get_name(encoder) << "`:\n"; - const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); for (int i = 0; params[i]; i++) { const char* name = heif_encoder_parameter_get_name(params[i]); @@ -336,7 +684,7 @@ } -void set_params(struct heif_encoder* encoder, const std::vector& params) +void set_params(heif_encoder* encoder, const std::vector& params) { for (const std::string& p : params) { auto pos = p.find_first_of('='); @@ -479,6 +827,9 @@ else if (ends_with(filename_lowercase, ".vvic")) { return heif_compression_VVC; } + else if (ends_with(filename_lowercase, ".avci")) { + return heif_compression_AVC; + } else if (ends_with(filename_lowercase, ".heic")) { return heif_compression_HEVC; } @@ -496,6 +847,7 @@ switch (format) { case heif_compression_AV1: return "avif"; case heif_compression_VVC: return "vvic"; + case heif_compression_AVC: return "avci"; case heif_compression_HEVC: return "heic"; case heif_compression_JPEG2000: return "hej2"; default: return "data"; @@ -518,7 +870,7 @@ enum { - PNG, JPEG, Y4M, TIFF + PNG, JPEG, Y4M, TIFF, RAW, WEBP, HEIF } filetype = JPEG; if (suffix == "png") { filetype = PNG; @@ -529,32 +881,71 @@ else if (suffix == "tif" || suffix == "tiff") { filetype = TIFF; } + else if (suffix == "raw" || suffix == "img") { + filetype = RAW; + } + else if (suffix == "webp") { + filetype = WEBP; + } + else if (suffix == "heif" || suffix == "heic" || suffix == "hif" || + suffix == "avif" || suffix == "avifs") { + filetype = HEIF; + } + + if (force_raw_input) { + filetype = RAW; + } if (filetype == PNG) { heif_error err = loadPNG(input_filename.c_str(), output_bit_depth, &input_image); if (err.code != heif_error_Ok) { - std::cerr << "Can not load TIFF input_image: " << err.message << '\n'; + std::cerr << "Can not load PNG input image: " << err.message << '\n'; exit(1); } } else if (filetype == Y4M) { heif_error err = loadY4M(input_filename.c_str(), &input_image); if (err.code != heif_error_Ok) { - std::cerr << "Can not load TIFF input_image: " << err.message << '\n'; + std::cerr << "Can not load Y4M input image: " << err.message << '\n'; exit(1); } } +#if HAVE_LIBTIFF else if (filetype == TIFF) { - heif_error err = loadTIFF(input_filename.c_str(), &input_image); + heif_error err = loadTIFF(input_filename.c_str(), output_bit_depth, &input_image); + if (err.code != heif_error_Ok) { + std::cerr << "Can not load TIFF input image: " << err.message << '\n'; + exit(1); + } + } +#endif + else if (filetype == RAW) { + heif_error err = loadRAW(input_filename.c_str(), raw_input_params, &input_image); + if (err.code != heif_error_Ok) { + std::cerr << "Can not load raw input image: " << err.message << '\n'; + exit(1); + } + } +#if HAVE_LIBWEBP + else if (filetype == WEBP) { + heif_error err = loadWEBP(input_filename.c_str(), &input_image); + if (err.code != heif_error_Ok) { + std::cerr << "Can not load WEBP input image: " << err.message << '\n'; + exit(1); + } + } +#endif + else if (filetype == HEIF) { + heif_error err = loadHEIF(input_filename.c_str(), &input_image); if (err.code != heif_error_Ok) { - std::cerr << "Can not load TIFF input_image: " << err.message << '\n'; + std::cerr << "Can not load HEIF input image: " << err.message << '\n'; exit(1); } } else { heif_error err = loadJPEG(input_filename.c_str(), &input_image); if (err.code != heif_error_Ok) { - std::cerr << "Can not load JPEG input_image: " << err.message << '\n'; + std::cerr << "Can not load JPEG input image: " << err.message << '\n'; exit(1); } } @@ -565,8 +956,9 @@ heif_error create_output_nclx_profile_and_configure_encoder(heif_encoder* encoder, heif_color_profile_nclx** out_nclx, - std::shared_ptr input_image, - bool lossless) + const std::shared_ptr& input_image, + bool lossless, + heif_output_nclx_color_profile_preset profile_preset) { *out_nclx = heif_nclx_color_profile_alloc(); if (!*out_nclx) { @@ -575,6 +967,105 @@ heif_color_profile_nclx* nclx = *out_nclx; // abbreviation; + + // set NCLX based on preset + + switch (profile_preset) { + case heif_output_nclx_color_profile_preset_custom: { + heif_error error = heif_nclx_color_profile_set_matrix_coefficients(nclx, nclx_matrix_coefficients); + if (error.code) { + std::cerr << "Invalid matrix coefficients specified.\n"; + exit(5); + } + + error = heif_nclx_color_profile_set_transfer_characteristics(nclx, nclx_transfer_characteristic); + if (error.code) { + std::cerr << "Invalid transfer characteristics specified.\n"; + exit(5); + } + + error = heif_nclx_color_profile_set_color_primaries(nclx, nclx_colour_primaries); + if (error.code) { + std::cerr << "Invalid color primaries specified.\n"; + exit(5); + } + + nclx->full_range_flag = (uint8_t) nclx_full_range; + break; + } + + case heif_output_nclx_color_profile_preset_automatic: { + heif_color_profile_nclx* input_nclx = nullptr; + + // --- use input image color profile, if it exists + + heif_error error = heif_image_get_nclx_color_profile(input_image.get(), &input_nclx); + if (error.code == heif_error_Color_profile_does_not_exist) { + + // input image has not color profile, guess one + + if (heif_image_get_colorspace(input_image.get()) == heif_colorspace_RGB) { + // sRGB + nclx->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_709_5; // will be overwritten below if lossless + nclx->color_primaries = heif_color_primaries_ITU_R_BT_709_5; + nclx->transfer_characteristics = heif_transfer_characteristic_IEC_61966_2_1; + } + else { + // BT.709 + nclx->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_709_5; // will be overwritten below if lossless + nclx->color_primaries = heif_color_primaries_ITU_R_BT_709_5; + nclx->transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_709_5; + } + } + else if (error.code) { + std::cerr << "Cannot get input NCLX color profile.\n"; + return error; + } + else { + // no error, we have an input color profile that we can use for output too + + nclx->matrix_coefficients = input_nclx->matrix_coefficients; + nclx->transfer_characteristics = input_nclx->transfer_characteristics; + nclx->color_primaries = input_nclx->color_primaries; + nclx->full_range_flag = input_nclx->full_range_flag; + + heif_nclx_color_profile_free(input_nclx); + input_nclx = nullptr; + } + + assert(!input_nclx); + break; + } + + case heif_output_nclx_color_profile_preset_Rec_601: + nclx->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; + nclx->color_primaries = heif_color_primaries_ITU_R_BT_601_6; + nclx->transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_601_6; + break; + + case heif_output_nclx_color_profile_preset_compatible: + case heif_output_nclx_color_profile_preset_Rec_709: + nclx->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_709_5; + nclx->color_primaries = heif_color_primaries_ITU_R_BT_709_5; + nclx->transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_709_5; + break; + + case heif_output_nclx_color_profile_preset_Rec_2020: + nclx->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance; + nclx->color_primaries = heif_color_primaries_ITU_R_BT_2020_2_and_2100_0; + + if (heif_image_has_channel(input_image.get(), heif_channel_Y) && + heif_image_get_bits_per_pixel(input_image.get(), heif_channel_Y) <= 10) { + nclx->transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_2020_2_10bit; + } + else { + nclx->transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_2020_2_12bit; + } + break; + } + + // modify NCLX depending on input image + if (lossless) { heif_encoder_set_lossless(encoder, true); @@ -588,27 +1079,7 @@ } } else { - heif_color_profile_nclx* input_nclx = nullptr; - - heif_error error = heif_image_get_nclx_color_profile(input_image.get(), &input_nclx); - if (error.code == heif_error_Color_profile_does_not_exist) { - // NOP, use default NCLX profile - } - else if (error.code) { - std::cerr << "Cannot get input NCLX color profile.\n"; - return error; - } - else { - nclx->matrix_coefficients = input_nclx->matrix_coefficients; - nclx->transfer_characteristics = input_nclx->transfer_characteristics; - nclx->color_primaries = input_nclx->color_primaries; - nclx->full_range_flag = input_nclx->full_range_flag; - - heif_nclx_color_profile_free(input_nclx); - input_nclx = nullptr; - } - - assert(!input_nclx); + heif_error error; // TODO: this assumes that the encoder plugin has a 'chroma' parameter. Currently, they do, but there should be a better way to set this. switch (heif_image_get_chroma_format(input_image.get())) { @@ -633,25 +1104,6 @@ } } - if (!lossless) { - heif_error error = heif_nclx_color_profile_set_matrix_coefficients(nclx, nclx_matrix_coefficients); - if (error.code) { - std::cerr << "Invalid matrix coefficients specified.\n"; - exit(5); - } - error = heif_nclx_color_profile_set_transfer_characteristics(nclx, nclx_transfer_characteristic); - if (error.code) { - std::cerr << "Invalid transfer characteristics specified.\n"; - exit(5); - } - error = heif_nclx_color_profile_set_color_primaries(nclx, nclx_colour_primaries); - if (error.code) { - std::cerr << "Invalid color primaries specified.\n"; - exit(5); - } - nclx->full_range_flag = (uint8_t) nclx_full_range; - } - return {heif_error_Ok}; } @@ -766,7 +1218,7 @@ return generator; } -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + class input_tiles_generator_cut_image : public input_tiles_generator { public: @@ -774,8 +1226,8 @@ { mImage = load_image(filename, output_bit_depth); - mWidth = heif_image_get_width(mImage.image.get(), heif_channel_Y); - mHeight = heif_image_get_height(mImage.image.get(), heif_channel_Y); + mWidth = heif_image_get_primary_width(mImage.image.get()); + mHeight = heif_image_get_primary_height(mImage.image.get()); mTileSize = tile_size; } @@ -790,7 +1242,7 @@ heif_get_global_security_limits(), &tileImage); if (err.code) { - std::cerr << "error extracting tile " << tx << ";" << ty << std::endl; + std::cerr << "error extracting tile " << tx << ";" << ty << '\n'; exit(1); } @@ -800,14 +1252,53 @@ return tile; } - uint32_t get_image_width() const { return heif_image_get_width(mImage.image.get(), heif_channel_Y); } - uint32_t get_image_height() const { return heif_image_get_height(mImage.image.get(), heif_channel_Y); } + uint32_t get_image_width() const { return heif_image_get_primary_width(mImage.image.get()); } + uint32_t get_image_height() const { return heif_image_get_primary_height(mImage.image.get()); } private: InputImage mImage; uint32_t mWidth, mHeight; int mTileSize; }; + + +#if HAVE_LIBTIFF +class input_tiles_generator_tiff : public input_tiles_generator +{ +public: + input_tiles_generator_tiff(std::shared_ptr reader) + : m_reader(std::move(reader)) + { + } + + uint32_t nColumns() const override { return m_reader->nColumns(); } + uint32_t nRows() const override { return m_reader->nRows(); } + + InputImage get_image(uint32_t tx, uint32_t ty, int output_bit_depth) override + { + heif_image* tile_image = nullptr; + heif_error err = m_reader->readTile(tx, ty, output_bit_depth, &tile_image); + if (err.code != heif_error_Ok) { + std::cerr << "Error reading TIFF tile " << tx << "," << ty << ": " << err.message << "\n"; + exit(1); + } + + InputImage input; + input.image = std::shared_ptr(tile_image, + [](heif_image* img) { heif_image_release(img); }); + return input; + } + + uint32_t imageWidth() const { return m_reader->imageWidth(); } + uint32_t imageHeight() const { return m_reader->imageHeight(); } + uint32_t tileWidth() const { return m_reader->tileWidth(); } + uint32_t tileHeight() const { return m_reader->tileHeight(); } + + void readExif(InputImage* input_image) { m_reader->readExif(input_image); } + +private: + std::shared_ptr m_reader; +}; #endif @@ -815,7 +1306,9 @@ heif_image_handle* encode_tiled(heif_context* ctx, heif_encoder* encoder, heif_encoding_options* options, int output_bit_depth, const std::shared_ptr& tile_generator, - const heif_image_tiling& tiling) + const heif_image_tiling& tiling, + [[maybe_unused]] const std::map, std::string>* tile_content_ids = nullptr, + [[maybe_unused]] int layer_index = 0) { heif_image_handle* tiled_image = nullptr; @@ -840,7 +1333,7 @@ tiled_params.image_height = tiling.image_height; tiled_params.tile_width = tiling.tile_width; tiled_params.tile_height = tiling.tile_height; - tiled_params.offset_field_length = 32; + tiled_params.offset_field_length = 40; tiled_params.size_field_length = 24; tiled_params.tiles_are_sequential = 1; @@ -850,6 +1343,8 @@ return nullptr; } } +#endif +#if WITH_UNCOMPRESSED_CODEC else if (tiling_method == "unci") { heif_unci_image_parameters params{}; params.version = 1; @@ -861,7 +1356,7 @@ InputImage prototype_image = tile_generator->get_image(0,0, output_bit_depth); - heif_error error = heif_context_add_unci_image(ctx, ¶ms, options, prototype_image.image.get(), &tiled_image); + heif_error error = heif_context_add_empty_unci_image(ctx, ¶ms, options, prototype_image.image.get(), &tiled_image); if (error.code != 0) { std::cerr << "Could not generate unci image: " << error.message << "\n"; return nullptr; @@ -879,22 +1374,13 @@ std::cout << "encoding tiled image, tile size: " << tiling.tile_width << "x" << tiling.tile_height << " image size: " << tiling.image_width << "x" << tiling.image_height << "\n"; - int tile_width = 0, tile_height = 0; + int tile_width = tiling.tile_width; + int tile_height = tiling.tile_height; for (uint32_t ty = 0; ty < tile_generator->nRows(); ty++) for (uint32_t tx = 0; tx < tile_generator->nColumns(); tx++) { InputImage input_image = tile_generator->get_image(tx,ty, output_bit_depth); - if (tile_width == 0) { - tile_width = heif_image_get_primary_width(input_image.image.get()); - tile_height = heif_image_get_primary_height(input_image.image.get()); - - if (tile_width <= 0 || tile_height <= 0) { - std::cerr << "Could not read input image size correctly\n"; - return nullptr; - } - } - heif_error error; error = heif_image_extend_to_size_fill_with_zero(input_image.image.get(), tile_width, tile_height); if (error.code) { @@ -905,6 +1391,15 @@ << " (of " << tile_generator->nRows() << "x" << tile_generator->nColumns() << ") \r"; std::cout.flush(); +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + if (tile_content_ids) { + auto it = tile_content_ids->find({layer_index, static_cast(tx), static_cast(ty)}); + if (it != tile_content_ids->end()) { + heif_image_set_gimi_sample_content_id(input_image.image.get(), it->second.c_str()); + } + } +#endif + error = heif_context_add_image_tile(ctx, tiled_image, tx, ty, input_image.image.get(), encoder); @@ -920,6 +1415,72 @@ } +template +std::vector parse_comma_separated_numeric_arguments(const std::string& arg, + std::vector max_val) +{ + std::istringstream ss(arg); + std::string token; + + std::vector results; + + for (size_t i = 0 ; i max_val[i]) return {}; + results.push_back(static_cast(val)); + } catch (...) { + return {}; + } + } + + // There should be no extra tokens + if (ss.rdbuf()->in_avail() != 0) return {}; + + return results; +} + +bool prefix_compare(const char* a, const char* b) +{ + auto minLen = std::min(strlen(a), strlen(b)); + return strncmp(a,b,minLen) == 0; +} + + +bool set_metadata_compression_method(const std::string& arg) +{ + if (arg == "auto") { + metadata_compression_method = heif_metadata_compression_auto; + return true; + } + else if (arg == "off") { + metadata_compression_method = heif_metadata_compression_off; + return true; + } + else if (arg == "brotli") { + metadata_compression_method = heif_metadata_compression_brotli; + return true; + } + else if (arg == "deflate") { + metadata_compression_method = heif_metadata_compression_deflate; + return true; + } + else if (arg == "zlib") { + metadata_compression_method = heif_metadata_compression_zlib; + return true; + } + else { + std::cerr << "Unknown metadata compression method '" << arg << "'. Choose between {auto,off,deflate,zlib,brotli}\n"; + return false; + } +} + + + class LibHeifInitializer { public: @@ -929,32 +1490,22 @@ }; +int do_encode_images(heif_context*, heif_encoder*, heif_encoding_options* options, const std::vector& args); +int do_encode_sequence(heif_context*, heif_encoder*, heif_encoding_options* options, std::vector args); +int add_mime_item(heif_context* context); + + int main(int argc, char** argv) { // This takes care of initializing libheif and also deinitializing it at the end to free all resources. LibHeifInitializer initializer; - int quality = 50; - bool lossless = false; - std::string output_filename; - int logging_level = 0; - bool option_show_parameters = false; - int thumbnail_bbox_size = 0; - int output_bit_depth = 10; - bool force_enc_av1f = false; - bool force_enc_vvc = false; - bool force_enc_uncompressed = false; - bool force_enc_jpeg = false; - bool force_enc_jpeg2000 = false; - bool force_enc_htj2k = false; - bool use_tiling = false; - std::vector raw_params; while (true) { int option_index = 0; - int c = getopt_long(argc, argv, "hq:Lo:vPp:t:b:Ae:C:T" + int c = getopt_long(argc, argv, "hq:Lo:vPp:t:b:Ae:C:TSV" #if WITH_UNCOMPRESSED_CODEC "U" #endif @@ -967,7 +1518,7 @@ show_help(argv[0]); return 0; case 'v': - show_version(); + heif_examples::show_version(); return 0; case 'q': quality = atoi(optarg); @@ -992,6 +1543,10 @@ break; case 'b': output_bit_depth = atoi(optarg); + if (output_bit_depth < 9 || output_bit_depth > 16) { + std::cerr << "Bit depth for input HDR images must be 9-16 bits.\n"; + return 5; + } break; case 'A': force_enc_av1f = true; @@ -1019,9 +1574,15 @@ case OPTION_PITM_DESCRIPTION: property_pitm_description = optarg; break; + case OPTION_USE_HEVC_COMPRESSION: + force_enc_hevc = true; + break; case OPTION_USE_VVC_COMPRESSION: force_enc_vvc = true; break; + case OPTION_USE_AVC_COMPRESSION: + force_enc_avc = true; + break; case OPTION_USE_JPEG_COMPRESSION: force_enc_jpeg = true; break; @@ -1035,7 +1596,7 @@ int nPlugins; heif_error error = heif_load_plugins(optarg, nullptr, &nPlugins, 0); if (error.code) { - std::cerr << "Error loading libheif plugins.\n"; + std::cerr << "Error loading libheif plugins: " << error.message << "\n"; return 1; } @@ -1054,8 +1615,11 @@ case OPTION_TILING_METHOD: tiling_method = optarg; if (tiling_method != "grid" +#if WITH_UNCOMPRESSED_CODEC + && tiling_method != "unci" +#endif #if HEIF_ENABLE_EXPERIMENTAL_FEATURES - && tiling_method != "tili" && tiling_method != "unci" + && tiling_method != "tili" #endif ) { std::cerr << "Invalid tiling method '" << tiling_method << "'\n"; @@ -1085,6 +1649,9 @@ } break; } + case OPTION_COMPONENT_CONTENT_IDS: + option_component_content_ids = true; + break; case 'C': chroma_downsampling = optarg; if (chroma_downsampling != "nn" && @@ -1107,6 +1674,238 @@ case 'T': use_tiling = true; break; + case 'S': + encode_sequence = true; + break; + case 'V': + use_video_handler = true; + break; + case OPTION_SEQUENCES_TIMEBASE: + sequence_timebase = atoi(optarg); + break; + case OPTION_SEQUENCES_DURATIONS: + sequence_durations = atoi(optarg); + break; + case OPTION_SEQUENCES_FPS: + if (strcmp(optarg,"29.97")==0) { + sequence_durations = 1001; + sequence_timebase = 30000; + } + else { + double fps = std::atof(optarg); + sequence_timebase = 90000; + sequence_durations = (uint32_t)(90000 / fps + 0.5); + } + break; + case OPTION_SEQUENCES_REPETITIONS: + if (strcmp(optarg, "infinite")==0) { + sequence_repetitions = heif_sequence_maximum_number_of_repetitions; + } + else { + sequence_repetitions = atoi(optarg); + if (sequence_repetitions == 0) { + std::cerr << "Sequence repetitions may not be 0.\n"; + return 5; + } + } + break; + case OPTION_SEQUENCES_GOP_STRUCTURE: + if (prefix_compare(optarg, "intra-only")) { + sequence_gop_structure = heif_sequence_gop_structure_intra_only; + } + else if (prefix_compare(optarg, "low-delay") || prefix_compare(optarg, "p")) { + sequence_gop_structure = heif_sequence_gop_structure_lowdelay; + } + else if (prefix_compare(optarg, "unrestricted") || prefix_compare(optarg, "b")) { + sequence_gop_structure = heif_sequence_gop_structure_unrestricted; + } + else { + std::cerr << "Invalid GOP structure argument\n"; + return 5; + } + break; + case OPTION_SEQUENCES_MIN_KEYFRAME_DISTANCE: + sequence_keyframe_distance_min = atoi(optarg); + if (sequence_keyframe_distance_min < 0) { + std::cerr << "Keyframe distance must be >= 0\n"; + return 5; + } + break; + case OPTION_SEQUENCES_MAX_KEYFRAME_DISTANCE: + sequence_keyframe_distance_max = atoi(optarg); + if (sequence_keyframe_distance_max < 0) { + std::cerr << "Keyframe distance must be >= 0\n"; + return 5; + } + break; + case OPTION_SEQUENCES_MAX_FRAMES: + sequence_max_frames = atoi(optarg); + if (sequence_max_frames <= 0) { + std::cerr << "Maximum number of frames must be >= 1\n"; + return 5; + } + break; + case OPTION_COLOR_PROFILE_PRESET: + if (strcmp(optarg, "auto")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_automatic; + } + else if (strcmp(optarg, "custom")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_custom; + } + else if (strcmp(optarg, "compatible")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_compatible; + } + else if (strcmp(optarg, "601")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_Rec_601; + } + else if (strcmp(optarg, "709")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_Rec_709; + } + else if (strcmp(optarg, "2020")==0) { + output_color_profile_preset = heif_output_nclx_color_profile_preset_Rec_2020; + } + else { + std::cerr << "Invalid color-profile preset.\n"; + return 5; + } + break; + case OPTION_VMT_METADATA_FILE: + vmt_metadata_file = optarg; + break; + case OPTION_BINARY_METADATA_TRACK: + binary_metadata_track = true; + break; + case OPTION_METADATA_TRACK_URI: + metadata_track_uri = optarg; + break; + case OPTION_SET_CLLI: { + auto clli_args = parse_comma_separated_numeric_arguments(optarg, + { + std::numeric_limits::max(), + std::numeric_limits::max() + }); + if (clli_args.empty()) { + std::cerr << "Invalid arguments for --clli option.\n"; + } + else { + heif_content_light_level clliVal; + clliVal.max_content_light_level = clli_args[0]; + clliVal.max_pic_average_light_level = clli_args[1]; + clli = clliVal; + } + break; + } + case OPTION_SET_PASP: { + auto pasp_args = parse_comma_separated_numeric_arguments(optarg, + { + std::numeric_limits::max(), + std::numeric_limits::max() + }); + if (pasp_args.empty()) { + std::cerr << "Invalid arguments for --pasp option.\n"; + } + else { + pixel_aspect_ratio aspect_ratio; + aspect_ratio.h = pasp_args[0]; + aspect_ratio.v = pasp_args[1]; + pasp = aspect_ratio; + } + break; + } + case OPTION_DO_ROTATE: { + auto angle = parse_int(optarg); + if (!angle || (*angle != 0 && *angle != 90 && *angle != 180 && *angle != 270)) { + std::cerr << "Invalid rotation angle. Must be 0, 90, 180, or 270.\n"; + return 5; + } + if (*angle == 90) { + transform = heif_orientation_concat(transform, heif_orientation_rotate_90_cw); + } + else if (*angle == 180) { + transform = heif_orientation_concat(transform, heif_orientation_rotate_180); + } + else if (*angle == 270) { + transform = heif_orientation_concat(transform, heif_orientation_rotate_270_cw); + } + break; + } + case OPTION_DO_FLIP_H: + transform = heif_orientation_concat(transform, heif_orientation_flip_horizontally); + break; + case OPTION_DO_FLIP_V: + transform = heif_orientation_concat(transform, heif_orientation_flip_vertically); + break; + case OPTION_ADD_MIME_ITEM: + option_mime_item_type = optarg; + break; + case OPTION_MIME_ITEM_FILE: + option_mime_item_file = optarg; + break; + case OPTION_MIME_ITEM_NAME: + option_mime_item_name = optarg; + break; +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + case OPTION_TURTLE: + option_turtle_file = optarg; + break; + case OPTION_EMBED_TURTLE: + option_embed_turtle = true; + break; +#endif + case OPTION_METADATA_COMPRESSION: { + bool success = set_metadata_compression_method(optarg); + if (!success) { + exit(5); + } + break; + } + case OPTION_SEQUENCES_GIMI_TRACK_ID: + option_gimi_track_id = optarg; + break; + case OPTION_SEQUENCES_SAI_DATA_FILE: + option_sai_data_file = optarg; + break; + case OPTION_SET_OMAF_IMAGE_PROJECTION: + if (strcmp(optarg, "equirectangular") == 0) { + omaf_image_projection = heif_omaf_image_projection_equirectangular; + } else if (strcmp(optarg, "cube-map") == 0) { + omaf_image_projection = heif_omaf_image_projection_cube_map; + } else { + std::cerr << "image projection must be 'equirectangular' or 'cube-map'\n"; + return 5; + } + break; + case OPTION_ADD_COMPATIBLE_BRAND: + if (strlen(optarg) != 4) { + std::cerr << "Brand must be exactly 4 characters\n"; + return 5; + } + additional_compatible_brands.push_back(heif_fourcc_to_brand(optarg)); + break; + case OPTION_UNIF: + option_unif = true; + break; + case OPTION_MINI: + option_mini = true; + break; + case OPTION_RAW: + force_raw_input = true; + break; + case OPTION_RAW_WIDTH: + raw_input_params.width = atoi(optarg); + break; + case OPTION_RAW_HEIGHT: + raw_input_params.height = atoi(optarg); + break; + case OPTION_RAW_TYPE: + if (!parse_raw_pixel_type(optarg, &raw_input_params.datatype, &raw_input_params.bit_depth)) { + std::cerr << "Unknown raw pixel type: " << optarg << "\n"; + exit(5); + } + break; + case OPTION_RAW_ENDIAN: + raw_input_params.big_endian = (std::string(optarg) == "big"); + break; } } @@ -1116,8 +1915,30 @@ } if ((force_enc_av1f ? 1 : 0) + (force_enc_vvc ? 1 : 0) + (force_enc_uncompressed ? 1 : 0) + (force_enc_jpeg ? 1 : 0) + - (force_enc_jpeg2000 ? 1 : 0) > 1) { + (force_enc_jpeg2000 ? 1 : 0) + (force_enc_avc ? 1 : 0) + (force_enc_hevc ? 1 : 0) > 1) { std::cerr << "Choose at most one output compression format.\n"; + return 5; + } + + if (encode_sequence && (use_tiling || cut_tiles)) { + std::cerr << "Image sequences cannot be used together with tiling.\n"; + return 5; + } + + if (sequence_timebase <= 0) { + std::cerr << "Sequence clock tick rate cannot be zero.\n"; + return 5; + } + + if (sequence_durations <= 0) { + std::cerr << "Sequence frame durations cannot be zero.\n"; + return 5; + } + + + if (!option_sai_data_file.empty() && !encode_sequence) { + std::cerr << "Image SAI data can only be used with sequences.\n"; + return 5; } if (logging_level > 0) { @@ -1148,6 +1969,9 @@ else if (force_enc_vvc) { compressionFormat = heif_compression_VVC; } + else if (force_enc_avc) { + compressionFormat = heif_compression_AVC; + } else if (force_enc_uncompressed) { compressionFormat = heif_compression_uncompressed; } @@ -1160,6 +1984,9 @@ else if (force_enc_htj2k) { compressionFormat = heif_compression_HTJ2K; } + else if (force_enc_hevc) { + compressionFormat = heif_compression_HEVC; + } else { compressionFormat = guess_compression_format_from_filename(output_filename); } @@ -1178,6 +2005,14 @@ return 1; } + if (option_unif) { + heif_context_set_unif(context.get(), 1); + } + + if (option_mini) { + heif_context_set_write_mini_format(context.get(), 1); + } + #define MAX_ENCODERS 10 const heif_encoder_descriptor* encoder_descriptors[MAX_ENCODERS]; @@ -1243,40 +2078,249 @@ } } - struct heif_error error; + std::vector args; + for (; optind < argc; optind++) { + args.emplace_back(argv[optind]); + } + + + // If we get a list of image filenames, but no '-o' option, assume that the last option + // denotes the output filename. + + if (output_filename.empty() && args.size() > 1) { + output_filename = args.back(); + args.pop_back(); + } + + + if (!lossless) { + heif_encoder_set_lossy_quality(encoder, quality); + } + + heif_encoder_set_logging_level(encoder, logging_level); + + set_params(encoder, raw_params); + struct heif_encoding_options* options = heif_encoding_options_alloc(); + options->save_two_colr_boxes_when_ICC_and_nclx_available = (uint8_t) two_colr_boxes; + + if (chroma_downsampling == "average") { + options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; + options->color_conversion_options.only_use_preferred_chroma_algorithm = true; + } + else if (chroma_downsampling == "sharp-yuv") { + options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_sharp_yuv; + options->color_conversion_options.only_use_preferred_chroma_algorithm = true; + } + else if (chroma_downsampling == "nearest-neighbor") { + options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_nearest_neighbor; + options->color_conversion_options.only_use_preferred_chroma_algorithm = true; + } + + // Stash unci compression in a parameters struct that 'options' will point at. + // Geometry fields are ignored on the heif_context_encode_image() path; only + // 'compression' is consulted there. + heif_unci_image_parameters unci_params_for_encode_image{}; + unci_params_for_encode_image.version = 1; + unci_params_for_encode_image.compression = unci_compression; + options->unci_parameters = &unci_params_for_encode_image; + + // --- if no output filename was given, synthesize one from the first input image filename + + if (output_filename.empty()) { + const std::string& first_input_filename = args[0]; + + std::string filename_without_suffix; + std::string::size_type dot_position = first_input_filename.find_last_of('.'); + if (dot_position != std::string::npos) { + filename_without_suffix = first_input_filename.substr(0, dot_position); + } + else { + filename_without_suffix = first_input_filename; + } + + std::string suffix = suffix_for_compression_format(compressionFormat); + output_filename = filename_without_suffix + '.' + suffix; + } + + + int ret; + + if (!encode_sequence) { + ret = do_encode_images(context.get(), encoder, options, args); + } + else { + ret = do_encode_sequence(context.get(), encoder, options, args); + } + + if (ret != 0) { + heif_encoding_options_free(options); + heif_encoder_release(encoder); + return ret; + } + + // --- add extra MIME item with user data + + ret = add_mime_item(context.get()); + if (ret != 0) { + heif_encoding_options_free(options); + heif_encoder_release(encoder); + return ret; + } + + + // --- write HEIF file + for (heif_brand2 brand : additional_compatible_brands) { + heif_context_add_compatible_brand(context.get(), brand); + } + + heif_error error = heif_context_write_to_file(context.get(), output_filename.c_str()); + if (error.code) { + std::cerr << error.message << "\n"; + return 5; + } + + heif_encoding_options_free(options); + heif_encoder_release(encoder); + + return 0; +} + + +int add_mime_item(heif_context* context) +{ + if (!option_mime_item_file.empty() || !option_mime_item_type.empty()) { + if (option_mime_item_file.empty() || option_mime_item_type.empty()) { + std::cerr << "Options --add-mime-item and --mime-item-file have to be used together\n"; + return 5; + } + + std::ifstream istr(option_mime_item_file.c_str(), std::ios::binary | std::ios::ate); + if (!istr) { + std::cerr << "Failed to open file for MIME item: '" << option_mime_item_file << "'\n"; + return 5; + } + + // Get size by seeking to the end (thanks to ios::ate) + std::streamsize size = istr.tellg(); + if (size < 0) { + std::cerr << "Querying size of file '" << option_mime_item_file << "' failed.\n"; + return 5; + } + + std::vector buffer(size); + + // Seek back to beginning and read + istr.seekg(0, std::ios::beg); + istr.read(reinterpret_cast(buffer.data()), size); + + heif_item_id itemId; + heif_context_add_mime_item(context, option_mime_item_type.c_str(), + metadata_compression_method, + buffer.data(), (int)buffer.size(), + &itemId); + + if (!option_mime_item_name.empty()) { + heif_item_set_item_name(context, itemId, option_mime_item_name.c_str()); + } + } + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + // --- attach Turtle (RDF) file as MIME item + + if (option_embed_turtle && !option_turtle_file.empty()) { + std::ifstream istr(option_turtle_file.c_str(), std::ios::binary | std::ios::ate); + if (!istr) { + std::cerr << "Failed to open turtle file: '" << option_turtle_file << "'\n"; + return 5; + } + + std::streamsize size = istr.tellg(); + if (size < 0) { + std::cerr << "Querying size of turtle file '" << option_turtle_file << "' failed.\n"; + return 5; + } + + std::vector buffer(size); + istr.seekg(0, std::ios::beg); + istr.read(reinterpret_cast(buffer.data()), size); + + heif_item_id itemId; + heif_context_add_mime_item(context, "text/turtle", + heif_metadata_compression_off, + buffer.data(), (int)buffer.size(), + &itemId); + } +#endif + + return 0; +} + + +int do_encode_images(heif_context* context, heif_encoder* encoder, heif_encoding_options* options, const std::vector& args) +{ std::shared_ptr primary_image; bool is_primary_image = true; std::vector encoded_image_ids; - for (; optind < argc; optind++) { - std::string input_filename = argv[optind]; + for (const std::string& input_filename : args) { - if (output_filename.empty()) { - std::string filename_without_suffix; - std::string::size_type dot_position = input_filename.find_last_of('.'); - if (dot_position != std::string::npos) { - filename_without_suffix = input_filename.substr(0, dot_position); - } - else { - filename_without_suffix = input_filename; - } + InputImage input_image; + heif_image_tiling tiling{}; + std::shared_ptr tile_generator; + std::shared_ptr tiff_reader_for_pyramid; - std::string suffix = suffix_for_compression_format(compressionFormat); - output_filename = filename_without_suffix + '.' + suffix; - } +#if HAVE_LIBTIFF + // Auto-detect tiled TIFFs when not using explicit tiling options + if (!use_tiling && cut_tiles == 0) { + std::string suffix; + auto suffix_pos = input_filename.find_last_of('.'); + if (suffix_pos != std::string::npos) { + suffix = input_filename.substr(suffix_pos + 1); + std::transform(suffix.begin(), suffix.end(), suffix.begin(), ::tolower); + } + + if (suffix == "tif" || suffix == "tiff") { + heif_error tiff_err; + auto tiff_reader = TiledTiffReader::open(input_filename.c_str(), &tiff_err); + if (tiff_err.code != heif_error_Ok) { + std::cerr << "Error opening TIFF: " << tiff_err.message << "\n"; + return 1; + } + if (tiff_reader) { + auto shared_tiff_reader = std::shared_ptr(std::move(tiff_reader)); + auto tiff_gen = std::make_shared(shared_tiff_reader); + + // Read tile (0,0) as representative for nclx profile + input_image = tiff_gen->get_image(0, 0, output_bit_depth); + tiff_gen->readExif(&input_image); + + tiling.version = 1; + tiling.num_columns = tiff_gen->nColumns(); + tiling.num_rows = tiff_gen->nRows(); + tiling.tile_width = tiff_gen->tileWidth(); + tiling.tile_height = tiff_gen->tileHeight(); + tiling.image_width = tiff_gen->imageWidth(); + tiling.image_height = tiff_gen->imageHeight(); + tiling.number_of_extra_dimensions = 0; - // ============================================================================== + tile_generator = tiff_gen; + tiff_reader_for_pyramid = shared_tiff_reader; + } + } + } +#endif - InputImage input_image = load_image(input_filename, output_bit_depth); + // If no tiled TIFF was detected, load the image normally + if (!input_image.image) { + input_image = load_image(input_filename, output_bit_depth); + } std::shared_ptr image = input_image.image; - heif_image_tiling tiling{}; - std::shared_ptr tile_generator; if (use_tiling) { tile_generator = determine_input_images_tiling(input_filename, tiled_input_x_y); if (tile_generator) { @@ -1299,7 +2343,6 @@ return 5; } } -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES else if (cut_tiles != 0) { auto cutting_tile_generator = std::make_shared(input_filename.c_str(), cut_tiles, output_bit_depth); @@ -1313,7 +2356,6 @@ tiling.image_height = cutting_tile_generator->get_image_height(); tiling.number_of_extra_dimensions = 0; } -#endif if (!primary_image) { primary_image = image; @@ -1326,57 +2368,46 @@ #endif heif_color_profile_nclx* nclx; - heif_error error = create_output_nclx_profile_and_configure_encoder(encoder, &nclx, primary_image, lossless); + heif_error error = create_output_nclx_profile_and_configure_encoder(encoder, &nclx, primary_image, + lossless, output_color_profile_preset); if (error.code) { std::cerr << error.message << "\n"; return 5; } - if (!lossless) { - heif_encoder_set_lossy_quality(encoder, quality); - } - - heif_encoder_set_logging_level(encoder, logging_level); - - set_params(encoder, raw_params); - struct heif_encoding_options* options = heif_encoding_options_alloc(); options->save_alpha_channel = (uint8_t) master_alpha; - options->save_two_colr_boxes_when_ICC_and_nclx_available = (uint8_t) two_colr_boxes; options->output_nclx_profile = nclx; - options->image_orientation = input_image.orientation; - - if (chroma_downsampling == "average") { - options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; - options->color_conversion_options.only_use_preferred_chroma_algorithm = true; - } - else if (chroma_downsampling == "sharp-yuv") { - options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_sharp_yuv; - options->color_conversion_options.only_use_preferred_chroma_algorithm = true; - } - else if (chroma_downsampling == "nearest-neighbor") { - options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_nearest_neighbor; - options->color_conversion_options.only_use_preferred_chroma_algorithm = true; - } + options->image_orientation = heif_orientation_concat(input_image.orientation, transform); if (premultiplied_alpha) { heif_image_set_premultiplied_alpha(image.get(), premultiplied_alpha); } - struct heif_image_handle* handle; + heif_image_handle* handle; - if (use_tiling || cut_tiles > 0) { - handle = encode_tiled(context.get(), encoder, options, output_bit_depth, tile_generator, tiling); +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + TurtleContentIDs turtle_ids; + if (!option_turtle_file.empty()) { + turtle_ids = parse_turtle_content_ids(option_turtle_file); + } +#endif + + if (tile_generator) { +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + handle = encode_tiled(context, encoder, options, output_bit_depth, tile_generator, tiling, + option_turtle_file.empty() ? nullptr : &turtle_ids.tile_content_ids, 0); +#else + handle = encode_tiled(context, encoder, options, output_bit_depth, tile_generator, tiling); +#endif } else { - error = heif_context_encode_image(context.get(), + error = heif_context_encode_image(context, image.get(), encoder, options, &handle); if (error.code != 0) { - heif_encoding_options_free(options); heif_nclx_color_profile_free(nclx); - heif_encoder_release(encoder); std::cerr << "Could not encode HEIF/AVIF file: " << error.message << "\n"; return 1; } @@ -1387,23 +2418,98 @@ return 1; } + if (clli) { + heif_image_handle_set_content_light_level(handle, &*clli); + } + + if (pasp) { + heif_image_handle_set_pixel_aspect_ratio(handle, pasp->h, pasp->v); + } + + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + if (!option_turtle_file.empty() && !turtle_ids.image_content_id.empty()) { + heif_image_handle_set_gimi_content_id(handle, turtle_ids.image_content_id.c_str()); + } +#endif + + if (omaf_image_projection) { + heif_image_handle_set_omaf_image_projection(handle, *omaf_image_projection); + } + + if (option_component_content_ids && force_enc_uncompressed) { + uint32_t num_components = heif_image_handle_get_number_of_cmpd_components(handle); + for (uint32_t i = 0; i < num_components; i++) { + std::string uuid = generate_random_uuid_urn(); + heif_image_handle_set_gimi_component_content_id(handle, i, uuid.c_str()); + } + } + if (is_primary_image) { - heif_context_set_primary_image(context.get(), handle); + heif_context_set_primary_image(context, handle); } encoded_image_ids.push_back(heif_image_handle_get_item_id(handle)); +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES && HAVE_LIBTIFF + // Auto-encode TIFF pyramid overviews + if (tiff_reader_for_pyramid && !tiff_reader_for_pyramid->overviews().empty()) { + heif_item_id fullres_id = heif_image_handle_get_item_id(handle); + std::vector pyramid_ids; + pyramid_ids.push_back(fullres_id); + + int ov_layer_index = 1; + for (const auto& ov : tiff_reader_for_pyramid->overviews()) { + if (!tiff_reader_for_pyramid->setDirectory(ov.dir_index)) { + std::cerr << "Warning: could not switch to TIFF overview directory " << ov.dir_index << "\n"; + continue; + } + + auto ov_gen = std::make_shared(tiff_reader_for_pyramid); + + heif_image_tiling ov_tiling{}; + ov_tiling.version = 1; + ov_tiling.num_columns = ov_gen->nColumns(); + ov_tiling.num_rows = ov_gen->nRows(); + ov_tiling.tile_width = ov_gen->tileWidth(); + ov_tiling.tile_height = ov_gen->tileHeight(); + ov_tiling.image_width = ov_gen->imageWidth(); + ov_tiling.image_height = ov_gen->imageHeight(); + ov_tiling.number_of_extra_dimensions = 0; + + heif_image_handle* ov_handle = encode_tiled(context, encoder, options, output_bit_depth, ov_gen, ov_tiling, + option_turtle_file.empty() ? nullptr : &turtle_ids.tile_content_ids, + ov_layer_index); + if (ov_handle) { + pyramid_ids.push_back(heif_image_handle_get_item_id(ov_handle)); + heif_image_handle_release(ov_handle); + } + ov_layer_index++; + } + + // Restore directory 0 for any subsequent use + tiff_reader_for_pyramid->setDirectory(0); + + if (pyramid_ids.size() > 1) { + error = heif_context_add_pyramid_entity_group(context, pyramid_ids.data(), pyramid_ids.size(), nullptr); + if (error.code) { + std::cerr << "Cannot create pyramid entity group: " << error.message << "\n"; + return 5; + } + std::cout << "Created pyramid entity group with " << pyramid_ids.size() << " layers\n"; + } + } +#endif + // write EXIF to HEIC if (!input_image.exif.empty()) { // Note: we do not modify the EXIF Orientation here because we want it to match the HEIF transforms. // TODO: is this a good choice? Or should we set it to 1 (normal) so that other, faulty software will not transform it once more? - error = heif_context_add_exif_metadata(context.get(), handle, + error = heif_context_add_exif_metadata(context, handle, input_image.exif.data(), (int) input_image.exif.size()); if (error.code != 0) { - heif_encoding_options_free(options); heif_nclx_color_profile_free(nclx); - heif_encoder_release(encoder); std::cerr << "Could not write EXIF metadata: " << error.message << "\n"; return 1; } @@ -1411,13 +2517,11 @@ // write XMP to HEIC if (!input_image.xmp.empty()) { - error = heif_context_add_XMP_metadata2(context.get(), handle, + error = heif_context_add_XMP_metadata2(context, handle, input_image.xmp.data(), (int) input_image.xmp.size(), - metadata_compression ? heif_metadata_compression_deflate : heif_metadata_compression_off); + metadata_compression_method); if (error.code != 0) { - heif_encoding_options_free(options); heif_nclx_color_profile_free(nclx); - heif_encoder_release(encoder); std::cerr << "Could not write XMP metadata: " << error.message << "\n"; return 1; } @@ -1430,7 +2534,7 @@ options->save_alpha_channel = master_alpha && thumb_alpha; - error = heif_context_encode_thumbnail(context.get(), + error = heif_context_encode_thumbnail(context, image.get(), handle, encoder, @@ -1438,9 +2542,7 @@ thumbnail_bbox_size, &thumbnail_handle); if (error.code) { - heif_encoding_options_free(options); heif_nclx_color_profile_free(nclx); - heif_encoder_release(encoder); std::cerr << "Could not generate thumbnail: " << error.message << "\n"; return 5; } @@ -1457,17 +2559,14 @@ #endif heif_image_handle_release(handle); - heif_encoding_options_free(options); heif_nclx_color_profile_free(nclx); is_primary_image = false; } - heif_encoder_release(encoder); - if (!property_pitm_description.empty()) { heif_image_handle* primary_image_handle; - struct heif_error err = heif_context_get_primary_image_handle(context.get(), &primary_image_handle); + struct heif_error err = heif_context_get_primary_image_handle(context, &primary_image_handle); if (err.code) { std::cerr << "No primary image set, cannot set user description\n"; return 5; @@ -1480,7 +2579,7 @@ udes.name = nullptr; udes.tags = nullptr; udes.description = property_pitm_description.c_str(); - err = heif_item_add_property_user_description(context.get(), pitm_id, &udes, nullptr); + err = heif_item_add_property_user_description(context, pitm_id, &udes, nullptr); if (err.code) { std::cerr << "Cannot set user description\n"; return 5; @@ -1491,7 +2590,7 @@ #if HEIF_ENABLE_EXPERIMENTAL_FEATURES if (add_pyramid_group && encoded_image_ids.size() > 1) { - error = heif_context_add_pyramid_entity_group(context.get(), encoded_image_ids.data(), encoded_image_ids.size(), nullptr); + heif_error error = heif_context_add_pyramid_entity_group(context, encoded_image_ids.data(), encoded_image_ids.size(), nullptr); if (error.code) { std::cerr << "Cannot set multi-resolution pyramid: " << error.message << "\n"; return 5; @@ -1499,12 +2598,6 @@ } #endif - error = heif_context_write_to_file(context.get(), output_filename.c_str()); - if (error.code) { - std::cerr << error.message << "\n"; - return 5; - } - if (run_benchmark) { double psnr = compute_psnr(primary_image.get(), output_filename); std::cout << "PSNR: " << std::setprecision(2) << std::fixed << psnr << " "; @@ -1521,4 +2614,237 @@ } return 0; +} + + + + +std::vector deflate_input_filenames(const std::string& filename_example) +{ + std::regex pattern(R"((.*\D)?(\d+)(\..+)$)"); + std::smatch match; + + if (!std::regex_match(filename_example, match, pattern)) { + return {filename_example}; + } + + std::string prefix = match[1]; + + auto p = std::filesystem::absolute(std::filesystem::path(prefix)); + std::filesystem::path directory = p.parent_path(); + std::string filename_prefix = p.filename().string(); // TODO: we could also use u8string(), but it is not well supported in C++20 + std::string number = match[2]; + std::string suffix = match[3]; + + + std::string patternString = filename_prefix + "(\\d+)" + suffix + "$"; + pattern = patternString; + + uint32_t digits = std::numeric_limits::max(); + uint32_t start = std::numeric_limits::max(); + uint32_t end = 0; + + for (const auto& dirEntry : std::filesystem::directory_iterator(directory)) + { + if (dirEntry.is_regular_file()) { + std::string s{dirEntry.path().filename().string()}; + + if (std::regex_match(s, match, pattern)) { + digits = std::min(digits, (uint32_t)match[1].length()); + + uint32_t number = std::stoi(match[1]); + start = std::min(start, number); + end = std::max(end, number); + } + } + } + + + std::vector files; + + for (uint32_t i=start;i<=end;i++) + { + std::stringstream sstr; + + sstr << prefix << std::setw(digits) << std::setfill('0') << i << suffix; + + std::filesystem::path p = directory / sstr.str(); + files.emplace_back(p.string()); + } + + return files; +} + + +int do_encode_sequence(heif_context* context, heif_encoder* encoder, heif_encoding_options* options, std::vector args) +{ + if (args.size() == 1) { + args = deflate_input_filenames(args[0]); + } + + size_t currImage = 0; + + size_t nImages = args.size(); + if (sequence_max_frames && nImages > static_cast(sequence_max_frames)) { + nImages = sequence_max_frames; + } + + // --- optionally load SAI data to be used for the frames + + SAI_datafile sai_data; + if (!option_sai_data_file.empty()) { + sai_data.load_sai_data_from_file(option_sai_data_file.c_str()); + } + + + uint16_t image_width=0, image_height=0; + + bool first_image = true; + + heif_track* track = nullptr; + heif_sequence_encoding_options* encoding_options = nullptr; + + for (const std::string& input_filename : args) { + currImage++; + if (currImage > nImages) { + break; + } + + std::cout << "\rencoding sequence image " << currImage << "/" << nImages; + std::cout.flush(); + + InputImage input_image = load_image(input_filename, output_bit_depth); + + std::shared_ptr image = input_image.image; + + int w = heif_image_get_primary_width(image.get()); + int h = heif_image_get_primary_height(image.get()); + + if (w > 0xFFFF || h > 0xFFFF) { + std::cerr << "maximum image size of 65535x65535 exceeded\n"; + return 5; + } + + if (first_image) { + heif_track_options* track_options = heif_track_options_alloc(); + heif_track_options_set_timescale(track_options, sequence_timebase); + + if (!option_gimi_track_id.empty()) { + heif_track_options_set_gimi_track_id(track_options, option_gimi_track_id.c_str()); + } + + if (sai_data.tai_clock_info) { + heif_track_options_enable_sample_tai_timestamps(track_options, + sai_data.tai_clock_info, + heif_sample_aux_info_presence_optional); + } + + if (!sai_data.gimi_content_ids.empty()) { + heif_track_options_enable_sample_gimi_content_ids(track_options, + heif_sample_aux_info_presence_optional); + } + + heif_context_set_sequence_timescale(context, sequence_timebase); + heif_context_set_number_of_sequence_repetitions(context, sequence_repetitions); + + encoding_options = heif_sequence_encoding_options_alloc(); + encoding_options->gop_structure = sequence_gop_structure; + encoding_options->keyframe_distance_min = sequence_keyframe_distance_min; + encoding_options->keyframe_distance_max = sequence_keyframe_distance_max; + encoding_options->save_alpha_channel = master_alpha; + + image_width = static_cast(w); + image_height = static_cast(h); + + heif_context_add_visual_sequence_track(context, + image_width, image_height, + use_video_handler ? heif_track_type_video : heif_track_type_image_sequence, + track_options, + encoding_options, + &track); + + heif_track_options_release(track_options); + + first_image = false; + } + + if (image_width != static_cast(w) || + image_height != static_cast(h)) { + std::cerr << "image '" << input_filename << "' has size " << w << "x" << h + << " which is different from the first image size " << image_width << "x" << image_height << "\n"; + return 5; + } + + heif_color_profile_nclx* nclx; + heif_error error = create_output_nclx_profile_and_configure_encoder(encoder, &nclx, image, lossless, output_color_profile_preset); + if (error.code) { + std::cerr << error.message << "\n"; + return 5; + } + + //seq_options->save_alpha_channel = false; // TODO: sequences with alpha ? + encoding_options->output_nclx_profile = nclx; + //seq_options->image_orientation = heif_orientation_normal; // input_image.orientation; TODO: sequence rotation + + heif_image_set_duration(image.get(), sequence_durations); + + // --- set SAI data + + if (currImage-1 < sai_data.gimi_content_ids.size()) { + if (!sai_data.gimi_content_ids[currImage-1].empty()) { + heif_image_set_gimi_sample_content_id(image.get(), sai_data.gimi_content_ids[currImage-1].c_str()); + } + } + + if (currImage-1 < sai_data.tai_timestamps.size()) { + if (sai_data.tai_timestamps[currImage-1]) { + heif_image_set_tai_timestamp(image.get(), sai_data.tai_timestamps[currImage-1]); + } + } + + // --- encode image + + error = heif_track_encode_sequence_image(track, image.get(), encoder, encoding_options); + if (error.code) { + heif_nclx_color_profile_free(nclx); + std::cerr << "Cannot encode sequence image: " << error.message << "\n"; + return 5; + } + + heif_nclx_color_profile_free(nclx); + } + + if (!track) { + std::cerr << "No input files could be encoded into sequence track\n"; + return 5; + } + + std::cout << "\n"; + + heif_error error = heif_track_encode_end_of_sequence(track, encoder); + if (error.code) { + std::cerr << "Cannot end sequence: " << error.message << "\n"; + return 5; + } + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + if (!vmt_metadata_file.empty()) { + int ret = encode_vmt_metadata_track(context, track, vmt_metadata_file, metadata_track_uri, binary_metadata_track); + if (ret) { + return ret; + } + } +#endif + + heif_track_release(track); + heif_sequence_encoding_options_release(encoding_options); + + + // --- add first image as image item + + if (!use_video_handler) { + do_encode_images(context, encoder, options, {args[0]}); + } + + return 0; } diff -Nru libheif-1.19.8/examples/heif_gen_bayer.cc libheif-1.23.4/examples/heif_gen_bayer.cc --- libheif-1.19.8/examples/heif_gen_bayer.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/heif_gen_bayer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,664 @@ +/* + libheif example application "heif-gen-bayer". + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "heifio/decoder_png.h" + + +struct PatternDefinition +{ + std::string name; + uint16_t width; + uint16_t height; + std::vector cpat; +}; + + +static const PatternDefinition patterns[] = { + // RGGB (standard Bayer) + // R G + // G B + { + "rggb", 2, 2, + { + heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue, + } + }, + + // RGBW (Red-Green-Blue-White) — 4×4 + // W G W R + // G W B W + // W B W G + // R W G W + // White is an unfiltered (panchromatic) pixel → Y component type. + { + "rgbw", 4, 4, + { + heif_cmpd_component_type_Y, + heif_cmpd_component_type_green, + heif_cmpd_component_type_Y, + heif_cmpd_component_type_red, + + heif_cmpd_component_type_green, + heif_cmpd_component_type_Y, + heif_cmpd_component_type_blue, + heif_cmpd_component_type_Y, + + heif_cmpd_component_type_Y, + heif_cmpd_component_type_blue, + heif_cmpd_component_type_Y, + heif_cmpd_component_type_green, + + heif_cmpd_component_type_red, + heif_cmpd_component_type_Y, + heif_cmpd_component_type_green, + heif_cmpd_component_type_Y, + } + }, + + // QBC (Quad Bayer Coding) — 4×4 + // G G R R + // G G R R + // B B G G + // B B G G + { + "qbc", 4, 4, + { + heif_cmpd_component_type_green, + heif_cmpd_component_type_green, + heif_cmpd_component_type_red, + heif_cmpd_component_type_red, + + heif_cmpd_component_type_green, + heif_cmpd_component_type_green, + heif_cmpd_component_type_red, + heif_cmpd_component_type_red, + + heif_cmpd_component_type_blue, + heif_cmpd_component_type_blue, + heif_cmpd_component_type_green, + heif_cmpd_component_type_green, + + heif_cmpd_component_type_blue, + heif_cmpd_component_type_blue, + heif_cmpd_component_type_green, + heif_cmpd_component_type_green, + } + }, +}; + +static constexpr int num_patterns = sizeof(patterns) / sizeof(patterns[0]); + + +static const PatternDefinition* find_pattern(const char* name) +{ + for (int i = 0; i < num_patterns; i++) { + if (strcasecmp(patterns[i].name.c_str(), name) == 0) { + return &patterns[i]; + } + } + return nullptr; +} + + +static std::optional parse_pattern_string(const char* str) +{ + std::string s(str); + size_t len = s.size(); + if (len != 4 && len != 16) { + return {}; + } + + uint16_t dim = (len == 4) ? 2 : 4; + std::vector cpat; + cpat.reserve(len); + + for (char c : s) { + switch (std::tolower(c)) { + case 'r': cpat.push_back(heif_cmpd_component_type_red); break; + case 'g': cpat.push_back(heif_cmpd_component_type_green); break; + case 'b': cpat.push_back(heif_cmpd_component_type_blue); break; + default: return {}; + } + } + + return PatternDefinition{str, dim, dim, std::move(cpat)}; +} + + +static std::vector deflate_input_filenames(const std::string& filename_example) +{ + std::regex pattern(R"((.*\D)?(\d+)(\..+)$)"); + std::smatch match; + + if (!std::regex_match(filename_example, match, pattern)) { + return {filename_example}; + } + + std::string prefix = match[1]; + + auto p = std::filesystem::absolute(std::filesystem::path(prefix)); + std::filesystem::path directory = p.parent_path(); + std::string filename_prefix = p.filename().string(); + std::string number = match[2]; + std::string suffix = match[3]; + + std::string patternString = filename_prefix + "(\\d+)" + suffix + "$"; + pattern = patternString; + + uint32_t digits = std::numeric_limits::max(); + uint32_t start = std::numeric_limits::max(); + uint32_t end = 0; + + for (const auto& dirEntry : std::filesystem::directory_iterator(directory)) + { + if (dirEntry.is_regular_file()) { + std::string s{dirEntry.path().filename().string()}; + + if (std::regex_match(s, match, pattern)) { + digits = std::min(digits, (uint32_t)match[1].length()); + + uint32_t number = std::stoi(match[1]); + start = std::min(start, number); + end = std::max(end, number); + } + } + } + + std::vector files; + + for (uint32_t i = start; i <= end; i++) + { + std::stringstream sstr; + + sstr << prefix << std::setw(digits) << std::setfill('0') << i << suffix; + + std::filesystem::path p = directory / sstr.str(); + files.emplace_back(p.string()); + } + + return files; +} + + +static void print_usage() +{ + std::cerr << "Usage: heif-gen-bayer [options] \n" + << " heif-gen-bayer -S [options] \n\n" + << "Options:\n" + << " -h, --help show this help\n" + << " -b, --bit-depth # output bit depth (default: 8, range: 8-16)\n" + << " -p, --pattern filter array pattern (default: rggb)\n" + << " -S, --sequence sequence mode (expand numbered PNGs)\n" + << " -V, --video use video track handler (vide) instead of pict\n" + << " --fps frames per second (default: 30)\n\n" + << "Patterns:\n"; + for (int i = 0; i < num_patterns; i++) { + std::cerr << " " << patterns[i].name + << " (" << patterns[i].width << "x" << patterns[i].height << ")" + << (i == 0 ? " [default]" : "") + << "\n"; + } + std::cerr << " Or specify a custom R/G/B string of length 4 (2x2) or 16 (4x4), e.g. -p BGGR\n"; +} + + +static struct option long_options[] = { + {(char* const) "help", no_argument, nullptr, 'h'}, + {(char* const) "bit-depth", required_argument, nullptr, 'b'}, + {(char* const) "pattern", required_argument, nullptr, 'p'}, + {(char* const) "sequence", no_argument, nullptr, 'S'}, + {(char* const) "video", no_argument, nullptr, 'V'}, + {(char* const) "fps", required_argument, nullptr, 'f'}, + {nullptr, 0, nullptr, 0} +}; + + +// Create a bayer image from a PNG file. Returns nullptr on error. +// If expected_width/expected_height are non-zero, the PNG must match those dimensions. +static heif_image* create_bayer_image_from_png(const char* png_filename, + const PatternDefinition* pat, + int output_bit_depth, + int expected_width, + int expected_height) +{ + InputImage input_image; + heif_error err = loadPNG(png_filename, output_bit_depth, &input_image); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot load PNG '" << png_filename << "': " << err.message << "\n"; + return nullptr; + } + + heif_image* src_img = input_image.image.get(); + + int width = heif_image_get_primary_width(src_img); + int height = heif_image_get_primary_height(src_img); + + if (expected_width != 0 && (width != expected_width || height != expected_height)) { + std::cerr << "Frame '" << png_filename << "' has dimensions " << width << "x" << height + << " but expected " << expected_width << "x" << expected_height << "\n"; + return nullptr; + } + + if (width % pat->width != 0 || height % pat->height != 0) { + std::cerr << "Image dimensions must be multiples of the pattern size (" + << pat->width << "x" << pat->height << "). Got " + << width << "x" << height << "\n"; + return nullptr; + } + + // Get source RGB data + int src_stride; + const uint8_t* src_data = heif_image_get_plane_readonly(src_img, heif_channel_interleaved, &src_stride); + if (!src_data) { + std::cerr << "Failed to get interleaved RGB plane from PNG.\n"; + return nullptr; + } + + // Create Bayer image + heif_image* bayer_img = nullptr; + err = heif_image_create(width, height, + heif_colorspace_filter_array, + heif_chroma_monochrome, + &bayer_img); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot create image: " << err.message << "\n"; + return nullptr; + } + + uint32_t filter_array_component_id; + + err = heif_image_add_component(bayer_img, width, height, heif_cmpd_component_type_filter_array, heif_component_datatype_unsigned_integer, output_bit_depth, &filter_array_component_id); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot add plane: " << err.message << "\n"; + heif_image_release(bayer_img); + return nullptr; + } + + // Convert RGB to filter array using the selected pattern + if (output_bit_depth == 8) { + int dst_stride; + uint8_t* dst_data = heif_image_get_plane(bayer_img, heif_channel_filter_array, &dst_stride); + + for (int y = 0; y < height; y++) { + const uint8_t* src_row = src_data + y * src_stride; + uint8_t* dst_row = dst_data + y * dst_stride; + + for (int x = 0; x < width; x++) { + uint8_t r = src_row[x * 3 + 0]; + uint8_t g = src_row[x * 3 + 1]; + uint8_t b = src_row[x * 3 + 2]; + + int px = x % pat->width; + int py = y % pat->height; + auto comp_type = pat->cpat[py * pat->width + px]; + + switch (comp_type) { + case heif_cmpd_component_type_red: dst_row[x] = r; break; + case heif_cmpd_component_type_green: dst_row[x] = g; break; + case heif_cmpd_component_type_blue: dst_row[x] = b; break; + case heif_cmpd_component_type_Y: dst_row[x] = static_cast((r + g + b) / 3); break; + default: + assert(false); + } + } + } + } + else { + int dst_stride; + uint8_t* dst_raw = heif_image_get_plane(bayer_img, heif_channel_filter_array, &dst_stride); + auto* dst_data = reinterpret_cast(dst_raw); + int dst_stride16 = dst_stride / 2; + + for (int y = 0; y < height; y++) { + const uint8_t* src_row = src_data + y * src_stride; + uint16_t* dst_row = dst_data + y * dst_stride16; + + for (int x = 0; x < width; x++) { + // Source is little-endian uint16_t per component, 3 components per pixel + uint16_t r = src_row[x * 6 + 0] | (src_row[x * 6 + 1] << 8); + uint16_t g = src_row[x * 6 + 2] | (src_row[x * 6 + 3] << 8); + uint16_t b = src_row[x * 6 + 4] | (src_row[x * 6 + 5] << 8); + + int px = x % pat->width; + int py = y % pat->height; + auto comp_type = pat->cpat[py * pat->width + px]; + + switch (comp_type) { + case heif_cmpd_component_type_red: dst_row[x] = r; break; + case heif_cmpd_component_type_green: dst_row[x] = g; break; + case heif_cmpd_component_type_blue: dst_row[x] = b; break; + case heif_cmpd_component_type_Y: dst_row[x] = static_cast((r + g + b) / 3); break; + default: + assert(false); + } + } + } + } + + // map component type to component id + + std::map map_type_to_id; + for (const auto& type : pat->cpat) { + if (map_type_to_id.find(type) == map_type_to_id.end()) { + uint32_t component_id; + heif_image_add_bayer_component(bayer_img, type, &component_id); + + map_type_to_id[type] = component_id; + } + } + + // Build heif_bayer_pattern_pixel array from component types. + // The component_index values here are the component types themselves — the encoder + // will resolve them to proper cmpd indices when writing the cpat box. + std::vector bayer_pixels(pat->cpat.size()); + for (size_t i = 0; i < pat->cpat.size(); i++) { + bayer_pixels[i].component_id = map_type_to_id[pat->cpat[i]]; + bayer_pixels[i].component_gain = 1.0f; + } + + // Set Bayer pattern metadata + err = heif_image_set_bayer_pattern(bayer_img, + filter_array_component_id, + pat->width, pat->height, + bayer_pixels.data()); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot set Bayer pattern: " << err.message << "\n"; + heif_image_release(bayer_img); + return nullptr; + } + + return bayer_img; +} + + +static int encode_sequence(const std::vector& filenames, + const PatternDefinition* pat, + int output_bit_depth, + int fps, + bool use_video_handler, + const char* output_filename) +{ + heif_context* ctx = heif_context_alloc(); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot get uncompressed encoder: " << err.message << "\n"; + heif_context_free(ctx); + return 1; + } + + heif_context_set_sequence_timescale(ctx, fps); + + heif_sequence_encoding_options* enc_options = heif_sequence_encoding_options_alloc(); + heif_track* track = nullptr; + int first_width = 0, first_height = 0; + + for (size_t i = 0; i < filenames.size(); i++) { + heif_image* bayer_img = create_bayer_image_from_png(filenames[i].c_str(), pat, + output_bit_depth, + first_width, first_height); + if (!bayer_img) { + heif_sequence_encoding_options_release(enc_options); + if (track) heif_track_release(track); + heif_encoder_release(encoder); + heif_context_free(ctx); + return 1; + } + + if (i == 0) { + first_width = heif_image_get_primary_width(bayer_img); + first_height = heif_image_get_primary_height(bayer_img); + + heif_track_type track_type = use_video_handler ? heif_track_type_video + : heif_track_type_image_sequence; + + heif_track_options* track_options = heif_track_options_alloc(); + heif_track_options_set_timescale(track_options, fps); + + err = heif_context_add_visual_sequence_track(ctx, + static_cast(first_width), + static_cast(first_height), + track_type, + track_options, + enc_options, + &track); + heif_track_options_release(track_options); + + if (err.code != heif_error_Ok) { + std::cerr << "Cannot create sequence track: " << err.message << "\n"; + heif_image_release(bayer_img); + heif_sequence_encoding_options_release(enc_options); + heif_encoder_release(encoder); + heif_context_free(ctx); + return 1; + } + } + + heif_image_set_duration(bayer_img, 1); + + err = heif_track_encode_sequence_image(track, bayer_img, encoder, enc_options); + heif_image_release(bayer_img); + + if (err.code != heif_error_Ok) { + std::cerr << "Cannot encode frame " << i << ": " << err.message << "\n"; + heif_sequence_encoding_options_release(enc_options); + heif_track_release(track); + heif_encoder_release(encoder); + heif_context_free(ctx); + return 1; + } + + std::cout << "Encoded frame " << (i + 1) << "/" << filenames.size() + << ": " << filenames[i] << "\n"; + } + + err = heif_track_encode_end_of_sequence(track, encoder); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot end sequence: " << err.message << "\n"; + } + + heif_sequence_encoding_options_release(enc_options); + heif_track_release(track); + heif_encoder_release(encoder); + + err = heif_context_write_to_file(ctx, output_filename); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot write file: " << err.message << "\n"; + heif_context_free(ctx); + return 1; + } + + heif_context_free(ctx); + + std::cout << "Wrote " << filenames.size() << " frame(s) to " << output_filename << "\n"; + return 0; +} + + +int main(int argc, char* argv[]) +{ + PatternDefinition custom_pattern; + const PatternDefinition* pat = &patterns[0]; // default: RGGB + int output_bit_depth = 8; + bool sequence_mode = false; + bool use_video_handler = false; + int fps = 30; + + while (true) { + int option_index = 0; + int c = getopt_long(argc, argv, "hb:p:SV", long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'h': + print_usage(); + return 0; + + case 'b': + output_bit_depth = std::atoi(optarg); + if (output_bit_depth < 8 || output_bit_depth > 16) { + std::cerr << "Invalid bit depth: " << optarg << " (must be 8-16)\n"; + return 1; + } + break; + + case 'p': + pat = find_pattern(optarg); + if (!pat) { + auto custom = parse_pattern_string(optarg); + if (custom) { + custom_pattern = std::move(*custom); + pat = &custom_pattern; + } + else { + std::cerr << "Unknown pattern: " << optarg << "\n"; + print_usage(); + return 1; + } + } + break; + + case 'S': + sequence_mode = true; + break; + + case 'V': + use_video_handler = true; + break; + + case 'f': // --fps + fps = std::atoi(optarg); + if (fps <= 0) { + std::cerr << "Invalid FPS value: " << optarg << "\n"; + return 1; + } + break; + + default: + print_usage(); + return 1; + } + } + + if (argc - optind != 2) { + print_usage(); + return 1; + } + + const char* input_filename = argv[optind]; + const char* output_filename = argv[optind + 1]; + + if (sequence_mode) { + // --- Sequence mode: expand numbered filenames and encode as sequence + + std::vector filenames = deflate_input_filenames(input_filename); + if (filenames.empty()) { + std::cerr << "No input files found matching pattern: " << input_filename << "\n"; + return 1; + } + + std::cout << "Found " << filenames.size() << " frame(s), encoding at " << fps << " fps\n"; + return encode_sequence(filenames, pat, output_bit_depth, fps, use_video_handler, output_filename); + } + + // --- Single image mode + + heif_image* bayer_img = create_bayer_image_from_png(input_filename, pat, output_bit_depth, 0, 0); + if (!bayer_img) { + return 1; + } + + // --- Encode + + heif_context* ctx = heif_context_alloc(); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot get uncompressed encoder: " << err.message << "\n"; + heif_image_release(bayer_img); + heif_context_free(ctx); + return 1; + } + + heif_encoding_options* options = heif_encoding_options_alloc(); + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, bayer_img, encoder, options, &handle); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot encode image: " << err.message << "\n"; + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_image_release(bayer_img); + heif_context_free(ctx); + return 1; + } + + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_image_handle_release(handle); + heif_image_release(bayer_img); + + // --- Write file + + err = heif_context_write_to_file(ctx, output_filename); + if (err.code != heif_error_Ok) { + std::cerr << "Cannot write file: " << err.message << "\n"; + heif_context_free(ctx); + return 1; + } + + heif_context_free(ctx); + + std::cout << "Wrote " << pat->name << " (" + << pat->width << "x" << pat->height + << ") Bayer image to " << output_filename << "\n"; + + return 0; +} diff -Nru libheif-1.19.8/examples/heif_info.cc libheif-1.23.4/examples/heif_info.cc --- libheif-1.19.8/examples/heif_info.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/heif_info.cc 2026-09-06 14:45:17.000000000 +0000 @@ -38,8 +38,14 @@ #endif #include +#include #include #include +#include +#include "libheif/heif_sequences.h" +#include +#include +#include #include #include @@ -50,6 +56,7 @@ #include #include #include "common.h" +#include "libheif/heif_items.h" /* @@ -69,7 +76,7 @@ int option_disable_limits = 0; -static struct option long_options[] = { +static option long_options[] = { //{"write-raw", required_argument, 0, 'w' }, //{"output", required_argument, 0, 'o' }, {(char* const) "dump-boxes", no_argument, 0, 'd'}, @@ -79,17 +86,6 @@ {0, 0, 0, 0} }; -// Note: the same function is also exists in common_utils.h, but is not in the public API. -static const char* fourcc_to_string(uint32_t fourcc) -{ - static char fcc[5]; - fcc[0] = (char) ((fourcc >> 24) & 0xFF); - fcc[1] = (char) ((fourcc >> 16) & 0xFF); - fcc[2] = (char) ((fourcc >> 8) & 0xFF); - fcc[3] = (char) ((fourcc >> 0) & 0xFF); - fcc[4] = 0; - return fcc; -} void show_help(const char* argv0) { @@ -124,6 +120,73 @@ }; +static const char* color_primaries_name(uint16_t v) +{ + switch (v) { + case heif_color_primaries_ITU_R_BT_709_5: return "BT.709"; + case heif_color_primaries_unspecified: return "unspecified"; + case heif_color_primaries_ITU_R_BT_470_6_System_M: return "BT.470 System M"; + case heif_color_primaries_ITU_R_BT_470_6_System_B_G: return "BT.470 System B/G"; + case heif_color_primaries_ITU_R_BT_601_6: return "BT.601"; + case heif_color_primaries_SMPTE_240M: return "SMPTE 240M"; + case heif_color_primaries_generic_film: return "generic film"; + case heif_color_primaries_ITU_R_BT_2020_2_and_2100_0: return "BT.2020 / BT.2100"; + case heif_color_primaries_SMPTE_ST_428_1: return "SMPTE ST 428-1"; + case heif_color_primaries_SMPTE_RP_431_2: return "SMPTE RP 431-2 (DCI P3)"; + case heif_color_primaries_SMPTE_EG_432_1: return "SMPTE EG 432-1 (Display P3)"; + case heif_color_primaries_EBU_Tech_3213_E: return "EBU Tech 3213-E"; + default: return "unknown"; + } +} + + +static const char* transfer_characteristics_name(uint16_t v) +{ + switch (v) { + case heif_transfer_characteristic_ITU_R_BT_709_5: return "BT.709"; + case heif_transfer_characteristic_unspecified: return "unspecified"; + case heif_transfer_characteristic_ITU_R_BT_470_6_System_M: return "BT.470 System M"; + case heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G: return "BT.470 System B/G"; + case heif_transfer_characteristic_ITU_R_BT_601_6: return "BT.601"; + case heif_transfer_characteristic_SMPTE_240M: return "SMPTE 240M"; + case heif_transfer_characteristic_linear: return "linear"; + case heif_transfer_characteristic_logarithmic_100: return "log 100:1"; + case heif_transfer_characteristic_logarithmic_100_sqrt10: return "log 100*sqrt(10):1"; + case heif_transfer_characteristic_IEC_61966_2_4: return "IEC 61966-2-4"; + case heif_transfer_characteristic_ITU_R_BT_1361: return "BT.1361"; + case heif_transfer_characteristic_IEC_61966_2_1: return "IEC 61966-2-1 (sRGB)"; + case heif_transfer_characteristic_ITU_R_BT_2020_2_10bit: return "BT.2020 10-bit"; + case heif_transfer_characteristic_ITU_R_BT_2020_2_12bit: return "BT.2020 12-bit"; + case heif_transfer_characteristic_ITU_R_BT_2100_0_PQ: return "BT.2100 PQ"; + case heif_transfer_characteristic_SMPTE_ST_428_1: return "SMPTE ST 428-1"; + case heif_transfer_characteristic_ITU_R_BT_2100_0_HLG: return "BT.2100 HLG"; + default: return "unknown"; + } +} + + +static const char* matrix_coefficients_name(uint16_t v) +{ + switch (v) { + case heif_matrix_coefficients_RGB_GBR: return "RGB/GBR"; + case heif_matrix_coefficients_ITU_R_BT_709_5: return "BT.709"; + case heif_matrix_coefficients_unspecified: return "unspecified"; + case heif_matrix_coefficients_US_FCC_T47: return "US FCC T47"; + case heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G: return "BT.470 System B/G"; + case heif_matrix_coefficients_ITU_R_BT_601_6: return "BT.601"; + case heif_matrix_coefficients_SMPTE_240M: return "SMPTE 240M"; + case heif_matrix_coefficients_YCgCo: return "YCgCo"; + case heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance: return "BT.2020 NCL"; + case heif_matrix_coefficients_ITU_R_BT_2020_2_constant_luminance: return "BT.2020 CL"; + case heif_matrix_coefficients_SMPTE_ST_2085: return "SMPTE ST 2085"; + case heif_matrix_coefficients_chromaticity_derived_non_constant_luminance: return "chromaticity-derived NCL"; + case heif_matrix_coefficients_chromaticity_derived_constant_luminance: return "chromaticity-derived CL"; + case heif_matrix_coefficients_ICtCp: return "ICtCp"; + default: return "unknown"; + } +} + + int main(int argc, char** argv) { // This takes care of initializing libheif and also deinitializing it at the end to free all resources. @@ -156,7 +219,7 @@ output_filename = optarg; break; case 'v': - show_version(); + heif_examples::show_version(); return 0; } } @@ -200,7 +263,7 @@ heif_brand2* brands = nullptr; int nBrands = 0; - struct heif_error err = heif_list_compatible_brands(buf, n, &brands, &nBrands); + heif_error err = heif_list_compatible_brands(buf, n, &brands, &nBrands); if (err.code) { std::cerr << "error reading brands: " << err.message << "\n"; } @@ -240,11 +303,17 @@ heif_context_set_security_limits(ctx.get(), heif_get_disabled_security_limits()); } - struct heif_error err; + heif_error err; err = heif_context_read_from_file(ctx.get(), input_filename, nullptr); if (dump_boxes) { heif_context_debug_dump_boxes_to_file(ctx.get(), STDOUT_FILENO); // dump to stdout + + if (err.code != 0) { + std::cerr << "Could not read HEIF/AVIF file: " << err.message << "\n"; + return 1; + } + return 0; } @@ -264,8 +333,8 @@ for (int i = 0; i < numImages; i++) { std::cout << "\n"; - struct heif_image_handle* handle; - struct heif_error err = heif_context_get_image_handle(ctx.get(), IDs[i], &handle); + heif_image_handle* handle; + heif_error err = heif_context_get_image_handle(ctx.get(), IDs[i], &handle); if (err.code) { std::cerr << err.message << "\n"; return 10; @@ -307,8 +376,8 @@ case heif_colorspace_monochrome: printf("monochrome"); break; - case heif_colorspace_nonvisual: - printf("non-visual"); + case heif_colorspace_custom: + printf("custom"); break; default: printf("unknown"); @@ -375,8 +444,28 @@ // --- color profile uint32_t profileType = heif_image_handle_get_color_profile_type(handle); - printf(" color profile: %s\n", profileType ? fourcc_to_string(profileType) : "no"); + printf(" color profile: %s\n", profileType ? heif_examples::fourcc_to_string(profileType).c_str() : "no"); + heif_color_profile_nclx* nclx = nullptr; + err = heif_image_handle_get_nclx_color_profile(handle, &nclx); + if (err.code == heif_error_Ok && nclx) { + printf(" colour primaries: %d (%s)\n", + nclx->color_primaries, color_primaries_name(nclx->color_primaries)); + printf(" transfer characteristics: %d (%s)\n", + nclx->transfer_characteristics, transfer_characteristics_name(nclx->transfer_characteristics)); + printf(" matrix coefficients: %d (%s)\n", + nclx->matrix_coefficients, matrix_coefficients_name(nclx->matrix_coefficients)); + printf(" full range flag: %d (%s)\n", + nclx->full_range_flag, nclx->full_range_flag ? "full" : "limited"); + } + heif_nclx_color_profile_free(nclx); + + heif_content_light_level cll = {0, 0}; + if (heif_image_handle_get_content_light_level(handle, &cll) && + (cll.max_content_light_level > 0 || cll.max_pic_average_light_level > 0)) { + printf(" content light level: MaxCLL=%u MaxFALL=%u\n", + cll.max_content_light_level, cll.max_pic_average_light_level); + } // --- depth information @@ -399,7 +488,7 @@ (void) nDepthImages; if (has_depth) { - struct heif_image_handle* depth_handle; + heif_image_handle* depth_handle; err = heif_image_handle_get_depth_image_handle(handle, depth_id, &depth_handle); if (err.code) { fprintf(stderr, "cannot get depth image: %s\n", err.message); @@ -414,7 +503,7 @@ printf(" bits per pixel: %d\n", depth_luma_bpp); - const struct heif_depth_representation_info* depth_info; + const heif_depth_representation_info* depth_info; if (heif_image_handle_get_depth_image_representation_info(handle, depth_id, &depth_info)) { printf(" z-near: "); @@ -471,13 +560,17 @@ ID = itemtype; } else if (itemtype == "uri ") { - ID = itemtype + "/" + item_uri_type; + ID = itemtype; + ID += "/"; + ID += item_uri_type; } else if (contenttype == "application/rdf+xml") { ID = "XMP"; } else { - ID = itemtype + "/" + contenttype; + ID = itemtype; + ID += "/"; + ID += contenttype; } printf(" %s: %zu bytes\n", ID.c_str(), heif_image_handle_get_metadata_size(handle, ids[n])); @@ -537,7 +630,7 @@ std::vector region_items(numRegionItems); heif_image_handle_get_list_of_region_item_ids(handle, region_items.data(), numRegionItems); for (heif_item_id region_item_id : region_items) { - struct heif_region_item* region_item; + heif_region_item* region_item; err = heif_context_get_region_item(ctx.get(), region_item_id, ®ion_item); uint32_t reference_width, reference_height; @@ -648,12 +741,42 @@ printf(" none\n"); } + // --- text items + int numTextItems = heif_image_handle_get_number_of_text_items(handle); + printf("text items:\n"); + + if (numTextItems > 0) { + std::vector text_items(numTextItems); + heif_image_handle_get_list_of_text_item_ids(handle, text_items.data(), numTextItems); + for (heif_item_id text_item_id : text_items) { + struct heif_text_item* text_item; + err = heif_context_get_text_item(ctx.get(), text_item_id, &text_item); + const char* text_content = heif_text_item_get_content(text_item); + printf(" text item: %s\n", text_content); + heif_string_release(text_content); + + char* elng; + err = heif_item_get_property_extended_language(ctx.get(), + text_item_id, + &elng); + if (err.code == 0) { + printf(" extended language: %s\n", elng); + } + heif_string_release(elng); + } + } + else { + printf(" none\n"); + } + // --- properties printf("properties:\n"); // user descriptions + bool properties_shown = false; + heif_property_id propertyIds[MAX_PROPERTIES]; int count; count = heif_item_get_properties_of_type(ctx.get(), IDs[i], heif_item_property_type_user_description, @@ -661,7 +784,7 @@ if (count > 0) { for (int p = 0; p < count; p++) { - struct heif_property_user_description* udes; + heif_property_user_description* udes; err = heif_item_get_property_user_description(ctx.get(), IDs[i], propertyIds[p], &udes); if (err.code) { std::cerr << "Error reading udes " << IDs[i] << "/" << propertyIds[p] << "\n"; @@ -672,6 +795,7 @@ printf(" name: %s\n", udes->name); printf(" description: %s\n", udes->description); printf(" tags: %s\n", udes->tags); + properties_shown = true; heif_property_user_description_release(udes); } @@ -687,6 +811,7 @@ printf(" focal length: %f; %f\n", matrix.focal_length_x, matrix.focal_length_y); printf(" principal point: %f; %f\n", matrix.principal_point_x, matrix.principal_point_y); printf(" skew: %f\n", matrix.skew); + properties_shown = true; } if (heif_image_handle_has_camera_extrinsic_matrix(handle)) { @@ -700,37 +825,154 @@ printf(" %6.3f %6.3f %6.3f\n", rot[3], rot[4], rot[5]); printf(" %6.3f %6.3f %6.3f\n", rot[6], rot[7], rot[8]); heif_camera_extrinsic_matrix_release(matrix); + properties_shown = true; } uint32_t aspect_h, aspect_v; int has_pasp = heif_image_handle_get_pixel_aspect_ratio(handle, &aspect_h, &aspect_v); if (has_pasp) { - std::cout << "pixel aspect ratio: " << aspect_h << "/" << aspect_v << "\n"; + std::cout << " pixel aspect ratio: " << aspect_h << "/" << aspect_v << "\n"; + properties_shown = true; } - struct heif_content_light_level clli{}; + heif_content_light_level clli{}; if (heif_image_handle_get_content_light_level(handle, &clli)) { - std::cout << "content light level (clli):\n" - << " max content light level: " << clli.max_content_light_level << "\n" - << " max pic average light level: " << clli.max_pic_average_light_level << "\n"; + std::cout << " content light level (clli):\n" + << " max content light level: " << clli.max_content_light_level << "\n" + << " max pic average light level: " << clli.max_pic_average_light_level << "\n"; + properties_shown = true; } - struct heif_mastering_display_colour_volume mdcv; + heif_mastering_display_colour_volume mdcv; if (heif_image_handle_get_mastering_display_colour_volume(handle, &mdcv)) { - struct heif_decoded_mastering_display_colour_volume decoded_mdcv; + heif_decoded_mastering_display_colour_volume decoded_mdcv; err = heif_mastering_display_colour_volume_decode(&mdcv, &decoded_mdcv); - std::cout << "mastering display color volume:\n" - << " display_primaries (x,y): " + std::cout << " mastering display color volume:\n" + << " display_primaries (x,y): " << "(" << decoded_mdcv.display_primaries_x[0] << ";" << decoded_mdcv.display_primaries_y[0] << "), " << "(" << decoded_mdcv.display_primaries_x[1] << ";" << decoded_mdcv.display_primaries_y[1] << "), " << "(" << decoded_mdcv.display_primaries_x[2] << ";" << decoded_mdcv.display_primaries_y[2] << ")\n"; - std::cout << " white point (x,y): (" << decoded_mdcv.white_point_x << ";" << decoded_mdcv.white_point_y << ")\n"; - std::cout << " max display mastering luminance: " << decoded_mdcv.max_display_mastering_luminance << "\n"; - std::cout << " min display mastering luminance: " << decoded_mdcv.min_display_mastering_luminance << "\n"; + std::cout << " white point (x,y): (" << decoded_mdcv.white_point_x << ";" << decoded_mdcv.white_point_y << ")\n"; + std::cout << " max display mastering luminance: " << decoded_mdcv.max_display_mastering_luminance << "\n"; + std::cout << " min display mastering luminance: " << decoded_mdcv.min_display_mastering_luminance << "\n"; + properties_shown = true; + } + + // --- GIMI + + const char* id = heif_image_handle_get_gimi_content_id(handle); + if (id) { + std::cout << " GIMI content ID: " << id << "\n"; + heif_string_release(id); + properties_shown = true; + } + + // --- cmpd components + + uint32_t num_cmpd_components = heif_image_handle_get_number_of_cmpd_components(handle); + if (num_cmpd_components > 0) { + int num_content_ids = heif_image_handle_has_gimi_component_content_ids(handle); + + auto get_component_type_name = [](uint16_t type) -> const char* { + switch (type) { + case 0: return "monochrome"; + case 1: return "Y"; + case 2: return "Cb"; + case 3: return "Cr"; + case 4: return "red"; + case 5: return "green"; + case 6: return "blue"; + case 7: return "alpha"; + case 8: return "depth"; + case 9: return "disparity"; + case 10: return "palette"; + case 11: return "filter_array"; + case 12: return "padded"; + case 13: return "cyan"; + case 14: return "magenta"; + case 15: return "yellow"; + case 16: return "key_black"; + default: return nullptr; + } + }; + + // Find the maximum display width of the "type_name (N)" column. + size_t max_type_width = 0; + for (uint32_t i = 0; i < num_cmpd_components; i++) { + uint16_t type = heif_image_handle_get_cmpd_component_type(handle, i); + const char* name = get_component_type_name(type); + std::ostringstream tmp; + if (name) { + tmp << name << " (" << type << ")"; + } + else { + tmp << type; + } + max_type_width = std::max(max_type_width, tmp.str().size()); + } + + std::cout << " components:\n"; + for (uint32_t i = 0; i < num_cmpd_components; i++) { + uint16_t type = heif_image_handle_get_cmpd_component_type(handle, i); + const char* type_name = get_component_type_name(type); + + std::ostringstream type_str; + if (type_name) { + type_str << type_name << " (" << type << ")"; + } + else { + type_str << type; + } + + std::cout << " [" << i << "] type: " + << std::left << std::setw(static_cast(max_type_width)) << type_str.str(); + + const char* uri = heif_image_handle_get_cmpd_component_type_uri(handle, i); + if (uri) { + std::cout << " (uri: " << uri << ")"; + heif_string_release(uri); + } + + if (num_content_ids > 0) { + const char* content_id = heif_image_handle_get_gimi_component_content_id(handle, i); + if (content_id) { + std::cout << " content ID: " << content_id; + heif_string_release(content_id); + } + } + + std::cout << "\n"; + } + properties_shown = true; + } + + // --- OMAF + + heif_omaf_image_projection projection = heif_image_handle_get_omaf_image_projection(handle); + if (projection != heif_omaf_image_projection_flat) { + std::cout << " image projection: "; + switch (projection) + { + case heif_omaf_image_projection_equirectangular: + std::cout << "equirectangular"; + break; + case heif_omaf_image_projection_cube_map: + std::cout << "cube-map"; + break; + default: + std::cout << "(unknown)"; + break; + } + std::cout << "\n"; + properties_shown = true; + } + + if (!properties_shown) { + std::cout << "none\n"; } heif_image_handle_release(handle); @@ -739,14 +981,14 @@ #if 0 std::cout << "num images: " << heif_context_get_number_of_top_level_images(ctx.get()) << "\n"; - struct heif_image_handle* handle; + heif_image_handle* handle; err = heif_context_get_primary_image_handle(ctx.get(), &handle); if (err.code != 0) { std::cerr << "Could not get primage image handle: " << err.message << "\n"; return 1; } - struct heif_image* image; + heif_image* image; err = heif_decode_image(handle, &image, heif_colorspace_undefined, heif_chroma_undefined, NULL); if (err.code != 0) { heif_image_handle_release(handle); @@ -758,5 +1000,168 @@ heif_image_handle_release(handle); #endif + // ============================================================================== + + heif_context* context = ctx.get(); + + uint32_t nTracks = heif_context_number_of_sequence_tracks(context); + + if (nTracks > 0) { + std::cout << "\n"; + + uint64_t timescale = heif_context_get_sequence_timescale(context); + std::cout << "sequence time scale: " << timescale << " Hz\n"; + + uint64_t duration = heif_context_get_sequence_duration(context); + std::cout << "sequence duration: " << ((double)duration)/(double)timescale << " seconds\n"; + + // TrackOptions + + std::vector track_ids(nTracks); + + heif_context_get_track_ids(context, track_ids.data()); + + for (uint32_t id : track_ids) { + heif_track* track = heif_context_get_track(context, id); + + heif_track_type handler = heif_track_get_track_handler_type(track); + std::cout << "track " << id << "\n"; + std::cout << " handler: '" << heif_examples::fourcc_to_string(handler) << "' = "; + + switch (handler) { + case heif_track_type_image_sequence: + std::cout << "image sequence\n"; + break; + case heif_track_type_video: + std::cout << "video\n"; + break; + case heif_track_type_auxiliary: + std::cout << "auxiliary "; + { + heif_auxiliary_track_info_type type = heif_track_get_auxiliary_info_type(track); + switch (type) { + case heif_auxiliary_track_info_type_unknown: { + const char* s = heif_track_get_auxiliary_info_type_urn(track); + std::cout << "(unknown: " << s << ")\n"; + heif_string_release(s); + break; + } + case heif_auxiliary_track_info_type_alpha: + std::cout << "(alpha)\n"; + break; + } + } + break; + case heif_track_type_metadata: + std::cout << "metadata\n"; + break; + default: + std::cout << "unknown\n"; + break; + } + + if (handler == heif_track_type_video || + handler == heif_track_type_image_sequence) { + uint16_t w, h; + heif_track_get_image_resolution(track, &w, &h); + std::cout << " resolution: " << w << "x" << h << "\n"; + } + + uint32_t repetitions = heif_track_get_number_of_repetitions(track); + std::cout << " repetitions: "; + if (repetitions == 0) { + std::cout << "unsupported editlist\n"; + } + else if (repetitions == heif_sequence_track_number_of_repetitions_infinite) { + std::cout << "infinite\n"; + } + else { + std::cout << repetitions; + if (repetitions == 1) { + std::cout << " (single playback)"; + } + std::cout << "\n"; + } + + uint32_t sampleEntryType = heif_track_get_sample_entry_type_of_first_cluster(track); + std::cout << " sample entry type: " << heif_examples::fourcc_to_string(sampleEntryType) << "\n"; + + if (sampleEntryType == heif_fourcc('u', 'r', 'i', 'm')) { + const char* uri; + err = heif_track_get_urim_sample_entry_uri_of_first_cluster(track, &uri); + if (err.code) { + std::cerr << "error reading urim-track uri: " << err.message << "\n"; + } + std::cout << " uri: " << uri << "\n"; + heif_string_release(uri); + } + + std::cout << " sample auxiliary information: "; + int nSampleAuxTypes = heif_track_get_number_of_sample_aux_infos(track); + + std::vector aux_types(nSampleAuxTypes); + heif_track_get_sample_aux_info_types(track, aux_types.data()); + + for (size_t i=0;i 0) { + std::cout << " references:\n"; + std::vector refTypes(nRefTypes); + heif_track_get_track_reference_types(track, refTypes.data()); + + for (uint32_t refType : refTypes) { + std::cout << " " << heif_examples::fourcc_to_string(refType) << ": "; + + size_t n = heif_track_get_number_of_track_reference_of_type(track, refType); + std::vector track_ids(n); + heif_track_get_references_from_track(track, refType, track_ids.data()); + for (size_t i=0;i0) std::cout << ", "; + std::cout << "track#" << track_ids[i]; + } + std::cout << "\n"; + } + } + + const char* gimi_track_id = heif_track_get_gimi_track_content_id(track); + if (gimi_track_id) { + std::cout << " GIMI track id: " << gimi_track_id << "\n"; + heif_string_release(gimi_track_id); + } + + heif_track_release(track); + } + } + + // ============================================================================== + + { + int nItems = heif_context_get_number_of_items(context); + if (nItems > 0) { + std::cout << "MIME items:\n"; + std::vector item_ids(nItems); + heif_context_get_list_of_item_IDs(context, item_ids.data(), nItems); + + for (auto id : item_ids) { + uint32_t type = heif_item_get_item_type(context, id); + if (type == heif_item_type_mime) { + const char* mimeType = heif_item_get_mime_item_content_type(context, id); + std::cout << " " << id << " : " << mimeType << "\n"; + } + } + } + } + return 0; } diff -Nru libheif-1.19.8/examples/heif_test.cc libheif-1.23.4/examples/heif_test.cc --- libheif-1.19.8/examples/heif_test.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/heif_test.cc 2026-09-06 14:45:17.000000000 +0000 @@ -105,7 +105,7 @@ show_help(argv[0]); return 0; case 'v': - show_version(); + heif_examples::show_version(); return 0; } } @@ -149,8 +149,8 @@ int height = img.get_height(channel); int bytes = (img.get_bits_per_pixel(channel) + 7) / 8; - int stride; - const uint8_t* p = img.get_plane(channel, &stride); + size_t stride; + const uint8_t* p = img.get_plane2(channel, &stride); for (int y = 0; y < height; y++) { fwrite(p + y * stride, width, bytes, stdout); } diff -Nru libheif-1.19.8/examples/heif_thumbnailer.cc libheif-1.23.4/examples/heif_thumbnailer.cc --- libheif-1.19.8/examples/heif_thumbnailer.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/examples/heif_thumbnailer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -78,7 +78,7 @@ thumbnail_from_primary_image_only = true; break; case 'v': - show_version(); + heif_examples::show_version(); return 0; case 'h': default: diff -Nru libheif-1.19.8/examples/heif_view.cc libheif-1.23.4/examples/heif_view.cc --- libheif-1.19.8/examples/heif_view.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/heif_view.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,482 @@ +/* + libheif example application. + + MIT License + + Copyright (c) 2025 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include +#include "libheif/heif_experimental.h" +#include "libheif/heif_sequences.h" +#include + +#if defined(HAVE_UNISTD_H) + +#include + +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "common.h" +#include "sdl.hh" + +#if defined(_MSC_VER) +#include "getopt.h" +#endif + +#if !SDL_VERSION_ATLEAST(2, 0, 18) +#define SDL_GetTicks64 SDL_GetTicks +#endif + +#define UNUSED(x) (void)x + + +static void show_help(const char* argv0) +{ + std::cerr << " " << argv0 << " libheif version: " << heif_get_version() << "\n" + << "---------------------------------------\n" + "Usage: " << argv0 << " [options] \n" + << "\n" + "Options:\n" + " -h, --help show help\n" + " -v, --version show version\n" + " --list-decoders list all available decoders (built-in and plugins)\n" + " -d, --decoder ID use a specific decoder (see --list-decoders)\n" + " --speedup FACTOR increase playback speed by FACTOR\n" + " --show-sai show sample auxiliary information\n" + " --show-frame-duration show each frame duration in milliseconds\n" + " --show-track-metadata show metadata attached to the track (e.g. TAI config)\n" + " --show-metadata-text show data in metadata track as text\n" + " --show-metadata-hex show data in metadata track as hex bytes\n" + " --show-all show all extra information\n" + " --ignore-editlist show the raw media timeline without repetitions\n"; +} + + +class ContextReleaser { +public: + ContextReleaser(struct heif_context* ctx) : ctx_(ctx) {} + + ~ContextReleaser() + { + heif_context_free(ctx_); + } + +private: + struct heif_context* ctx_; +}; + + +int option_list_decoders = 0; +double option_speedup = 1.0; +bool option_show_sai = false; +bool option_show_frame_duration = false; +bool option_show_track_metadata = false; +bool option_ignore_editlist = false; +enum { + metadata_output_none, + metadata_output_text, + metadata_output_hex +} option_metadata_output = metadata_output_none; + +const int OPTION_SPEEDUP = 1000; +const int OPTION_SHOW_SAI = 1001; +const int OPTION_SHOW_FRAME_DURATION = 1002; +const int OPTION_SHOW_TRACK_METADATA = 1003; +const int OPTION_SHOW_ALL = 1004; +const int OPTION_SHOW_METADATA_TEXT = 1005; +const int OPTION_SHOW_METADATA_HEX = 1006; +const int OPTION_IGNORE_EDITLIST = 1007; + +static struct option long_options[] = { + {(char* const) "decoder", required_argument, 0, 'd'}, + {(char* const) "list-decoders", no_argument, &option_list_decoders, 1}, + {(char* const) "help", no_argument, 0, 'h'}, + {(char* const) "version", no_argument, 0, 'v'}, + {(char* const) "speedup", required_argument, 0, OPTION_SPEEDUP}, + {(char* const) "show-sai", no_argument, 0, OPTION_SHOW_SAI}, + {(char* const) "show-frame-duration", no_argument, 0, OPTION_SHOW_FRAME_DURATION}, + {(char* const) "show-track-metadata", no_argument, 0, OPTION_SHOW_TRACK_METADATA}, + {(char* const) "show-metadata-text", no_argument, 0, OPTION_SHOW_METADATA_TEXT}, + {(char* const) "show-metadata-hex", no_argument, 0, OPTION_SHOW_METADATA_HEX}, + {(char* const) "show-all", no_argument, 0, OPTION_SHOW_ALL}, + {(char* const) "ignore-editlist", no_argument, 0, OPTION_IGNORE_EDITLIST}, + {nullptr, no_argument, nullptr, 0} +}; + + +void output_hex(const uint8_t* data, size_t size) +{ + std::cout << std::hex << std::setfill('0'); + + for (size_t i=0;itime_uncertainty << " / " << taic->clock_resolution << " / " + << taic->clock_drift_rate << " / " << int(taic->clock_type) << "\n"; + } + } + + + // --- find metadata track + + heif_track* metadata_track = nullptr; + if (option_metadata_output != metadata_output_none) { + uint32_t metadata_track_id; + size_t nMetadataTracks = heif_track_find_referring_tracks(track, heif_track_reference_type_description, &metadata_track_id, 1); + + if (nMetadataTracks == 1) { + metadata_track = heif_context_get_track(ctx, metadata_track_id); + } + } + + + // --- open output window + + SDL_YUV_Display sdlWindow; + std::optional sdlError = sdlWindow.init(w, h, SDL_YUV_Display::SDL_CHROMA_420, "heif-view"); + if (sdlError) { + std::cerr << "Cannot open output window. " << *sdlError << "\n"; + return 10; + } + + std::unique_ptr decode_options(heif_decoding_options_alloc(), heif_decoding_options_free); + decode_options->convert_hdr_to_8bit = true; + decode_options->decoder_id = decoder_id; + decode_options->ignore_sequence_editlist = option_ignore_editlist; + + + // --- decoding loop + + for (int frameNr = 1;; frameNr++) { + heif_image* out_image = nullptr; + + // --- decode next sequence image + + err = heif_track_decode_next_image(track, &out_image, + heif_colorspace_YCbCr, // TODO: find best format + heif_chroma_420, + decode_options.get()); + if (err.code == heif_error_End_of_sequence) { + break; + } + else if (err.code) { + std::cerr << err.message << "\n"; + return 1; + } + + // --- wait for image presentation time + + uint32_t duration = heif_image_get_duration(out_image); + uint64_t timescale = heif_track_get_timescale(track); + uint64_t duration_ms = duration * 1000 / timescale; + + if (option_show_frame_duration) { + std::cout << "sample duration " << heif_image_get_duration(out_image) << " = " << duration_ms << " ms\n"; + } + + static const uint64_t start_time = SDL_GetTicks64(); + static uint64_t next_frame_pts = 0; + + uint64_t now_time = SDL_GetTicks64(); + uint64_t elapsed_time = (now_time - start_time); + if (elapsed_time < next_frame_pts) { + SDL_Delay((Uint32)(next_frame_pts - elapsed_time)); + } + + next_frame_pts += static_cast(static_cast(duration_ms) / option_speedup); + + + // --- verify that the decoded frame is large enough for the display window + // + // The SDL window/texture was sized from the track's declared resolution + // (VisualSampleEntry), which is attacker-controlled and may be larger than + // the actual decoded frame. The display routines copy 'rect.w * rect.h' + // samples from the decoded planes, so a smaller frame would be read out of + // bounds. Bail out instead. + + int decoded_w = heif_image_get_width(out_image, heif_channel_Y); + int decoded_h = heif_image_get_height(out_image, heif_channel_Y); + if (decoded_w < (w & ~7) || decoded_h < (h & ~7)) { + std::cerr << "Decoded frame (" << decoded_w << "x" << decoded_h + << ") is smaller than the declared track resolution (" << w << "x" << h << ").\n"; + heif_image_release(out_image); + return 1; + } + + // --- display image + + size_t stride_Y, stride_Cb, stride_Cr; + const uint8_t* p_Y = heif_image_get_plane_readonly2(out_image, heif_channel_Y, &stride_Y); + const uint8_t* p_Cb = heif_image_get_plane_readonly2(out_image, heif_channel_Cb, &stride_Cb); + const uint8_t* p_Cr = heif_image_get_plane_readonly2(out_image, heif_channel_Cr, &stride_Cr); + + sdlWindow.display(p_Y, p_Cb, p_Cr, static_cast(stride_Y), static_cast(stride_Cb)); + + if (show_frame_number) { + std::cout << "--- frame " << frameNr << "\n"; + } + + if (option_show_sai) { + const char* contentID = heif_image_get_gimi_sample_content_id(out_image); + if (contentID) { + std::cout << "GIMI content id: " << contentID << "\n"; + heif_string_release(contentID); + } + + heif_tai_timestamp_packet* timestamp; + err = heif_image_get_tai_timestamp(out_image, ×tamp); + if (err.code) { + std::cerr << err.message << "\n"; + return 10; + } + else if (timestamp) { + std::cout << "TAI timestamp: " << timestamp->tai_timestamp << "\n"; + heif_tai_timestamp_packet_release(timestamp); + } + } + + // --- get metadata sample + + static heif_raw_sequence_sample* metadata_sample = nullptr; + static uint64_t metadata_sample_display_time = 0; + + if (metadata_track && metadata_sample == nullptr) { + err = heif_track_get_next_raw_sequence_sample(metadata_track, &metadata_sample); + if (err.code != heif_error_Ok && err.code != heif_error_End_of_sequence) { + std::cerr << err.message << "\n"; + return 10; + } + } + + // --- show metadata + + while (metadata_sample && static_cast((double)metadata_sample_display_time / option_speedup) <= elapsed_time) { + size_t size; + const uint8_t* data = heif_raw_sequence_sample_get_data(metadata_sample, &size); + + std::cout << "timestamp: " << ((double)metadata_sample_display_time)/1000.0f << "sec\n"; + + if (option_metadata_output == metadata_output_text) { + std::cout << ((const char*) data) << "\n"; + } + else if (option_metadata_output == metadata_output_hex) { + output_hex(data, size); + std::cout << '\n'; + } + + uint64_t metadata_timescale = heif_track_get_timescale(metadata_track); + uint32_t metadata_duration = heif_raw_sequence_sample_get_duration(metadata_sample); + metadata_sample_display_time += metadata_duration * 1000 / metadata_timescale; + + heif_raw_sequence_sample_release(metadata_sample); + metadata_sample = nullptr; + + // get next metadata sample + + err = heif_track_get_next_raw_sequence_sample(metadata_track, &metadata_sample); + if (err.code != heif_error_Ok && err.code != heif_error_End_of_sequence) { + std::cerr << err.message << "\n"; + return 10; + } + } + + heif_image_release(out_image); + + if (sdlWindow.doQuit()) { + break; + } + } + + sdlWindow.close(); + + heif_track_release(track); + heif_track_release(metadata_track); + + return 0; +} diff -Nru libheif-1.19.8/examples/sdl.cc libheif-1.23.4/examples/sdl.cc --- libheif-1.19.8/examples/sdl.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/sdl.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,302 @@ +/* + This file is part of dec265, an example application using libde265. + + MIT License + + Copyright (c) 2013-2014 struktur AG, Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "sdl.hh" +#include + + +std::optional SDL_YUV_Display::init(int frame_width, int frame_height, enum SDL_Chroma chroma, + const char* window_title) +{ + // reduce image size to a multiple of 8 (apparently required by YUV overlay) + + frame_width &= ~7; + frame_height &= ~7; + + mChroma = chroma; + + if (SDL_Init(SDL_INIT_VIDEO) < 0 ) { + std::stringstream sstr; + sstr << "SDL_Init() failed: " << SDL_GetError(); + SDL_Quit(); + return sstr.str(); + } + + // set window title + mWindow = SDL_CreateWindow(window_title, + SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + frame_width, frame_height, 0); + if (!mWindow) { + std::stringstream sstr; + sstr << "SDL: Couldn't set video mode to " << frame_width << "x" << frame_height << ": " << SDL_GetError(); + SDL_Quit(); + return sstr.str(); + } + + Uint32 flags = 0; // Empty flags prioritize SDL_RENDERER_ACCELERATED. + mRenderer = SDL_CreateRenderer(mWindow, -1, flags); + if (!mRenderer) { + std::stringstream sstr; + sstr << "SDL: Couldn't create renderer: " << SDL_GetError(); + SDL_Quit(); + return sstr.str(); + } + + Uint32 pixelFormat = 0; + switch (mChroma) { + case SDL_CHROMA_MONO: pixelFormat = SDL_PIXELFORMAT_YV12; break; + case SDL_CHROMA_420: pixelFormat = SDL_PIXELFORMAT_YV12; break; + case SDL_CHROMA_422: pixelFormat = SDL_PIXELFORMAT_YV12; break; + case SDL_CHROMA_444: pixelFormat = SDL_PIXELFORMAT_YV12; break; + default: { + std::stringstream sstr; + sstr << "Unsupported chroma: " << (int)mChroma; + SDL_Quit(); + return sstr.str(); + } + } + + mTexture = SDL_CreateTexture(mRenderer, pixelFormat, + SDL_TEXTUREACCESS_STREAMING, frame_width, frame_height); + if (!mTexture ) { + std::stringstream sstr; + sstr << "SDL: Couldn't create SDL texture: " << SDL_GetError(); + SDL_Quit(); + return sstr.str(); + } + + rect.x = 0; + rect.y = 0; + rect.w = frame_width; + rect.h = frame_height; + + mWindowOpen=true; + + return {}; +} + +void SDL_YUV_Display::display(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride) +{ + if (!mWindowOpen) return; + if (SDL_LockTexture(mTexture, nullptr, + reinterpret_cast(&mPixels), &mStride) < 0) return; + + if (mChroma == SDL_CHROMA_420) { + display420(Y,U,V,stride,chroma_stride); + } + else if (mChroma == SDL_CHROMA_422) { + display422(Y,U,V,stride,chroma_stride); + } + else if (mChroma == SDL_CHROMA_444) { + display444as420(Y,U,V,stride,chroma_stride); + //display444as422(Y,U,V,stride,chroma_stride); + } + else if (mChroma == SDL_CHROMA_MONO) { + display400(Y,stride); + } + + SDL_UnlockTexture(mTexture); + + SDL_RenderCopy(mRenderer, mTexture, nullptr, nullptr); + SDL_RenderPresent(mRenderer); +} + + +void SDL_YUV_Display::display420(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride) +{ + if (stride == mStride && chroma_stride == mStride/2) { + + // fast copy + + memcpy(mPixels, Y, rect.w * rect.h); + memcpy(&mPixels[rect.w * rect.h], V, rect.w * rect.h / 4); + memcpy(&mPixels[(rect.w * rect.h) + (rect.w * rect.h / 4)], U, rect.w * rect.h / 4); + } + else { + // copy line by line, because sizes are different + uint8_t *dest = mPixels; + + for (int y=0;y((U[(y + 0) * chroma_stride + x] + U[(y + 0) * chroma_stride + x + 1] + + U[(y + 1) * chroma_stride + x] + U[(y + 1) * chroma_stride + x + 1]) / 4); + v[x / 2] = static_cast((V[(y + 0) * chroma_stride + x] + V[(y + 0) * chroma_stride + x + 1] + + V[(y + 1) * chroma_stride + x] + V[(y + 1) * chroma_stride + x + 1]) / 4); + + //u[x/2] = U[y*chroma_stride + x]; + //v[x/2] = V[y*chroma_stride + x]; + } + } +} + + +bool SDL_YUV_Display::doQuit() const +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + return true; + } + } + + return false; +} + +void SDL_YUV_Display::close() +{ + if (mTexture) { + SDL_DestroyTexture(mTexture); + mTexture = nullptr; + } + if (mRenderer) { + SDL_DestroyRenderer(mRenderer); + mRenderer = nullptr; + } + if (mWindow) { + SDL_DestroyWindow(mWindow); + mWindow = nullptr; + } + SDL_Quit(); + + mWindowOpen=false; +} diff -Nru libheif-1.19.8/examples/sdl.hh libheif-1.23.4/examples/sdl.hh --- libheif-1.19.8/examples/sdl.hh 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/sdl.hh 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,82 @@ +/* + This file is part of dec265, an example application using libde265. + + MIT License + + Copyright (c) 2013-2014 struktur AG, Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +#include +#include +#include + + +class SDL_YUV_Display +{ +public: + + enum SDL_Chroma { + SDL_CHROMA_MONO=400, + SDL_CHROMA_420 =420, + SDL_CHROMA_422 =422, + SDL_CHROMA_444 =444 + }; + + // Returns error message or nullopt on success. + std::optional init(int frame_width, int frame_height, enum SDL_Chroma chroma, const char* window_title); + void display(const unsigned char *Y, const unsigned char *U, const unsigned char *V, + int stride, int chroma_stride); + void close(); + + bool doQuit() const; + + bool isOpen() const { return mWindowOpen; } + +private: + SDL_Window *mWindow = nullptr; + SDL_Renderer *mRenderer = nullptr; + SDL_Texture *mTexture = nullptr; + SDL_Rect rect; + bool mWindowOpen; + uint8_t *mPixels = nullptr; + int mStride = 0; + + SDL_Chroma mChroma; + + void display400(const unsigned char *Y, + int stride); + void display420(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride); + void display422(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride); + void display444as422(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride); + void display444as420(const unsigned char *Y, + const unsigned char *U, + const unsigned char *V, + int stride, int chroma_stride); +}; diff -Nru libheif-1.19.8/examples/utf8.manifest libheif-1.23.4/examples/utf8.manifest --- libheif-1.19.8/examples/utf8.manifest 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/utf8.manifest 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,9 @@ + + + + + true + UTF-8 + + + diff -Nru libheif-1.19.8/examples/utf8.rc libheif-1.23.4/examples/utf8.rc --- libheif-1.19.8/examples/utf8.rc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/utf8.rc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,3 @@ +#include + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "utf8.manifest" diff -Nru libheif-1.19.8/examples/vmt.cc libheif-1.23.4/examples/vmt.cc --- libheif-1.19.8/examples/vmt.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/vmt.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,314 @@ +/* + libheif example application "heif-enc" - VMT metadata track support. + + MIT License + + Copyright (c) 2025 Dirk Farin + Copyright (c) 2026 Rob Smith + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "vmt.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +static const uint32_t BAD_VMT_TIMESTAMP = 0xFFFFFFFE; + + +static std::optional nibble_to_val(char c) +{ + if (c>='0' && c<='9') { + return c - '0'; + } + if (c>='a' && c<='f') { + return c - 'a' + 10; + } + if (c>='A' && c<='F') { + return c - 'A' + 10; + } + + return std::nullopt; +} + +// Convert hex data to raw binary. Ignore any non-hex characters. +static std::vector hex_to_binary(const std::string& line) +{ + std::vector data; + uint8_t current_value = 0; + + bool high_nibble = true; + for (auto c : line) { + auto v = nibble_to_val(c); + if (v) { + if (high_nibble) { + current_value = static_cast(*v << 4); + high_nibble = false; + } + else { + current_value |= *v; + data.push_back(current_value); + high_nibble = true; + } + } + } + + return data; +} + + +// Convert base64 data to raw binary. +static std::vector decode_base64(const std::string& line) +{ + const std::string base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector data; + + size_t len = line.size(); + if (len % 4 != 0) { + len = int(len / 4) * 4; + } + + for (size_t i = 0; i < len; i += 4) { + uint8_t buf[4]; + for (uint8_t j = 0; j < 4; j++) { + size_t k = base64.find(line[i + j]); + buf[j] = (uint8_t)(k == std::string::npos ? base64.size() : k); + } + + data.push_back((uint8_t)(buf[0] << 2) + (buf[1] >> 4)); + + if (line[i + 2] != '=') { + data.push_back((uint8_t)((buf[1] & 0x0f) << 4) + (buf[2] >> 2)); + } + + if (line[i + 3] != '=') { + data.push_back((uint8_t)((buf[2] & 0x03) << 6) + buf[3]); + } + } + + return data; +} + + +// Parse metadata from WebVMT sync commmands +static std::vector parse_vmt_sync_data(const std::string& content) +{ + std::vector data; + + std::regex pattern_sync(R"(\s*\{\s*\"sync\"\s*:\s*\{(.*?)\}\s*\}\s*)"); + const std::sregex_token_iterator ti_end; + + for (std::sregex_token_iterator ti(content.begin(), content.end(), pattern_sync, 1); ti != ti_end; ++ti) + { + std::string sync = *ti; + std::regex pattern_type(R"(.*\"type\"\s*:\s*\"(.*?)\".*)"); + std::regex pattern_data(R"(.*\"data\"\s*:\s*\"(.*?)\".*)"); + std::smatch match; + + if (std::regex_match(sync, match, pattern_type)) { + std::string type = match[1]; + + std::regex pattern_hex(R"(.*\.hex$)"); + std::regex pattern_b64(R"(.*\.base64$)"); + + std::string textData; + if (std::regex_match(sync, match, pattern_data)) { + textData = match[1]; + } + + if (std::regex_match(type, match, pattern_hex)) { + std::vector binaryData = hex_to_binary(textData); + data.insert(data.end(), binaryData.begin(), binaryData.end()); + } + else if (std::regex_match(type, match, pattern_b64)) { + std::vector binaryData = decode_base64(textData); + data.insert(data.end(), binaryData.begin(), binaryData.end()); + } + else { + data.insert(data.end(), textData.data(), textData.data() + textData.length()); + } + } + } + + return data; +} + + +static uint32_t parse_vmt_timestamp(const std::string& vmt_time) +{ + std::regex pattern(R"(-?((\d*):)?(\d\d):(\d\d)(\.(\d*))?)"); + std::smatch match; + + if (!std::regex_match(vmt_time, match, pattern)) { + return 0; // no match + } + + std::string hh = match[2]; // optional + std::string mm = match[3]; + std::string ss = match[4]; + std::string fs = match[6]; // optional + + if (vmt_time.find('-') != std::string::npos) { + return 0; // negative time not supported + } + + uint32_t ms = 0; + + if (fs != "") { + if (fs.length() == 3) { + ms = std::stoi(fs); + } + else { + return BAD_VMT_TIMESTAMP; // invalid + } + } + + uint32_t ts = ((hh != "" ? std::stoi(hh) : 0) * 3600 * 1000 + + std::stoi(mm) * 60 * 1000 + + std::stoi(ss) * 1000 + + ms); + + return ts; +} + +int encode_vmt_metadata_track(heif_context* context, heif_track* visual_track, + const std::string& vmt_metadata_file, + const std::string& track_uri, bool binary) +{ + // --- add metadata track + + heif_track* track = nullptr; + + heif_track_options* track_options = heif_track_options_alloc(); + heif_track_options_set_timescale(track_options, 1000); + + heif_context_add_uri_metadata_sequence_track(context, track_uri.c_str(), track_options, &track); + heif_raw_sequence_sample* sample = heif_raw_sequence_sample_alloc(); + + + std::ifstream istr(vmt_metadata_file.c_str()); + + std::regex pattern_cue(R"(^\s*(-?(\d|:|\.)*)\s*-->\s*(-?(\d|:|\.)*)?.*)"); + std::regex pattern_note(R"(^\s*(NOTE).*)"); + + static std::vector prev_metadata; + static std::optional prev_ts; + + std::string line; + while (std::getline(istr, line)) + { + std::smatch match; + + if (std::regex_match(line, match, pattern_note)) { + while (std::getline(istr, line)) { + if (line.empty()) { + break; + } + } + + continue; + } + + if (!std::regex_match(line, match, pattern_cue)) { + continue; + } + + std::string cue_start = match[1]; + std::string cue_end = match[3]; // == "" for unbounded cues + + uint32_t ts = parse_vmt_timestamp(cue_start); + + std::vector concat; + + if (binary) { + while (std::getline(istr, line)) { + if (line.empty()) { + break; + } + + std::vector binaryData = hex_to_binary(line); + concat.insert(concat.end(), binaryData.begin(), binaryData.end()); + } + + } + else { + while (std::getline(istr, line)) { + if (line.empty()) { + break; + } + + concat.insert(concat.end(), line.data(), line.data() + line.length()); + concat.push_back('\n'); + } + + concat.push_back(0); + std::string content(concat.begin(), concat.end()); + concat = parse_vmt_sync_data(content); + } + + if (ts != BAD_VMT_TIMESTAMP) { + + if (ts > *prev_ts) { + heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size()); + heif_raw_sequence_sample_set_duration(sample, ts - *prev_ts); + heif_track_add_raw_sequence_sample(track, sample); + } + else if (ts == *prev_ts) { + concat.insert(concat.begin(), prev_metadata.begin(), prev_metadata.end()); + } + else { + std::cerr << "Bad WebVMT timestamp order: " << cue_start << "\n"; + } + + prev_ts = ts; + prev_metadata = concat; + } + else { + std::cerr << "Bad WebVMT timestamp: " << cue_start << "\n"; + } + } + + // --- flush last metadata packet + + heif_raw_sequence_sample_set_data(sample, (const uint8_t*)prev_metadata.data(), prev_metadata.size()); + heif_raw_sequence_sample_set_duration(sample, 1); + heif_track_add_raw_sequence_sample(track, sample); + + // --- add track reference + + heif_track_add_reference_to_track(track, heif_track_reference_type_description, visual_track); + + // --- release all objects + + heif_raw_sequence_sample_release(sample); + heif_track_options_release(track_options); + heif_track_release(track); + + return 0; +} diff -Nru libheif-1.19.8/examples/vmt.h libheif-1.23.4/examples/vmt.h --- libheif-1.19.8/examples/vmt.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/examples/vmt.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,38 @@ +/* + libheif example application "heif-enc" - VMT metadata track support. + + MIT License + + Copyright (c) 2025 Dirk Farin + Copyright (c) 2026 Rob Smith + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#pragma once + +#include +#include + +struct heif_context; +struct heif_track; + +int encode_vmt_metadata_track(heif_context* context, heif_track* visual_track, + const std::string& vmt_metadata_file, + const std::string& track_uri, bool binary); diff -Nru libheif-1.19.8/funding.json libheif-1.23.4/funding.json --- libheif-1.19.8/funding.json 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/funding.json 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,123 @@ +{ + "$schema": "https://fundingjson.org/schema/v1.0.0/funding.schema.json", + "version": "v1.0.0", + "entity": { + "type": "individual", + "role": "maintainer", + "name": "Dirk Farin", + "email": "dirk.farin@gmail.com", + "phone": "", + "description": "Author and sole maintainer of libheif (HEIF/AVIF file format library) and libde265 (H.265/HEVC decoder). These libraries are used by practically all open-source software that reads or writes HEIC and AVIF images, including ImageMagick, GIMP, libvips/sharp, Pillow, darktable, digiKam, GDAL and the GNOME and KDE desktops, and through these by a large number of web services and image pipelines. The work is currently almost entirely unfunded: in 2026 alone, 37 security advisories had to be investigated, fixed and released in unpaid evenings and weekends.", + "webpageUrl": { + "url": "https://github.com/farindk" + } + }, + "projects": [ + { + "guid": "libheif", + "name": "libheif", + "description": "ISO/IEC 23008-12 HEIF and AVIF file format decoder and encoder. Supports HEIC, AVIF, VVC, AVC, JPEG, JPEG 2000 and uncompressed (ISO/IEC 23001-17) codecs, image sequences, tiling, HDR, metadata and region annotations. Continuously fuzzed on OSS-Fuzz. In continuous development for 9 years and shipped by all major Linux distributions, Homebrew and vcpkg as the standard open-source HEIF/AVIF library (2.3k GitHub stars).", + "webpageUrl": { + "url": "https://github.com/strukturag/libheif" + }, + "repositoryUrl": { + "url": "https://github.com/strukturag/libheif" + }, + "licenses": ["spdx:LGPL-3.0-or-later"], + "tags": ["heif", "heic", "avif", "image", "codec", "cpp", "security", "multimedia"] + }, + { + "guid": "libde265", + "name": "libde265", + "description": "Open source H.265/HEVC video decoder, used by libheif as the default HEIC decoder. In development for 13 years (1.9k GitHub stars), it is packaged by all major Linux distributions and runs wherever open-source software decodes HEIC images.", + "webpageUrl": { + "url": "https://github.com/strukturag/libde265", + "wellKnown": "https://github.com/strukturag/libde265/blob/master/.well-known/funding-manifest-urls" + }, + "repositoryUrl": { + "url": "https://github.com/strukturag/libde265", + "wellKnown": "https://github.com/strukturag/libde265/blob/master/.well-known/funding-manifest-urls" + }, + "licenses": ["spdx:LGPL-3.0-or-later"], + "tags": ["hevc", "h265", "video", "codec", "cpp", "multimedia"] + } + ], + "funding": { + "channels": [ + { + "guid": "github-sponsors", + "type": "payment-provider", + "address": "https://github.com/sponsors/farindk", + "description": "GitHub Sponsors, monthly or one-time. Organizations can pay by invoice through GitHub." + }, + { + "guid": "invoice", + "type": "bank", + "address": "", + "description": "Bank transfer against an invoice for commercial support agreements, sponsored features and grants. Contact dirk.farin@gmail.com." + } + ], + "plans": [ + { + "guid": "maintenance", + "status": "active", + "name": "Sustained maintenance and security response", + "description": "Funds about two days per week of maintenance for libheif and libde265: triage and fixing of security reports, fuzzing, security releases, keeping up with the ISO/IEC 23008-12 specification, and platform/compiler support. This is the amount that would move security work from weekends into regular working hours.", + "amount": 5000, + "currency": "USD", + "frequency": "monthly", + "channels": ["github-sponsors", "invoice"] + }, + { + "guid": "hardening-sprint", + "status": "active", + "name": "Security hardening sprint", + "description": "A scoped, one-time project: systematic hardening of one subsystem (box parser, image sequences, uncompressed codec, derived images), extension of the fuzzing corpus and harnesses, and a security release. Deliverables and scope are agreed in advance.", + "amount": 25000, + "currency": "USD", + "frequency": "one-time", + "channels": ["invoice", "github-sponsors"] + }, + { + "guid": "hdr-gainmap", + "status": "active", + "name": "HDR Gain Map / Ultra HDR support for libheif", + "description": "Implementation of HDR Gain Maps in libheif, including reading and writing Ultra HDR JPEG images. A gain map stores an SDR base image together with the information to reconstruct the full HDR image, so that photos render correctly on both SDR and HDR displays. This is the HDR format that current iPhone and Android cameras produce; the project gives libheif-based applications access to these photos and interoperable conversion between HEIC, AVIF and Ultra HDR JPEG.", + "amount": 25000, + "currency": "USD", + "frequency": "one-time", + "channels": ["invoice", "github-sponsors"] + }, + { + "guid": "gpu-decoding", + "status": "active", + "name": "GPU hardware decoding of HEIC or AVIF for libheif", + "description": "Implementation of GPU hardware decoding in libheif for one of the two main image formats, HEIC or AVIF, using the video decoding hardware built into common GPUs. Offloading the decoding speeds up viewing of large and tiled images considerably, frees the CPU for the application, and lowers energy consumption. This benefits image viewers, photo management applications and services that display or thumbnail large numbers of HEIC/AVIF images.", + "amount": 40000, + "currency": "USD", + "frequency": "one-time", + "channels": ["invoice", "github-sponsors"] + }, + { + "guid": "feature", + "status": "active", + "name": "Sponsored feature development", + "description": "Development of a specific feature or codec integration that your product needs (for example hardware-accelerated codecs, new ISO/IEC 23008-12 amendments, GIMI/geospatial extensions). Priced per project.", + "amount": 0, + "currency": "USD", + "frequency": "other", + "channels": ["invoice"] + }, + { + "guid": "any", + "status": "active", + "name": "Any amount", + "description": "Any recurring or one-time contribution helps to keep libheif and libde265 maintained.", + "amount": 0, + "currency": "USD", + "frequency": "other", + "channels": ["github-sponsors"] + } + ] + } +} diff -Nru libheif-1.19.8/fuzzing/CMakeLists.txt libheif-1.23.4/fuzzing/CMakeLists.txt --- libheif-1.19.8/fuzzing/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/fuzzing/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -9,6 +9,17 @@ add_executable(encoder_fuzzer encoder_fuzzer.cc) target_link_libraries(encoder_fuzzer PRIVATE heif) +configure_file(encoder_fuzzer.options ${CMAKE_CURRENT_BINARY_DIR}/encoder_fuzzer.options COPYONLY) +configure_file(encoder_fuzzer_lsan_suppressions.txt ${CMAKE_CURRENT_BINARY_DIR}/encoder_fuzzer_lsan_suppressions.txt COPYONLY) add_executable(file_fuzzer file_fuzzer.cc) target_link_libraries(file_fuzzer PRIVATE heif) + +add_executable(tile_fuzzer tile_fuzzer.cc) +target_link_libraries(tile_fuzzer PRIVATE heif) + +add_executable(api_fuzzer api_fuzzer.cc) +target_link_libraries(api_fuzzer PRIVATE heif) + +add_executable(sequence_fuzzer sequence_fuzzer.cc) +target_link_libraries(sequence_fuzzer PRIVATE heif) diff -Nru libheif-1.19.8/fuzzing/api_fuzzer.cc libheif-1.23.4/fuzzing/api_fuzzer.cc --- libheif-1.19.8/fuzzing/api_fuzzer.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/fuzzing/api_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,325 @@ +/* + * HEIF codec. + * Copyright (c) 2026 struktur AG + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +/* + * Companion to file_fuzzer that exercises API surfaces beyond the basic + * decode-and-iterate path: image sequences, region items, depth and + * auxiliary images, tile-by-tile grid decode, item-level data access, + * entity groups, color profiles, and item properties. + */ + +#include +#include +#include + +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "libheif/heif_regions.h" +#include "libheif/heif_aux_images.h" +#include "libheif/heif_tiling.h" +#include "libheif/heif_properties.h" +#include "libheif/heif_entity_groups.h" +#include "libheif/heif_items.h" +#include "libheif/heif_color.h" + +static void TestSequences(struct heif_context* ctx) +{ + if (!heif_context_has_sequence(ctx)) { + return; + } + + int ntracks = heif_context_number_of_sequence_tracks(ctx); + if (ntracks <= 0 || ntracks > 16) { + return; + } + + uint32_t* track_ids = static_cast(malloc(ntracks * sizeof(uint32_t))); + if (!track_ids) return; + heif_context_get_track_ids(ctx, track_ids); + + for (int t = 0; t < ntracks && t < 4; t++) { + struct heif_track* track = heif_context_get_track(ctx, track_ids[t]); + if (!track) continue; + + heif_track_get_track_handler_type(track); + + uint16_t tw = 0, th = 0; + heif_track_get_image_resolution(track, &tw, &th); + + for (int f = 0; f < 8; f++) { + struct heif_image* img = nullptr; + struct heif_error err = heif_track_decode_next_image(track, &img, + heif_colorspace_YCbCr, heif_chroma_420, nullptr); + if (err.code != heif_error_Ok || !img) break; + int stride; + heif_image_get_plane_readonly(img, heif_channel_Y, &stride); + heif_image_release(img); + } + heif_track_release(track); + } + free(track_ids); +} + +static void TestRegions(struct heif_context* ctx, const struct heif_image_handle* handle) +{ + int nregion_items = heif_image_handle_get_number_of_region_items(handle); + if (nregion_items <= 0 || nregion_items > 32) { + return; + } + + heif_item_id* region_ids = static_cast(malloc(nregion_items * sizeof(heif_item_id))); + if (!region_ids) return; + heif_image_handle_get_list_of_region_item_ids(handle, region_ids, nregion_items); + + for (int r = 0; r < nregion_items && r < 8; r++) { + struct heif_region_item* region_item = nullptr; + struct heif_error err = heif_context_get_region_item(ctx, region_ids[r], ®ion_item); + if (err.code != heif_error_Ok || !region_item) continue; + + uint32_t ref_w = 0, ref_h = 0; + heif_region_item_get_reference_size(region_item, &ref_w, &ref_h); + + int nregions = heif_region_item_get_number_of_regions(region_item); + if (nregions > 0 && nregions < 64) { + struct heif_region** regions = static_cast(malloc(nregions * sizeof(void*))); + if (regions) { + int got = heif_region_item_get_list_of_regions(region_item, regions, nregions); + for (int i = 0; i < got && i < 16; i++) { + enum heif_region_type rtype = heif_region_get_type(regions[i]); + (void)rtype; + + int npts = heif_region_get_polygon_num_points(regions[i]); + if (npts > 0 && npts < 1024) { + int32_t* pts = static_cast(malloc(2 * npts * sizeof(int32_t))); + if (pts) { + heif_region_get_polygon_points(regions[i], pts); + free(pts); + } + } + + size_t mask_len = heif_region_get_inline_mask_data_len(regions[i]); + if (mask_len > 0 && mask_len < 1024 * 1024) { + uint8_t* mask_data = static_cast(malloc(mask_len)); + if (mask_data) { + int32_t mx, my; + uint32_t mw, mh; + heif_region_get_inline_mask_data(regions[i], &mx, &my, &mw, &mh, mask_data); + free(mask_data); + } + } + } + heif_region_release_many(regions, got); + free(regions); + } + } + heif_region_item_release(region_item); + } + free(region_ids); +} + +static void TestDepthAux(const struct heif_image_handle* handle) +{ + if (heif_image_handle_has_depth_image(handle)) { + int ndepth = heif_image_handle_get_number_of_depth_images(handle); + if (ndepth > 0 && ndepth < 8) { + heif_item_id depth_ids[8]; + heif_image_handle_get_list_of_depth_image_IDs(handle, depth_ids, ndepth); + for (int i = 0; i < ndepth && i < 2; i++) { + struct heif_image_handle* depth_handle = nullptr; + struct heif_error err = heif_image_handle_get_depth_image_handle(handle, depth_ids[i], &depth_handle); + if (err.code == heif_error_Ok && depth_handle) { + struct heif_image* img = nullptr; + heif_decode_image(depth_handle, &img, + heif_colorspace_monochrome, heif_chroma_monochrome, nullptr); + if (img) heif_image_release(img); + heif_image_handle_release(depth_handle); + } + } + + const struct heif_depth_representation_info* depth_info = nullptr; + heif_image_handle_get_depth_image_representation_info(handle, depth_ids[0], &depth_info); + if (depth_info) { + heif_depth_representation_info_free(depth_info); + } + } + } + + int naux = heif_image_handle_get_number_of_auxiliary_images(handle, 0); + if (naux > 0 && naux < 16) { + heif_item_id aux_ids[16]; + heif_image_handle_get_list_of_auxiliary_image_IDs(handle, 0, aux_ids, naux); + for (int i = 0; i < naux && i < 4; i++) { + struct heif_image_handle* aux_handle = nullptr; + struct heif_error err = heif_image_handle_get_auxiliary_image_handle(handle, aux_ids[i], &aux_handle); + if (err.code == heif_error_Ok && aux_handle) { + const char* aux_type = nullptr; + heif_image_handle_get_auxiliary_type(aux_handle, &aux_type); + if (aux_type) heif_image_handle_release_auxiliary_type(aux_handle, &aux_type); + + struct heif_image* img = nullptr; + heif_decode_image(aux_handle, &img, + heif_colorspace_YCbCr, heif_chroma_420, nullptr); + if (img) heif_image_release(img); + heif_image_handle_release(aux_handle); + } + } + } +} + +static void TestGridTiles(const struct heif_image_handle* handle) +{ + struct heif_image_tiling tiling = {}; + struct heif_error err = heif_image_handle_get_image_tiling(handle, 1, &tiling); + if (err.code != heif_error_Ok) { + return; + } + if (tiling.num_columns == 0 || tiling.num_rows == 0) { + return; + } + if (tiling.num_columns > 8 || tiling.num_rows > 8) { + return; + } + + int decoded = 0; + for (uint32_t y = 0; y < tiling.num_rows && decoded < 4; y++) { + for (uint32_t x = 0; x < tiling.num_columns && decoded < 4; x++) { + struct heif_image* tile_img = nullptr; + err = heif_image_handle_decode_image_tile(handle, &tile_img, + heif_colorspace_YCbCr, heif_chroma_420, nullptr, x, y); + if (err.code == heif_error_Ok && tile_img) { + int stride; + heif_image_get_plane_readonly(tile_img, heif_channel_Y, &stride); + heif_image_release(tile_img); + } + decoded++; + } + } +} + +static void TestProperties(struct heif_context* ctx, const struct heif_image_handle* handle) +{ + heif_item_id item_id = heif_image_handle_get_item_id(handle); + + heif_property_id trans_props[16]; + int ntrans = heif_item_get_transformation_properties(ctx, item_id, trans_props, 16); + (void)ntrans; + + size_t icc_size = heif_image_handle_get_raw_color_profile_size(handle); + if (icc_size > 0 && icc_size < 2 * 1024 * 1024) { + uint8_t* icc_data = static_cast(malloc(icc_size)); + if (icc_data) { + heif_image_handle_get_raw_color_profile(handle, icc_data); + free(icc_data); + } + } + + struct heif_color_profile_nclx* nclx = nullptr; + heif_image_handle_get_nclx_color_profile(handle, &nclx); + if (nclx) heif_nclx_color_profile_free(nclx); +} + +static void TestEntityGroups(struct heif_context* ctx) +{ + int num_groups = 0; + struct heif_entity_group* groups = heif_context_get_entity_groups(ctx, 0, 0, &num_groups); + if (groups) { + heif_entity_groups_release(groups, num_groups); + } +} + +static void TestItems(struct heif_context* ctx) +{ + int nItems = heif_context_get_number_of_items(ctx); + if (nItems <= 0 || nItems > 128) { + return; + } + + heif_item_id* item_ids = static_cast(malloc(nItems * sizeof(heif_item_id))); + if (!item_ids) return; + heif_context_get_list_of_item_IDs(ctx, item_ids, nItems); + + for (int i = 0; i < nItems && i < 16; i++) { + uint32_t item_type = heif_item_get_item_type(ctx, item_ids[i]); + (void)item_type; + int hidden = heif_item_is_item_hidden(ctx, item_ids[i]); + (void)hidden; + + const char* mime_type = heif_item_get_mime_item_content_type(ctx, item_ids[i]); + (void)mime_type; + const char* uri_type = heif_item_get_uri_item_uri_type(ctx, item_ids[i]); + (void)uri_type; + const char* item_name = heif_item_get_item_name(ctx, item_ids[i]); + (void)item_name; + + enum heif_metadata_compression compression; + uint8_t* item_data = nullptr; + size_t item_data_size = 0; + struct heif_error err = heif_item_get_item_data(ctx, item_ids[i], &compression, + &item_data, &item_data_size); + if (err.code == heif_error_Ok && item_data) { + heif_release_item_data(ctx, &item_data); + } + + char* ext_lang = nullptr; + heif_item_get_property_extended_language(ctx, item_ids[i], &ext_lang); + if (ext_lang) heif_string_release(ext_lang); + } + free(item_ids); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 12 || size > 512 * 1024) { + return 0; + } + + struct heif_context* ctx = heif_context_alloc(); + if (!ctx) return 0; + + auto* limits = heif_context_get_security_limits(ctx); + limits->max_memory_block_size = 32 * 1024 * 1024; + limits->max_total_memory = 128 * 1024 * 1024; + limits->max_image_size_pixels = 512 * 512; + + struct heif_error err = heif_context_read_from_memory(ctx, data, size, nullptr); + if (err.code != heif_error_Ok) { + goto quit; + } + + TestSequences(ctx); + TestEntityGroups(ctx); + TestItems(ctx); + + { + struct heif_image_handle* primary = nullptr; + err = heif_context_get_primary_image_handle(ctx, &primary); + if (err.code == heif_error_Ok && primary) { + TestRegions(ctx, primary); + TestDepthAux(primary); + TestGridTiles(primary); + TestProperties(ctx, primary); + heif_image_handle_release(primary); + } + } + +quit: + heif_context_free(ctx); + return 0; +} diff -Nru libheif-1.19.8/fuzzing/box_fuzzer.cc libheif-1.23.4/fuzzing/box_fuzzer.cc --- libheif-1.19.8/fuzzing/box_fuzzer.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/fuzzing/box_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,16 +22,24 @@ #include "box.h" #include "bitstream.h" +#include "context.h" #include "logging.h" extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { auto reader = std::make_shared(data, size, false); + // --- OSS-Fuzz assumes a bug if the allocated memory exceeds 2560 MB. + // Set a lower allocation limit to prevent this. + + // Use a context for tracking the memory usage and set the reduced limit. + HeifContext ctx; + ctx.get_security_limits()->max_total_memory = UINT64_C(2) * 1024 * 1024 * 1024; + BitstreamRange range(reader, size); for (;;) { std::shared_ptr box; - Error error = Box::read(range, &box, heif_get_global_security_limits()); + Error error = Box::read(range, &box, ctx.get_security_limits()); if (error != Error::Ok || range.error()) { break; } diff -Nru libheif-1.19.8/fuzzing/color_conversion_fuzzer.cc libheif-1.23.4/fuzzing/color_conversion_fuzzer.cc --- libheif-1.19.8/fuzzing/color_conversion_fuzzer.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/fuzzing/color_conversion_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,7 +24,7 @@ #include "bitstream.h" #include "color-conversion/colorconversion.h" -#include "pixelimage.h" +#include "image/pixelimage.h" static bool is_valid_chroma(uint8_t chroma) { @@ -70,11 +70,11 @@ if (!range->prepare_read(static_cast(width) * height)) { return false; } - if (auto err = image->add_plane(channel, width, height, bit_depth, heif_get_disabled_security_limits())) { + if (auto err = image->add_channel(channel, width, height, bit_depth, heif_get_disabled_security_limits())) { return false; } size_t stride; - uint8_t* plane = image->get_plane(channel, &stride); + uint8_t* plane = image->get_channel_memory(channel, &stride); assert(stride >= width); auto stream = range->get_istream(); for (uint32_t y = 0; y < height; y++, plane += stride) { @@ -96,11 +96,11 @@ if (!range->prepare_read(static_cast(width) * height * comps)) { return false; } - if (auto err = image->add_plane(channel, width, height, bit_depth, heif_get_disabled_security_limits())) { + if (auto err = image->add_channel(channel, width, height, bit_depth, heif_get_disabled_security_limits())) { return false; } size_t stride; - uint8_t* plane = image->get_plane(channel, &stride); + uint8_t* plane = image->get_channel_memory(channel, &stride); assert(stride >= width * comps); auto stream = range->get_istream(); for (uint32_t y = 0; y < height; y++, plane += stride) { @@ -250,17 +250,21 @@ int output_bpp = 0; // Same as input. heif_encoding_options* options = heif_encoding_options_alloc(); + heif_color_conversion_options_ext* options_ext = heif_color_conversion_options_ext_alloc(); + auto out_image_result = convert_colorspace(in_image, static_cast(out_colorspace), static_cast(out_chroma), - nullptr, + nclx_profile::undefined(), output_bpp, options->color_conversion_options, + options_ext, heif_get_disabled_security_limits()); heif_encoding_options_free(options); + heif_color_conversion_options_ext_free(options_ext); - if (out_image_result.error) { + if (!out_image_result) { // Conversion is not supported. return 0; } Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/av1-huge-frame-header-oom.heic and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/av1-huge-frame-header-oom.heic differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/avc32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avc32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/avif32-mini.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avif32-mini.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/avif32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/avif32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/hevc32-mini.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/hevc32-mini.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/hevc32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/hevc32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/j2k32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/j2k32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/jpeg32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/jpeg32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/unci32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/unci32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/corpus/vvc32.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/corpus/vvc32.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/fuzzing/data/sequence_corpus/seq_seed.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/fuzzing/data/sequence_corpus/seq_seed.heif differ diff -Nru libheif-1.19.8/fuzzing/encoder_fuzzer.cc libheif-1.23.4/fuzzing/encoder_fuzzer.cc --- libheif-1.19.8/fuzzing/encoder_fuzzer.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/fuzzing/encoder_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -144,14 +144,21 @@ } int quality = (data[0] & 0x7F) % 101; - bool lossless = (data[1] & 0x80); - bool use_avif = (data[1] & 0x40); + bool lossless = (data[0] & 0x80); + uint8_t format_uint8 = ((data[1] & 0xf0) >> 4); + uint8_t encoder_idx = data[1] & 0x0f; + + if (format_uint8==0 || format_uint8 > 10) { + return 0; + } + heif_compression_format format = (heif_compression_format)format_uint8; + data += 2; size -= 2; static const size_t kMaxEncoders = 5; const heif_encoder_descriptor* encoder_descriptors[kMaxEncoders]; - int count = heif_get_encoder_descriptors(use_avif ? heif_compression_AV1 : heif_compression_HEVC, + int count = heif_get_encoder_descriptors(format, nullptr, encoder_descriptors, kMaxEncoders); assert(count >= 0); @@ -159,8 +166,12 @@ return 0; } + if (encoder_idx >= count) { + return 0; + } + heif_encoder* encoder; - err = heif_context_get_encoder(context.get(), encoder_descriptors[0], &encoder); + err = heif_context_get_encoder(context.get(), encoder_descriptors[encoder_idx], &encoder); if (err.code != heif_error_Ok) { return 0; } diff -Nru libheif-1.19.8/fuzzing/encoder_fuzzer.options libheif-1.23.4/fuzzing/encoder_fuzzer.options --- libheif-1.19.8/fuzzing/encoder_fuzzer.options 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/fuzzing/encoder_fuzzer.options 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,4 @@ +[libfuzzer] + +[lsan] +suppressions=encoder_fuzzer_lsan_suppressions.txt diff -Nru libheif-1.19.8/fuzzing/encoder_fuzzer_lsan_suppressions.txt libheif-1.23.4/fuzzing/encoder_fuzzer_lsan_suppressions.txt --- libheif-1.19.8/fuzzing/encoder_fuzzer_lsan_suppressions.txt 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/fuzzing/encoder_fuzzer_lsan_suppressions.txt 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1 @@ +leak:x265_encoder_open diff -Nru libheif-1.19.8/fuzzing/file_fuzzer.cc libheif-1.23.4/fuzzing/file_fuzzer.cc --- libheif-1.19.8/fuzzing/file_fuzzer.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/fuzzing/file_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -37,6 +37,7 @@ (void) primary; int width = heif_image_handle_get_width(handle); int height = heif_image_handle_get_height(handle); + (void)width; (void)height; assert(width >= 0); assert(height >= 0); int metadata_count = heif_image_handle_get_number_of_metadata_blocks(handle, nullptr); @@ -47,6 +48,7 @@ int metadata_ids_count = heif_image_handle_get_list_of_metadata_block_IDs(handle, nullptr, metadata_ids, metadata_count); assert(metadata_count == metadata_ids_count); + (void)metadata_ids_count; for (int i = 0; i < metadata_count; i++) { heif_image_handle_get_metadata_type(handle, metadata_ids[i]); heif_image_handle_get_metadata_content_type(handle, metadata_ids[i]); @@ -101,6 +103,18 @@ ctx = heif_context_alloc(); assert(ctx); + + auto* limits = heif_context_get_security_limits(ctx); + limits->max_total_memory = UINT64_C(2) * 1024 * 1024 * 1024; + limits->max_memory_block_size = 128 * 1024 * 1024; // 128 MB + + // Also bound the image size itself. libheif's memory accounting only covers its own + // allocations, but the codec libraries allocate their frame buffers before libheif + // sees anything (dav1d needs ~12.5 bytes/pixel for a frame header alone). Under MSan, + // where every allocation costs about three times its size in RSS, a 56 Mpixel AV1 frame + // header was enough to exceed libFuzzer's 2560 MB limit. + limits->max_image_size_pixels = 16 * 1024 * 1024; // 16 Mpixel + err = heif_context_read_from_memory(ctx, data, size, nullptr); if (err.code != heif_error_Ok) { // Not a valid HEIF file passed (which is most likely while fuzzing). diff -Nru libheif-1.19.8/fuzzing/sequence_fuzzer.cc libheif-1.23.4/fuzzing/sequence_fuzzer.cc --- libheif-1.19.8/fuzzing/sequence_fuzzer.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/fuzzing/sequence_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,78 @@ +/* + * HEIF codec. + * Copyright (c) 2026 struktur AG, Arthur SC Chan + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include + +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" + +#define MAX_FRAMES 32 + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + heif_context* ctx = heif_context_alloc(); + if (!ctx) { + return 0; + } + + auto* limits = heif_context_get_security_limits(ctx); + limits->max_total_memory = UINT64_C(2) * 1024 * 1024 * 1024; + limits->max_memory_block_size = 128 * 1024 * 1024; + // Also bound the image size itself. libheif's memory accounting only covers its own + // allocations, but the codec libraries allocate their frame buffers before libheif + // sees anything (dav1d needs ~12.5 bytes/pixel for a frame header alone). Under MSan, + // where every allocation costs about three times its size in RSS, a 56 Mpixel AV1 frame + // header was enough to exceed libFuzzer's 2560 MB limit. + limits->max_image_size_pixels = 16 * 1024 * 1024; // 16 Mpixel + + heif_error err = heif_context_read_from_memory(ctx, data, size, nullptr); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return 0; + } + + if (heif_context_has_sequence(ctx)) { + heif_track* track = heif_context_get_track(ctx, 0); + if (track) { + uint16_t w = 0, h = 0; + heif_track_get_image_resolution(track, &w, &h); + heif_track_get_timescale(track); + heif_track_get_track_handler_type(track); + + int frames = 0; + while (frames++ < MAX_FRAMES) { + heif_image* img = nullptr; + err = heif_track_decode_next_image(track, &img, + heif_colorspace_YCbCr, + heif_chroma_420, nullptr); + if (err.code != heif_error_Ok || img == nullptr) { + break; + } + heif_image_release(img); + } + + heif_track_release(track); + } + } + + heif_context_free(ctx); + return 0; +} diff -Nru libheif-1.19.8/fuzzing/tile_fuzzer.cc libheif-1.23.4/fuzzing/tile_fuzzer.cc --- libheif-1.19.8/fuzzing/tile_fuzzer.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/fuzzing/tile_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,174 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +// Fuzz harness for the per-tile read API (heif_image_handle_get_image_tiling, +// heif_image_handle_get_grid_image_tile_id, heif_image_handle_decode_image_tile). +// file_fuzzer.cc only calls heif_decode_image, which routes grid images through +// decode_full_grid_image and never exercises decode_grid_tile or the surrounding +// tiling-API surface. This harness fills that gap. + +#include +#include +#include +#include + +#include "libheif/heif.h" +#include "libheif/heif_tiling.h" + +static const enum heif_colorspace kFuzzColorSpace = heif_colorspace_YCbCr; +static const enum heif_chroma kFuzzChroma = heif_chroma_420; + +// Cap how many tiles we try to decode per image. Synthetic grids can claim +// millions of tiles; bounding here keeps the per-input time budget sane +// without losing meaningful coverage (crashes show up on the first few tiles). +static const uint32_t kMaxTilesPerImage = 32; + +static void TestTileAPI(const struct heif_image_handle* handle, + int process_image_transformations) +{ + struct heif_image_tiling tiling = {}; + struct heif_error err = heif_image_handle_get_image_tiling( + handle, process_image_transformations, &tiling); + if (err.code != heif_error_Ok) { + return; + } + + // Probe per-tile id lookup and decode within the declared grid, capped. + uint32_t cols = tiling.num_columns; + uint32_t rows = tiling.num_rows; + uint32_t total = 0; + + for (uint32_t ty = 0; ty < rows && total < kMaxTilesPerImage; ty++) { + for (uint32_t tx = 0; tx < cols && total < kMaxTilesPerImage; tx++) { + heif_item_id tile_id = 0; + // Return value intentionally ignored; we only care that the call does + // not crash on malformed input. + heif_image_handle_get_grid_image_tile_id( + handle, process_image_transformations, tx, ty, &tile_id); + + struct heif_image* tile_img = nullptr; + err = heif_image_handle_decode_image_tile( + handle, &tile_img, kFuzzColorSpace, kFuzzChroma, nullptr, tx, ty); + if (err.code == heif_error_Ok && tile_img != nullptr) { + heif_image_release(tile_img); + } else if (tile_img != nullptr) { + // Defensive: some error paths may still produce an image we own. + heif_image_release(tile_img); + } + + total++; + } + } +} + +static int clip_int(size_t size) +{ + return size > INT_MAX ? INT_MAX : static_cast(size); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + struct heif_context* ctx; + struct heif_error err; + struct heif_image_handle* primary_handle = nullptr; + int images_count; + heif_item_id* image_IDs = nullptr; + bool explicit_init = size == 0 || data[size - 1] & 1; + + if (explicit_init) { + heif_init(nullptr); + } + + heif_check_filetype(data, clip_int(size)); + + ctx = heif_context_alloc(); + assert(ctx); + + auto* limits = heif_context_get_security_limits(ctx); + limits->max_total_memory = UINT64_C(2) * 1024 * 1024 * 1024; + limits->max_memory_block_size = 128 * 1024 * 1024; + // Also bound the image size itself. libheif's memory accounting only covers its own + // allocations, but the codec libraries allocate their frame buffers before libheif + // sees anything (dav1d needs ~12.5 bytes/pixel for a frame header alone). Under MSan, + // where every allocation costs about three times its size in RSS, a 56 Mpixel AV1 frame + // header was enough to exceed libFuzzer's 2560 MB limit. + limits->max_image_size_pixels = 16 * 1024 * 1024; // 16 Mpixel + + err = heif_context_read_from_memory(ctx, data, size, nullptr); + if (err.code != heif_error_Ok) { + goto quit; + } + + err = heif_context_get_primary_image_handle(ctx, &primary_handle); + if (err.code == heif_error_Ok) { + TestTileAPI(primary_handle, /*process_image_transformations=*/1); + TestTileAPI(primary_handle, /*process_image_transformations=*/0); + heif_image_handle_release(primary_handle); + primary_handle = nullptr; + } + + images_count = heif_context_get_number_of_top_level_images(ctx); + if (!images_count) { + goto quit; + } + + image_IDs = static_cast(malloc(images_count * sizeof(heif_item_id))); + assert(image_IDs); + images_count = heif_context_get_list_of_top_level_image_IDs(ctx, image_IDs, images_count); + if (!images_count) { + goto quit; + } + + for (int i = 0; i < images_count; ++i) { + struct heif_image_handle* image_handle = nullptr; + err = heif_context_get_image_handle(ctx, image_IDs[i], &image_handle); + if (err.code != heif_error_Ok) { + heif_image_handle_release(image_handle); + continue; + } + + TestTileAPI(image_handle, /*process_image_transformations=*/1); + + // Also iterate thumbnails — these can themselves be grid/overlay items + // and have separate decoder paths. + int num_thumbnails = heif_image_handle_get_number_of_thumbnails(image_handle); + for (int t = 0; t < num_thumbnails; ++t) { + struct heif_image_handle* thumbnail_handle = nullptr; + heif_image_handle_get_thumbnail(image_handle, t, &thumbnail_handle); + if (thumbnail_handle) { + TestTileAPI(thumbnail_handle, /*process_image_transformations=*/1); + heif_image_handle_release(thumbnail_handle); + } + } + + heif_image_handle_release(image_handle); + } + +quit: + heif_image_handle_release(primary_handle); + heif_context_free(ctx); + free(image_IDs); + + if (explicit_init) { + heif_deinit(); + } + + return 0; +} diff -Nru libheif-1.19.8/gdk-pixbuf/pixbufloader-heif.c libheif-1.23.4/gdk-pixbuf/pixbufloader-heif.c --- libheif-1.19.8/gdk-pixbuf/pixbufloader-heif.c 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/gdk-pixbuf/pixbufloader-heif.c 2026-09-06 14:45:17.000000000 +0000 @@ -81,25 +81,29 @@ err = heif_init(NULL); if (err.code != heif_error_Ok) { - g_warning("%s", err.message); + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, + "HEIF decoding failed: %s", err.message); goto cleanup; } hc = heif_context_alloc(); if (!hc) { - g_warning("cannot allocate heif_context"); + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, + "cannot allocate heif_context"); goto cleanup; } err = heif_context_read_from_memory_without_copy(hc, hpc->data->data, hpc->data->len, NULL); if (err.code != heif_error_Ok) { - g_warning("%s", err.message); + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, + "HEIF decoding failed: %s", err.message); goto cleanup; } err = heif_context_get_primary_image_handle(hc, &hdl); if (err.code != heif_error_Ok) { - g_warning("%s", err.message); + g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, + "HEIF decoding failed: %s", err.message); goto cleanup; } @@ -109,7 +113,10 @@ has_alpha ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB, NULL); if (err.code != heif_error_Ok) { - g_warning("%s", err.message); + g_set_error(error, GDK_PIXBUF_ERROR, + err.code == heif_error_Unsupported_feature ? GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION + : GDK_PIXBUF_ERROR_CORRUPT_IMAGE, + "HEIF decoding failed: %s", err.message); goto cleanup; } diff -Nru libheif-1.19.8/go/heif/heif.go libheif-1.23.4/go/heif/heif.go --- libheif-1.19.8/go/heif/heif.go 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/go/heif/heif.go 2026-09-06 14:45:17.000000000 +0000 @@ -49,7 +49,7 @@ return C.GoString(C.heif_get_version()) } -type Compression C.enum_heif_compression_format +type Compression C.heif_compression_format const ( CompressionUndefined = C.heif_compression_undefined @@ -65,11 +65,10 @@ CompressionHTJ2K = C.heif_compression_HTJ2K ) -type Chroma C.enum_heif_chroma +type Chroma C.heif_chroma const ( ChromaUndefined = C.heif_chroma_undefined - ChromaMonochrome = C.heif_chroma_monochrome Chroma420 = C.heif_chroma_420 Chroma422 = C.heif_chroma_422 Chroma444 = C.heif_chroma_444 @@ -79,12 +78,14 @@ ChromaInterleavedRRGGBBAA_LE = C.heif_chroma_interleaved_RRGGBBAA_LE ChromaInterleavedRRGGBB_BE = C.heif_chroma_interleaved_RRGGBB_BE ChromaInterleavedRRGGBB_LE = C.heif_chroma_interleaved_RRGGBB_LE + ChromaPlanar = C.heif_chroma_planar + ChromaMonochrome = C.heif_chroma_monochrome ChromaInterleaved24Bit = C.heif_chroma_interleaved_24bit ChromaInterleaved32Bit = C.heif_chroma_interleaved_32bit ) -type ChromaDownsamplingAlgorithm C.enum_heif_chroma_downsampling_algorithm +type ChromaDownsamplingAlgorithm C.heif_chroma_downsampling_algorithm const ( ChromaDownsamplingAverage = C.heif_chroma_downsampling_average @@ -92,24 +93,26 @@ ChromaDownsamplingSharpYUV = C.heif_chroma_downsampling_sharp_yuv ) -type ChromaUpsamplingAlgorithm C.enum_heif_chroma_upsampling_algorithm +type ChromaUpsamplingAlgorithm C.heif_chroma_upsampling_algorithm const ( ChromaUpsamplingNearestNeighbor = C.heif_chroma_upsampling_nearest_neighbor ChromaUpsamplingBilinear = C.heif_chroma_upsampling_bilinear ) -type Colorspace C.enum_heif_colorspace +type Colorspace C.heif_colorspace const ( - ColorspaceUndefined = C.heif_colorspace_undefined - ColorspaceYCbCr = C.heif_colorspace_YCbCr - ColorspaceRGB = C.heif_colorspace_RGB - ColorspaceMonochrome = C.heif_colorspace_monochrome - ColorspaceNonvisual = C.heif_colorspace_nonvisual + ColorspaceUndefined = C.heif_colorspace_undefined + ColorspaceYCbCr = C.heif_colorspace_YCbCr + ColorspaceRGB = C.heif_colorspace_RGB + ColorspaceCustom = C.heif_colorspace_custom + ColorspaceMonochrome = C.heif_colorspace_monochrome + ColorspaceFilterArray = C.heif_colorspace_filter_array + ColorspaceNonvisual = C.heif_colorspace_nonvisual ) -type Channel C.enum_heif_channel +type Channel C.heif_channel const ( ChannelY = C.heif_channel_Y @@ -123,9 +126,10 @@ ChannelFilterArray = C.heif_channel_filter_array ChannelDepth = C.heif_channel_depth ChannelDisparity = C.heif_channel_disparity + ChannelUnknown = C.heif_channel_unknown ) -type ProgressStep C.enum_heif_progress_step +type ProgressStep C.heif_progress_step const ( ProgressStepTotal = C.heif_progress_step_total @@ -150,7 +154,7 @@ // --- HeifError -type ErrorCode C.enum_heif_error_code +type ErrorCode C.heif_error_code const ( ErrorOK = C.heif_error_Ok @@ -182,6 +186,8 @@ // Error during encoding or when writing to the output ErrorEncoding = C.heif_error_Encoding_error + ErrorEndOfSequence = C.heif_error_End_of_sequence + // Application has asked for a color profile type that does not exist ErrorColorProfileDoesNotExist = C.heif_error_Color_profile_does_not_exist @@ -190,7 +196,7 @@ ErrorCanceled = C.heif_error_Canceled ) -type ErrorSubcode C.enum_heif_suberror_code +type ErrorSubcode C.heif_suberror_code const ( // no further information available @@ -211,6 +217,8 @@ SuberrorNoMetaBox = C.heif_suberror_No_meta_box + SuberrorNoMoovBox = C.heif_suberror_No_moov_box + SuberrorNoHdlrBox = C.heif_suberror_No_hdlr_box SuberrorNoHvcCBox = C.heif_suberror_No_hvcC_box @@ -246,6 +254,9 @@ // Tile-images in a grid image are missing SuberrorMissingGridImages = C.heif_suberror_Missing_grid_images + // The colr (NCLX) box and the codec bitstream VUI/color signalling disagree. + SuberrorNCLXColrVUIMismatch = C.heif_suberror_NCLX_colr_VUI_mismatch + SuberrorNoAV1CBox = C.heif_suberror_No_av1C_box SuberrorNoAVCCBox = C.heif_suberror_No_avcC_box @@ -365,6 +376,8 @@ SuberrorUnsupportedHeaderCompressionMethod = C.heif_suberror_Unsupported_header_compression_method + SubErrorUnsupportedTrackType = C.heif_suberror_Unsupported_track_type + // --- Encoder_plugin_error --- SuberrorUnsupportedBitDepth = C.heif_suberror_Unsupported_bit_depth @@ -507,7 +520,7 @@ func (c *Context) NewEncoder(compression Compression) (*Encoder, error) { const max = 1 descriptors := make([]*C.struct_heif_encoder_descriptor, max) - num := int(C.heif_context_get_encoder_descriptors(c.context, uint32(compression), nil, &descriptors[0], C.int(max))) + num := int(C.heif_context_get_encoder_descriptors(c.context, C.heif_compression_format(compression), nil, &descriptors[0], C.int(max))) runtime.KeepAlive(c) if num == 0 { return nil, fmt.Errorf("no encoder for compression %v", compression) @@ -708,7 +721,7 @@ func NewImage(width, height int, colorspace Colorspace, chroma Chroma) (*Image, error) { var image Image - err := C.heif_image_create(C.int(width), C.int(height), uint32(colorspace), uint32(chroma), &image.image) + err := C.heif_image_create(C.int(width), C.int(height), C.heif_colorspace(colorspace), C.heif_chroma(chroma), &image.image) if err := convertHeifError(err); err != nil { return nil, err } @@ -729,7 +742,7 @@ opt = options.options } - err := C.heif_decode_image(h.handle, &image.image, uint32(colorspace), uint32(chroma), opt) + err := C.heif_decode_image(h.handle, &image.image, C.heif_colorspace(colorspace), C.heif_chroma(chroma), opt) runtime.KeepAlive(h) if err := convertHeifError(err); err != nil { return nil, err @@ -752,25 +765,25 @@ } func (img *Image) GetWidth(channel Channel) int { - i := int(C.heif_image_get_width(img.image, uint32(channel))) + i := int(C.heif_image_get_width(img.image, C.heif_channel(channel))) runtime.KeepAlive(img) return i } func (img *Image) GetHeight(channel Channel) int { - i := int(C.heif_image_get_height(img.image, uint32(channel))) + i := int(C.heif_image_get_height(img.image, C.heif_channel(channel))) runtime.KeepAlive(img) return i } func (img *Image) GetBitsPerPixel(channel Channel) int { - i := int(C.heif_image_get_bits_per_pixel(img.image, uint32(channel))) + i := int(C.heif_image_get_bits_per_pixel(img.image, C.heif_channel(channel))) runtime.KeepAlive(img) return i } func (img *Image) GetBitsPerPixelRange(channel Channel) int { - i := int(C.heif_image_get_bits_per_pixel_range(img.image, uint32(channel))) + i := int(C.heif_image_get_bits_per_pixel_range(img.image, C.heif_channel(channel))) runtime.KeepAlive(img) return i } @@ -1157,24 +1170,37 @@ i.Plane = C.GoBytes(i.planePtr, C.int(i.height*i.Stride)) } +// C.GoBytes takes a C.int length, so it can copy at most this many bytes. +const maxGoBytesLen = int64(^uint32(0) >> 1) // math.MaxInt32 + func (img *Image) GetPlane(channel Channel) (*ImageAccess, error) { - height := C.heif_image_get_height(img.image, uint32(channel)) + height := C.heif_image_get_height(img.image, C.heif_channel(channel)) runtime.KeepAlive(img) if height == -1 { return nil, fmt.Errorf("No such channel %v", channel) } - var stride C.int - plane := C.heif_image_get_plane(img.image, uint32(channel), &stride) + // Use the size_t-stride variant. The deprecated heif_image_get_plane() returns + // an int stride; multiplying that by the height overflows int32 for large + // multi-byte images, which then makes C.GoBytes() panic on a negative length. + var stride C.size_t + plane := C.heif_image_get_plane2(img.image, C.heif_channel(channel), &stride) runtime.KeepAlive(img) if plane == nil { return nil, fmt.Errorf("No such channel %v", channel) } ptr := unsafe.Pointer(plane) - size := stride * height + + // Compute the plane size in 64-bit arithmetic and reject planes larger than + // C.GoBytes() can represent instead of overflowing into a negative length. + size := int64(stride) * int64(height) + if size < 0 || size > maxGoBytesLen { + return nil, fmt.Errorf("Image plane too large: %d bytes", size) + } + access := &ImageAccess{ - Plane: C.GoBytes(ptr, size), + Plane: C.GoBytes(ptr, C.int(size)), planePtr: ptr, Stride: int(stride), height: int(height), @@ -1184,7 +1210,7 @@ } func (img *Image) NewPlane(channel Channel, width, height, depth int) (*ImageAccess, error) { - err := C.heif_image_add_plane(img.image, uint32(channel), C.int(width), C.int(height), C.int(depth)) + err := C.heif_image_add_plane(img.image, C.heif_channel(channel), C.int(width), C.int(height), C.int(depth)) runtime.KeepAlive(img) if err := convertHeifError(err); err != nil { return nil, err diff -Nru libheif-1.19.8/heifio/CMakeLists.txt libheif-1.23.4/heifio/CMakeLists.txt --- libheif-1.19.8/heifio/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -3,6 +3,10 @@ add_library(heifio STATIC decoder.h + decoder_heif.cc + decoder_heif.h + decoder_raw.cc + decoder_raw.h decoder_y4m.cc decoder_y4m.h encoder.h @@ -30,6 +34,18 @@ target_sources(heifio PRIVATE decoder_tiff.cc decoder_tiff.h encoder_tiff.h encoder_tiff.cc) target_link_libraries(heifio PRIVATE TIFF::TIFF) target_compile_definitions(heifio PUBLIC HAVE_LIBTIFF=1) + + option(WITH_GEOTIFF "Print GeoTIFF metadata when encoding tiled TIFFs (requires libgeotiff)" OFF) + if (WITH_GEOTIFF) + find_library(GEOTIFF_LIBRARY geotiff) + find_path(GEOTIFF_INCLUDE_DIR geotiff/geotiff.h) + if (GEOTIFF_LIBRARY AND GEOTIFF_INCLUDE_DIR) + set(GEOTIFF_FOUND TRUE) + target_compile_definitions(heifio PUBLIC HAVE_GEOTIFF=1) + target_link_libraries(heifio PRIVATE ${GEOTIFF_LIBRARY}) + target_include_directories(heifio PRIVATE ${GEOTIFF_INCLUDE_DIR}) + endif() + endif() endif() find_package(JPEG) @@ -65,8 +81,23 @@ endif () -find_package(PNG) -set(PNG_FOUND ${PNG_FOUND} PARENT_SCOPE) +option(WITH_LIBPNG_INTERNAL "Use the libpng build in the 'third-party' directory" OFF) + +if (WITH_LIBPNG_INTERNAL) + add_library(PNG::PNG STATIC IMPORTED) + set_target_properties(PNG::PNG PROPERTIES + IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/third-party/libpng/build/dist/lib/libpng.a" + INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/third-party/libpng/build/dist/include" + ) + find_package(ZLIB REQUIRED) + target_link_libraries(PNG::PNG INTERFACE ZLIB::ZLIB m) + set(PNG_FOUND ON) + set(PNG_FOUND ON PARENT_SCOPE) +else() + find_package(PNG) + set(PNG_FOUND ${PNG_FOUND} PARENT_SCOPE) +endif() + if (TARGET PNG::PNG) target_compile_definitions(heifio PUBLIC -DHAVE_LIBPNG=1) @@ -76,6 +107,26 @@ target_sources(heifio PRIVATE decoder_png.cc decoder_png.h) endif () +find_package(WEBP) +if(WEBP_FOUND) + target_compile_definitions(heifio PUBLIC -DHAVE_LIBWEBP=1) + + target_link_libraries(heifio PRIVATE WebP::webp) + target_link_libraries(heifio PRIVATE WebP::libwebpmux) + + # libwebp uses pthreads internally, but depending on how it was built + # (e.g. autotools, where libtool may drop '-pthread') its shared object can + # lack a DT_NEEDED entry for libpthread. On systems where pthread is not + # part of libc (glibc < 2.34) linking against libwebp then fails with + # unresolved 'pthread_create'/'pthread_join'. Link Threads defensively so + # the symbols are always satisfied; this is a no-op where pthread is in libc. + find_package(Threads REQUIRED) + target_link_libraries(heifio PRIVATE Threads::Threads) + + target_sources(heifio PRIVATE encoder_webp.cc encoder_webp.h) + target_sources(heifio PRIVATE decoder_webp.cc decoder_webp.h) +endif() + message("") message("=== Active input formats for heif-enc ===") if (JPEG_FOUND) @@ -88,8 +139,20 @@ else () message("PNG: ------ (libpng not found)") endif () +if (WEBP_FOUND) + message("WEBP: active") +else () + message("WEBP: ------ (libwebp not found)") +endif () if (TIFF_FOUND) message("TIFF: active") + if (NOT WITH_GEOTIFF) + message("GeoTIFF: ------ (WITH_GEOTIFF=OFF)") + elseif (GEOTIFF_FOUND) + message("GeoTIFF: active") + else () + message("GeoTIFF: ------ (libgeotiff not found)") + endif () else () message("TIFF: ------ (libtiff not found)") endif () diff -Nru libheif-1.19.8/heifio/decoder_heif.cc libheif-1.23.4/heifio/decoder_heif.cc --- libheif-1.19.8/heifio/decoder_heif.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_heif.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,140 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include +#include + +#include "decoder_heif.h" + +static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; + +// libheif's heif_error.message is owned by the heif_context (or heif_image_handle) +// and becomes invalid once the owning object is released. Copy the message into a +// thread-local string so it stays valid for the caller after we have released the +// context, and so concurrent callers on different threads do not race on the buffer. +// The caller is expected to log the error promptly (heif-enc calls exit() immediately). +static heif_error stable_error(const heif_error& err) +{ + static thread_local std::string msg; + msg = err.message ? err.message : ""; + return {err.code, err.subcode, msg.c_str()}; +} + + +heif_error loadHEIF(const char* filename, InputImage* input_image) +{ + heif_context* ctx = heif_context_alloc(); + if (!ctx) { + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, + "Cannot allocate HEIF context"}; + } + + heif_error err = heif_context_read_from_file(ctx, filename, nullptr); + if (err.code != heif_error_Ok) { + heif_error stable = stable_error(err); + heif_context_free(ctx); + return stable; + } + + heif_item_id primary_id; + err = heif_context_get_primary_image_ID(ctx, &primary_id); + if (err.code != heif_error_Ok) { + heif_error stable = stable_error(err); + heif_context_free(ctx); + return stable; + } + + heif_image_handle* handle = nullptr; + err = heif_context_get_image_handle(ctx, primary_id, &handle); + if (err.code != heif_error_Ok) { + heif_error stable = stable_error(err); + heif_context_free(ctx); + return stable; + } + + // Decode in the file's native colorspace/chroma so the encoder side does not have + // to perform any extra colorspace conversion. Transforms (irot/imir) are applied + // during decode (default ignore_transformations=0), so the resulting pixels are + // already in display orientation and input_image->orientation stays 'normal'. + + heif_decoding_options* opts = heif_decoding_options_alloc(); + opts->output_image_nclx_profile_passthrough = true; + + heif_image* image = nullptr; + err = heif_decode_image(handle, &image, + heif_colorspace_undefined, heif_chroma_undefined, + opts); + + heif_decoding_options_free(opts); + + if (err.code != heif_error_Ok) { + heif_error stable = stable_error(err); + heif_image_handle_release(handle); + heif_context_free(ctx); + return stable; + } + + input_image->image = std::shared_ptr(image, + [](heif_image* img) { heif_image_release(img); }); + + // Extract EXIF and XMP metadata. Pass the bytes through verbatim, including the + // 4-byte TIFF-offset prefix that libheif's EXIF item format carries — the encoder + // side (heif_context_add_exif_metadata) expects the same layout. + + int numMetadata = heif_image_handle_get_number_of_metadata_blocks(handle, nullptr); + if (numMetadata > 0) { + std::vector ids(numMetadata); + heif_image_handle_get_list_of_metadata_block_IDs(handle, nullptr, ids.data(), numMetadata); + + for (int n = 0; n < numMetadata; n++) { + std::string itemtype = heif_image_handle_get_metadata_type(handle, ids[n]); + std::string contenttype = heif_image_handle_get_metadata_content_type(handle, ids[n]); + + if (itemtype == "Exif" && input_image->exif.empty()) { + size_t size = heif_image_handle_get_metadata_size(handle, ids[n]); + input_image->exif.resize(size); + heif_error e = heif_image_handle_get_metadata(handle, ids[n], input_image->exif.data()); + if (e.code != heif_error_Ok) { + input_image->exif.clear(); + } + } + else if (contenttype == "application/rdf+xml" && input_image->xmp.empty()) { + size_t size = heif_image_handle_get_metadata_size(handle, ids[n]); + input_image->xmp.resize(size); + heif_error e = heif_image_handle_get_metadata(handle, ids[n], input_image->xmp.data()); + if (e.code != heif_error_Ok) { + input_image->xmp.clear(); + } + } + } + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + + return heif_error_ok; +} diff -Nru libheif-1.19.8/heifio/decoder_heif.h libheif-1.23.4/heifio/decoder_heif.h --- libheif-1.19.8/heifio/decoder_heif.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_heif.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,35 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef LIBHEIF_DECODER_HEIF_H +#define LIBHEIF_DECODER_HEIF_H + +#include "decoder.h" + +LIBHEIF_API +heif_error loadHEIF(const char* filename, InputImage* input_image); + +#endif //LIBHEIF_DECODER_HEIF_H diff -Nru libheif-1.19.8/heifio/decoder_jpeg.cc libheif-1.23.4/heifio/decoder_jpeg.cc --- libheif-1.19.8/heifio/decoder_jpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_jpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -303,8 +303,8 @@ err = heif_image_add_plane(image, heif_channel_Y, cinfo.output_width, cinfo.output_height, 8); if (err.code) { goto cleanup; } - int y_stride; - uint8_t* py = heif_image_get_plane(image, heif_channel_Y, &y_stride); + size_t y_stride; + uint8_t* py = heif_image_get_plane2(image, heif_channel_Y, &y_stride); // read the image @@ -385,11 +385,11 @@ err = heif_image_add_plane(image, heif_channel_Cr, cw, ch, 8); if (err.code) { goto cleanup; } - int stride[3]; + size_t stride[3]; uint8_t* p[3]; - p[0] = heif_image_get_plane(image, heif_channel_Y, &stride[0]); - p[1] = heif_image_get_plane(image, heif_channel_Cb, &stride[1]); - p[2] = heif_image_get_plane(image, heif_channel_Cr, &stride[2]); + p[0] = heif_image_get_plane2(image, heif_channel_Y, &stride[0]); + p[1] = heif_image_get_plane2(image, heif_channel_Cb, &stride[1]); + p[2] = heif_image_get_plane2(image, heif_channel_Cr, &stride[2]); // read the image @@ -495,8 +495,60 @@ } } } + else if (cinfo.jpeg_color_space == JCS_CMYK || cinfo.jpeg_color_space == JCS_YCCK) { + // libjpeg converts YCCK to CMYK internally when out_color_space is JCS_CMYK + cinfo.out_color_space = JCS_CMYK; + + jpeg_start_decompress(&cinfo); + + JSAMPARRAY buffer; + buffer = (*cinfo.mem->alloc_sarray) + ((j_common_ptr) &cinfo, JPOOL_IMAGE, cinfo.output_width * 4, 1); + + // create interleaved RGB destination image + + err = heif_image_create(cinfo.output_width, cinfo.output_height, + heif_colorspace_RGB, + heif_chroma_interleaved_RGB, + &image); + if (err.code) { goto cleanup; } + + err = heif_image_add_plane(image, heif_channel_interleaved, cinfo.output_width, cinfo.output_height, 8); + if (err.code) { goto cleanup; } + + size_t rgb_stride; + uint8_t* pRGB = heif_image_get_plane2(image, heif_channel_interleaved, &rgb_stride); + + while (cinfo.output_scanline < cinfo.output_height) { + (void) jpeg_read_scanlines(&cinfo, buffer, 1); + + JOCTET* bufp = buffer[0]; + size_t y = cinfo.output_scanline - 1; + uint8_t* row = pRGB + y * rgb_stride; + + for (unsigned int x = 0; x < cinfo.output_width; x++) { + uint8_t C = bufp[0]; + uint8_t M = bufp[1]; + uint8_t Y = bufp[2]; + uint8_t K = bufp[3]; + bufp += 4; + + // CMYK to RGB — JPEG CMYK uses inverted values (255 = no ink) + row[0] = (uint8_t)(C * K / 255); + row[1] = (uint8_t)(M * K / 255); + row[2] = (uint8_t)(Y * K / 255); + row += 3; + } + } + } else { - // TODO: error, unsupported JPEG colorspace + jpeg_destroy_decompress(&cinfo); + free(iccBuffer); + fclose(infile); + err = {heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + "Unsupported JPEG color space"}; + return err; } if (embeddedIccFlag && iccLen > 0) { diff -Nru libheif-1.19.8/heifio/decoder_png.cc libheif-1.23.4/heifio/decoder_png.cc --- libheif-1.19.8/heifio/decoder_png.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_png.cc 2026-09-06 14:45:17.000000000 +0000 @@ -30,6 +30,7 @@ #include #include #include "decoder_png.h" +#include "common_utils.h" #include "exif.h" extern "C" { @@ -73,8 +74,24 @@ #else png_bytep png_profile_data; #endif - uint8_t* profile_data = nullptr; + // 'volatile' so the value survives a longjmp back to the setjmp handler below, + // where it is freed (a non-volatile local modified after setjmp() has an + // indeterminate value after longjmp()). + uint8_t* volatile profile_data = nullptr; png_uint_32 profile_length = 5; +#ifdef PNG_cICP_SUPPORTED + uint8_t color_primaries, transfer_characteristics, matrix_coefficients, video_full_range; + bool has_cICP = false; +#endif +#ifdef PNG_cLLI_SUPPORTED + uint32_t maximum_content_light_level_scaled_by_10000, maximum_frame_average_light_level_scaled_by_10000; + bool has_cLL = false; +#endif +#ifdef PNG_mDCV_SUPPORTED + png_fixed_point white_point_x, white_point_y, display_primaries_x[3], display_primaries_y[3]; + png_uint_32 max_display_mastering_luminance, min_display_mastering_luminance; + bool has_mDCV = false; +#endif /* Create and initialize the png_struct with the desired error handler * functions. If you want to use the default stderr and longjump method, @@ -83,24 +100,47 @@ * was compiled with a compatible version of the library. REQUIRED */ png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - assert(png_ptr != NULL); + if (png_ptr == NULL) { + fclose(fh); + struct heif_error err = { + .code = heif_error_Memory_allocation_error, + .subcode = heif_suberror_Unspecified, + .message = "Could not create PNG read structure"}; + return err; + } /* Allocate/initialize the memory for image information. REQUIRED. */ info_ptr = png_create_info_struct(png_ptr); if (info_ptr == NULL) { png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL); - assert(false); // , "could not create info_ptr"); + fclose(fh); + struct heif_error err = { + .code = heif_error_Memory_allocation_error, + .subcode = heif_suberror_Unspecified, + .message = "Could not create PNG info structure"}; + return err; } // if /* Set error handling if you are using the setjmp/longjmp method (this is * the normal method of doing things with libpng). REQUIRED unless you * set up your own error handlers in the png_create_read_struct() earlier. + * + * Note: this error path must NOT rely on assert(), which is compiled out in + * NDEBUG builds. If it did, a corrupt PNG would fall through and continue + * running with the now-destroyed (NULL) png_ptr/info_ptr and uninitialized + * width/height, leading to out-of-bounds reads written into the output. */ if (setjmp(png_jmpbuf(png_ptr))) { /* Free all of the memory associated with the png_ptr and info_ptr */ png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL); + free(profile_data); + fclose(fh); /* If we get here, we had a problem reading the file */ - assert(false); // , "fatal error in png library"); + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Error reading PNG file"}; + return err; } // if /* If you are using replacement read functions, instead of calling @@ -181,15 +221,76 @@ */ png_read_update_info(png_ptr, info_ptr); + /* PNGv3 header */ +#ifdef PNG_cLLI_SUPPORTED + /* Set content light level + */ + if (png_get_cLLI_fixed(png_ptr, info_ptr, &maximum_content_light_level_scaled_by_10000, &maximum_frame_average_light_level_scaled_by_10000) == PNG_INFO_cLLI) + { + has_cLL = true; + } + else { + has_cLL = false; + } +#endif +#ifdef PNG_cICP_SUPPORTED + if (png_get_cICP(png_ptr, info_ptr, &color_primaries, &transfer_characteristics, &matrix_coefficients, &video_full_range) == PNG_INFO_cICP) + { + has_cICP = true; + } + else { + has_cICP = false; + } +#endif +#ifdef PNG_mDCV_SUPPORTED + if (png_get_mDCV_fixed(png_ptr, info_ptr, &white_point_x, &white_point_y, + &display_primaries_x[0], &display_primaries_y[0], &display_primaries_x[1], &display_primaries_y[1], &display_primaries_x[2], &display_primaries_y[2], + &max_display_mastering_luminance, &min_display_mastering_luminance) == PNG_INFO_cICP) { + has_mDCV = true; + } + else { + has_mDCV = false; + } +#endif + /* Allocate the memory to hold the image using the fields of info_ptr. */ /* The easiest way to read the image: */ uint8_t** row_pointers = new png_bytep[height]; assert(row_pointers != NULL); - for (uint32_t y = 0; y < height; y++) { - row_pointers[y] = (png_bytep) malloc(png_get_rowbytes(png_ptr, info_ptr)); - assert(row_pointers[y] != NULL); + size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr); + // make it 16 bytes aligned, with overflow check + size_t aligned_rowbytes = (rowbytes + 15) & ~(size_t)15; + + // check for integer overflows in alignment and total allocation size + if (aligned_rowbytes < rowbytes || (height > 0 && aligned_rowbytes > SIZE_MAX / height)) { + delete[] row_pointers; + free(profile_data); + png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL); + fclose(fh); + struct heif_error err = { + .code = heif_error_Memory_allocation_error, + .subcode = heif_suberror_Security_limit_exceeded, + .message = "PNG image too large"}; + return err; + } + + rowbytes = aligned_rowbytes; + row_pointers[0] = (png_bytep)malloc(rowbytes * height); + if (row_pointers[0] == NULL) { + delete[] row_pointers; + free(profile_data); + png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL); + fclose(fh); + struct heif_error err = { + .code = heif_error_Memory_allocation_error, + .subcode = heif_suberror_Unspecified, + .message = "Could not allocate memory for PNG image"}; + return err; + } + for (uint32_t y = 1; y < height; y++) { + row_pointers[y] = row_pointers[0] + rowbytes * y; } // for /* Now it's time to read the image. One of these methods is REQUIRED */ @@ -255,15 +356,15 @@ heif_image_add_plane(image, heif_channel_Y, (int) width, (int) height, 8); - int y_stride; - int a_stride; - uint8_t* py = heif_image_get_plane(image, heif_channel_Y, &y_stride); + size_t y_stride; + size_t a_stride; + uint8_t* py = heif_image_get_plane2(image, heif_channel_Y, &y_stride); uint8_t* pa = nullptr; if (has_alpha) { heif_image_add_plane(image, heif_channel_Alpha, (int) width, (int) height, 8); - pa = heif_image_get_plane(image, heif_channel_Alpha, &a_stride); + pa = heif_image_get_plane2(image, heif_channel_Alpha, &a_stride); } @@ -294,15 +395,15 @@ heif_image_add_plane(image, heif_channel_Y, (int) width, (int) height, output_bit_depth); - int y_stride; - int a_stride = 0; - uint16_t* py = (uint16_t*) heif_image_get_plane(image, heif_channel_Y, &y_stride); + size_t y_stride; + size_t a_stride = 0; + uint16_t* py = (uint16_t*) heif_image_get_plane2(image, heif_channel_Y, &y_stride); uint16_t* pa = nullptr; if (has_alpha) { heif_image_add_plane(image, heif_channel_Alpha, (int) width, (int) height, output_bit_depth); - pa = (uint16_t*) heif_image_get_plane(image, heif_channel_Alpha, &a_stride); + pa = (uint16_t*) heif_image_get_plane2(image, heif_channel_Alpha, &a_stride); } y_stride /= 2; @@ -343,11 +444,11 @@ heif_image_add_plane(image, heif_channel_Y, (int) width, (int) height, 8); heif_image_add_plane(image, heif_channel_Alpha, (int) width, (int) height, 8); - int stride; - uint8_t* p = heif_image_get_plane(image, heif_channel_Y, &stride); + size_t stride; + uint8_t* p = heif_image_get_plane2(image, heif_channel_Y, &stride); - int strideA; - uint8_t* pA = heif_image_get_plane(image, heif_channel_Alpha, &strideA); + size_t strideA; + uint8_t* pA = heif_image_get_plane2(image, heif_channel_Alpha, &strideA); for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < width; x++) { @@ -366,8 +467,8 @@ heif_image_add_plane(image, heif_channel_interleaved, (int) width, (int) height, has_alpha ? 32 : 24); - int stride; - uint8_t* p = heif_image_get_plane(image, heif_channel_interleaved, &stride); + size_t stride; + uint8_t* p = heif_image_get_plane2(image, heif_channel_interleaved, &stride); for (uint32_t y = 0; y < height; y++) { if (has_alpha) { @@ -401,8 +502,8 @@ heif_image_add_plane(image, heif_channel_interleaved, (int) width, (int) height, output_bit_depth); - int stride; - uint8_t* p_out = (uint8_t*) heif_image_get_plane(image, heif_channel_interleaved, &stride); + size_t stride; + uint8_t* p_out = (uint8_t*) heif_image_get_plane2(image, heif_channel_interleaved, &stride); if (output_bit_depth==8) { // convert HDR to SDR @@ -419,6 +520,74 @@ } } else { + // band > 2, output_bit_depth > 8 +#ifdef PNG_cICP_SUPPORTED + if (has_cICP) { + heif_color_profile_nclx* nclx = heif_nclx_color_profile_alloc(); + if (nclx) { + heif_nclx_color_profile_set_color_primaries(nclx, color_primaries); + heif_nclx_color_profile_set_transfer_characteristics(nclx, transfer_characteristics); + + // Since matrix_coefficients are always 0 for PNGs, choose coefficients as default that + // match the color primaries. + // TODO: should we move this into libheif? + + switch (color_primaries) { + default: + case heif_color_primaries_ITU_R_BT_709_5: // 1: HD, web, sRGB + matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_709_5; // 1 + break; + case heif_color_primaries_ITU_R_BT_470_6_System_B_G: // 5: PAL/SECAM + case heif_color_primaries_EBU_Tech_3213_E: // 22: EBU legacy + matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G; // 5 + break; + case heif_color_primaries_ITU_R_BT_601_6: // 6: DVD, SD web video + case heif_color_primaries_ITU_R_BT_470_6_System_M: // 4: Legacy NTSC + matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; // 6 + break; + case heif_color_primaries_SMPTE_240M: // 7 + matrix_coefficients = heif_matrix_coefficients_SMPTE_240M; // 7 + break; + case heif_color_primaries_ITU_R_BT_2020_2_and_2100_0: // 9: HDR10, UHD broadcast + case heif_color_primaries_SMPTE_RP_431_2: // 11: DCI-P3 (D65) + case heif_color_primaries_SMPTE_EG_432_1: // 12: DCI-P3 (theater) + matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance; // 9 + break; + } + heif_nclx_color_profile_set_matrix_coefficients(nclx, matrix_coefficients); + nclx->full_range_flag = true; + heif_image_set_nclx_color_profile(image, nclx); + heif_nclx_color_profile_free(nclx); + } + } +#endif + +#ifdef PNG_cLLI_SUPPORTED + if (has_cLL) { + heif_content_light_level clli; + clli.max_content_light_level = clip_int_u16(maximum_content_light_level_scaled_by_10000 / 10000, 10000); + clli.max_pic_average_light_level = clip_int_u16(maximum_frame_average_light_level_scaled_by_10000 / 10000, 10000); + heif_image_set_content_light_level(image, &clli); + } +#endif + +#ifdef PNG_mDCV_SUPPORTED + if (has_mDCV) { + heif_mastering_display_colour_volume mdcv; + mdcv.white_point_x = clip_int_u16(white_point_x, 37000); + mdcv.white_point_y = clip_int_u16(white_point_y, 42000); + mdcv.display_primaries_x[0] = clip_int_u16(display_primaries_x[0], 37000); + mdcv.display_primaries_x[1] = clip_int_u16(display_primaries_x[1], 37000); + mdcv.display_primaries_x[2] = clip_int_u16(display_primaries_x[2], 37000); + mdcv.display_primaries_y[0] = clip_int_u16(display_primaries_y[0], 42000); + mdcv.display_primaries_y[1] = clip_int_u16(display_primaries_y[1], 42000); + mdcv.display_primaries_y[2] = clip_int_u16(display_primaries_y[2], 42000); + mdcv.max_display_mastering_luminance = max_display_mastering_luminance; + mdcv.min_display_mastering_luminance = min_display_mastering_luminance; + heif_image_set_mastering_display_colour_volume(image, &mdcv); + } +#endif + for (uint32_t y = 0; y < height; y++) { uint8_t* p = row_pointers[y]; @@ -439,9 +608,7 @@ } free(profile_data); - for (uint32_t y = 0; y < height; y++) { - free(row_pointers[y]); - } // for + free(row_pointers[0]); delete[] row_pointers; fclose(fh); diff -Nru libheif-1.19.8/heifio/decoder_raw.cc libheif-1.23.4/heifio/decoder_raw.cc --- libheif-1.19.8/heifio/decoder_raw.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_raw.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,220 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "decoder_raw.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; + + +bool parse_raw_pixel_type(const std::string& type_string, + heif_component_datatype* out_datatype, + int* out_bit_depth) +{ + if (type_string == "uint8") { + *out_datatype = heif_component_datatype_unsigned_integer; + *out_bit_depth = 8; + } + else if (type_string == "sint8") { + *out_datatype = heif_component_datatype_signed_integer; + *out_bit_depth = 8; + } + else if (type_string == "uint16") { + *out_datatype = heif_component_datatype_unsigned_integer; + *out_bit_depth = 16; + } + else if (type_string == "sint16") { + *out_datatype = heif_component_datatype_signed_integer; + *out_bit_depth = 16; + } + else if (type_string == "uint32") { + *out_datatype = heif_component_datatype_unsigned_integer; + *out_bit_depth = 32; + } + else if (type_string == "sint32") { + *out_datatype = heif_component_datatype_signed_integer; + *out_bit_depth = 32; + } + else if (type_string == "float32") { + *out_datatype = heif_component_datatype_floating_point; + *out_bit_depth = 32; + } + else if (type_string == "float64") { + *out_datatype = heif_component_datatype_floating_point; + *out_bit_depth = 64; + } + else { + return false; + } + return true; +} + + +static void byte_swap_buffer(uint8_t* data, size_t num_elements, int bytes_per_element) +{ + for (size_t i = 0; i < num_elements; i++) { + uint8_t* elem = data + i * bytes_per_element; + for (int lo = 0, hi = bytes_per_element - 1; lo < hi; lo++, hi--) { + std::swap(elem[lo], elem[hi]); + } + } +} + + +heif_error loadRAW(const char* filename, const RawImageParameters& params, InputImage* input_image) +{ + if (params.width < 0 || params.height < 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw image width and height must not be negative"}; + } + + if (params.width == 0 && params.height == 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "At least one of --raw-width or --raw-height must be specified"}; + } + + if (params.datatype == heif_component_datatype_undefined || params.bit_depth <= 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw image pixel type must be specified (use --raw-type)"}; + } + + int bytes_per_pixel = params.bit_depth / 8; + + // Open file and get size + FILE* fp = fopen(filename, "rb"); + if (!fp) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Cannot open raw input file"}; + } + + fseek(fp, 0, SEEK_END); + long file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + if (file_size <= 0) { + fclose(fp); + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw input file is empty or unreadable"}; + } + + // Compute missing dimension from file size + int width = params.width; + int height = params.height; + + if (width == 0) { + size_t row_bytes = static_cast(height) * bytes_per_pixel; + if (static_cast(file_size) % row_bytes != 0) { + fclose(fp); + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw file size is not divisible by height * bytes_per_pixel"}; + } + width = static_cast(static_cast(file_size) / row_bytes); + } + else if (height == 0) { + size_t row_bytes = static_cast(width) * bytes_per_pixel; + if (static_cast(file_size) % row_bytes != 0) { + fclose(fp); + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw file size is not divisible by width * bytes_per_pixel"}; + } + height = static_cast(static_cast(file_size) / row_bytes); + } + + size_t expected_size = static_cast(width) * height * bytes_per_pixel; + + if (static_cast(file_size) != expected_size) { + fclose(fp); + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Raw file size does not match width * height * bytes_per_pixel"}; + } + + // Read entire file + std::vector buffer(expected_size); + size_t n = fread(buffer.data(), 1, expected_size, fp); + fclose(fp); + + if (n != expected_size) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Failed to read raw input file"}; + } + + // Byte-swap from big-endian to host byte order if needed + if (params.big_endian && bytes_per_pixel > 1) { + if constexpr (std::endian::native == std::endian::little) { + byte_swap_buffer(buffer.data(), static_cast(width) * height, bytes_per_pixel); + } + } + + // Create image with nonvisual colorspace for typed component API + heif_image* image = nullptr; + heif_error err = heif_image_create(width, height, + heif_colorspace_custom, + heif_chroma_planar, + &image); + if (err.code != heif_error_Ok) { + return err; + } + + uint32_t component_idx = 0; + err = heif_image_add_component(image, width, height, + heif_cmpd_component_type_monochrome, + params.datatype, params.bit_depth, + &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(image); + return err; + } + + // Get writable pointer and copy data row by row + size_t stride = 0; + uint8_t* plane = heif_image_get_component(image, component_idx, &stride); + if (!plane) { + heif_image_release(image); + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Failed to get component plane from image"}; + } + + size_t row_bytes = static_cast(width) * bytes_per_pixel; + size_t stride_bytes = stride; + + for (int y = 0; y < height; y++) { + memcpy(plane + y * stride_bytes, + buffer.data() + y * row_bytes, + row_bytes); + } + + input_image->image = std::shared_ptr(image, + [](heif_image* img) { heif_image_release(img); }); + + return heif_error_ok; +} diff -Nru libheif-1.19.8/heifio/decoder_raw.h libheif-1.23.4/heifio/decoder_raw.h --- libheif-1.19.8/heifio/decoder_raw.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_raw.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,51 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef LIBHEIF_DECODER_RAW_H +#define LIBHEIF_DECODER_RAW_H + +#include "decoder.h" +#include +#include + +struct RawImageParameters { + int width = 0; + int height = 0; + heif_component_datatype datatype = heif_component_datatype_undefined; + int bit_depth = 0; + bool big_endian = false; +}; + +// Maps a CLI string like "uint16" or "float32" to datatype + bit_depth. +// Returns false if the string is not recognized. +bool parse_raw_pixel_type(const std::string& type_string, + heif_component_datatype* out_datatype, + int* out_bit_depth); + +LIBHEIF_API +heif_error loadRAW(const char* filename, const RawImageParameters& params, InputImage* input_image); + +#endif //LIBHEIF_DECODER_RAW_H diff -Nru libheif-1.19.8/heifio/decoder_tiff.cc libheif-1.23.4/heifio/decoder_tiff.cc --- libheif-1.19.8/heifio/decoder_tiff.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_tiff.cc 2026-09-06 14:45:17.000000000 +0000 @@ -4,6 +4,7 @@ MIT License Copyright (c) 2024 Joachim Bauch + Copyright (c) 2026 Dirk Farin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,19 +26,48 @@ */ #include -#include #include #include #include +#include +#include extern "C" { #include #include } +#if HAVE_GEOTIFF +#include +#include +#include +#include +#endif + #include "decoder_tiff.h" +#include "libheif/heif_uncompressed.h" -static struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; +static heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; + +// Forward declarations for YCbCr helpers (defined after validateTiffFormat) +static YCbCrInfo getYCbCrInfo(TIFF* tif); +static heif_chroma ycbcrChroma(const YCbCrInfo& ycbcr); +static void deinterleaveYCbCr(const uint8_t* src, uint32_t block_w, uint32_t block_h, + uint32_t actual_w, uint32_t actual_h, + uint16_t horiz_sub, uint16_t vert_sub, + uint8_t* y_plane, size_t y_stride, + uint8_t* cb_plane, size_t cb_stride, + uint8_t* cr_plane, size_t cr_stride); + +// Forward declarations for YCbCr helpers (defined after validateTiffFormat) +static YCbCrInfo getYCbCrInfo(TIFF* tif); +static heif_chroma ycbcrChroma(const YCbCrInfo& ycbcr); +static void deinterleaveYCbCr(const uint8_t* src, uint32_t block_w, uint32_t block_h, + uint32_t actual_w, uint32_t actual_h, + uint16_t horiz_sub, uint16_t vert_sub, + uint8_t* y_plane, size_t y_stride, + uint8_t* cb_plane, size_t cb_stride, + uint8_t* cr_plane, size_t cr_stride); static bool seekTIFF(TIFF* tif, toff_t offset, int whence) { TIFFSeekProc seekProc = TIFFGetSeekProc(tif); @@ -53,6 +83,37 @@ return seekProc(handle, offset, whence) != static_cast(-1); } +// Returns the total size of the underlying TIFF stream, or -1 if it cannot be +// determined. Restores the original stream position on success. +static int64_t sizeTIFF(TIFF* tif) { + TIFFSeekProc seekProc = TIFFGetSeekProc(tif); + if (!seekProc) { + return -1; + } + + thandle_t handle = TIFFClientdata(tif); + if (!handle) { + return -1; + } + + toff_t cur = seekProc(handle, 0, SEEK_CUR); + if (cur == static_cast(-1)) { + return -1; + } + + toff_t end = seekProc(handle, 0, SEEK_END); + if (end == static_cast(-1)) { + return -1; + } + + // restore original position + if (seekProc(handle, cur, SEEK_SET) == static_cast(-1)) { + return -1; + } + + return static_cast(end); +} + static bool readTIFF(TIFF* tif, void* dest, size_t size) { TIFFReadWriteProc readProc = TIFFGetReadProc(tif); if (!readProc) { @@ -73,7 +134,7 @@ } static bool readTIFFUint16(TIFF* tif, uint16_t* dest) { - if (!readTIFF(tif, &dest, 2)) { + if (!readTIFF(tif, dest, 2)) { return false; } @@ -84,7 +145,7 @@ } static bool readTIFFUint32(TIFF* tif, uint32_t* dest) { - if (!readTIFF(tif, &dest, 4)) { + if (!readTIFF(tif, dest, 4)) { return false; } @@ -173,18 +234,39 @@ tags->tags_.push_back(std::move(tag)); } + const int64_t file_size = sizeTIFF(tif); + for (const auto& tag : tags->tags_) { - size_t size = tag->len * TIFFDataWidth(static_cast(tag->type)); + // Compute the data size in 64-bit to avoid overflow when multiplying the + // file-supplied length by the type width. + uint64_t size = static_cast(tag->len) * + static_cast(TIFFDataWidth(static_cast(tag->type))); if (size <= 4) { continue; } + // Reject tags whose data cannot fit in the file. Without this bound a + // crafted tag (e.g. len=0xFFFFFFFF) would request a multi-gigabyte + // allocation from a tiny file, and the resulting std::bad_alloc would + // propagate to std::terminate. + if (file_size < 0 || + tag->offset > static_cast(file_size) || + size > static_cast(file_size) - tag->offset) { + return nullptr; + } + if (!seekTIFF(tif, tag->offset, SEEK_SET)) { return nullptr; } - tag->data.resize(size); - if (!readTIFF(tif, tag->data.data(), size)) { + try { + tag->data.resize(static_cast(size)); + } + catch (const std::bad_alloc&) { + return nullptr; + } + + if (!readTIFF(tif, tag->data.data(), static_cast(size))) { return nullptr; } } @@ -256,6 +338,22 @@ } } +static heif_chroma get_heif_chroma(uint16_t outSpp, int output_bit_depth) +{ + if (outSpp == 1) { + return heif_chroma_monochrome; + } + if (output_bit_depth > 8) { +#if IS_BIG_ENDIAN + return (outSpp == 4) ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE; +#else + return (outSpp == 4) ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBB_LE; +#endif + } + return (outSpp == 4) ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB; +} + + heif_error getImageWidthAndHeight(TIFF *tif, uint32_t &width, uint32_t &height) { if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &width) || @@ -267,10 +365,15 @@ .message = "Can not read width and/or height from TIFF image."}; return err; } + + if (width == 0 || height == 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Zero TIFF image size is invalid."}; + } + return heif_error_ok; } -heif_error readMono(TIFF *tif, heif_image **image) +heif_error readMono(TIFF *tif, uint16_t bps, int output_bit_depth, heif_image **image) { uint32_t width, height; heif_error err = getImageWidthAndHeight(tif, width, height); @@ -281,109 +384,341 @@ if (err.code != heif_error_Ok) { return err; } - heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8); - int y_stride; - uint8_t *py = heif_image_get_plane(*image, heif_channel_Y, &y_stride); - for (uint32_t row = 0; row < height; row++) - { - TIFFReadScanline(tif, py, row, 0); - py += y_stride; + if (bps <= 8) { + heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, heif_channel_Y, &y_stride); + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, py, row, 0) < 0) { + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + py += y_stride; + } + } + else { + heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, output_bit_depth); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, heif_channel_Y, &y_stride); + int bdShift = bps - output_bit_depth; + tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); + + if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = static_cast(buf); + uint8_t* dst = py + row * y_stride; + for (uint32_t x = 0; x < width; x++) { + dst[x] = static_cast(src[x] >> (bps - 8)); + } + } + } + else { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = static_cast(buf); + uint16_t* dst = reinterpret_cast(py + row * y_stride); + for (uint32_t x = 0; x < width; x++) { + dst[x] = static_cast(src[x] >> bdShift); + } + } + } + _TIFFfree(buf); } return heif_error_ok; } -heif_error readPixelInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, heif_image **image) +heif_error readPixelInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image) { uint32_t width, height; heif_error err = getImageWidthAndHeight(tif, width, height); if (err.code != heif_error_Ok) { return err; } - heif_chroma chroma = heif_chroma_interleaved_RGB; - if (samplesPerPixel == 4) { - chroma = heif_chroma_interleaved_RGBA; - } - err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image); - if (err.code != heif_error_Ok) - { - return err; + // --- YCbCr strip path: TIFFReadScanline doesn't work with packed YCbCr --- + YCbCrInfo ycbcr = getYCbCrInfo(tif); + if (ycbcr.is_ycbcr) { + if (bps != 8) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only 8-bit YCbCr TIFF is supported."}; + } + + heif_chroma chroma = ycbcrChroma(ycbcr); + err = heif_image_create((int)width, (int)height, heif_colorspace_YCbCr, chroma, image); + if (err.code != heif_error_Ok) return err; + + heif_image_add_plane(*image, heif_channel_Y, (int)width, (int)height, 8); + uint32_t chroma_w = (width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub; + uint32_t chroma_h = (height + ycbcr.vert_sub - 1) / ycbcr.vert_sub; + heif_image_add_plane(*image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8); + heif_image_add_plane(*image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8); + + size_t y_stride, cb_stride, cr_stride; + uint8_t* y_plane = heif_image_get_plane2(*image, heif_channel_Y, &y_stride); + uint8_t* cb_plane = heif_image_get_plane2(*image, heif_channel_Cb, &cb_stride); + uint8_t* cr_plane = heif_image_get_plane2(*image, heif_channel_Cr, &cr_stride); + + uint32_t rows_per_strip = 0; + TIFFGetFieldDefaulted(tif, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (rows_per_strip == 0) rows_per_strip = height; + + tmsize_t strip_size = TIFFStripSize(tif); + std::vector strip_buf(strip_size); + + uint32_t n_strips = TIFFNumberOfStrips(tif); + for (uint32_t s = 0; s < n_strips; s++) { + tmsize_t read = TIFFReadEncodedStrip(tif, s, strip_buf.data(), strip_size); + if (read < 0) { + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF strip"}; + } + + uint32_t strip_y = s * rows_per_strip; + uint32_t actual_h = std::min(rows_per_strip, height - strip_y); + // Packed YCbCr block dimensions must be multiples of subsampling factors + uint32_t block_w = ((width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub) * ycbcr.horiz_sub; + uint32_t block_h = ((actual_h + ycbcr.vert_sub - 1) / ycbcr.vert_sub) * ycbcr.vert_sub; + + deinterleaveYCbCr(strip_buf.data(), block_w, block_h, width, actual_h, + ycbcr.horiz_sub, ycbcr.vert_sub, + y_plane + strip_y * y_stride, y_stride, + cb_plane + (strip_y / ycbcr.vert_sub) * cb_stride, cb_stride, + cr_plane + (strip_y / ycbcr.vert_sub) * cr_stride, cr_stride); + } + + return heif_error_ok; } - heif_channel channel = heif_channel_interleaved; - heif_image_add_plane(*image, channel, (int)width, (int)height, samplesPerPixel * 8); - int y_stride; - uint8_t *py = heif_image_get_plane(*image, channel, &y_stride); + uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel; - tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); - for (uint32_t row = 0; row < height; row++) - { - TIFFReadScanline(tif, buf, row, 0); - memcpy(py, buf, width * samplesPerPixel); - py += y_stride; + if (bps <= 8) { + heif_chroma chroma = get_heif_chroma(outSpp, 8); + err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image); + if (err.code != heif_error_Ok) { + return err; + } + heif_channel channel = heif_channel_interleaved; + heif_image_add_plane(*image, channel, (int)width, (int)height, outSpp * 8); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride); + tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint8_t* src = static_cast(buf); + if (outSpp == samplesPerPixel) { + memcpy(py, src, width * outSpp); + } + else { + for (uint32_t x = 0; x < width; x++) { + memcpy(py + x * outSpp, src + x * samplesPerPixel, outSpp); + } + } + py += y_stride; + } + _TIFFfree(buf); + } + else { + heif_chroma chroma = get_heif_chroma(outSpp, output_bit_depth); + err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image); + if (err.code != heif_error_Ok) { + return err; + } + heif_channel channel = heif_channel_interleaved; + int planeBitDepth = (output_bit_depth <= 8) ? outSpp * 8 : output_bit_depth; + heif_image_add_plane(*image, channel, (int)width, (int)height, planeBitDepth); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride); + int bdShift = bps - output_bit_depth; + tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); + + if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = static_cast(buf); + uint8_t* dst = py + row * y_stride; + if (outSpp == samplesPerPixel) { + for (uint32_t x = 0; x < width * outSpp; x++) { + dst[x] = static_cast(src[x] >> (bps - 8)); + } + } + else { + for (uint32_t x = 0; x < width; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * samplesPerPixel + c] >> (bps - 8)); + } + } + } + } + } + else { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = static_cast(buf); + uint16_t* dst = reinterpret_cast(py + row * y_stride); + if (outSpp == samplesPerPixel) { + for (uint32_t x = 0; x < width * outSpp; x++) { + dst[x] = static_cast(src[x] >> bdShift); + } + } + else { + for (uint32_t x = 0; x < width; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * samplesPerPixel + c] >> bdShift); + } + } + } + } + } + _TIFFfree(buf); } - _TIFFfree(buf); return heif_error_ok; } -heif_error readPixelInterleave(TIFF *tif, uint16_t samplesPerPixel, heif_image **image) +heif_error readPixelInterleave(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image) { if (samplesPerPixel == 1) { - return readMono(tif, image); + return readMono(tif, bps, output_bit_depth, image); } else { - return readPixelInterleaveRGB(tif, samplesPerPixel, image); + return readPixelInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image); } } -heif_error readBandInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, heif_image **image) +heif_error readBandInterleaveRGB(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image) { uint32_t width, height; heif_error err = getImageWidthAndHeight(tif, width, height); if (err.code != heif_error_Ok) { return err; } - if (samplesPerPixel == 3) { - err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, heif_chroma_interleaved_RGB, image); - } else { - err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, image); - } - if (err.code != heif_error_Ok) { - return err; - } - heif_channel channel = heif_channel_interleaved; - heif_image_add_plane(*image, channel, (int)width, (int)height, samplesPerPixel * 8); - int y_stride; - uint8_t *py = heif_image_get_plane(*image, channel, &y_stride); + uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel; - uint8_t *buf = static_cast(_TIFFmalloc(TIFFScanlineSize(tif))); - for (uint16_t i = 0; i < samplesPerPixel; i++) - { - uint8_t *dest = py + i; - for (uint32_t row = 0; row < height; row++) - { - TIFFReadScanline(tif, buf, row, i); - for (uint32_t x = 0; x < width; x++, dest += samplesPerPixel) - { - *dest = buf[x]; + if (bps <= 8) { + heif_chroma chroma = get_heif_chroma(outSpp, 8); + err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image); + if (err.code != heif_error_Ok) { + return err; + } + heif_channel channel = heif_channel_interleaved; + heif_image_add_plane(*image, channel, (int)width, (int)height, outSpp * 8); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride); + + uint8_t *buf = static_cast(_TIFFmalloc(TIFFScanlineSize(tif))); + for (uint16_t i = 0; i < outSpp; i++) { + uint8_t *dest = py + i; + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, i) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + for (uint32_t x = 0; x < width; x++, dest += outSpp) { + *dest = buf[x]; + } + dest += (y_stride - width * outSpp); } - dest += (y_stride - width * samplesPerPixel); } + _TIFFfree(buf); + } + else { + heif_chroma chroma = get_heif_chroma(outSpp, output_bit_depth); + err = heif_image_create((int)width, (int)height, heif_colorspace_RGB, chroma, image); + if (err.code != heif_error_Ok) { + return err; + } + heif_channel channel = heif_channel_interleaved; + int planeBitDepth = (output_bit_depth <= 8) ? outSpp * 8 : output_bit_depth; + heif_image_add_plane(*image, channel, (int)width, (int)height, planeBitDepth); + + size_t y_stride; + uint8_t *py = heif_image_get_plane2(*image, channel, &y_stride); + int bdShift = bps - output_bit_depth; + uint8_t *buf = static_cast(_TIFFmalloc(TIFFScanlineSize(tif))); + + if (output_bit_depth <= 8) { + for (uint16_t i = 0; i < outSpp; i++) { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, i) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = reinterpret_cast(buf); + uint8_t* dst = py + row * y_stride + i; + for (uint32_t x = 0; x < width; x++) { + dst[x * outSpp] = static_cast(src[x] >> (bps - 8)); + } + } + } + } + else { + for (uint16_t i = 0; i < outSpp; i++) { + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, i) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + uint16_t* src = reinterpret_cast(buf); + uint16_t* dst = reinterpret_cast(py + row * y_stride); + for (uint32_t x = 0; x < width; x++) { + dst[x * outSpp + i] = static_cast(src[x] >> bdShift); + } + } + } + } + _TIFFfree(buf); } - _TIFFfree(buf); return heif_error_ok; } -heif_error readBandInterleave(TIFF *tif, uint16_t samplesPerPixel, heif_image **image) +heif_error readBandInterleave(TIFF *tif, uint16_t samplesPerPixel, bool hasAlpha, uint16_t bps, int output_bit_depth, heif_image **image) { if (samplesPerPixel == 1) { - return readMono(tif, image); + return readMono(tif, bps, output_bit_depth, image); } else if (samplesPerPixel == 3) { - return readBandInterleaveRGB(tif, samplesPerPixel, image); + return readBandInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image); } else if (samplesPerPixel == 4) { - return readBandInterleaveRGB(tif, samplesPerPixel, image); + return readBandInterleaveRGB(tif, samplesPerPixel, hasAlpha, bps, output_bit_depth, image); } else { struct heif_error err = { .code = heif_error_Unsupported_feature, @@ -394,85 +729,763 @@ } -static void suppress_warnings(const char* module, const char* fmt, va_list ap) { - // Do nothing +#if WITH_UNCOMPRESSED_CODEC +static heif_error readMonoFloat(TIFF* tif, heif_image** image) +{ + uint32_t width, height; + heif_error err = getImageWidthAndHeight(tif, width, height); + if (err.code != heif_error_Ok) { + return err; + } + + err = heif_image_create((int)width, (int)height, heif_colorspace_custom, heif_chroma_planar, image); + if (err.code != heif_error_Ok) { + return err; + } + + uint32_t component_idx; + err = heif_image_add_component(*image, (int)width, (int)height, + heif_cmpd_component_type_monochrome, + heif_component_datatype_floating_point, 32, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*image); + *image = nullptr; + return err; + } + + size_t stride; + float* plane = heif_image_get_component_float32(*image, component_idx, &stride); + if (!plane) { + heif_image_release(*image); + *image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"}; + } + + tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + memcpy(plane + row * stride, buf, width * sizeof(float)); + } + _TIFFfree(buf); + + return heif_error_ok; } -heif_error loadTIFF(const char* filename, InputImage *input_image) { - TIFFSetWarningHandler(suppress_warnings); +static heif_error readMonoSignedInt(TIFF* tif, uint16_t bps, heif_image** image) +{ + uint32_t width, height; + heif_error err = getImageWidthAndHeight(tif, width, height); + if (err.code != heif_error_Ok) { + return err; + } - std::unique_ptr tifPtr(TIFFOpen(filename, "r"), [](TIFF* tif) { TIFFClose(tif); }); - if (!tifPtr) { - struct heif_error err = { - .code = heif_error_Invalid_input, - .subcode = heif_suberror_Unspecified, - .message = "Cannot open TIFF ile"}; + err = heif_image_create((int)width, (int)height, heif_colorspace_custom, heif_chroma_planar, image); + if (err.code != heif_error_Ok) { return err; } - TIFF* tif = tifPtr.get(); - if (TIFFIsTiled(tif)) { - struct heif_error err = { - .code = heif_error_Unsupported_feature, - .subcode = heif_suberror_Unspecified, - .message = "Tiled TIFF images are not supported yet"}; + uint32_t component_idx; + err = heif_image_add_component(*image, (int)width, (int)height, + heif_cmpd_component_type_monochrome, + heif_component_datatype_signed_integer, bps, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*image); + *image = nullptr; return err; } - uint16_t shortv, samplesPerPixel, bps, config, format; + size_t row_elements; // int8 or int16 elements per row, depending on bps + uint8_t* plane; + if (bps == 8) { + plane = reinterpret_cast(heif_image_get_component_int8(*image, component_idx, &row_elements)); + } + else { + plane = reinterpret_cast(heif_image_get_component_int16(*image, component_idx, &row_elements)); + } + if (!plane) { + heif_image_release(*image); + *image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get signed int plane"}; + } + + int bytesPerSample = bps > 8 ? 2 : 1; + size_t row_bytes = row_elements * bytesPerSample; + tdata_t buf = _TIFFmalloc(TIFFScanlineSize(tif)); + for (uint32_t row = 0; row < height; row++) { + if (TIFFReadScanline(tif, buf, row, 0) < 0) { + _TIFFfree(buf); + heif_image_release(*image); + *image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF scanline"}; + } + memcpy(plane + row * row_bytes, buf, width * bytesPerSample); + } + _TIFFfree(buf); + + return heif_error_ok; +} +#endif + + +static void suppress_warnings(const char* module, const char* fmt, va_list ap) { + // Do nothing +} + + +static heif_error validateTiffFormat(TIFF* tif, uint16_t& samplesPerPixel, uint16_t& bps, + uint16_t& config, bool& hasAlpha, uint16_t& sampleFormat) +{ + uint16_t shortv; if (TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &shortv) && shortv == PHOTOMETRIC_PALETTE) { - struct heif_error err = { - .code = heif_error_Unsupported_feature, - .subcode = heif_suberror_Unspecified, - .message = "Palette TIFF images are not supported yet"}; - return err; + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Palette TIFF images are not supported yet"}; } TIFFGetField(tif, TIFFTAG_PLANARCONFIG, &config); TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel); if (samplesPerPixel != 1 && samplesPerPixel != 3 && samplesPerPixel != 4) { - struct heif_error err = { - .code = heif_error_Invalid_input, - .subcode = heif_suberror_Unspecified, - .message = "Only 1, 3 and 4 samples per pixel are supported."}; - return err; + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Only 1, 3 and 4 samples per pixel are supported."}; + } + + // Determine whether the 4th sample is true alpha or an unrelated extra sample + hasAlpha = false; + if (samplesPerPixel == 4) { + uint16_t extraCount = 0; + uint16_t* extraTypes = nullptr; + if (TIFFGetField(tif, TIFFTAG_EXTRASAMPLES, &extraCount, &extraTypes) && extraCount > 0) { + hasAlpha = (extraTypes[0] == EXTRASAMPLE_ASSOCALPHA || extraTypes[0] == EXTRASAMPLE_UNASSALPHA); + } + else { + // No EXTRASAMPLES tag with 4 spp — assume the extra sample is not alpha + hasAlpha = false; + } } TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bps); - if (bps != 8) { - struct heif_error err = { - .code = heif_error_Invalid_input, - .subcode = heif_suberror_Unspecified, - .message = "Only 8 bits per sample are supported."}; - return err; + + if (!TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &sampleFormat)) { + sampleFormat = SAMPLEFORMAT_UINT; } - if (TIFFGetField(tif, TIFFTAG_SAMPLEFORMAT, &format) && format != SAMPLEFORMAT_UINT) { - struct heif_error err = { - .code = heif_error_Invalid_input, - .subcode = heif_suberror_Unspecified, - .message = "Only UINT sample format is supported."}; - return err; + if (sampleFormat == SAMPLEFORMAT_IEEEFP) { + if (bps != 32) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only 32-bit floating point TIFF is supported."}; + } + if (samplesPerPixel != 1) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only monochrome floating point TIFF is supported."}; + } + } + else if (sampleFormat == SAMPLEFORMAT_INT) { + if (bps < 8 || bps > 16) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Only 8 to 16 bits per sample are supported for signed integer TIFF."}; + } + if (samplesPerPixel != 1) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only monochrome signed integer TIFF is supported."}; + } + } + else if (sampleFormat == SAMPLEFORMAT_UINT) { + if (bps < 8 || bps > 16) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Only 8 to 16 bits per sample are supported."}; + } + } + else { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Unsupported TIFF sample format."}; } - struct heif_error err; - struct heif_image* image = nullptr; + return heif_error_ok; +} + + +static YCbCrInfo getYCbCrInfo(TIFF* tif) +{ + YCbCrInfo info; + uint16_t photometric; + if (TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &photometric) && photometric == PHOTOMETRIC_YCBCR) { + info.is_ycbcr = true; + TIFFGetFieldDefaulted(tif, TIFFTAG_YCBCRSUBSAMPLING, &info.horiz_sub, &info.vert_sub); + + // A file-supplied subsampling factor of 0 would cause a division by zero in + // the chroma-size computations below. libtiff normally rejects this at open + // time, but guard defensively against a value of 0 reaching us. + if (info.horiz_sub == 0) { + info.horiz_sub = 1; + } + if (info.vert_sub == 0) { + info.vert_sub = 1; + } + } + return info; +} + + +static heif_chroma ycbcrChroma(const YCbCrInfo& ycbcr) +{ + if (ycbcr.horiz_sub == 2 && ycbcr.vert_sub == 2) return heif_chroma_420; + if (ycbcr.horiz_sub == 2 && ycbcr.vert_sub == 1) return heif_chroma_422; + return heif_chroma_444; +} + + +// Deinterleave libtiff's packed YCbCr format into separate Y/Cb/Cr planes. +// Packed format: MCUs of (horiz_sub * vert_sub) Y samples + 1 Cb + 1 Cr. +// block_w/block_h must be multiples of horiz_sub/vert_sub respectively. +// actual_w/actual_h are the clipped dimensions for edge tiles/strips. +static void deinterleaveYCbCr( + const uint8_t* src, + uint32_t block_w, uint32_t block_h, + uint32_t actual_w, uint32_t actual_h, + uint16_t horiz_sub, uint16_t vert_sub, + uint8_t* y_plane, size_t y_stride, + uint8_t* cb_plane, size_t cb_stride, + uint8_t* cr_plane, size_t cr_stride) +{ + uint32_t mcus_h = block_w / horiz_sub; + uint32_t mcu_rows = block_h / vert_sub; + uint32_t mcu_size = horiz_sub * vert_sub + 2; + + uint32_t chroma_w = (actual_w + horiz_sub - 1) / horiz_sub; + uint32_t chroma_h = (actual_h + vert_sub - 1) / vert_sub; + + for (uint32_t mcu_y = 0; mcu_y < mcu_rows; mcu_y++) { + for (uint32_t mcu_x = 0; mcu_x < mcus_h; mcu_x++) { + const uint8_t* mcu = src + (mcu_y * mcus_h + mcu_x) * mcu_size; + + // Scatter Y samples + for (uint32_t vy = 0; vy < vert_sub; vy++) { + uint32_t py = mcu_y * vert_sub + vy; + if (py >= actual_h) break; + for (uint32_t hx = 0; hx < horiz_sub; hx++) { + uint32_t px = mcu_x * horiz_sub + hx; + if (px >= actual_w) break; + y_plane[py * y_stride + px] = mcu[vy * horiz_sub + hx]; + } + } + + // Scatter Cb/Cr + if (mcu_x < chroma_w && mcu_y < chroma_h) { + cb_plane[mcu_y * cb_stride + mcu_x] = mcu[horiz_sub * vert_sub]; + cr_plane[mcu_y * cr_stride + mcu_x] = mcu[horiz_sub * vert_sub + 1]; + } + } + } +} + + +// Create a YCbCr heif_image from a single packed YCbCr tile/block. +static heif_error readYCbCrBlock( + const uint8_t* tile_buf, + uint32_t block_w, uint32_t block_h, + uint32_t actual_w, uint32_t actual_h, + const YCbCrInfo& ycbcr, + heif_image** out_image) +{ + heif_chroma chroma = ycbcrChroma(ycbcr); + + heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_YCbCr, chroma, out_image); + if (err.code != heif_error_Ok) return err; + + heif_image_add_plane(*out_image, heif_channel_Y, (int)actual_w, (int)actual_h, 8); + uint32_t chroma_w = (actual_w + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub; + uint32_t chroma_h = (actual_h + ycbcr.vert_sub - 1) / ycbcr.vert_sub; + heif_image_add_plane(*out_image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8); + heif_image_add_plane(*out_image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8); + + size_t y_stride, cb_stride, cr_stride; + uint8_t* y_plane = heif_image_get_plane2(*out_image, heif_channel_Y, &y_stride); + uint8_t* cb_plane = heif_image_get_plane2(*out_image, heif_channel_Cb, &cb_stride); + uint8_t* cr_plane = heif_image_get_plane2(*out_image, heif_channel_Cr, &cr_stride); + + deinterleaveYCbCr(tile_buf, block_w, block_h, actual_w, actual_h, + ycbcr.horiz_sub, ycbcr.vert_sub, + y_plane, y_stride, cb_plane, cb_stride, cr_plane, cr_stride); + + return heif_error_ok; +} - switch (config) { - case PLANARCONFIG_CONTIG: - err = readPixelInterleave(tif, samplesPerPixel, &image); - break; - case PLANARCONFIG_SEPARATE: - err = readBandInterleave(tif, samplesPerPixel, &image); - break; - default: - struct heif_error err = { - .code = heif_error_Invalid_input, - .subcode = heif_suberror_Unspecified, - .message = "Unsupported planar configuration"}; + +static heif_error readTiledContiguous(TIFF* tif, uint32_t width, uint32_t height, + uint32_t tile_width, uint32_t tile_height, + uint16_t samplesPerPixel, bool hasAlpha, + uint16_t bps, int output_bit_depth, + uint16_t sampleFormat, heif_image** out_image) +{ + // --- YCbCr path: deinterleave packed MCU data into planar Y/Cb/Cr --- + YCbCrInfo ycbcr = getYCbCrInfo(tif); + if (ycbcr.is_ycbcr) { + if (bps != 8) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only 8-bit YCbCr TIFF is supported."}; + } + + if (width > std::numeric_limits::max() || height > std::numeric_limits::max()) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "TIFF image size exceeds maximum supported by libheif."}; + } + + + heif_chroma chroma = ycbcrChroma(ycbcr); + heif_error err = heif_image_create((int)width, (int)height, heif_colorspace_YCbCr, chroma, out_image); + if (err.code != heif_error_Ok) return err; + + heif_image_add_plane(*out_image, heif_channel_Y, (int)width, (int)height, 8); + uint32_t chroma_w = (width + ycbcr.horiz_sub - 1) / ycbcr.horiz_sub; + uint32_t chroma_h = (height + ycbcr.vert_sub - 1) / ycbcr.vert_sub; + heif_image_add_plane(*out_image, heif_channel_Cb, (int)chroma_w, (int)chroma_h, 8); + heif_image_add_plane(*out_image, heif_channel_Cr, (int)chroma_w, (int)chroma_h, 8); + + size_t y_stride, cb_stride, cr_stride; + uint8_t* y_plane = heif_image_get_plane2(*out_image, heif_channel_Y, &y_stride); + uint8_t* cb_plane = heif_image_get_plane2(*out_image, heif_channel_Cb, &cb_stride); + uint8_t* cr_plane = heif_image_get_plane2(*out_image, heif_channel_Cr, &cr_stride); + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + uint32_t n_cols = (width - 1) / tile_width + 1; + uint32_t n_rows = (height - 1) / tile_height + 1; + + for (uint32_t ty = 0; ty < n_rows; ty++) { + for (uint32_t tx = 0; tx < n_cols; tx++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + uint32_t actual_w = std::min(tile_width, width - tx * tile_width); + uint32_t actual_h = std::min(tile_height, height - ty * tile_height); + + uint32_t y_offset = ty * tile_height; + uint32_t x_offset = tx * tile_width; + uint32_t chroma_x_offset = x_offset / ycbcr.horiz_sub; + uint32_t chroma_y_offset = y_offset / ycbcr.vert_sub; + + deinterleaveYCbCr(tile_buf.data(), tile_width, tile_height, actual_w, actual_h, + ycbcr.horiz_sub, ycbcr.vert_sub, + y_plane + y_offset * y_stride + x_offset, y_stride, + cb_plane + chroma_y_offset * cb_stride + chroma_x_offset, cb_stride, + cr_plane + chroma_y_offset * cr_stride + chroma_x_offset, cr_stride); + } + } + + return heif_error_ok; + } + + bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP); + + if (isFloat) { +#if WITH_UNCOMPRESSED_CODEC + heif_error err = heif_image_create((int)width, (int)height, heif_colorspace_custom, heif_chroma_planar, out_image); + if (err.code != heif_error_Ok) return err; + + uint32_t component_idx; + err = heif_image_add_component(*out_image, (int)width, (int)height, + heif_cmpd_component_type_monochrome, + heif_component_datatype_floating_point, 32, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*out_image); + *out_image = nullptr; + return err; + } + + size_t out_stride; + float* out_plane = heif_image_get_component_float32(*out_image, component_idx, &out_stride); + if (!out_plane) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"}; + } + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + uint32_t n_cols = (width - 1) / tile_width + 1; + uint32_t n_rows = (height - 1) / tile_height + 1; + + for (uint32_t ty = 0; ty < n_rows; ty++) { + for (uint32_t tx = 0; tx < n_cols; tx++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + uint32_t actual_w = std::min(tile_width, width - tx * tile_width); + uint32_t actual_h = std::min(tile_height, height - ty * tile_height); + + for (uint32_t row = 0; row < actual_h; row++) { + float* dst = out_plane + (size_t)(ty * tile_height + row) * out_stride + + (size_t)tx * tile_width; + float* src = reinterpret_cast(tile_buf.data() + (size_t)row * tile_width * sizeof(float)); + memcpy(dst, src, actual_w * sizeof(float)); + } + } + } + + return heif_error_ok; +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Floating point TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + + bool isSignedInt = (sampleFormat == SAMPLEFORMAT_INT); + + if (isSignedInt) { +#if WITH_UNCOMPRESSED_CODEC + heif_error err = heif_image_create((int)width, (int)height, heif_colorspace_custom, heif_chroma_planar, out_image); + if (err.code != heif_error_Ok) return err; + + uint32_t component_idx; + err = heif_image_add_component(*out_image, (int)width, (int)height, + heif_cmpd_component_type_monochrome, + heif_component_datatype_signed_integer, bps, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*out_image); + *out_image = nullptr; return err; + } + + size_t row_elements; // int8 or int16 elements per row, depending on bps + uint8_t* out_plane; + if (bps == 8) { + out_plane = reinterpret_cast(heif_image_get_component_int8(*out_image, component_idx, &row_elements)); + } + else { + out_plane = reinterpret_cast(heif_image_get_component_int16(*out_image, component_idx, &row_elements)); + } + if (!out_plane) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get signed int plane"}; + } + + int bytesPerSample = bps > 8 ? 2 : 1; + size_t row_bytes = row_elements * bytesPerSample; + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + uint32_t n_cols = (width - 1) / tile_width + 1; + uint32_t n_rows = (height - 1) / tile_height + 1; + + for (uint32_t ty = 0; ty < n_rows; ty++) { + for (uint32_t tx = 0; tx < n_cols; tx++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + uint32_t actual_w = std::min(tile_width, width - tx * tile_width); + uint32_t actual_h = std::min(tile_height, height - ty * tile_height); + + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + ((size_t)ty * tile_height + row) * row_bytes + + (size_t)tx * tile_width * bytesPerSample; + uint8_t* src = tile_buf.data() + (size_t)row * tile_width * bytesPerSample; + memcpy(dst, src, actual_w * bytesPerSample); + } + } + } + + return heif_error_ok; +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Signed integer TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + + uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel; + int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth; + heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth); + heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB; + heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved; + + heif_error err = heif_image_create((int)width, (int)height, colorspace, chroma, out_image); + if (err.code != heif_error_Ok) return err; + + int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth; + heif_image_add_plane(*out_image, channel, (int)width, (int)height, planeBitDepth); + + size_t out_stride; + uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride); + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + uint32_t n_cols = (width - 1) / tile_width + 1; + uint32_t n_rows = (height - 1) / tile_height + 1; + + for (uint32_t ty = 0; ty < n_rows; ty++) { + for (uint32_t tx = 0; tx < n_cols; tx++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + uint32_t actual_w = std::min(tile_width, width - tx * tile_width); + uint32_t actual_h = std::min(tile_height, height - ty * tile_height); + + if (bps <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + ((size_t)ty * tile_height + row) * out_stride + (size_t)tx * tile_width * outSpp; + uint8_t* src = tile_buf.data() + (size_t)row * tile_width * samplesPerPixel; + if (outSpp == samplesPerPixel) { + memcpy(dst, src, actual_w * outSpp); + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + memcpy(dst + x * outSpp, src + x * samplesPerPixel, outSpp); + } + } + } + } + else if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + ((size_t)ty * tile_height + row) * out_stride + (size_t)tx * tile_width * outSpp; + uint16_t* src = reinterpret_cast(tile_buf.data() + (size_t)row * tile_width * samplesPerPixel * 2); + if (outSpp == samplesPerPixel) { + for (uint32_t x = 0; x < actual_w * outSpp; x++) { + dst[x] = static_cast(src[x] >> (bps - 8)); + } + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * samplesPerPixel + c] >> (bps - 8)); + } + } + } + } + } + else { + int bdShift = bps - output_bit_depth; + for (uint32_t row = 0; row < actual_h; row++) { + uint16_t* dst = reinterpret_cast(out_plane + ((size_t)ty * tile_height + row) * out_stride + + (size_t)tx * tile_width * outSpp * 2); + uint16_t* src = reinterpret_cast(tile_buf.data() + (size_t)row * tile_width * samplesPerPixel * 2); + if (outSpp == samplesPerPixel) { + if (bdShift == 0) { + memcpy(dst, src, actual_w * outSpp * 2); + } + else { + for (uint32_t x = 0; x < actual_w * outSpp; x++) { + dst[x] = static_cast(src[x] >> bdShift); + } + } + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * samplesPerPixel + c] >> bdShift); + } + } + } + } + } + } + } + + return heif_error_ok; +} + + +static heif_error readTiledSeparate(TIFF* tif, uint32_t width, uint32_t height, + uint32_t tile_width, uint32_t tile_height, + uint16_t samplesPerPixel, bool hasAlpha, + uint16_t bps, int output_bit_depth, + uint16_t sampleFormat, heif_image** out_image) +{ + // For mono float/signed int, separate layout is the same as contiguous (1 sample) + if (sampleFormat == SAMPLEFORMAT_IEEEFP || sampleFormat == SAMPLEFORMAT_INT) { +#if WITH_UNCOMPRESSED_CODEC + return readTiledContiguous(tif, width, height, tile_width, tile_height, + samplesPerPixel, hasAlpha, bps, output_bit_depth, + sampleFormat, out_image); +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Float/signed integer TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + + uint16_t outSpp = (samplesPerPixel == 4 && !hasAlpha) ? 3 : samplesPerPixel; + int effectiveBitDepth = (bps <= 8) ? 8 : output_bit_depth; + heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth); + heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB; + heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved; + + heif_error err = heif_image_create((int)width, (int)height, colorspace, chroma, out_image); + if (err.code != heif_error_Ok) return err; + + int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth; + heif_image_add_plane(*out_image, channel, (int)width, (int)height, planeBitDepth); + + size_t out_stride; + uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride); + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + uint32_t n_cols = (width - 1) / tile_width + 1; + uint32_t n_rows = (height - 1) / tile_height + 1; + + for (uint16_t s = 0; s < outSpp; s++) { + for (uint32_t ty = 0; ty < n_rows; ty++) { + for (uint32_t tx = 0; tx < n_cols; tx++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * tile_width, ty * tile_height, 0, s), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + uint32_t actual_w = std::min(tile_width, width - tx * tile_width); + uint32_t actual_h = std::min(tile_height, height - ty * tile_height); + + if (bps <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + ((size_t)ty * tile_height + row) * out_stride + (size_t)tx * tile_width * outSpp + s; + uint8_t* src = tile_buf.data() + (size_t)row * tile_width; + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = src[x]; + } + } + } + else if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + ((size_t)ty * tile_height + row) * out_stride + (size_t)tx * tile_width * outSpp + s; + uint16_t* src = reinterpret_cast(tile_buf.data() + (size_t)row * tile_width * 2); + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = static_cast(src[x] >> (bps - 8)); + } + } + } + else { + int bdShift = bps - output_bit_depth; + for (uint32_t row = 0; row < actual_h; row++) { + uint16_t* dst = reinterpret_cast(out_plane + ((size_t)ty * tile_height + row) * out_stride) + (size_t)tx * tile_width * outSpp + s; + uint16_t* src = reinterpret_cast(tile_buf.data() + (size_t)row * tile_width * 2); + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = static_cast(src[x] >> bdShift); + } + } + } + } + } } + + return heif_error_ok; +} + + +heif_error loadTIFF(const char* filename, int output_bit_depth, InputImage *input_image) { + TIFFSetWarningHandler(suppress_warnings); + + std::unique_ptr tifPtr(TIFFOpen(filename, "r"), [](TIFF* tif) { TIFFClose(tif); }); + if (!tifPtr) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot open TIFF file"}; + } + + TIFF* tif = tifPtr.get(); + + uint16_t samplesPerPixel, bps, config, sampleFormat; + bool hasAlpha; + heif_error err = validateTiffFormat(tif, samplesPerPixel, bps, config, hasAlpha, sampleFormat); + if (err.code != heif_error_Ok) return err; + + // For PLANARCONFIG_SEPARATE + YCbCr, tell libtiff to convert to RGB on the fly + YCbCrInfo ycbcr = getYCbCrInfo(tif); + if (ycbcr.is_ycbcr && config == PLANARCONFIG_SEPARATE) { + TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + } + + bool isFloat = (sampleFormat == SAMPLEFORMAT_IEEEFP); + bool isSignedInt = (sampleFormat == SAMPLEFORMAT_INT); + + // For 8-bit source, always produce 8-bit output (ignore output_bit_depth). + // For float, use 32-bit. For signed int, preserve original bit depth. + int effectiveOutputBitDepth = isFloat ? 32 : (isSignedInt ? bps : ((bps <= 8) ? 8 : ((bps < 16) ? bps : output_bit_depth))); + + struct heif_image* image = nullptr; + + if (TIFFIsTiled(tif)) { + uint32_t width, height, tile_width, tile_height; + err = getImageWidthAndHeight(tif, width, height); + if (err.code != heif_error_Ok) return err; + + if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tile_width) || + !TIFFGetField(tif, TIFFTAG_TILELENGTH, &tile_height)) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF tile dimensions"}; + } + + if (tile_width == 0 || tile_height == 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Invalid TIFF tile dimensions"}; + } + + switch (config) { + case PLANARCONFIG_CONTIG: + err = readTiledContiguous(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, sampleFormat, &image); + break; + case PLANARCONFIG_SEPARATE: + err = readTiledSeparate(tif, width, height, tile_width, tile_height, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, sampleFormat, &image); + break; + default: + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"}; + } + } + else { + if (isFloat) { +#if WITH_UNCOMPRESSED_CODEC + err = readMonoFloat(tif, &image); +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Floating point TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + else if (isSignedInt) { +#if WITH_UNCOMPRESSED_CODEC + err = readMonoSignedInt(tif, bps, &image); +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Signed integer TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + else { + switch (config) { + case PLANARCONFIG_CONTIG: + err = readPixelInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image); + break; + case PLANARCONFIG_SEPARATE: + err = readBandInterleave(tif, samplesPerPixel, hasAlpha, bps, effectiveOutputBitDepth, &image); + break; + default: + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Unsupported planar configuration"}; + } + } + } + if (err.code != heif_error_Ok) { return err; } @@ -490,3 +1503,507 @@ return heif_error_ok; } + +// --- TiledTiffReader --- + +void TiledTiffReader::TiffCloser::operator()(void* tif) const { + if (tif) { + TIFFClose(static_cast(tif)); + } +} + + +std::unique_ptr TiledTiffReader::open(const char* filename, heif_error* out_err) +{ + TIFFSetWarningHandler(suppress_warnings); + + TIFF* tif = TIFFOpen(filename, "r"); + if (!tif) { + *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot open TIFF file"}; + return nullptr; + } + + if (!TIFFIsTiled(tif)) { + TIFFClose(tif); + *out_err = heif_error_ok; + return nullptr; + } + + auto reader = std::unique_ptr(new TiledTiffReader()); + reader->m_tif.reset(tif); + + uint16_t bps; + heif_error err = validateTiffFormat(tif, reader->m_samples_per_pixel, bps, reader->m_planar_config, reader->m_has_alpha, reader->m_sample_format); + if (err.code != heif_error_Ok) { + *out_err = err; + return nullptr; + } + reader->m_bits_per_sample = bps; + + reader->m_ycbcr = getYCbCrInfo(tif); + if (reader->m_ycbcr.is_ycbcr && reader->m_planar_config == PLANARCONFIG_SEPARATE) { + TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + reader->m_ycbcr.is_ycbcr = false; // data comes out as RGB now + } + + if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &reader->m_image_width) || + !TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &reader->m_image_height)) { + *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF image dimensions"}; + return nullptr; + } + + if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &reader->m_tile_width) || + !TIFFGetField(tif, TIFFTAG_TILELENGTH, &reader->m_tile_height)) { + *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Cannot read TIFF tile dimensions"}; + return nullptr; + } + + if (reader->m_tile_width == 0 || reader->m_tile_height == 0) { + *out_err = {heif_error_Invalid_input, heif_suberror_Unspecified, "Invalid TIFF tile dimensions"}; + return nullptr; + } + + reader->m_n_columns = (reader->m_image_width - 1) / reader->m_tile_width + 1; + reader->m_n_rows = (reader->m_image_height - 1) / reader->m_tile_height + 1; + + // Detect overview directories (reduced-resolution images) + tdir_t n_dirs = TIFFNumberOfDirectories(tif); + for (uint16_t d = 1; d < n_dirs; d++) { + if (!TIFFSetDirectory(tif, d)) { + continue; + } + + uint32_t subfiletype = 0; + TIFFGetField(tif, TIFFTAG_SUBFILETYPE, &subfiletype); + if (!(subfiletype & FILETYPE_REDUCEDIMAGE)) { + continue; + } + + if (!TIFFIsTiled(tif)) { + continue; + } + + uint16_t spp, bps_ov, config_ov, sampleFormat_ov; + bool hasAlpha_ov; + heif_error valErr = validateTiffFormat(tif, spp, bps_ov, config_ov, hasAlpha_ov, sampleFormat_ov); + if (valErr.code != heif_error_Ok) { + continue; + } + + uint32_t ov_width, ov_height, ov_tw, ov_th; + if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &ov_width) || + !TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &ov_height)) { + continue; + } + if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &ov_tw) || + !TIFFGetField(tif, TIFFTAG_TILELENGTH, &ov_th)) { + continue; + } + + if (ov_width == 0 || ov_height == 0) { + continue; + } + + if (ov_tw == 0 || ov_th == 0) { + continue; + } + + reader->m_overviews.push_back({d, ov_width, ov_height, ov_tw, ov_th}); + } + + // Switch back to directory 0 + TIFFSetDirectory(tif, 0); + + *out_err = heif_error_ok; + return reader; +} + + +TiledTiffReader::~TiledTiffReader() = default; + + +bool TiledTiffReader::setDirectory(uint32_t dir_index) +{ + TIFF* tif = static_cast(m_tif.get()); + + if (!TIFFSetDirectory(tif, static_cast(dir_index))) { + return false; + } + + if (!TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &m_image_width) || + !TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &m_image_height)) { + return false; + } + + if (!TIFFGetField(tif, TIFFTAG_TILEWIDTH, &m_tile_width) || + !TIFFGetField(tif, TIFFTAG_TILELENGTH, &m_tile_height)) { + return false; + } + + if (m_image_width == 0 || m_image_height == 0) { + return false; + } + + if (m_tile_width == 0 || m_tile_height == 0) { + return false; + } + + uint16_t bps; + heif_error err = validateTiffFormat(tif, m_samples_per_pixel, bps, m_planar_config, m_has_alpha, m_sample_format); + if (err.code != heif_error_Ok) { + return false; + } + m_bits_per_sample = bps; + + m_ycbcr = getYCbCrInfo(tif); + if (m_ycbcr.is_ycbcr && m_planar_config == PLANARCONFIG_SEPARATE) { + TIFFSetField(tif, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + m_ycbcr.is_ycbcr = false; + } + + m_n_columns = (m_image_width - 1) / m_tile_width + 1; + m_n_rows = (m_image_height - 1) / m_tile_height + 1; + + return true; +} + + +heif_error TiledTiffReader::readTile(uint32_t tx, uint32_t ty, int output_bit_depth, heif_image** out_image) +{ + TIFF* tif = static_cast(m_tif.get()); + + uint32_t actual_w = std::min(m_tile_width, m_image_width - tx * m_tile_width); + uint32_t actual_h = std::min(m_tile_height, m_image_height - ty * m_tile_height); + + // --- YCbCr path (PLANARCONFIG_CONTIG only; SEPARATE was converted to RGB in open/setDirectory) --- + if (m_ycbcr.is_ycbcr && m_planar_config == PLANARCONFIG_CONTIG) { + if (m_bits_per_sample != 8) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Only 8-bit YCbCr TIFF is supported."}; + } + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + return readYCbCrBlock(tile_buf.data(), m_tile_width, m_tile_height, actual_w, actual_h, m_ycbcr, out_image); + } + + if (m_sample_format == SAMPLEFORMAT_IEEEFP) { +#if WITH_UNCOMPRESSED_CODEC + heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_custom, heif_chroma_planar, out_image); + if (err.code != heif_error_Ok) return err; + + uint32_t component_idx; + err = heif_image_add_component(*out_image, (int)actual_w, (int)actual_h, + heif_cmpd_component_type_monochrome, + heif_component_datatype_floating_point, 32, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*out_image); + *out_image = nullptr; + return err; + } + + size_t out_stride; + float* out_plane = heif_image_get_component_float32(*out_image, component_idx, &out_stride); + if (!out_plane) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get float plane"}; + } + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + for (uint32_t row = 0; row < actual_h; row++) { + float* dst = out_plane + row * out_stride; + float* src = reinterpret_cast(tile_buf.data() + row * m_tile_width * sizeof(float)); + memcpy(dst, src, actual_w * sizeof(float)); + } + + return heif_error_ok; +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Floating point TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + + if (m_sample_format == SAMPLEFORMAT_INT) { +#if WITH_UNCOMPRESSED_CODEC + heif_error err = heif_image_create((int)actual_w, (int)actual_h, heif_colorspace_custom, heif_chroma_planar, out_image); + if (err.code != heif_error_Ok) return err; + + uint32_t component_idx; + err = heif_image_add_component(*out_image, (int)actual_w, (int)actual_h, + heif_cmpd_component_type_monochrome, + heif_component_datatype_signed_integer, m_bits_per_sample, &component_idx); + if (err.code != heif_error_Ok) { + heif_image_release(*out_image); + *out_image = nullptr; + return err; + } + + size_t row_elements; // int8 or int16 elements per row, depending on bps + uint8_t* out_plane; + if (m_bits_per_sample == 8) { + out_plane = reinterpret_cast(heif_image_get_component_int8(*out_image, component_idx, &row_elements)); + } + else { + out_plane = reinterpret_cast(heif_image_get_component_int16(*out_image, component_idx, &row_elements)); + } + if (!out_plane) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, "Failed to get signed int plane"}; + } + + int bytesPerSample = m_bits_per_sample > 8 ? 2 : 1; + size_t row_bytes = row_elements * bytesPerSample; + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + row * row_bytes; + uint8_t* src = tile_buf.data() + row * m_tile_width * bytesPerSample; + memcpy(dst, src, actual_w * bytesPerSample); + } + + return heif_error_ok; +#else + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Signed integer TIFF requires uncompressed codec support (WITH_UNCOMPRESSED_CODEC)."}; +#endif + } + + int effectiveBitDepth = (m_bits_per_sample <= 8) ? 8 : output_bit_depth; + uint16_t outSpp = (m_samples_per_pixel == 4 && !m_has_alpha) ? 3 : m_samples_per_pixel; + heif_chroma chroma = get_heif_chroma(outSpp, effectiveBitDepth); + heif_colorspace colorspace = (outSpp == 1) ? heif_colorspace_monochrome : heif_colorspace_RGB; + heif_channel channel = (outSpp == 1) ? heif_channel_Y : heif_channel_interleaved; + + heif_error err = heif_image_create((int)actual_w, (int)actual_h, colorspace, chroma, out_image); + if (err.code != heif_error_Ok) return err; + + int planeBitDepth = (effectiveBitDepth <= 8) ? outSpp * 8 : effectiveBitDepth; + heif_image_add_plane(*out_image, channel, (int)actual_w, (int)actual_h, planeBitDepth); + + size_t out_stride; + uint8_t* out_plane = heif_image_get_plane2(*out_image, channel, &out_stride); + + tmsize_t tile_buf_size = TIFFTileSize(tif); + std::vector tile_buf(tile_buf_size); + + if (m_planar_config == PLANARCONFIG_CONTIG) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, 0), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + if (m_bits_per_sample <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + row * out_stride; + uint8_t* src = tile_buf.data() + row * m_tile_width * m_samples_per_pixel; + if (outSpp == m_samples_per_pixel) { + memcpy(dst, src, actual_w * outSpp); + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + memcpy(dst + x * outSpp, src + x * m_samples_per_pixel, outSpp); + } + } + } + } + else if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + row * out_stride; + uint16_t* src = reinterpret_cast(tile_buf.data() + row * m_tile_width * m_samples_per_pixel * 2); + if (outSpp == m_samples_per_pixel) { + for (uint32_t x = 0; x < actual_w * outSpp; x++) { + dst[x] = static_cast(src[x] >> (m_bits_per_sample - 8)); + } + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * m_samples_per_pixel + c] >> (m_bits_per_sample - 8)); + } + } + } + } + } + else { + int bdShift = m_bits_per_sample - output_bit_depth; + for (uint32_t row = 0; row < actual_h; row++) { + uint16_t* dst = reinterpret_cast(out_plane + row * out_stride); + uint16_t* src = reinterpret_cast(tile_buf.data() + row * m_tile_width * m_samples_per_pixel * 2); + if (outSpp == m_samples_per_pixel) { + if (bdShift == 0) { + memcpy(dst, src, actual_w * outSpp * 2); + } + else { + for (uint32_t x = 0; x < actual_w * outSpp; x++) { + dst[x] = static_cast(src[x] >> bdShift); + } + } + } + else { + for (uint32_t x = 0; x < actual_w; x++) { + for (uint16_t c = 0; c < outSpp; c++) { + dst[x * outSpp + c] = static_cast(src[x * m_samples_per_pixel + c] >> bdShift); + } + } + } + } + } + } + else { + // PLANARCONFIG_SEPARATE + for (uint16_t s = 0; s < outSpp; s++) { + tmsize_t read = TIFFReadEncodedTile(tif, TIFFComputeTile(tif, tx * m_tile_width, ty * m_tile_height, 0, s), + tile_buf.data(), tile_buf_size); + if (read < 0) { + heif_image_release(*out_image); + *out_image = nullptr; + return {heif_error_Invalid_input, heif_suberror_Unspecified, "Failed to read TIFF tile"}; + } + + if (m_bits_per_sample <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + row * out_stride + s; + uint8_t* src = tile_buf.data() + row * m_tile_width; + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = src[x]; + } + } + } + else if (output_bit_depth <= 8) { + for (uint32_t row = 0; row < actual_h; row++) { + uint8_t* dst = out_plane + row * out_stride + s; + uint16_t* src = reinterpret_cast(tile_buf.data() + row * m_tile_width * 2); + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = static_cast(src[x] >> (m_bits_per_sample - 8)); + } + } + } + else { + int bdShift = m_bits_per_sample - output_bit_depth; + for (uint32_t row = 0; row < actual_h; row++) { + uint16_t* dst = reinterpret_cast(out_plane + row * out_stride) + s; + uint16_t* src = reinterpret_cast(tile_buf.data() + row * m_tile_width * 2); + for (uint32_t x = 0; x < actual_w; x++) { + dst[x * outSpp] = static_cast(src[x] >> bdShift); + } + } + } + } + } + + return heif_error_ok; +} + + +void TiledTiffReader::readExif(InputImage* input_image) +{ + TIFF* tif = static_cast(m_tif.get()); + std::unique_ptr tags = ExifTags::Parse(tif); + if (tags) { + tags->Encode(&(input_image->exif)); + } +} + +/* +void TiledTiffReader::printGeoInfo(const char* filename) const +{ +#if HAVE_GEOTIFF + TIFF* tif = XTIFFOpen(filename, "r"); + if (!tif) { + return; + } + + GTIF* gtif = GTIFNew(tif); + if (!gtif) { + XTIFFClose(tif); + return; + } + + // Get EPSG code + unsigned short epsg = 0; + GTIFKeyGetSHORT(gtif, ProjectedCSTypeGeoKey, &epsg, 0, 1); + + // Get CRS citation string + char citation[256] = {}; + GTIFKeyGetASCII(gtif, GTCitationGeoKey, citation, sizeof(citation)); + if (citation[0] == '\0') { + GTIFKeyGetASCII(gtif, GeogCitationGeoKey, citation, sizeof(citation)); + } + + if (epsg > 0 || citation[0] != '\0') { + std::cout << "GeoTIFF: "; + if (epsg > 0) { + std::cout << "EPSG:" << epsg; + } + if (citation[0] != '\0') { + if (epsg > 0) { + std::cout << " (" << citation << ")"; + } + else { + std::cout << citation; + } + } + std::cout << "\n"; + } + + // Get pixel scale + uint16_t scale_count = 0; + double* scale = nullptr; + if (TIFFGetField(tif, TIFFTAG_GEOPIXELSCALE, &scale_count, &scale) && scale_count >= 2) { + std::cout << std::fixed << std::setprecision(3) + << " pixel scale: " << scale[0] << " x " << scale[1] << "\n"; + } + + // Get tiepoint (origin) + uint16_t tp_count = 0; + double* tiepoints = nullptr; + if (TIFFGetField(tif, TIFFTAG_GEOTIEPOINTS, &tp_count, &tiepoints) && tp_count >= 6) { + std::cout << std::fixed << std::setprecision(3) + << " origin: (" << tiepoints[3] << ", " << tiepoints[4] << ")\n"; + } + + GTIFFree(gtif); + XTIFFClose(tif); +#endif + + if (!m_overviews.empty()) { + std::cout << " overviews: "; + for (size_t i = 0; i < m_overviews.size(); i++) { + if (i > 0) std::cout << ", "; + std::cout << m_overviews[i].image_width << "x" << m_overviews[i].image_height; + } + std::cout << "\n"; + } +} +*/ \ No newline at end of file diff -Nru libheif-1.19.8/heifio/decoder_tiff.h libheif-1.23.4/heifio/decoder_tiff.h --- libheif-1.19.8/heifio/decoder_tiff.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_tiff.h 2026-09-06 14:45:17.000000000 +0000 @@ -29,8 +29,69 @@ #include "decoder.h" #include "libheif/heif.h" +#include +#include +#include + +struct YCbCrInfo { + bool is_ycbcr = false; + uint16_t horiz_sub = 1; // horizontal subsampling factor + uint16_t vert_sub = 1; // vertical subsampling factor +}; LIBHEIF_API -heif_error loadTIFF(const char *filename, InputImage *input_image); +heif_error loadTIFF(const char *filename, int output_bit_depth, InputImage *input_image); + +class LIBHEIF_API TiledTiffReader { +public: + ~TiledTiffReader(); + + struct OverviewInfo { + uint32_t dir_index; + uint32_t image_width; + uint32_t image_height; + uint32_t tile_width; + uint32_t tile_height; + }; + + // Returns a reader if the file is a tiled TIFF. If the TIFF is not tiled, + // returns nullptr with heif_error_Ok (caller should fall back to loadTIFF). + static std::unique_ptr open(const char* filename, heif_error* out_err); + + uint32_t imageWidth() const { return m_image_width; } + uint32_t imageHeight() const { return m_image_height; } + uint32_t tileWidth() const { return m_tile_width; } + uint32_t tileHeight() const { return m_tile_height; } + uint32_t nColumns() const { return m_n_columns; } + uint32_t nRows() const { return m_n_rows; } + + const std::vector& overviews() const { return m_overviews; } + bool setDirectory(uint32_t dir_index); + + uint16_t bitsPerSample() const { return m_bits_per_sample; } + uint16_t sampleFormat() const { return m_sample_format; } + + heif_error readTile(uint32_t tx, uint32_t ty, int output_bit_depth, heif_image** out_image); + void readExif(InputImage* input_image); + //void printGeoInfo(const char* filename) const; + +private: + TiledTiffReader() = default; + + struct TiffCloser { void operator()(void* tif) const; }; + std::unique_ptr m_tif; + + uint32_t m_image_width = 0, m_image_height = 0; + uint32_t m_tile_width = 0, m_tile_height = 0; + uint32_t m_n_columns = 0, m_n_rows = 0; + uint16_t m_samples_per_pixel = 0; + uint16_t m_bits_per_sample = 0; + uint16_t m_planar_config = 0; + uint16_t m_sample_format = 1; // SAMPLEFORMAT_UINT + bool m_has_alpha = false; + YCbCrInfo m_ycbcr; + + std::vector m_overviews; +}; #endif // LIBHEIF_DECODER_TIFF_H diff -Nru libheif-1.19.8/heifio/decoder_webp.cc libheif-1.23.4/heifio/decoder_webp.cc --- libheif-1.19.8/heifio/decoder_webp.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_webp.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,326 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2023 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "decoder_webp.h" +#include "exif.h" + +extern "C" { +#include +#include +} + +static struct heif_error heif_error_ok = { heif_error_Ok, heif_suberror_Unspecified, "Success" }; + +// WebP bitstream format values returned by WebPGetFeatures() in WebPBitstreamFeatures::format. +// libwebp does not expose these as named symbols. +//static const int WEBP_FORMAT_UNDEFINED = 0; +static const int WEBP_FORMAT_LOSSY = 1; +static const int WEBP_FORMAT_LOSSLESS = 2; +//static const int WEBP_FORMAT_MIXED = 3; + +heif_error loadWEBP(const char* filename, InputImage* input_image) +{ + struct heif_image* image = nullptr; + // Owns the image locally so that any early error return releases it. + // Only handed to input_image->image once decoding fully succeeds. + std::shared_ptr image_ptr; + struct heif_error err = heif_error_ok; + + // open input file + + FILE* infile; + if ((infile = fopen(filename, "rb")) == NULL) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Cannot open WEBP file" }; + return err; + } + std::vector bitstream; + fseek(infile, 0, SEEK_END); + long fsize = ftell(infile); + if (fsize <= 0) { + fclose(infile); + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "WEBP file is empty or unreadable" }; + return err; + } + fseek(infile, 0, SEEK_SET); + bitstream.resize(static_cast(fsize)); + size_t bytes_read = fread(bitstream.data(), 1, static_cast(fsize), infile); + fclose(infile); + if (bytes_read != static_cast(fsize)) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Could not read WEBP file" }; + return err; + } + + // initialize decompressor + int copy_data = 0; + WebPData webpdata; + WebPDataInit(&webpdata); + webpdata.bytes = bitstream.data(); + webpdata.size = bitstream.size(); + + WebPMux* mux = WebPMuxCreate(&webpdata, copy_data); + if (!mux) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "WebPMuxCreate failed" }; + return err; + } + std::unique_ptr mux_deleter(mux, &WebPMuxDelete); + + // Animated WebP: warn that only the first frame is converted. + int num_frames = 0; + if (WebPMuxNumChunks(mux, WEBP_CHUNK_ANMF, &num_frames) == WEBP_MUX_OK && num_frames > 1) { + fprintf(stderr, "WebP: animated input with %d frames -- only the first frame will be converted.\n", num_frames); + } + + WebPMuxFrameInfo frame; + if (WebPMuxGetFrame(mux, 1, &frame) != WEBP_MUX_OK) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "WebPMuxGetFrame failed" }; + return err; + } + std::unique_ptr frame_deleter(&frame.bitstream, &WebPDataClear); + // Getting ICC, XMP and EXIF + WebPData icc_chunk; + bool has_icc = false; + WebPDataInit(&icc_chunk); + if (WebPMuxGetChunk(mux, "ICCP", &icc_chunk) == WEBP_MUX_OK) { + has_icc = true; + } + WebPData exif_chunk; + bool has_exif = false; + WebPDataInit(&exif_chunk); + if (WebPMuxGetChunk(mux, "EXIF", &exif_chunk) == WEBP_MUX_OK) { + has_exif = true; + } + WebPData xmp_chunk; + bool has_xmp = false; + WebPDataInit(&xmp_chunk); + if (WebPMuxGetChunk(mux, "XMP ", &xmp_chunk) == WEBP_MUX_OK) { + has_xmp = true; + } + + WebPDecoderConfig config; + if (!WebPInitDecoderConfig(&config)) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Cannot open WEBP file" }; + return err; + } + + // retrieve the bitstream's features. + if (WebPGetFeatures(frame.bitstream.bytes, frame.bitstream.size, &config.input) != VP8_STATUS_OK) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Cannot open WEBP file" }; + return err; + } + + if (config.input.format == WEBP_FORMAT_LOSSLESS) { + // Lossless: RGB(A) + if (config.input.has_alpha) { + config.output.colorspace = MODE_RGBA; + } + else { + config.output.colorspace = MODE_RGB; + } + + // initialize heif image + const int width = config.input.width; + const int height = config.input.height; + if (config.input.has_alpha) { + err = heif_image_create((int)width, (int)height, + heif_colorspace_RGB, + heif_chroma_interleaved_RGBA, + &image); + } + else { + err = heif_image_create((int)width, (int)height, + heif_colorspace_RGB, + heif_chroma_interleaved_RGB, + &image); + } + if (err.code) + return err; + // Take ownership now so that any later error return releases the image. + image_ptr = std::shared_ptr(image, + [](heif_image* img) { heif_image_release(img); }); + + err = heif_image_add_plane(image, heif_channel_interleaved, (int)width, (int)height, 8); + if (err.code) + return err; + size_t stride; + uint8_t* ptr = heif_image_get_plane2(image, heif_channel_interleaved, &stride); + // decode into heif image + config.output.is_external_memory = 1; + config.output.u.RGBA.rgba = ptr; + config.output.u.RGBA.size = stride * height; + config.output.u.RGBA.stride = static_cast(stride); + config.output.width = width; + config.output.height = height; + // D) Decode! + if (WebPDecode(frame.bitstream.bytes, frame.bitstream.size, &config) != VP8_STATUS_OK) + { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "WEBP file decoding error!" }; + return err; + } + } + else if (config.input.format == WEBP_FORMAT_LOSSY) { + // Lossy: YUV420 + if (config.input.has_alpha) { + config.output.colorspace = MODE_YUVA; + } + else { + config.output.colorspace = MODE_YUV; + } + + const int width = config.input.width; + const int height = config.input.height; + + err = heif_image_create((int)width, (int)height, + heif_colorspace_YCbCr, + heif_chroma_420, + &image); + if (err.code) + return err; + + // Take ownership now so that any later error return releases the image. + image_ptr = std::shared_ptr(image, + [](heif_image* img) { heif_image_release(img); }); + + size_t stride[4] = {}; + uint8_t* ptr[4] = {}; + const int uv_width = (width + 1) / 2; + const int uv_height = (height + 1) / 2; + err = heif_image_add_plane(image, heif_channel_Y, (int)width, (int)height, 8); + if (err.code) + return err; + err = heif_image_add_plane(image, heif_channel_Cb, uv_width, uv_height, 8); + if (err.code) + return err; + err = heif_image_add_plane(image, heif_channel_Cr, uv_width, uv_height, 8); + if (err.code) + return err; + if (config.input.has_alpha) { + err = heif_image_add_plane(image, heif_channel_Alpha, (int)width, (int)height, 8); + if (err.code) + return err; + } + ptr[0] = heif_image_get_plane2(image, heif_channel_Y, &stride[0]); + ptr[1] = heif_image_get_plane2(image, heif_channel_Cb, &stride[1]); + ptr[2] = heif_image_get_plane2(image, heif_channel_Cr, &stride[2]); + if (config.input.has_alpha) { + ptr[3] = heif_image_get_plane2(image, heif_channel_Alpha, &stride[3]); + } + config.output.is_external_memory = 1; + config.output.u.YUVA.y = ptr[0]; + config.output.u.YUVA.y_stride = static_cast(stride[0]); + config.output.u.YUVA.y_size = height * stride[0]; + config.output.u.YUVA.u = ptr[1]; + config.output.u.YUVA.u_stride = static_cast(stride[1]); + config.output.u.YUVA.u_size = uv_height * stride[1]; + config.output.u.YUVA.v = ptr[2]; + config.output.u.YUVA.v_stride = static_cast(stride[2]); + config.output.u.YUVA.v_size = uv_height * stride[2]; + if (config.input.has_alpha) { + config.output.u.YUVA.a = ptr[3]; + config.output.u.YUVA.a_stride = static_cast(stride[3]); + config.output.u.YUVA.a_size = height * stride[3]; + } + else { + config.output.u.YUVA.a = NULL; + config.output.u.YUVA.a_stride = 0; + config.output.u.YUVA.a_size = 0; + } + // D) Decode! + if (WebPDecode(frame.bitstream.bytes, frame.bitstream.size, &config) != VP8_STATUS_OK) { + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "WEBP file decoding error!" }; + return err; + } + } + else { + // WEBP_FORMAT_UNDEFINED or WEBP_FORMAT_MIXED -- not supported here. + struct heif_error err = { + .code = heif_error_Invalid_input, + .subcode = heif_suberror_Unspecified, + .message = "Unsupported WebP bitstream format (mixed or undefined)" }; + return err; + } + + if (config.input.has_alpha) { + heif_image_set_premultiplied_alpha(image, 0); + } + + if (has_exif) { + input_image->exif.resize(exif_chunk.size); + memcpy(input_image->exif.data(), exif_chunk.bytes, exif_chunk.size); + } + if (has_icc) { + heif_image_set_raw_color_profile(image, "prof", icc_chunk.bytes, icc_chunk.size); + } + if (has_xmp) { + input_image->xmp.resize(xmp_chunk.size); + memcpy(input_image->xmp.data(), xmp_chunk.bytes, xmp_chunk.size); + } + + + // F) Reclaim memory allocated in config's object. It's safe to call + // this function even if the memory is external and wasn't allocated + // by WebPDecode(). + WebPFreeDecBuffer(&config.output); + + input_image->image = std::move(image_ptr); + return err; +} diff -Nru libheif-1.19.8/heifio/decoder_webp.h libheif-1.23.4/heifio/decoder_webp.h --- libheif-1.19.8/heifio/decoder_webp.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_webp.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,35 @@ +/* + libheif example application "heif". + + MIT License + + Copyright (c) 2023 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef LIBHEIF_DECODER_WEBP_H +#define LIBHEIF_DECODER_WEBP_H + +#include "decoder.h" + +LIBHEIF_API +heif_error loadWEBP(const char *filename, InputImage *input_image); + +#endif //LIBHEIF_DECODER_WEBP_H diff -Nru libheif-1.19.8/heifio/decoder_y4m.cc libheif-1.23.4/heifio/decoder_y4m.cc --- libheif-1.19.8/heifio/decoder_y4m.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/decoder_y4m.cc 2026-09-06 14:45:17.000000000 +0000 @@ -125,10 +125,10 @@ heif_image_add_plane(image, heif_channel_Cb, (w + 1) / 2, (h + 1) / 2, 8); heif_image_add_plane(image, heif_channel_Cr, (w + 1) / 2, (h + 1) / 2, 8); - int y_stride, cb_stride, cr_stride; - uint8_t* py = heif_image_get_plane(image, heif_channel_Y, &y_stride); - uint8_t* pcb = heif_image_get_plane(image, heif_channel_Cb, &cb_stride); - uint8_t* pcr = heif_image_get_plane(image, heif_channel_Cr, &cr_stride); + size_t y_stride, cb_stride, cr_stride; + uint8_t* py = heif_image_get_plane2(image, heif_channel_Y, &y_stride); + uint8_t* pcb = heif_image_get_plane2(image, heif_channel_Cb, &cb_stride); + uint8_t* pcr = heif_image_get_plane2(image, heif_channel_Cr, &cr_stride); for (int y = 0; y < h; y++) { istr.read((char*) (py + y * y_stride), w); diff -Nru libheif-1.19.8/heifio/encoder.h libheif-1.23.4/heifio/encoder.h --- libheif-1.19.8/heifio/encoder.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder.h 2026-09-06 14:45:17.000000000 +0000 @@ -43,21 +43,23 @@ virtual heif_chroma chroma(bool has_alpha, int bit_depth) const = 0; - virtual void UpdateDecodingOptions(const struct heif_image_handle* handle, - struct heif_decoding_options* options) const + virtual bool supports_alpha() const = 0; + + virtual void UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const { // Override if necessary. } - virtual bool Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) = 0; + virtual bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) = 0; protected: - static bool HasExifMetaData(const struct heif_image_handle* handle); + static bool HasExifMetaData(const heif_image_handle* handle); - static uint8_t* GetExifMetaData(const struct heif_image_handle* handle, size_t* size); + static uint8_t* GetExifMetaData(const heif_image_handle* handle, size_t* size); - static std::vector get_xmp_metadata(const struct heif_image_handle* handle); + static std::vector get_xmp_metadata(const heif_image_handle* handle); }; #endif // EXAMPLE_ENCODER_H diff -Nru libheif-1.19.8/heifio/encoder_jpeg.cc libheif-1.23.4/heifio/encoder_jpeg.cc --- libheif-1.19.8/heifio/encoder_jpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_jpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -35,6 +35,8 @@ #include +#include "common_utils.h" + #define JPEG_XMP_MARKER (JPEG_APP0+1) /* JPEG marker code for XMP */ #define JPEG_XMP_MARKER_ID "http://ns.adobe.com/xap/1.0/" @@ -175,116 +177,121 @@ // --- Write EXIF - size_t exifsize = 0; - uint8_t* exifdata = GetExifMetaData(handle, &exifsize); - if (exifdata) { - if (exifsize > 4) { - static const uint8_t kExifMarker = JPEG_APP0 + 1; - - uint32_t skip = (exifdata[0]<<24) | (exifdata[1]<<16) | (exifdata[2]<<8) | exifdata[3]; - if (skip > (exifsize - 4)) { - fprintf(stderr, "Invalid EXIF data (offset too large)\n"); - free(exifdata); - jpeg_destroy_compress(&cinfo); - fclose(fp); - return false; - } - skip += 4; - - uint8_t* ptr = exifdata + skip; - size_t size = exifsize - skip; - - if (size > std::numeric_limits::max()) { - fprintf(stderr, "EXIF larger than 4GB is not supported"); - free(exifdata); - jpeg_destroy_compress(&cinfo); - fclose(fp); - return false; - } - - auto size32 = static_cast(size); + if (handle) { + size_t exifsize = 0; + uint8_t* exifdata = GetExifMetaData(handle, &exifsize); + if (exifdata) { + if (exifsize > 4) { + static const uint8_t kExifMarker = JPEG_APP0 + 1; + + uint32_t skip = four_bytes_to_uint32(exifdata[0], exifdata[1], exifdata[2], exifdata[3]); + if (skip > (exifsize - 4)) { + fprintf(stderr, "Invalid EXIF data (offset too large)\n"); + free(exifdata); + jpeg_destroy_compress(&cinfo); + fclose(fp); + return false; + } + skip += 4; + + uint8_t* ptr = exifdata + skip; + size_t size = exifsize - skip; + + if (size > std::numeric_limits::max()) { + fprintf(stderr, "EXIF larger than 4GB is not supported"); + free(exifdata); + jpeg_destroy_compress(&cinfo); + fclose(fp); + return false; + } + + auto size32 = static_cast(size); + + // libheif by default normalizes the image orientation, so that we have to set the EXIF Orientation to "Horizontal (normal)" + modify_exif_orientation_tag_if_it_exists(ptr, size32, 1); + overwrite_exif_image_size_if_it_exists(ptr, size32, cinfo.image_width, cinfo.image_height); + + // We have to limit the size for the memcpy, otherwise GCC warns that we exceed the maximum size. + if (size > 0x1000000) { + size = 0x1000000; + } + + std::vector jpegExifMarkerData(6 + size); + memcpy(jpegExifMarkerData.data() + 6, ptr, size); + jpegExifMarkerData[0] = 'E'; + jpegExifMarkerData[1] = 'x'; + jpegExifMarkerData[2] = 'i'; + jpegExifMarkerData[3] = 'f'; + jpegExifMarkerData[4] = 0; + jpegExifMarkerData[5] = 0; + + ptr = jpegExifMarkerData.data(); + size = jpegExifMarkerData.size(); + + while (size > MAX_BYTES_IN_MARKER) { + jpeg_write_marker(&cinfo, kExifMarker, ptr, + static_cast(MAX_BYTES_IN_MARKER)); + + ptr += MAX_BYTES_IN_MARKER; + size -= MAX_BYTES_IN_MARKER; + } - // libheif by default normalizes the image orientation, so that we have to set the EXIF Orientation to "Horizontal (normal)" - modify_exif_orientation_tag_if_it_exists(ptr, size32, 1); - overwrite_exif_image_size_if_it_exists(ptr, size32, cinfo.image_width, cinfo.image_height); - - // We have to limit the size for the memcpy, otherwise GCC warns that we exceed the maximum size. - if (size>0x1000000) { - size = 0x1000000; - } - - std::vector jpegExifMarkerData(6+size); - memcpy(jpegExifMarkerData.data()+6, ptr, size); - jpegExifMarkerData[0]='E'; - jpegExifMarkerData[1]='x'; - jpegExifMarkerData[2]='i'; - jpegExifMarkerData[3]='f'; - jpegExifMarkerData[4]=0; - jpegExifMarkerData[5]=0; - - ptr = jpegExifMarkerData.data(); - size = jpegExifMarkerData.size(); - - while (size > MAX_BYTES_IN_MARKER) { jpeg_write_marker(&cinfo, kExifMarker, ptr, - static_cast(MAX_BYTES_IN_MARKER)); - - ptr += MAX_BYTES_IN_MARKER; - size -= MAX_BYTES_IN_MARKER; + static_cast(size)); } - jpeg_write_marker(&cinfo, kExifMarker, ptr, - static_cast(size)); + free(exifdata); } - - free(exifdata); } // --- Write XMP // spec: https://raw.githubusercontent.com/adobe/xmp-docs/master/XMPSpecifications/XMPSpecificationPart3.pdf - auto xmp = get_xmp_metadata(handle); - if (xmp.size() > 65502) { - fprintf(stderr, "XMP data too large, ExtendedXMP is not supported yet.\n"); - } - else if (!xmp.empty()) { - std::vector xmpWithId; - xmpWithId.resize(xmp.size() + strlen(JPEG_XMP_MARKER_ID)+1); - strcpy((char*)xmpWithId.data(), JPEG_XMP_MARKER_ID); - memcpy(xmpWithId.data() + strlen(JPEG_XMP_MARKER_ID) + 1, xmp.data(), xmp.size()); + if (handle) { + auto xmp = get_xmp_metadata(handle); + if (xmp.size() > 65502) { + fprintf(stderr, "XMP data too large, ExtendedXMP is not supported yet.\n"); + } + else if (!xmp.empty()) { + std::vector xmpWithId; + xmpWithId.resize(xmp.size() + strlen(JPEG_XMP_MARKER_ID) + 1); + strcpy((char*) xmpWithId.data(), JPEG_XMP_MARKER_ID); + memcpy(xmpWithId.data() + strlen(JPEG_XMP_MARKER_ID) + 1, xmp.data(), xmp.size()); - jpeg_write_marker(&cinfo, JPEG_XMP_MARKER, xmpWithId.data(), static_cast(xmpWithId.size())); + jpeg_write_marker(&cinfo, JPEG_XMP_MARKER, xmpWithId.data(), static_cast(xmpWithId.size())); + } } // --- Write ICC - size_t profile_size = heif_image_handle_get_raw_color_profile_size(handle); - if (profile_size > 0) { - uint8_t* profile_data = static_cast(malloc(profile_size)); - heif_image_handle_get_raw_color_profile(handle, profile_data); - jpeg_write_icc_profile(&cinfo, profile_data, (unsigned int) profile_size); - free(profile_data); - } + if (handle) { + size_t profile_size = heif_image_handle_get_raw_color_profile_size(handle); + if (profile_size > 0) { + uint8_t* profile_data = static_cast(malloc(profile_size)); + heif_image_handle_get_raw_color_profile(handle, profile_data); + jpeg_write_icc_profile(&cinfo, profile_data, (unsigned int) profile_size); + free(profile_data); + } - if (heif_image_get_bits_per_pixel(image, heif_channel_Y) != 8) { - fprintf(stderr, "JPEG writer cannot handle image with >8 bpp.\n"); - jpeg_destroy_compress(&cinfo); - fclose(fp); - return false; + if (heif_image_get_bits_per_pixel(image, heif_channel_Y) != 8) { + fprintf(stderr, "JPEG writer cannot handle image with >8 bpp.\n"); + jpeg_destroy_compress(&cinfo); + fclose(fp); + return false; + } } - - int stride_y; - const uint8_t* row_y = heif_image_get_plane_readonly(image, heif_channel_Y, - &stride_y); - int stride_u; - const uint8_t* row_u = heif_image_get_plane_readonly(image, heif_channel_Cb, - &stride_u); - int stride_v; - const uint8_t* row_v = heif_image_get_plane_readonly(image, heif_channel_Cr, - &stride_v); + size_t stride_y; + const uint8_t* row_y = heif_image_get_plane_readonly2(image, heif_channel_Y, + &stride_y); + size_t stride_u; + const uint8_t* row_u = heif_image_get_plane_readonly2(image, heif_channel_Cb, + &stride_u); + size_t stride_v; + const uint8_t* row_v = heif_image_get_plane_readonly2(image, heif_channel_Cr, + &stride_v); JSAMPARRAY buffer = cinfo.mem->alloc_sarray( reinterpret_cast(&cinfo), JPOOL_IMAGE, diff -Nru libheif-1.19.8/heifio/encoder_jpeg.h libheif-1.23.4/heifio/encoder_jpeg.h --- libheif-1.19.8/heifio/encoder_jpeg.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_jpeg.h 2026-09-06 14:45:17.000000000 +0000 @@ -50,11 +50,13 @@ return heif_chroma_420; } - void UpdateDecodingOptions(const struct heif_image_handle* handle, - struct heif_decoding_options* options) const override; + bool supports_alpha() const override { return false; } - bool Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) override; + void UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const override; + + bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) override; private: static const int kDefaultQuality = 90; diff -Nru libheif-1.19.8/heifio/encoder_png.cc libheif-1.23.4/heifio/encoder_png.cc --- libheif-1.19.8/heifio/encoder_png.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_png.cc 2026-09-06 14:45:17.000000000 +0000 @@ -30,12 +30,23 @@ #include #include "encoder_png.h" + +#include "common_utils.h" #include "exif.h" PngEncoder::PngEncoder() = default; -bool PngEncoder::Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) + +void PngEncoder::UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const +{ +#ifdef PNG_cICP_SUPPORTED + options->output_image_nclx_profile_passthrough = true; +#endif +} + +bool PngEncoder::Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) { png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); @@ -93,71 +104,120 @@ // --- write ICC profile - size_t profile_size = heif_image_handle_get_raw_color_profile_size(handle); - if (profile_size > 0) { - uint8_t* profile_data = static_cast(malloc(profile_size)); - heif_image_handle_get_raw_color_profile(handle, profile_data); - char profile_name[] = "unknown"; - png_set_iCCP(png_ptr, info_ptr, profile_name, PNG_COMPRESSION_TYPE_BASE, + if (handle) { + size_t profile_size = heif_image_handle_get_raw_color_profile_size(handle); + if (profile_size > 0) { + uint8_t* profile_data = static_cast(malloc(profile_size)); + heif_image_handle_get_raw_color_profile(handle, profile_data); + if (fix_icc_profile(profile_data, profile_size)) { + char profile_name[] = "unknown"; + png_set_iCCP(png_ptr, info_ptr, profile_name, PNG_COMPRESSION_TYPE_BASE, #if PNG_LIBPNG_VER < 10500 - (png_charp)profile_data, + (png_charp)profile_data, #else - (png_const_bytep) profile_data, + (png_const_bytep) profile_data, #endif - (png_uint_32) profile_size); - free(profile_data); - } + (png_uint_32) profile_size); + } + else { + fprintf(stderr, "Invalid ICC profile. Writing PNG file without ICC.\n"); + } + free(profile_data); + } +#ifdef PNG_cICP_SUPPORTED + if (heif_image_get_color_profile_type(image) == heif_color_profile_type_nclx) { + heif_color_profile_nclx* nclx = nullptr; + heif_image_get_nclx_color_profile(image, &nclx); + if (nclx) { + // Setting matrix coefficients to 0 (RGB) and full range flag to 1 (full range) + // because data is converted into RGB specified by PngEncoder::colorspace (heif_colorspace_RGB) + png_set_cICP(png_ptr, info_ptr, nclx->color_primaries, nclx->transfer_characteristics, 0, 1); + heif_nclx_color_profile_free(nclx); + } + } +#endif + } // --- write EXIF metadata #ifdef PNG_eXIf_SUPPORTED - size_t exifsize = 0; - uint8_t* exifdata = GetExifMetaData(handle, &exifsize); - if (exifdata) { - if (exifsize > 4) { - uint32_t skip = (exifdata[0]<<24) | (exifdata[1]<<16) | (exifdata[2]<<8) | exifdata[3]; - if (skip < (exifsize - 4)) { - skip += 4; - uint8_t* ptr = exifdata + skip; - size_t size = exifsize - skip; - - // libheif by default normalizes the image orientation, so that we have to set the EXIF Orientation to "Horizontal (normal)" - modify_exif_orientation_tag_if_it_exists(ptr, (int)size, 1); - overwrite_exif_image_size_if_it_exists(ptr, (int)size, width, height); + if (handle) { + size_t exifsize = 0; + uint8_t* exifdata = GetExifMetaData(handle, &exifsize); + if (exifdata) { + if (exifsize > 4) { + uint32_t skip = four_bytes_to_uint32(exifdata[0], exifdata[1], exifdata[2], exifdata[3]); + if (skip > (exifsize - 4)) { + fprintf(stderr, "Invalid EXIF data (offset too large)\n"); + } + else { + skip += 4; + uint8_t* ptr = exifdata + skip; + size_t size = exifsize - skip; + + // libheif by default normalizes the image orientation, so that we have to set the EXIF Orientation to "Horizontal (normal)" + modify_exif_orientation_tag_if_it_exists(ptr, (int) size, 1); + overwrite_exif_image_size_if_it_exists(ptr, (int) size, width, height); - png_set_eXIf_1(png_ptr, info_ptr, (png_uint_32)size, ptr); + png_set_eXIf_1(png_ptr, info_ptr, (png_uint_32) size, ptr); + } } - } - free(exifdata); + free(exifdata); + } } #endif // --- write XMP metadata #ifdef PNG_iTXt_SUPPORTED - // spec: https://raw.githubusercontent.com/adobe/xmp-docs/master/XMPSpecifications/XMPSpecificationPart3.pdf - std::vector xmp = get_xmp_metadata(handle); - if (!xmp.empty()) { - // make sure that XMP string is always null terminated. - if (xmp.back() != 0) { - xmp.push_back(0); - } + if (handle) { + // spec: https://raw.githubusercontent.com/adobe/xmp-docs/master/XMPSpecifications/XMPSpecificationPart3.pdf + std::vector xmp = get_xmp_metadata(handle); + if (!xmp.empty()) { + // make sure that XMP string is always null terminated. + if (xmp.back() != 0) { + xmp.push_back(0); + } + + // compute XMP string length + size_t text_length = 0; + while (xmp[text_length] != 0) { + text_length++; + } - // compute XMP string length - size_t text_length = 0; - while (xmp[text_length] != 0) { - text_length++; + png_text xmp_text{}; // important to zero-initialize the structure so that the remaining fields are NULL ! + xmp_text.compression = PNG_ITXT_COMPRESSION_NONE; + xmp_text.key = (char*) "XML:com.adobe.xmp"; + xmp_text.text = (char*) xmp.data(); + xmp_text.text_length = 0; // should be 0 for ITXT according the libpng documentation + xmp_text.itxt_length = text_length; + png_set_text(png_ptr, info_ptr, &xmp_text, 1); } + } +#endif + +#ifdef PNG_cLLI_SUPPORTED + if (heif_image_has_content_light_level(image)) { + heif_content_light_level cll; + heif_image_get_content_light_level(image, &cll); + png_set_cLLI_fixed(png_ptr, info_ptr, cll.max_content_light_level * 10000, cll.max_pic_average_light_level * 10000); + } +#endif - png_text xmp_text{}; // important to zero-initialize the structure so that the remaining fields are NULL ! - xmp_text.compression = PNG_ITXT_COMPRESSION_NONE; - xmp_text.key = (char*) "XML:com.adobe.xmp"; - xmp_text.text = (char*) xmp.data(); - xmp_text.text_length = 0; // should be 0 for ITXT according the libpng documentation - xmp_text.itxt_length = text_length; - png_set_text(png_ptr, info_ptr, &xmp_text, 1); +#ifdef PNG_mDCV_SUPPORTED + if (heif_image_has_mastering_display_colour_volume(image)) { + heif_mastering_display_colour_volume mdcv; + heif_image_get_mastering_display_colour_volume(image, &mdcv); + // The fixed point precision of PNG MDCV values are the same as HEIF (XY coordinates: 0.00002, luminance: 0.0001) + png_set_mDCV_fixed(png_ptr, info_ptr, + mdcv.white_point_x, mdcv.white_point_y, + mdcv.display_primaries_x[0], mdcv.display_primaries_y[0], + mdcv.display_primaries_x[1], mdcv.display_primaries_y[1], + mdcv.display_primaries_x[2], mdcv.display_primaries_y[2], + mdcv.max_display_mastering_luminance, + mdcv.min_display_mastering_luminance); } #endif @@ -165,9 +225,9 @@ uint8_t** row_pointers = new uint8_t* [height]; - int stride_rgb; - const uint8_t* row_rgb = heif_image_get_plane_readonly(image, - heif_channel_interleaved, &stride_rgb); + size_t stride_rgb; + const uint8_t* row_rgb = heif_image_get_plane_readonly2(image, + heif_channel_interleaved, &stride_rgb); for (int y = 0; y < height; ++y) { row_pointers[y] = const_cast(&row_rgb[y * stride_rgb]); @@ -179,7 +239,7 @@ int shift = 16 - input_bpp; if (shift > 0) { for (int y = 0; y < height; ++y) { - for (int x = 0; x < stride_rgb; x += 2) { + for (size_t x = 0; x < stride_rgb; x += 2) { uint8_t* p = (&row_pointers[y][x]); int v = (p[0] << 8) | p[1]; v = (v << shift) | (v >> (16 - shift)); diff -Nru libheif-1.19.8/heifio/encoder_png.h libheif-1.23.4/heifio/encoder_png.h --- libheif-1.19.8/heifio/encoder_png.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_png.h 2026-09-06 14:45:17.000000000 +0000 @@ -64,8 +64,13 @@ } } - bool Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) override; + bool supports_alpha() const override { return true; } + + void UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const override; + + bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) override; private: int m_compression_level = -1; diff -Nru libheif-1.19.8/heifio/encoder_tiff.cc libheif-1.23.4/heifio/encoder_tiff.cc --- libheif-1.19.8/heifio/encoder_tiff.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_tiff.cc 2026-09-06 14:45:17.000000000 +0000 @@ -63,9 +63,9 @@ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE); - int stride_rgb; - const uint8_t *row_rgb = heif_image_get_plane_readonly(image, - heif_channel_interleaved, &stride_rgb); + size_t stride_rgb; + const uint8_t *row_rgb = heif_image_get_plane_readonly2(image, + heif_channel_interleaved, &stride_rgb); for (int i = 0; i < height; i++) { diff -Nru libheif-1.19.8/heifio/encoder_tiff.h libheif-1.23.4/heifio/encoder_tiff.h --- libheif-1.19.8/heifio/encoder_tiff.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_tiff.h 2026-09-06 14:45:17.000000000 +0000 @@ -57,8 +57,10 @@ } } - bool Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) override; + bool supports_alpha() const override { return true; } + + bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) override; }; #endif // EXAMPLE_ENCODER_TIFF_H diff -Nru libheif-1.19.8/heifio/encoder_webp.cc libheif-1.23.4/heifio/encoder_webp.cc --- libheif-1.19.8/heifio/encoder_webp.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_webp.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,246 @@ +/* + libheif example application. + + MIT License + + Copyright (c) 2017 struktur AG, Joachim Bauch + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#include +#include +#include +#include +#include +#include "webp/mux.h" +#include "webp/encode.h" + +#include "encoder_webp.h" + +#include "common_utils.h" +#include "exif.h" + +WebPEncoder::WebPEncoder(int quality) : quality_(quality) +{ + if (quality_ == 100) { + fprintf(stderr, "WebP: quality 100 selected -- encoding in lossless mode.\n"); + } +} + +void WebPEncoder::UpdateDecodingOptions(const struct heif_image_handle* handle, + struct heif_decoding_options* options) const +{ + options->convert_hdr_to_8bit = 1; +} + +static int WebPWriter(const uint8_t* data, size_t data_size, + const WebPPicture* picture) { + std::vector* vec = (std::vector*)picture->custom_ptr; + assert(vec); + size_t size = vec->size(); + vec->resize(size + data_size); + memcpy(vec->data() + size, data, data_size); + return 1; +} + +bool WebPEncoder::Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) +{ + int width = heif_image_get_primary_width(image); + int height = heif_image_get_primary_height(image); + if (width > WEBP_MAX_DIMENSION || height > WEBP_MAX_DIMENSION || width <= 0 || height <= 0) { + fprintf(stderr, "Image dimension is too large for WEBP\n"); + return false; + } + WebPConfig cfg; + if (!WebPConfigInit(&cfg)) { + fprintf(stderr, "libwebp init failed!\n"); + return false; + } + bool islossless = quality_ == 100; + if (islossless) { + if(!WebPConfigLosslessPreset(&cfg, 6)){ + fprintf(stderr, "libwebp init failed!\n"); + return false; + } + } + else { + cfg.quality = static_cast(quality_); + cfg.alpha_quality = quality_; + } + cfg.thread_level = 1; + + WebPPicture webp; + if (!WebPPictureInit(&webp)) { + fprintf(stderr, "libwebp init failed!\n"); + return false; + } + // WebPPictureFree is a no-op on a picture that owns no internal buffers + // (i.e. the lossy path where y/u/v/a point to libheif planes), so this + // RAII guard is safe in both modes. + std::unique_ptr webp_deleter(&webp, &WebPPictureFree); + webp.width = width; + webp.height = height; + if (!islossless) { // Lossy + webp.y = (uint8_t*)heif_image_get_plane_readonly(image, heif_channel_Y, + &webp.y_stride); + webp.u = (uint8_t*)heif_image_get_plane_readonly(image, heif_channel_Cb, + &webp.uv_stride); + webp.v = (uint8_t*)heif_image_get_plane_readonly(image, heif_channel_Cr, + &webp.uv_stride); + webp.a = (uint8_t*)heif_image_get_plane_readonly(image, heif_channel_Alpha, + &webp.a_stride); + if (webp.a) { + webp.colorspace = WEBP_YUV420A; + } + else { + webp.colorspace = WEBP_YUV420; + } + } + else { // Lossless + const uint8_t* rgba; + size_t rgba_stride; + int ok; + rgba = heif_image_get_plane_readonly2(image, heif_channel_interleaved, + &rgba_stride); + assert(rgba); + heif_chroma format = heif_image_get_chroma_format(image); + if(format == heif_chroma_interleaved_RGBA) + ok = WebPPictureImportRGBA(&webp, rgba, static_cast(rgba_stride)); + else if(format == heif_chroma_interleaved_RGB) + ok = WebPPictureImportRGB(&webp, rgba, static_cast(rgba_stride)); + else + ok = 0; + if (!ok) { + fprintf(stderr, "libwebp framebuffer allocation failed!\n"); + return false; + } + } + std::vector mem; + webp.custom_ptr = &mem; + webp.writer = WebPWriter; + if (!WebPEncode(&cfg, &webp)) { + fprintf(stderr, "Error while encoding image\n"); + return false; + } + + FILE* fp = fopen(filename.c_str(), "wb"); + if (!fp) { + fprintf(stderr, "Can't open %s: %s\n", filename.c_str(), strerror(errno)); + return false; + } + std::unique_ptr fp_deleter(fp, &fclose); + WebPMux* mux = WebPMuxNew(); + if (!mux) { + fprintf(stderr, "Error creating WEBP muxer\n"); + return false; + } + std::unique_ptr mux_deleter(mux, &WebPMuxDelete); + WebPData bitstream; + WebPDataInit(&bitstream); + bitstream.bytes = mem.data(); + bitstream.size = mem.size(); + if (WebPMuxSetImage(mux, &bitstream, 0) != WEBP_MUX_OK) { + fprintf(stderr, "Error setting WEBP image\n"); + return false; + } + + // --- write ICC profile + + if (handle) { + WebPData icc_bitstream; + WebPDataInit(&icc_bitstream); + icc_bitstream.size = heif_image_handle_get_raw_color_profile_size(handle); + if (icc_bitstream.size > 0) { + icc_bitstream.bytes = static_cast(malloc(icc_bitstream.size)); + heif_image_handle_get_raw_color_profile(handle, (void*)icc_bitstream.bytes); + if (fix_icc_profile(icc_bitstream.bytes, icc_bitstream.size)) { + if (WebPMuxSetChunk(mux, "ICCP", &icc_bitstream, 1) != WEBP_MUX_OK) { + fprintf(stderr, "Error setting image ICC\n"); + } + } + else { + fprintf(stderr, "Invalid ICC profile. Writing WebP file without ICC.\n"); + } + free((void*)icc_bitstream.bytes); + } + } + + // --- write EXIF metadata + if (handle) { + WebPData exif_bitstream; + WebPDataInit(&exif_bitstream); + exif_bitstream.bytes = GetExifMetaData(handle, &exif_bitstream.size); + if (exif_bitstream.bytes) { + if (exif_bitstream.size > 4) { + uint32_t skip = four_bytes_to_uint32(exif_bitstream.bytes[0], exif_bitstream.bytes[1], exif_bitstream.bytes[2], exif_bitstream.bytes[3]); + if (skip < (exif_bitstream.size - 4)) { + skip += 4; + uint8_t* ptr = (uint8_t*)exif_bitstream.bytes + skip; + size_t size = exif_bitstream.size - skip; + + // libheif by default normalizes the image orientation, so that we have to set the EXIF Orientation to "Horizontal (normal)" + modify_exif_orientation_tag_if_it_exists(ptr, (int) size, 1); + overwrite_exif_image_size_if_it_exists(ptr, (int) size, width, height); + + if(WebPMuxSetChunk(mux, "EXIF", &exif_bitstream, 1) != WEBP_MUX_OK) { + fprintf(stderr, "Error setting image EXIF\n"); + } + } + } + + free((uint8_t*)exif_bitstream.bytes); + } + } + + // --- write XMP metadata + + if (handle) { + // spec: https://raw.githubusercontent.com/adobe/xmp-docs/master/XMPSpecifications/XMPSpecificationPart3.pdf + std::vector xmp = get_xmp_metadata(handle); + if (!xmp.empty()) { + // make sure that XMP string is always null terminated. + if (xmp.back() != 0) { + xmp.push_back(0); + } + + WebPData xmp_bitstream; + WebPDataInit(&xmp_bitstream); + xmp_bitstream.bytes = xmp.data(); + xmp_bitstream.size = xmp.size(); + if(WebPMuxSetChunk(mux, "XMP ", &xmp_bitstream, 1) != WEBP_MUX_OK) { + fprintf(stderr, "Error setting image XMP\n"); + } + } + } + + WebPData webp_data; + WebPDataInit(&webp_data); + if (WebPMuxAssemble(mux, &webp_data) != WEBP_MUX_OK) { + fprintf(stderr, "Error while encoding image\n"); + WebPDataClear(&webp_data); + return false; + } + std::unique_ptr webp_data_deleter(&webp_data, &WebPDataClear); + if (fwrite(webp_data.bytes, 1, webp_data.size, fp) != webp_data.size) { + fprintf(stderr, "Writing WEBP file failed\n"); + return false; + } + return true; +} diff -Nru libheif-1.19.8/heifio/encoder_webp.h libheif-1.23.4/heifio/encoder_webp.h --- libheif-1.19.8/heifio/encoder_webp.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_webp.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,64 @@ +/* + libheif example application. + + MIT License + + Copyright (c) 2017 struktur AG, Joachim Bauch + Copyright (c) 2023 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef EXAMPLE_ENCODER_WEBP_H +#define EXAMPLE_ENCODER_WEBP_H + +#include + +#include "encoder.h" + +class WebPEncoder : public Encoder +{ +public: + WebPEncoder(int quality); + + heif_colorspace colorspace(bool has_alpha) const override + { + return quality_ == 100 ? heif_colorspace_RGB : heif_colorspace_YCbCr; + } + + heif_chroma chroma(bool has_alpha, int bit_depth) const override + { + if (quality_ == 100) { + return has_alpha ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB; + } + return heif_chroma_420; + } + + bool supports_alpha() const override { return true; } + + void UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const override; + + bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) override; + +private: + int quality_; // WebP quality, quality 100 = switch to lossless +}; + +#endif // EXAMPLE_ENCODER_WEBP_H diff -Nru libheif-1.19.8/heifio/encoder_y4m.cc libheif-1.23.4/heifio/encoder_y4m.cc --- libheif-1.19.8/heifio/encoder_y4m.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_y4m.cc 2026-09-06 14:45:17.000000000 +0000 @@ -50,10 +50,10 @@ return false; } - int y_stride, cb_stride, cr_stride; - const uint8_t* yp = heif_image_get_plane_readonly(image, heif_channel_Y, &y_stride); - const uint8_t* cbp = heif_image_get_plane_readonly(image, heif_channel_Cb, &cb_stride); - const uint8_t* crp = heif_image_get_plane_readonly(image, heif_channel_Cr, &cr_stride); + size_t y_stride, cb_stride, cr_stride; + const uint8_t* yp = heif_image_get_plane_readonly2(image, heif_channel_Y, &y_stride); + const uint8_t* cbp = heif_image_get_plane_readonly2(image, heif_channel_Cb, &cb_stride); + const uint8_t* crp = heif_image_get_plane_readonly2(image, heif_channel_Cr, &cr_stride); assert(y_stride > 0); assert(cb_stride > 0); diff -Nru libheif-1.19.8/heifio/encoder_y4m.h libheif-1.23.4/heifio/encoder_y4m.h --- libheif-1.19.8/heifio/encoder_y4m.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/encoder_y4m.h 2026-09-06 14:45:17.000000000 +0000 @@ -45,11 +45,13 @@ return heif_chroma_420; } - void UpdateDecodingOptions(const struct heif_image_handle* handle, - struct heif_decoding_options* options) const override; + bool supports_alpha() const override { return false; } - bool Encode(const struct heif_image_handle* handle, - const struct heif_image* image, const std::string& filename) override; + void UpdateDecodingOptions(const heif_image_handle* handle, + heif_decoding_options* options) const override; + + bool Encode(const heif_image_handle* handle, + const heif_image* image, const std::string& filename) override; private: }; diff -Nru libheif-1.19.8/heifio/exif.cc libheif-1.23.4/heifio/exif.cc --- libheif-1.19.8/heifio/exif.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/exif.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,8 +19,11 @@ */ #include +#include #include "exif.h" +#include "common_utils.h" + #define EXIF_TYPE_SHORT 3 #define EXIF_TYPE_LONG 4 #define DEFAULT_EXIF_ORIENTATION 1 @@ -41,10 +44,10 @@ const uint8_t* p = data + pos; if (littleEndian) { - return (p[3] << 24) | (p[2] << 16) | (p[1] << 8) | p[0]; + return four_bytes_to_uint32(p[3], p[2], p[1], p[0]); } else { - return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; + return four_bytes_to_uint32(p[0], p[1], p[2], p[3]); } } @@ -56,10 +59,10 @@ const uint8_t* p = data + pos; if (littleEndian) { - return static_cast((p[1] << 8) | p[0]); + return two_bytes_to_uint16(p[1], p[0]); } else { - return static_cast((p[0] << 8) | p[1]); + return two_bytes_to_uint16(p[0], p[1]); } } @@ -169,7 +172,7 @@ { // read TIFF header - if (size < 4) { + if (size < 8) { return 0; } @@ -259,3 +262,35 @@ return DEFAULT_EXIF_ORIENTATION; } + + +bool fix_icc_profile(const uint8_t* profile_data, size_t& profile_size) +{ + if (profile_size < 128) { + return false; + } + + // --- check that profile size specified in header matches the real size + + uint32_t size_in_header = four_bytes_to_uint32(profile_data[0], + profile_data[1], + profile_data[2], + profile_data[3]); + + if (size_in_header != profile_size) { + + // Size in header is smaller than actual size, but alignment indicates that it might + // be correct. Replace real data length with size in header. + if (size_in_header < profile_size && (size_in_header & 3) == 0) { + fprintf(stderr, "Input ICC profile has wrong size in header (%u instead of %zu). Skipping extra bytes at the end. " + "Note that this may still be incorrect and the ICC profile may be broken.\n", size_in_header, profile_size); + + profile_size = size_in_header; + } + else { + return false; + } + } + + return true; +} diff -Nru libheif-1.19.8/heifio/exif.h libheif-1.23.4/heifio/exif.h --- libheif-1.19.8/heifio/exif.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/heifio/exif.h 2026-09-06 14:45:17.000000000 +0000 @@ -30,4 +30,8 @@ void overwrite_exif_image_size_if_it_exists(uint8_t* exif, uint32_t size, uint32_t width, uint32_t height); +// Validates an ICC profile and adjusts profile_size if a trailing-padding +// fixup is applied. Returns false if the profile is unrecoverably invalid. +bool fix_icc_profile(const uint8_t* profile_data, size_t& profile_size); + #endif //LIBHEIF_EXIF_H diff -Nru libheif-1.19.8/libheif/CMakeLists.txt libheif-1.23.4/libheif/CMakeLists.txt --- libheif-1.19.8/libheif/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -4,11 +4,32 @@ set(libheif_headers api/libheif/heif.h - api/libheif/heif_cxx.h + api/libheif/heif_export.h + api/libheif/heif_library.h + api/libheif/heif_image.h + api/libheif/heif_color.h + api/libheif/heif_error.h api/libheif/heif_plugin.h api/libheif/heif_properties.h api/libheif/heif_regions.h api/libheif/heif_items.h + api/libheif/heif_sequences.h + api/libheif/heif_tai_timestamps.h + api/libheif/heif_brands.h + api/libheif/heif_metadata.h + api/libheif/heif_aux_images.h + api/libheif/heif_entity_groups.h + api/libheif/heif_security.h + api/libheif/heif_encoding.h + api/libheif/heif_decoding.h + api/libheif/heif_image_handle.h + api/libheif/heif_context.h + api/libheif/heif_tiling.h + api/libheif/heif_components.h + api/libheif/heif_uncompressed.h + api/libheif/heif_text.h + api/libheif/heif_omaf.h + api/libheif/heif_cxx.h ${CMAKE_CURRENT_BINARY_DIR}/heif_version.h) set(libheif_sources @@ -24,8 +45,10 @@ file.h file_layout.h file_layout.cc - pixelimage.cc - pixelimage.h + image/pixelimage.cc + image/pixelimage.h + image/image_description.cc + image/image_description.h plugin_registry.cc nclx.cc nclx.h @@ -37,28 +60,60 @@ logging.h logging.cc compression.h + compression.cc compression_brotli.cc compression_zlib.cc common_utils.cc common_utils.h region.cc region.h - api/libheif/api_structs.h + brands.cc + brands.h + id_creator.cc + id_creator.h + text.cc + text.h + api_structs.h api/libheif/heif.cc + api/libheif/heif_library.cc + api/libheif/heif_image.cc + api/libheif/heif_color.cc api/libheif/heif_regions.cc api/libheif/heif_plugin.cc api/libheif/heif_properties.cc api/libheif/heif_items.cc + api/libheif/heif_sequences.cc + api/libheif/heif_tai_timestamps.cc + api/libheif/heif_brands.cc + api/libheif/heif_metadata.cc + api/libheif/heif_aux_images.cc + api/libheif/heif_entity_groups.cc + api/libheif/heif_security.cc + api/libheif/heif_encoding.cc + api/libheif/heif_decoding.cc + api/libheif/heif_image_handle.cc + api/libheif/heif_context.cc + api/libheif/heif_tiling.cc + api/libheif/heif_components.cc + api/libheif/heif_uncompressed.cc + api/libheif/heif_text.cc + api/libheif/heif_omaf.cc codecs/decoder.h codecs/decoder.cc + codecs/encoder.h + codecs/encoder.cc image-items/hevc.cc image-items/hevc.h codecs/hevc_boxes.cc codecs/hevc_boxes.h codecs/hevc_dec.cc codecs/hevc_dec.h + codecs/hevc_enc.cc + codecs/hevc_enc.h image-items/avif.cc image-items/avif.h + codecs/avif_enc.cc + codecs/avif_enc.h codecs/avif_dec.cc codecs/avif_dec.h codecs/avif_boxes.cc @@ -69,16 +124,22 @@ codecs/jpeg_boxes.cc codecs/jpeg_dec.h codecs/jpeg_dec.cc + codecs/jpeg_enc.h + codecs/jpeg_enc.cc image-items/jpeg2000.h image-items/jpeg2000.cc codecs/jpeg2000_dec.h codecs/jpeg2000_dec.cc + codecs/jpeg2000_enc.h + codecs/jpeg2000_enc.cc codecs/jpeg2000_boxes.h codecs/jpeg2000_boxes.cc image-items/vvc.h image-items/vvc.cc codecs/vvc_dec.h codecs/vvc_dec.cc + codecs/vvc_enc.h + codecs/vvc_enc.cc codecs/vvc_boxes.h codecs/vvc_boxes.cc image-items/avc.h @@ -87,6 +148,8 @@ codecs/avc_boxes.cc codecs/avc_dec.h codecs/avc_dec.cc + codecs/avc_enc.h + codecs/avc_enc.cc image-items/mask_image.h image-items/mask_image.cc image-items/image_item.h @@ -117,6 +180,18 @@ color-conversion/alpha.h color-conversion/chroma_sampling.cc color-conversion/chroma_sampling.h + color-conversion/bayer_bilinear.cc + color-conversion/bayer_bilinear.h + sequences/seq_boxes.h + sequences/seq_boxes.cc + sequences/chunk.h + sequences/chunk.cc + sequences/track.h + sequences/track.cc + sequences/track_visual.h + sequences/track_visual.cc + sequences/track_metadata.h + sequences/track_metadata.cc ${libheif_headers}) add_library(heif ${libheif_sources}) @@ -189,7 +264,7 @@ if (LIBSHARPYUV_FOUND) message("Compiling in 'libsharpyuv'") - target_compile_definitions(heif PUBLIC HAVE_LIBSHARPYUV=1) + target_compile_definitions(heif PRIVATE HAVE_LIBSHARPYUV=1) target_include_directories(heif PRIVATE ${LIBSHARPYUV_INCLUDE_DIRS}) target_link_libraries(heif PRIVATE ${LIBSHARPYUV_LIBRARIES}) else () @@ -202,7 +277,7 @@ endif () if (Brotli_FOUND) - target_compile_definitions(heif PUBLIC HAVE_BROTLI=1) + target_compile_definitions(heif PRIVATE HAVE_BROTLI=1) target_include_directories(heif PRIVATE ${BROTLI_INCLUDE_DIRS}) target_link_libraries(heif PRIVATE ${BROTLI_LIBS}) endif() @@ -228,26 +303,45 @@ codecs/uncompressed/unc_codec.cc codecs/uncompressed/unc_dec.h codecs/uncompressed/unc_dec.cc - codecs/uncompressed/decoder_abstract.h - codecs/uncompressed/decoder_abstract.cc - codecs/uncompressed/decoder_component_interleave.h - codecs/uncompressed/decoder_component_interleave.cc - codecs/uncompressed/decoder_pixel_interleave.h - codecs/uncompressed/decoder_pixel_interleave.cc - codecs/uncompressed/decoder_mixed_interleave.h - codecs/uncompressed/decoder_mixed_interleave.cc - codecs/uncompressed/decoder_row_interleave.h - codecs/uncompressed/decoder_row_interleave.cc - codecs/uncompressed/decoder_tile_component_interleave.h - codecs/uncompressed/decoder_tile_component_interleave.cc) + codecs/uncompressed/unc_enc.h + codecs/uncompressed/unc_enc.cc + codecs/uncompressed/unc_decoder.h + codecs/uncompressed/unc_decoder.cc + codecs/uncompressed/unc_decoder_legacybase.h + codecs/uncompressed/unc_decoder_legacybase.cc + codecs/uncompressed/unc_decoder_component_interleave.h + codecs/uncompressed/unc_decoder_component_interleave.cc + codecs/uncompressed/unc_decoder_pixel_interleave.h + codecs/uncompressed/unc_decoder_pixel_interleave.cc + codecs/uncompressed/unc_decoder_block_pixel_interleave.h + codecs/uncompressed/unc_decoder_block_pixel_interleave.cc + codecs/uncompressed/unc_decoder_bytealign_component_interleave.h + codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc + codecs/uncompressed/unc_decoder_block_component_interleave.h + codecs/uncompressed/unc_decoder_block_component_interleave.cc + codecs/uncompressed/unc_decoder_mixed_interleave.h + codecs/uncompressed/unc_decoder_mixed_interleave.cc + codecs/uncompressed/unc_decoder_row_interleave.h + codecs/uncompressed/unc_decoder_row_interleave.cc + codecs/uncompressed/unc_encoder.h + codecs/uncompressed/unc_encoder.cc + codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc + codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h + codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc + codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h + codecs/uncompressed/unc_encoder_component_interleave.cc + codecs/uncompressed/unc_encoder_component_interleave.h + codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc + codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h) endif () -if (ENABLE_EXPERIMENTAL_MINI_FORMAT) - target_compile_definitions(heif PUBLIC ENABLE_EXPERIMENTAL_MINI_FORMAT=1) - target_sources(heif PRIVATE - mini.h - mini.cc) -endif () +target_sources(heif PRIVATE + mini.h + mini.cc) + +target_sources(heif PRIVATE + omaf_boxes.h + omaf_boxes.cc) write_basic_package_version_file(${PROJECT_NAME}-config-version.cmake COMPATIBILITY ExactVersion) diff -Nru libheif-1.19.8/libheif/Doxyfile.in libheif-1.23.4/libheif/Doxyfile.in --- libheif-1.19.8/libheif/Doxyfile.in 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/Doxyfile.in 2026-09-06 14:45:17.000000000 +0000 @@ -856,9 +856,13 @@ # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = @CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif.h \ +INPUT = @CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif.h \ @CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_items.h \ -@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_regions.h +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_regions.h \ +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_text.h \ +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_library.h \ +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_encoding.h \ +@CMAKE_CURRENT_SOURCE_DIR@/libheif/api/libheif/heif_sequences.h # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff -Nru libheif-1.19.8/libheif/api/libheif/api_structs.h libheif-1.23.4/libheif/api/libheif/api_structs.h --- libheif-1.19.8/libheif/api/libheif/api_structs.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/api_structs.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2017 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef HEIF_API_STRUCTS_H -#define HEIF_API_STRUCTS_H - -#include "pixelimage.h" -#include "context.h" - -#include -#include "image-items/image_item.h" - -struct heif_image_handle -{ - std::shared_ptr image; - - // store reference to keep the context alive while we are using the handle (issue #147) - std::shared_ptr context; -}; - - -struct heif_image -{ - std::shared_ptr image; -}; - - -struct heif_context -{ - std::shared_ptr context; -}; - - -struct heif_encoder -{ - heif_encoder(const struct heif_encoder_plugin* plugin); - - ~heif_encoder(); - - struct heif_error alloc(); - - void release(); - - - const struct heif_encoder_plugin* plugin; - void* encoder = nullptr; -}; - - -struct heif_region_item -{ - std::shared_ptr context; - std::shared_ptr region_item; -}; - - -struct heif_region -{ - std::shared_ptr context; // we need this to perform coordinate transformation - //heif_item_id parent_region_item_id; // we need this to perform coordinate transformation - std::shared_ptr region_item; - std::shared_ptr region; -}; - -#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif.cc libheif-1.23.4/libheif/api/libheif/heif.cc --- libheif-1.19.8/libheif/api/libheif/heif.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif.cc 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,6 @@ /* * HEIF codec. - * Copyright (c) 2017 Dirk Farin + * Copyright (c) 2017-2025 Dirk Farin * * This file is part of libheif. * @@ -18,3830 +18,14 @@ * along with libheif. If not, see . */ -#include "heif_plugin.h" -#include "security_limits.h" -#include "region.h" -#include "common_utils.h" -#include #include "heif.h" -#include "file.h" -#include "pixelimage.h" -#include "api_structs.h" -#include "context.h" -#include "plugin_registry.h" -#include "error.h" -#include "bitstream.h" -#include "init.h" -#include "image-items/grid.h" -#include "image-items/overlay.h" -#include "image-items/tiled.h" -#include -#include - -#if WITH_UNCOMPRESSED_CODEC -#include "image-items/unc_image.h" -#endif #if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_STANDALONE_WASM__) +#include "api_structs.h" #include "heif_emscripten.h" #endif -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -// for _write -#include -#else - -#include - -#endif - -#include - - -const struct heif_error heif_error_success = {heif_error_Ok, heif_suberror_Unspecified, Error::kSuccess}; -static struct heif_error error_unsupported_parameter = {heif_error_Usage_error, - heif_suberror_Unsupported_parameter, - "Unsupported encoder parameter"}; -static struct heif_error error_invalid_parameter_value = {heif_error_Usage_error, - heif_suberror_Invalid_parameter_value, - "Invalid parameter value"}; -static struct heif_error error_unsupported_plugin_version = {heif_error_Usage_error, - heif_suberror_Unsupported_plugin_version, - "Unsupported plugin version"}; -static struct heif_error error_null_parameter = {heif_error_Usage_error, - heif_suberror_Null_pointer_argument, - "NULL passed"}; - -const char* heif_get_version(void) -{ - return (LIBHEIF_VERSION); -} - -uint32_t heif_get_version_number(void) -{ - return (LIBHEIF_NUMERIC_VERSION); -} - -int heif_get_version_number_major(void) -{ - return ((LIBHEIF_NUMERIC_VERSION) >> 24) & 0xFF; -} - -int heif_get_version_number_minor(void) -{ - return ((LIBHEIF_NUMERIC_VERSION) >> 16) & 0xFF; -} - -int heif_get_version_number_maintenance(void) -{ - return ((LIBHEIF_NUMERIC_VERSION) >> 8) & 0xFF; -} - - -heif_filetype_result heif_check_filetype(const uint8_t* data, int len) -{ - if (len < 8) { - return heif_filetype_maybe; - } - - if (data[4] != 'f' || - data[5] != 't' || - data[6] != 'y' || - data[7] != 'p') { - return heif_filetype_no; - } - - if (len >= 12) { - heif_brand2 brand = heif_read_main_brand(data, len); - - if (brand == heif_brand2_heic) { - return heif_filetype_yes_supported; - } - else if (brand == heif_brand2_heix) { - return heif_filetype_yes_supported; - } - else if (brand == heif_brand2_avif) { - return heif_filetype_yes_supported; - } - else if (brand == heif_brand2_jpeg) { - return heif_filetype_yes_supported; - } - else if (brand == heif_brand2_j2ki) { - return heif_filetype_yes_supported; - } - else if (brand == heif_brand2_mif1) { - return heif_filetype_maybe; - } - else if (brand == heif_brand2_mif2) { - return heif_filetype_maybe; - } - else { - return heif_filetype_yes_unsupported; - } - } - - return heif_filetype_maybe; -} - - -heif_error heif_has_compatible_filetype(const uint8_t* data, int len) -{ - // Get compatible brands first, because that does validity checks - heif_brand2* compatible_brands = nullptr; - int nBrands = 0; - struct heif_error err = heif_list_compatible_brands(data, len, &compatible_brands, &nBrands); - if (err.code) { - return err; - } - - heif_brand2 main_brand = heif_read_main_brand(data, len); - - std::set supported_brands{ - heif_brand2_avif, - heif_brand2_heic, - heif_brand2_heix, - heif_brand2_j2ki, - heif_brand2_jpeg, - heif_brand2_miaf, - heif_brand2_mif1, - heif_brand2_mif2 -#if ENABLE_EXPERIMENTAL_MINI_FORMAT - , heif_brand2_mif3 -#endif - }; - - auto it = supported_brands.find(main_brand); - if (it != supported_brands.end()) { - heif_free_list_of_compatible_brands(compatible_brands); - return heif_error_ok; - } - - for (int i = 0; i < nBrands; i++) { - heif_brand2 compatible_brand = compatible_brands[i]; - it = supported_brands.find(compatible_brand); - if (it != supported_brands.end()) { - heif_free_list_of_compatible_brands(compatible_brands); - return heif_error_ok; - } - } - heif_free_list_of_compatible_brands(compatible_brands); - return {heif_error_Invalid_input, heif_suberror_Unsupported_image_type, "No supported brands found."};; -} - - -int heif_check_jpeg_filetype(const uint8_t* data, int len) -{ - if (len < 4 || data == nullptr) { - return -1; - } - - return (data[0] == 0xFF && - data[1] == 0xD8 && - data[2] == 0xFF && - (data[3] & 0xF0) == 0xE0); -} - - -heif_brand heif_fourcc_to_brand_enum(const char* fourcc) -{ - if (fourcc == nullptr || !fourcc[0] || !fourcc[1] || !fourcc[2] || !fourcc[3]) { - return heif_unknown_brand; - } - - char brand[5]; - brand[0] = fourcc[0]; - brand[1] = fourcc[1]; - brand[2] = fourcc[2]; - brand[3] = fourcc[3]; - brand[4] = 0; - - if (strcmp(brand, "heic") == 0) { - return heif_heic; - } - else if (strcmp(brand, "heix") == 0) { - return heif_heix; - } - else if (strcmp(brand, "hevc") == 0) { - return heif_hevc; - } - else if (strcmp(brand, "hevx") == 0) { - return heif_hevx; - } - else if (strcmp(brand, "heim") == 0) { - return heif_heim; - } - else if (strcmp(brand, "heis") == 0) { - return heif_heis; - } - else if (strcmp(brand, "hevm") == 0) { - return heif_hevm; - } - else if (strcmp(brand, "hevs") == 0) { - return heif_hevs; - } - else if (strcmp(brand, "mif1") == 0) { - return heif_mif1; - } - else if (strcmp(brand, "msf1") == 0) { - return heif_msf1; - } - else if (strcmp(brand, "avif") == 0) { - return heif_avif; - } - else if (strcmp(brand, "avis") == 0) { - return heif_avis; - } - else if (strcmp(brand, "vvic") == 0) { - return heif_vvic; - } - else if (strcmp(brand, "j2ki") == 0) { - return heif_j2ki; - } - else if (strcmp(brand, "j2is") == 0) { - return heif_j2is; - } - else { - return heif_unknown_brand; - } -} - - -enum heif_brand heif_main_brand(const uint8_t* data, int len) -{ - if (len < 12) { - return heif_unknown_brand; - } - - return heif_fourcc_to_brand_enum((char*) (data + 8)); -} - - -heif_brand2 heif_read_main_brand(const uint8_t* data, int len) -{ - if (len < 12) { - return heif_unknown_brand; - } - - return heif_fourcc_to_brand((char*) (data + 8)); -} - - -heif_brand2 heif_fourcc_to_brand(const char* fourcc_string) -{ - if (fourcc_string == nullptr || !fourcc_string[0] || !fourcc_string[1] || !fourcc_string[2] || !fourcc_string[3]) { - return 0; - } - - return fourcc(fourcc_string); -} - -heif_brand2 heif_read_minor_version_brand(const uint8_t* data, int len) -{ - if (len < 16) { - return heif_unknown_brand; - } - return heif_fourcc_to_brand((char*) (data + 12)); -} - -void heif_brand_to_fourcc(heif_brand2 brand, char* out_fourcc) -{ - if (out_fourcc) { - out_fourcc[0] = (char) ((brand >> 24) & 0xFF); - out_fourcc[1] = (char) ((brand >> 16) & 0xFF); - out_fourcc[2] = (char) ((brand >> 8) & 0xFF); - out_fourcc[3] = (char) ((brand >> 0) & 0xFF); - } -} - - -int heif_has_compatible_brand(const uint8_t* data, int len, const char* brand_fourcc) -{ - if (data == nullptr || len <= 0 || brand_fourcc == nullptr || !brand_fourcc[0] || !brand_fourcc[1] || !brand_fourcc[2] || !brand_fourcc[3]) { - return -1; - } - - auto stream = std::make_shared(data, len, false); - BitstreamRange range(stream, len); - - std::shared_ptr box; - Error err = Box::read(range, &box, heif_get_global_security_limits()); - if (err) { - if (err.sub_error_code == heif_suberror_End_of_data) { - return -1; - } - - return -2; - } - - auto ftyp = std::dynamic_pointer_cast(box); - if (!ftyp) { - return -2; - } - - return ftyp->has_compatible_brand(fourcc(brand_fourcc)) ? 1 : 0; -} - - -struct heif_error heif_list_compatible_brands(const uint8_t* data, int len, heif_brand2** out_brands, int* out_size) -{ - if (data == nullptr || out_brands == nullptr || out_size == nullptr) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument"}; - } - - if (len <= 0) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "data length must be positive"}; - } - - auto stream = std::make_shared(data, len, false); - BitstreamRange range(stream, len); - - std::shared_ptr box; - Error err = Box::read(range, &box, heif_get_global_security_limits()); - if (err) { - if (err.sub_error_code == heif_suberror_End_of_data) { - return {err.error_code, err.sub_error_code, "insufficient input data"}; - } - - return {err.error_code, err.sub_error_code, "error reading ftyp box"}; - } - - auto ftyp = std::dynamic_pointer_cast(box); - if (!ftyp) { - return {heif_error_Invalid_input, heif_suberror_No_ftyp_box, "input is not a ftyp box"}; - } - - auto brands = ftyp->list_brands(); - size_t nBrands = brands.size(); - *out_brands = (heif_brand2*) malloc(sizeof(heif_brand2) * nBrands); - *out_size = (int)nBrands; - - for (size_t i = 0; i < nBrands; i++) { - (*out_brands)[i] = brands[i]; - } - - return heif_error_success; -} - - -void heif_free_list_of_compatible_brands(heif_brand2* brands_list) -{ - if (brands_list) { - free(brands_list); - } -} - - -enum class TriBool -{ - No, Yes, Unknown -}; - -TriBool is_jpeg(const uint8_t* data, int len) -{ - if (len < 12) { - return TriBool::Unknown; - } - - if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF && data[3] == 0xE0 && - data[4] == 0x00 && data[5] == 0x10 && data[6] == 0x4A && data[7] == 0x46 && - data[8] == 0x49 && data[9] == 0x46 && data[10] == 0x00 && data[11] == 0x01) { - return TriBool::Yes; - } - if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF && data[3] == 0xE1 && - data[6] == 0x45 && data[7] == 0x78 && data[8] == 0x69 && data[9] == 0x66 && - data[10] == 0x00 && data[11] == 0x00) { - return TriBool::Yes; - } - else { - return TriBool::No; - } -} - - -TriBool is_png(const uint8_t* data, int len) -{ - if (len < 8) { - return TriBool::Unknown; - } - - if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 && - data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A) { - return TriBool::Yes; - } - else { - return TriBool::No; - } -} - - -const char* heif_get_file_mime_type(const uint8_t* data, int len) -{ - heif_brand mainBrand = heif_main_brand(data, len); - - if (mainBrand == heif_heic || - mainBrand == heif_heix || - mainBrand == heif_heim || - mainBrand == heif_heis) { - return "image/heic"; - } - else if (mainBrand == heif_mif1) { - return "image/heif"; - } - else if (mainBrand == heif_hevc || - mainBrand == heif_hevx || - mainBrand == heif_hevm || - mainBrand == heif_hevs) { - return "image/heic-sequence"; - } - else if (mainBrand == heif_msf1) { - return "image/heif-sequence"; - } - else if (mainBrand == heif_avif) { - return "image/avif"; - } - else if (mainBrand == heif_avis) { - return "image/avif-sequence"; - } -#if ENABLE_EXPERIMENTAL_MINI_FORMAT - else if (mainBrand == heif_brand2_mif3) { - heif_brand2 minorBrand = heif_read_minor_version_brand(data, len); - if (minorBrand == heif_brand2_avif) { - return "image/avif"; - } - if (minorBrand == heif_brand2_heic || - minorBrand == heif_brand2_heix || - minorBrand == heif_brand2_heim || - minorBrand == heif_brand2_heis) { - return "image/heic"; - } - // There could be other options in here, like VVC or J2K - return "image/heif"; - } -#endif - else if (mainBrand == heif_j2ki) { - return "image/hej2k"; - } - else if (mainBrand == heif_j2is) { - return "image/j2is"; - } - else if (is_jpeg(data, len) == TriBool::Yes) { - return "image/jpeg"; - } - else if (is_png(data, len) == TriBool::Yes) { - return "image/png"; - } - else { - return ""; - } -} - - -const struct heif_security_limits* heif_get_global_security_limits() -{ - return &global_security_limits; -} - - -const struct heif_security_limits* heif_get_disabled_security_limits() -{ - return &disabled_security_limits; -} - - -struct heif_security_limits* heif_context_get_security_limits(const struct heif_context* ctx) -{ - if (!ctx) { - return nullptr; - } - - return ctx->context->get_security_limits(); -} - - -struct heif_error heif_context_set_security_limits(struct heif_context* ctx, const struct heif_security_limits* limits) -{ - if (ctx==nullptr || limits==nullptr) { - return {heif_error_Usage_error, - heif_suberror_Null_pointer_argument}; - } - - ctx->context->set_security_limits(limits); - - return heif_error_ok; -} - - - -heif_context* heif_context_alloc() -{ - load_plugins_if_not_initialized_yet(); - - struct heif_context* ctx = new heif_context; - ctx->context = std::make_shared(); - - return ctx; -} - -void heif_context_free(heif_context* ctx) -{ - delete ctx; -} - -heif_error heif_context_read_from_file(heif_context* ctx, const char* filename, - const struct heif_reading_options*) -{ - Error err = ctx->context->read_from_file(filename); - return err.error_struct(ctx->context.get()); -} - -heif_error heif_context_read_from_memory(heif_context* ctx, const void* mem, size_t size, - const struct heif_reading_options*) -{ - Error err = ctx->context->read_from_memory(mem, size, true); - return err.error_struct(ctx->context.get()); -} - -heif_error heif_context_read_from_memory_without_copy(heif_context* ctx, const void* mem, size_t size, - const struct heif_reading_options*) -{ - Error err = ctx->context->read_from_memory(mem, size, false); - return err.error_struct(ctx->context.get()); -} - -heif_error heif_context_read_from_reader(struct heif_context* ctx, - const struct heif_reader* reader_func_table, - void* userdata, - const struct heif_reading_options*) -{ - auto reader = std::make_shared(reader_func_table, userdata); - - Error err = ctx->context->read(reader); - return err.error_struct(ctx->context.get()); -} - -// TODO: heif_error heif_context_read_from_file_descriptor(heif_context*, int fd); - -void heif_context_debug_dump_boxes_to_file(struct heif_context* ctx, int fd) -{ - if (!ctx) { - return; - } - - std::string dump = ctx->context->debug_dump_boxes(); - // TODO(fancycode): Should we return an error if writing fails? -#ifdef _WIN32 - auto written = _write(fd, dump.c_str(), static_cast(dump.size())); -#else - auto written = write(fd, dump.c_str(), dump.size()); -#endif - (void) written; -} - -heif_error heif_context_get_primary_image_handle(heif_context* ctx, heif_image_handle** img) -{ - if (!img) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(ctx->context.get()); - } - - std::shared_ptr primary_image = ctx->context->get_primary_image(true); - - // It is a requirement of an HEIF file there is always a primary image. - // If there is none, an error is generated when loading the file. - if (!primary_image) { - Error err(heif_error_Invalid_input, - heif_suberror_No_or_invalid_primary_item); - return err.error_struct(ctx->context.get()); - } - - if (auto errImage = std::dynamic_pointer_cast(primary_image)) { - Error error = errImage->get_item_error(); - return error.error_struct(ctx->context.get()); - } - - *img = new heif_image_handle(); - (*img)->image = std::move(primary_image); - (*img)->context = ctx->context; - - return Error::Ok.error_struct(ctx->context.get()); -} - - -struct heif_error heif_context_get_primary_image_ID(struct heif_context* ctx, heif_item_id* id) -{ - if (!id) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); - } - - std::shared_ptr primary = ctx->context->get_primary_image(true); - if (!primary) { - return Error(heif_error_Invalid_input, - heif_suberror_No_or_invalid_primary_item).error_struct(ctx->context.get()); - } - - *id = primary->get_id(); - - return Error::Ok.error_struct(ctx->context.get()); -} - - -int heif_context_is_top_level_image_ID(struct heif_context* ctx, heif_item_id id) -{ - const std::vector> images = ctx->context->get_top_level_images(true); - - for (const auto& img : images) { - if (img->get_id() == id) { - return true; - } - } - - return false; -} - - -int heif_context_get_number_of_top_level_images(heif_context* ctx) -{ - return (int) ctx->context->get_top_level_images(true).size(); -} - - -int heif_context_get_list_of_top_level_image_IDs(struct heif_context* ctx, - heif_item_id* ID_array, - int count) -{ - if (ID_array == nullptr || count == 0 || ctx == nullptr) { - return 0; - } - - - // fill in ID values into output array - - const std::vector> imgs = ctx->context->get_top_level_images(true); - int n = (int) std::min(count, (int) imgs.size()); - for (int i = 0; i < n; i++) { - ID_array[i] = imgs[i]->get_id(); - } - - return n; -} - - -struct heif_error heif_context_get_image_handle(struct heif_context* ctx, - heif_item_id id, - struct heif_image_handle** imgHdl) -{ - if (!imgHdl) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, ""}; - } - - auto image = ctx->context->get_image(id, true); - - if (auto errImage = std::dynamic_pointer_cast(image)) { - Error error = errImage->get_item_error(); - return error.error_struct(ctx->context.get()); - } - - if (!image) { - *imgHdl = nullptr; - - return {heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced, ""}; - } - - *imgHdl = new heif_image_handle(); - (*imgHdl)->image = std::move(image); - (*imgHdl)->context = ctx->context; - - return heif_error_success; -} - - -int heif_image_handle_is_primary_image(const struct heif_image_handle* handle) -{ - return handle->image->is_primary(); -} - - -heif_item_id heif_image_handle_get_item_id(const struct heif_image_handle* handle) -{ - return handle->image->get_id(); -} - - -int heif_image_handle_get_number_of_thumbnails(const struct heif_image_handle* handle) -{ - return (int) handle->image->get_thumbnails().size(); -} - - -int heif_image_handle_get_list_of_thumbnail_IDs(const struct heif_image_handle* handle, - heif_item_id* ids, int count) -{ - if (ids == nullptr) { - return 0; - } - - auto thumbnails = handle->image->get_thumbnails(); - int n = (int) std::min(count, (int) thumbnails.size()); - - for (int i = 0; i < n; i++) { - ids[i] = thumbnails[i]->get_id(); - } - - return n; -} - - -heif_error heif_image_handle_get_thumbnail(const struct heif_image_handle* handle, - heif_item_id thumbnail_id, - struct heif_image_handle** out_thumbnail_handle) -{ - if (!out_thumbnail_handle) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(handle->image.get()); - } - - auto thumbnails = handle->image->get_thumbnails(); - for (const auto& thumb : thumbnails) { - if (thumb->get_id() == thumbnail_id) { - *out_thumbnail_handle = new heif_image_handle(); - (*out_thumbnail_handle)->image = thumb; - (*out_thumbnail_handle)->context = handle->context; - - return Error::Ok.error_struct(handle->image.get()); - } - } - - Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); - return err.error_struct(handle->image.get()); -} - - -int heif_image_handle_get_number_of_auxiliary_images(const struct heif_image_handle* handle, - int include_alpha_image) -{ - return (int) handle->image->get_aux_images(include_alpha_image).size(); -} - - -int heif_image_handle_get_list_of_auxiliary_image_IDs(const struct heif_image_handle* handle, - int include_alpha_image, - heif_item_id* ids, int count) -{ - if (ids == nullptr) { - return 0; - } - - auto auxImages = handle->image->get_aux_images(include_alpha_image); - int n = (int) std::min(count, (int) auxImages.size()); - - for (int i = 0; i < n; i++) { - ids[i] = auxImages[i]->get_id(); - } - - return n; -} - - -struct heif_error heif_image_handle_get_auxiliary_type(const struct heif_image_handle* handle, - const char** out_type) -{ - if (out_type == nullptr) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(handle->image.get()); - } - - *out_type = nullptr; - - const auto& auxType = handle->image->get_aux_type(); - - char* buf = (char*) malloc(auxType.length() + 1); - - if (buf == nullptr) { - return Error(heif_error_Memory_allocation_error, - heif_suberror_Unspecified, - "Failed to allocate memory for the type string").error_struct(handle->image.get()); - } - - strcpy(buf, auxType.c_str()); - *out_type = buf; - - return heif_error_success; -} - - -void heif_image_handle_release_auxiliary_type(const struct heif_image_handle* handle, - const char** out_type) -{ - if (out_type && *out_type) { - free((void*) *out_type); - *out_type = nullptr; - } -} - -// DEPRECATED (typo) -void heif_image_handle_free_auxiliary_types(const struct heif_image_handle* handle, - const char** out_type) -{ - heif_image_handle_release_auxiliary_type(handle, out_type); -} - - -struct heif_error heif_image_handle_get_auxiliary_image_handle(const struct heif_image_handle* main_image_handle, - heif_item_id auxiliary_id, - struct heif_image_handle** out_auxiliary_handle) -{ - if (!out_auxiliary_handle) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(main_image_handle->image.get()); - } - - *out_auxiliary_handle = nullptr; - - auto auxImages = main_image_handle->image->get_aux_images(); - for (const auto& aux : auxImages) { - if (aux->get_id() == auxiliary_id) { - *out_auxiliary_handle = new heif_image_handle(); - (*out_auxiliary_handle)->image = aux; - (*out_auxiliary_handle)->context = main_image_handle->context; - - return Error::Ok.error_struct(main_image_handle->image.get()); - } - } - - Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); - return err.error_struct(main_image_handle->image.get()); -} - - -int heif_image_handle_get_width(const struct heif_image_handle* handle) -{ - if (handle && handle->image) { - return handle->image->get_width(); - } - else { - return 0; - } -} - - -int heif_image_handle_get_height(const struct heif_image_handle* handle) -{ - if (handle && handle->image) { - return handle->image->get_height(); - } - else { - return 0; - } -} - - -int heif_image_handle_get_ispe_width(const struct heif_image_handle* handle) -{ - if (handle && handle->image) { - return handle->image->get_ispe_width(); - } - else { - return 0; - } -} - - -int heif_image_handle_get_ispe_height(const struct heif_image_handle* handle) -{ - if (handle && handle->image) { - return handle->image->get_ispe_height(); - } - else { - return 0; - } -} - - -struct heif_context* heif_image_handle_get_context(const struct heif_image_handle* handle) -{ - auto ctx = new heif_context(); - ctx->context = handle->context; - return ctx; -} - - -heif_error heif_image_handle_get_image_tiling(const struct heif_image_handle* handle, int process_image_transformations, struct heif_image_tiling* tiling) -{ - if (!handle || !tiling) { - return {heif_error_Usage_error, - heif_suberror_Null_pointer_argument, - "NULL passed to heif_image_handle_get_image_tiling()"}; - } - - *tiling = handle->image->get_heif_image_tiling(); - - if (process_image_transformations) { - Error error = handle->image->process_image_transformations_on_tiling(*tiling); - if (error) { - return error.error_struct(handle->context.get()); - } - } - - return heif_error_ok; -} - - -struct heif_error heif_image_handle_get_grid_image_tile_id(const struct heif_image_handle* handle, - int process_image_transformations, - uint32_t tile_x, uint32_t tile_y, - heif_item_id* tile_item_id) -{ - if (!handle || !tile_item_id) { - return { heif_error_Usage_error, - heif_suberror_Null_pointer_argument }; - } - - std::shared_ptr gridItem = std::dynamic_pointer_cast(handle->image); - if (!gridItem) { - return { heif_error_Usage_error, - heif_suberror_Unspecified, - "Image is no grid image" }; - } - - const ImageGrid& gridspec = gridItem->get_grid_spec(); - if (tile_x >= gridspec.get_columns() || tile_y >= gridspec.get_rows()) { - return { heif_error_Usage_error, - heif_suberror_Unspecified, - "Grid tile index out of range" }; - } - - if (process_image_transformations) { - gridItem->transform_requested_tile_position_to_original_tile_position(tile_x, tile_y); - } - - *tile_item_id = gridItem->get_grid_tiles()[tile_y * gridspec.get_columns() + tile_x]; - - return heif_error_ok; -} - - -#if 0 -// TODO: do we need this ? This does not handle rotations. We can use heif_image_handle_get_image_tiling() to get the same information. -struct heif_error heif_image_handle_get_tile_size(const struct heif_image_handle* handle, - uint32_t* tile_width, uint32_t* tile_height) -{ - if (!handle) { - return error_null_parameter; - } - - - uint32_t w,h; - - handle->image->get_tile_size(w,h); - - if (tile_width) { - *tile_width = w; - } - - if (tile_height) { - *tile_height = h; - } - - return heif_error_success; -} -#endif - - -struct heif_entity_group* heif_context_get_entity_groups(const struct heif_context* ctx, - uint32_t type_filter, - heif_item_id item_filter, - int* out_num_groups) -{ - std::shared_ptr grplBox = ctx->context->get_heif_file()->get_grpl_box(); - if (!grplBox) { - *out_num_groups = 0; - return nullptr; - } - - std::vector> all_entity_group_boxes = grplBox->get_all_child_boxes(); - if (all_entity_group_boxes.empty()) { - *out_num_groups = 0; - return nullptr; - } - - // --- filter groups - - std::vector> entity_group_boxes; - for (auto& group : all_entity_group_boxes) { - if (type_filter != 0 && group->get_short_type() != type_filter) { - continue; - } - - auto groupBox = std::dynamic_pointer_cast(group); - const std::vector& items = groupBox->get_item_ids(); - - if (item_filter != 0 && std::all_of(items.begin(), items.end(), [item_filter](heif_item_id item) { - return item != item_filter; - })) { - continue; - } - - entity_group_boxes.emplace_back(groupBox); - } - - // --- convert to C structs - - auto* groups = new heif_entity_group[entity_group_boxes.size()]; - for (size_t i = 0; i < entity_group_boxes.size(); i++) { - const auto& groupBox = entity_group_boxes[i]; - const std::vector& items = groupBox->get_item_ids(); - - groups[i].entity_group_id = groupBox->get_group_id(); - groups[i].entity_group_type = groupBox->get_short_type(); - groups[i].entities = (items.empty() ? nullptr : new heif_item_id[items.size()]); - groups[i].num_entities = static_cast(items.size()); - - if (groups[i].entities) { // avoid clang static analyzer false positive - for (size_t k = 0; k < items.size(); k++) { - groups[i].entities[k] = items[k]; - } - } - } - - *out_num_groups = static_cast(entity_group_boxes.size()); - return groups; -} - - -void heif_entity_groups_release(struct heif_entity_group* grp, int num_groups) -{ - for (int i=0;i layers(num_layers); - for (size_t i = 0; i < num_layers; i++) { - layers[i] = layer_item_ids[i]; - } - - Result result = ctx->context->add_pyramid_group(layers); - - if (result) { - if (out_group_id) { - *out_group_id = result.value; - } - return heif_error_success; - } - else { - return result.error.error_struct(ctx->context.get()); - } -} - - -struct heif_pyramid_layer_info* heif_context_get_pyramid_entity_group_info(struct heif_context* ctx, heif_entity_group_id id, int* out_num_layers) -{ - if (!out_num_layers) { - return nullptr; - } - - std::shared_ptr groupBox = ctx->context->get_heif_file()->get_entity_group(id); - if (!groupBox) { - return nullptr; - } - - const auto pymdBox = std::dynamic_pointer_cast(groupBox); - if (!pymdBox) { - return nullptr; - } - - const std::vector pymd_layers = pymdBox->get_layers(); - if (pymd_layers.empty()) { - return nullptr; - } - - auto items = pymdBox->get_item_ids(); - assert(items.size() == pymd_layers.size()); - - auto* layerInfo = new heif_pyramid_layer_info[pymd_layers.size()]; - for (size_t i=0; i(pymd_layers.size()); - - return layerInfo; -} - - -void heif_pyramid_layer_info_release(struct heif_pyramid_layer_info* infos) -{ - delete[] infos; -} - - -struct heif_error heif_image_handle_get_preferred_decoding_colorspace(const struct heif_image_handle* image_handle, - enum heif_colorspace* out_colorspace, - enum heif_chroma* out_chroma) -{ - Error err = image_handle->image->get_coded_image_colorspace(out_colorspace, out_chroma); - if (err) { - return err.error_struct(image_handle->image.get()); - } - - return heif_error_success; -} - - -int heif_image_handle_has_alpha_channel(const struct heif_image_handle* handle) -{ - // TODO: for now, also scan the grid tiles for alpha information (issue #708), but depending about - // how the discussion about this structure goes forward, we might remove this again. - - return handle->context->has_alpha(handle->image->get_id()); // handle case in issue #708 - //return handle->image->get_alpha_channel() != nullptr; // old alpha check that fails on alpha in grid tiles -} - - -int heif_image_handle_is_premultiplied_alpha(const struct heif_image_handle* handle) -{ - // TODO: what about images that have the alpha in the grid tiles (issue #708) ? - return handle->image->is_premultiplied_alpha(); -} - - -int heif_image_handle_get_luma_bits_per_pixel(const struct heif_image_handle* handle) -{ - return handle->image->get_luma_bits_per_pixel(); -} - - -int heif_image_handle_get_chroma_bits_per_pixel(const struct heif_image_handle* handle) -{ - return handle->image->get_chroma_bits_per_pixel(); -} - - -int heif_image_handle_has_depth_image(const struct heif_image_handle* handle) -{ - return handle->image->get_depth_channel() != nullptr; -} - -void heif_depth_representation_info_free(const struct heif_depth_representation_info* info) -{ - delete info; -} - -int heif_image_handle_get_depth_image_representation_info(const struct heif_image_handle* handle, - heif_item_id depth_image_id, - const struct heif_depth_representation_info** out) -{ - std::shared_ptr depth_image; - - if (out) { - if (handle->image->is_depth_channel()) { - // Because of an API bug before v1.11.0, the input handle may be the depth image (#422). - depth_image = handle->image; - } - else { - depth_image = handle->image->get_depth_channel(); - } - - if (depth_image->has_depth_representation_info()) { - auto info = new heif_depth_representation_info; - *info = depth_image->get_depth_representation_info(); - *out = info; - return true; - } - else { - *out = nullptr; - } - } - - return false; -} - - -int heif_image_handle_get_number_of_depth_images(const struct heif_image_handle* handle) -{ - auto depth_image = handle->image->get_depth_channel(); - - if (depth_image) { - return 1; - } - else { - return 0; - } -} - - -int heif_image_handle_get_list_of_depth_image_IDs(const struct heif_image_handle* handle, - heif_item_id* ids, int count) -{ - auto depth_image = handle->image->get_depth_channel(); - - if (count == 0) { - return 0; - } - - if (depth_image) { - ids[0] = depth_image->get_id(); - return 1; - } - else { - return 0; - } -} - - -struct heif_error heif_image_handle_get_depth_image_handle(const struct heif_image_handle* handle, - heif_item_id depth_id, - struct heif_image_handle** out_depth_handle) -{ - if (out_depth_handle == nullptr) { - return {heif_error_Usage_error, - heif_suberror_Null_pointer_argument, - "NULL out_depth_handle passed to heif_image_handle_get_depth_image_handle()"}; - } - - auto depth_image = handle->image->get_depth_channel(); - - if (depth_image->get_id() != depth_id) { - *out_depth_handle = nullptr; - - Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); - return err.error_struct(handle->image.get()); - } - - *out_depth_handle = new heif_image_handle(); - (*out_depth_handle)->image = depth_image; - (*out_depth_handle)->context = handle->context; - - return Error::Ok.error_struct(handle->image.get()); -} - - -void fill_default_decoding_options(heif_decoding_options& options) -{ - options.version = 6; - - options.ignore_transformations = false; - - options.start_progress = nullptr; - options.on_progress = nullptr; - options.end_progress = nullptr; - options.progress_user_data = nullptr; - - // version 2 - - options.convert_hdr_to_8bit = false; - - // version 3 - - options.strict_decoding = false; - - // version 4 - - options.decoder_id = nullptr; - - // version 5 - - options.color_conversion_options.version = 1; - options.color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; - options.color_conversion_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; - options.color_conversion_options.only_use_preferred_chroma_algorithm = false; - - // version 6 - - options.cancel_decoding = nullptr; -} - - -// overwrite the (possibly lower version) input options over the default options -static heif_decoding_options normalize_options(const heif_decoding_options* input_options) -{ - heif_decoding_options options{}; - fill_default_decoding_options(options); - - if (input_options) { - switch (input_options->version) { - case 6: - options.cancel_decoding = input_options->cancel_decoding; - // fallthrough - case 5: - options.color_conversion_options = input_options->color_conversion_options; - // fallthrough - case 4: - options.decoder_id = input_options->decoder_id; - // fallthrough - case 3: - options.strict_decoding = input_options->strict_decoding; - // fallthrough - case 2: - options.convert_hdr_to_8bit = input_options->convert_hdr_to_8bit; - // fallthrough - case 1: - options.ignore_transformations = input_options->ignore_transformations; - - options.start_progress = input_options->start_progress; - options.on_progress = input_options->on_progress; - options.end_progress = input_options->end_progress; - options.progress_user_data = input_options->progress_user_data; - } - } - - return options; -} - - -void heif_color_conversion_options_set_defaults(struct heif_color_conversion_options* options) -{ - options->version = 1; -#if HAVE_LIBSHARPYUV - options->preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_sharp_yuv; -#else - options->preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; -#endif - - options->preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; - options->only_use_preferred_chroma_algorithm = true; -} - - -heif_decoding_options* heif_decoding_options_alloc() -{ - auto options = new heif_decoding_options; - - fill_default_decoding_options(*options); - - return options; -} - - -void heif_decoding_options_free(heif_decoding_options* options) -{ - delete options; -} - - -struct heif_error heif_decode_image(const struct heif_image_handle* in_handle, - struct heif_image** out_img, - heif_colorspace colorspace, - heif_chroma chroma, - const struct heif_decoding_options* input_options) -{ - if (out_img == nullptr) { - return {heif_error_Usage_error, - heif_suberror_Null_pointer_argument, - "NULL out_img passed to heif_decode_image()"}; - } - - *out_img = nullptr; - - heif_item_id id = in_handle->image->get_id(); - - heif_decoding_options dec_options = normalize_options(input_options); - - Result> decodingResult = in_handle->context->decode_image(id, - colorspace, - chroma, - dec_options, - false, 0,0); - if (decodingResult.error.error_code != heif_error_Ok) { - return decodingResult.error.error_struct(in_handle->image.get()); - } - - std::shared_ptr img = decodingResult.value; - - *out_img = new heif_image(); - (*out_img)->image = std::move(img); - - return Error::Ok.error_struct(in_handle->image.get()); -} - - -struct heif_error heif_image_handle_decode_image_tile(const struct heif_image_handle* in_handle, - struct heif_image** out_img, - enum heif_colorspace colorspace, - enum heif_chroma chroma, - const struct heif_decoding_options* input_options, - uint32_t x0, uint32_t y0) -{ - if (!in_handle) { - return error_null_parameter; - } - - heif_item_id id = in_handle->image->get_id(); - - heif_decoding_options dec_options = normalize_options(input_options); - - Result> decodingResult = in_handle->context->decode_image(id, - colorspace, - chroma, - dec_options, - true, x0,y0); - if (decodingResult.error.error_code != heif_error_Ok) { - return decodingResult.error.error_struct(in_handle->image.get()); - } - - std::shared_ptr img = decodingResult.value; - - *out_img = new heif_image(); - (*out_img)->image = std::move(img); - - return Error::Ok.error_struct(in_handle->image.get()); -} - - -struct heif_error heif_image_create(int width, int height, - heif_colorspace colorspace, - heif_chroma chroma, - struct heif_image** image) -{ - if (image == nullptr) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "heif_image_create: NULL passed as image pointer."}; - } - - // auto-correct colorspace_YCbCr + chroma_monochrome to colorspace_monochrome - // TODO: this should return an error in a later version (see below) - if (chroma == heif_chroma_monochrome && colorspace == heif_colorspace_YCbCr) { - colorspace = heif_colorspace_monochrome; - - std::cerr << "libheif warning: heif_image_create() used with an illegal colorspace/chroma combination. This will return an error in a future version.\n"; - } - - // return error if invalid colorspace + chroma combination is used - auto validChroma = get_valid_chroma_values_for_colorspace(colorspace); - if (std::find(validChroma.begin(), validChroma.end(), chroma) == validChroma.end()) { - *image = nullptr; - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "Invalid colorspace/chroma combination."}; - } - - struct heif_image* img = new heif_image; - img->image = std::make_shared(); - - img->image->create(width, height, colorspace, chroma); - - *image = img; - - return heif_error_success; -} - -int heif_image_get_decoding_warnings(struct heif_image* image, - int first_warning_idx, - struct heif_error* out_warnings, - int max_output_buffer_entries) -{ - if (max_output_buffer_entries == 0) { - return (int) image->image->get_warnings().size(); - } - else { - const auto& warnings = image->image->get_warnings(); - int n; - for (n = 0; n + first_warning_idx < (int) warnings.size(); n++) { - out_warnings[n] = warnings[n + first_warning_idx].error_struct(image->image.get()); - } - return n; - } -} - -void heif_image_add_decoding_warning(struct heif_image* image, - struct heif_error err) -{ - image->image->add_warning(Error(err.code, err.subcode)); -} - - -int heif_image_has_content_light_level(const struct heif_image* image) -{ - return image->image->has_clli(); -} - -void heif_image_get_content_light_level(const struct heif_image* image, struct heif_content_light_level* out) -{ - if (out) { - *out = image->image->get_clli(); - } -} - -int heif_image_handle_get_content_light_level(const struct heif_image_handle* handle, struct heif_content_light_level* out) -{ - auto clli = handle->image->get_property(); - if (out && clli) { - *out = clli->clli; - } - - return clli ? 1 : 0; -} - -void heif_image_set_content_light_level(const struct heif_image* image, const struct heif_content_light_level* in) -{ - if (in == nullptr) { - return; - } - - image->image->set_clli(*in); -} - - -int heif_image_has_mastering_display_colour_volume(const struct heif_image* image) -{ - return image->image->has_mdcv(); -} - -void heif_image_get_mastering_display_colour_volume(const struct heif_image* image, struct heif_mastering_display_colour_volume* out) -{ - *out = image->image->get_mdcv(); -} - -int heif_image_handle_get_mastering_display_colour_volume(const struct heif_image_handle* handle, struct heif_mastering_display_colour_volume* out) -{ - auto mdcv = handle->image->get_property(); - if (out && mdcv) { - *out = mdcv->mdcv; - } - - return mdcv ? 1 : 0; -} - -void heif_image_set_mastering_display_colour_volume(const struct heif_image* image, const struct heif_mastering_display_colour_volume* in) -{ - if (in == nullptr) { - return; - } - - image->image->set_mdcv(*in); -} - -float mdcv_coord_decode_x(uint16_t coord) -{ - // check for unspecified value - if (coord < 5 || coord > 37000) { - return 0.0f; - } - - return (float) (coord * 0.00002); -} - -float mdcv_coord_decode_y(uint16_t coord) -{ - // check for unspecified value - if (coord < 5 || coord > 42000) { - return 0.0f; - } - - return (float) (coord * 0.00002); -} - -struct heif_error heif_mastering_display_colour_volume_decode(const struct heif_mastering_display_colour_volume* in, - struct heif_decoded_mastering_display_colour_volume* out) -{ - if (in == nullptr || out == nullptr) { - return error_null_parameter; - } - - for (int c = 0; c < 3; c++) { - out->display_primaries_x[c] = mdcv_coord_decode_x(in->display_primaries_x[c]); - out->display_primaries_y[c] = mdcv_coord_decode_y(in->display_primaries_y[c]); - } - - out->white_point_x = mdcv_coord_decode_x(in->white_point_x); - out->white_point_y = mdcv_coord_decode_y(in->white_point_y); - - if (in->max_display_mastering_luminance < 50000 || in->max_display_mastering_luminance > 100000000) { - out->max_display_mastering_luminance = 0; - } - else { - out->max_display_mastering_luminance = in->max_display_mastering_luminance * 0.0001; - } - - if (in->min_display_mastering_luminance < 1 || in->min_display_mastering_luminance > 50000) { - out->min_display_mastering_luminance = 0; - } - else { - out->min_display_mastering_luminance = in->min_display_mastering_luminance * 0.0001; - } - - return heif_error_success; -} - - -void heif_image_get_pixel_aspect_ratio(const struct heif_image* image, uint32_t* aspect_h, uint32_t* aspect_v) -{ - image->image->get_pixel_ratio(aspect_h, aspect_v); -} - -int heif_image_handle_get_pixel_aspect_ratio(const struct heif_image_handle* handle, uint32_t* aspect_h, uint32_t* aspect_v) -{ - auto pasp = handle->image->get_property(); - if (pasp) { - *aspect_h = pasp->hSpacing; - *aspect_v = pasp->vSpacing; - return 1; - } - else { - *aspect_h = 1; - *aspect_v = 1; - return 0; - } -} - -void heif_image_set_pixel_aspect_ratio(struct heif_image* image, uint32_t aspect_h, uint32_t aspect_v) -{ - image->image->set_pixel_ratio(aspect_h, aspect_v); -} - - -void heif_image_release(const struct heif_image* img) -{ - delete img; -} - -void heif_image_handle_release(const struct heif_image_handle* handle) -{ - delete handle; -} - - -heif_colorspace heif_image_get_colorspace(const struct heif_image* img) -{ - return img->image->get_colorspace(); -} - -enum heif_chroma heif_image_get_chroma_format(const struct heif_image* img) -{ - return img->image->get_chroma_format(); -} - - -static int uint32_to_int(uint32_t v) -{ - if (v == 0 || v > static_cast(std::numeric_limits::max())) { - return -1; - } - else { - return static_cast(v); - } -} - - -int heif_image_get_width(const struct heif_image* img, enum heif_channel channel) -{ - return uint32_to_int(img->image->get_width(channel)); -} - - -int heif_image_get_height(const struct heif_image* img, enum heif_channel channel) -{ - return uint32_to_int(img->image->get_height(channel)); -} - - -int heif_image_get_primary_width(const struct heif_image* img) -{ - if (img->image->get_colorspace() == heif_colorspace_RGB) { - if (img->image->get_chroma_format() == heif_chroma_444) { - return uint32_to_int(img->image->get_width(heif_channel_G)); - } - else { - return uint32_to_int(img->image->get_width(heif_channel_interleaved)); - } - } - else { - return uint32_to_int(img->image->get_width(heif_channel_Y)); - } -} - - -int heif_image_get_primary_height(const struct heif_image* img) -{ - if (img->image->get_colorspace() == heif_colorspace_RGB) { - if (img->image->get_chroma_format() == heif_chroma_444) { - return uint32_to_int(img->image->get_height(heif_channel_G)); - } - else { - return uint32_to_int(img->image->get_height(heif_channel_interleaved)); - } - } - else { - return uint32_to_int(img->image->get_height(heif_channel_Y)); - } -} - - -heif_error heif_image_crop(struct heif_image* img, - int left, int right, int top, int bottom) -{ - uint32_t w = img->image->get_width(); - uint32_t h = img->image->get_height(); - - if (w==0 || w>0x7FFFFFFF || - h==0 || h>0x7FFFFFFF) { - return heif_error{heif_error_Usage_error, - heif_suberror_Invalid_image_size, - "Image size exceeds maximum supported size"}; - } - - auto cropResult = img->image->crop(left, static_cast(w) - 1 - right, top, static_cast(h) - 1 - bottom, nullptr); - if (cropResult.error) { - return cropResult.error.error_struct(img->image.get()); - } - - img->image = cropResult.value; - - return heif_error_success; -} - - -int heif_image_get_bits_per_pixel(const struct heif_image* img, enum heif_channel channel) -{ - return img->image->get_storage_bits_per_pixel(channel); -} - - -int heif_image_get_bits_per_pixel_range(const struct heif_image* img, enum heif_channel channel) -{ - return img->image->get_bits_per_pixel(channel); -} - - -int heif_image_has_channel(const struct heif_image* img, enum heif_channel channel) -{ - return img->image->has_channel(channel); -} - - -struct heif_error heif_image_add_plane(struct heif_image* image, - heif_channel channel, int width, int height, int bit_depth) -{ - // Note: no security limit, because this is explicitly requested by the user. - if (auto err = image->image->add_plane(channel, width, height, bit_depth, nullptr)) { - return err.error_struct(image->image.get()); - } - else { - return heif_error_success; - } -} - - -struct heif_error heif_image_add_channel(struct heif_image* image, - enum heif_channel channel, - int width, int height, - heif_channel_datatype datatype, int bit_depth) -{ - if (auto err = image->image->add_channel(channel, width, height, datatype, bit_depth, nullptr)) { - return err.error_struct(image->image.get()); - } - else { - return heif_error_success; - } -} - - -const uint8_t* heif_image_get_plane_readonly(const struct heif_image* image, - enum heif_channel channel, - int* out_stride) -{ - if (!out_stride) { - return nullptr; - } - - if (!image || !image->image) { - *out_stride = 0; - return nullptr; - } - - size_t stride; - const auto* p = image->image->get_plane(channel, &stride); - - // TODO: use C++20 std::cmp_greater() - if (stride > static_cast(std::numeric_limits::max())) { - return nullptr; - } - - *out_stride = static_cast(stride); - return p; -} - - -uint8_t* heif_image_get_plane(struct heif_image* image, - enum heif_channel channel, - int* out_stride) -{ - if (!out_stride) { - return nullptr; - } - - if (!image || !image->image) { - *out_stride = 0; - return nullptr; - } - - size_t stride; - uint8_t* p = image->image->get_plane(channel, &stride); - - // TODO: use C++20 std::cmp_greater() - if (stride > static_cast(std::numeric_limits::max())) { - return nullptr; - } - - *out_stride = static_cast(stride); - return p; -} - - -enum heif_channel_datatype heif_image_get_datatype(const struct heif_image* image, enum heif_channel channel) -{ - if (image == nullptr) { - return heif_channel_datatype_undefined; - } - - return image->image->get_datatype(channel); -} - - -int heif_image_list_channels(struct heif_image* image, - enum heif_channel** out_channels) -{ - if (!image || !out_channels) { - return 0; - } - - auto channels = image->image->get_channel_set(); - - *out_channels = new heif_channel[channels.size()]; - heif_channel* p = *out_channels; - for (heif_channel c : channels) { - *p++ = c; - } - - assert(channels.size() < static_cast(std::numeric_limits::max())); - - return static_cast(channels.size()); -} - - -void heif_channel_release_list(enum heif_channel** channels) -{ - delete[] channels; -} - - - -#define heif_image_get_channel_X(name, type, datatype, bits) \ -const type* heif_image_get_channel_ ## name ## _readonly(const struct heif_image* image, \ - enum heif_channel channel, \ - size_t* out_stride) \ -{ \ - if (!image || !image->image) { \ - *out_stride = 0; \ - return nullptr; \ - } \ - \ - if (image->image->get_datatype(channel) != datatype) { \ - return nullptr; \ - } \ - if (image->image->get_storage_bits_per_pixel(channel) != bits) { \ - return nullptr; \ - } \ - return image->image->get_channel(channel, out_stride); \ -} \ - \ -type* heif_image_get_channel_ ## name (struct heif_image* image, \ - enum heif_channel channel, \ - size_t* out_stride) \ -{ \ - if (!image || !image->image) { \ - *out_stride = 0; \ - return nullptr; \ - } \ - \ - if (image->image->get_datatype(channel) != datatype) { \ - return nullptr; \ - } \ - if (image->image->get_storage_bits_per_pixel(channel) != bits) { \ - return nullptr; \ - } \ - return image->image->get_channel(channel, out_stride); \ -} - -heif_image_get_channel_X(uint16, uint16_t, heif_channel_datatype_unsigned_integer, 16) -heif_image_get_channel_X(uint32, uint32_t, heif_channel_datatype_unsigned_integer, 32) -heif_image_get_channel_X(uint64, uint64_t, heif_channel_datatype_unsigned_integer, 64) -heif_image_get_channel_X(int16, int16_t, heif_channel_datatype_signed_integer, 16) -heif_image_get_channel_X(int32, int32_t, heif_channel_datatype_signed_integer, 32) -heif_image_get_channel_X(int64, int64_t, heif_channel_datatype_signed_integer, 64) -heif_image_get_channel_X(float32, float, heif_channel_datatype_floating_point, 32) -heif_image_get_channel_X(float64, double, heif_channel_datatype_floating_point, 64) -heif_image_get_channel_X(complex32, heif_complex32, heif_channel_datatype_complex_number, 64) -heif_image_get_channel_X(complex64, heif_complex64, heif_channel_datatype_complex_number, 64) - - -void heif_image_set_premultiplied_alpha(struct heif_image* image, - int is_premultiplied_alpha) -{ - if (image == nullptr) { - return; - } - - image->image->set_premultiplied_alpha(is_premultiplied_alpha); -} - - -int heif_image_is_premultiplied_alpha(struct heif_image* image) -{ - if (image == nullptr) { - return 0; - } - - return image->image->is_premultiplied_alpha(); -} - - -struct heif_error heif_image_extend_padding_to_size(struct heif_image* image, int min_physical_width, int min_physical_height) -{ - Error err = image->image->extend_padding_to_size(min_physical_width, min_physical_height, false, nullptr); - if (err) { - return err.error_struct(image->image.get()); - } - else { - return heif_error_success; - } -} - - -struct heif_error heif_image_scale_image(const struct heif_image* input, - struct heif_image** output, - int width, int height, - const struct heif_scaling_options* options) -{ - std::shared_ptr out_img; - - Error err = input->image->scale_nearest_neighbor(out_img, width, height, nullptr); - if (err) { - return err.error_struct(input->image.get()); - } - - *output = new heif_image; - (*output)->image = std::move(out_img); - - return Error::Ok.error_struct(input->image.get()); -} - - -struct heif_error heif_image_extend_to_size_fill_with_zero(struct heif_image* image, - uint32_t width, uint32_t height) -{ - Error err = image->image->extend_to_size_with_zero(width, height, nullptr); - if (err) { - return err.error_struct(image->image.get()); - } - - return heif_error_ok; -} - - -struct heif_error heif_image_set_raw_color_profile(struct heif_image* image, - const char* color_profile_type_fourcc, - const void* profile_data, - const size_t profile_size) -{ - if (strlen(color_profile_type_fourcc) != 4) { - heif_error err = {heif_error_Usage_error, - heif_suberror_Unspecified, - "Invalid color_profile_type (must be 4 characters)"}; - return err; - } - - uint32_t color_profile_type = fourcc(color_profile_type_fourcc); - - std::vector data; - data.insert(data.end(), - (const uint8_t*) profile_data, - (const uint8_t*) profile_data + profile_size); - - auto color_profile = std::make_shared(color_profile_type, data); - - image->image->set_color_profile_icc(color_profile); - - return heif_error_success; -} - - -struct heif_error heif_image_set_nclx_color_profile(struct heif_image* image, - const struct heif_color_profile_nclx* color_profile) -{ - auto nclx = std::make_shared(); - - nclx->set_colour_primaries(color_profile->color_primaries); - nclx->set_transfer_characteristics(color_profile->transfer_characteristics); - nclx->set_matrix_coefficients(color_profile->matrix_coefficients); - nclx->set_full_range_flag(color_profile->full_range_flag); - - image->image->set_color_profile_nclx(nclx); - - return heif_error_success; -} - - -/* -void heif_image_remove_color_profile(struct heif_image* image) -{ - image->image->set_color_profile(nullptr); -} -*/ - - -int heif_image_handle_get_number_of_metadata_blocks(const struct heif_image_handle* handle, - const char* type_filter) -{ - int cnt = 0; - for (const auto& metadata : handle->image->get_metadata()) { - if (type_filter == nullptr || - metadata->item_type == type_filter) { - cnt++; - } - } - - return cnt; -} - - -int heif_image_handle_get_list_of_metadata_block_IDs(const struct heif_image_handle* handle, - const char* type_filter, - heif_item_id* ids, int count) -{ - int cnt = 0; - for (const auto& metadata : handle->image->get_metadata()) { - if (type_filter == nullptr || - metadata->item_type == type_filter) { - if (cnt < count) { - ids[cnt] = metadata->item_id; - cnt++; - } - else { - break; - } - } - } - - return cnt; -} - - -const char* heif_image_handle_get_metadata_type(const struct heif_image_handle* handle, - heif_item_id metadata_id) -{ - for (auto& metadata : handle->image->get_metadata()) { - if (metadata->item_id == metadata_id) { - return metadata->item_type.c_str(); - } - } - - return nullptr; -} - - -const char* heif_image_handle_get_metadata_content_type(const struct heif_image_handle* handle, - heif_item_id metadata_id) -{ - for (auto& metadata : handle->image->get_metadata()) { - if (metadata->item_id == metadata_id) { - return metadata->content_type.c_str(); - } - } - - return nullptr; -} - - -const char* heif_image_handle_get_metadata_item_uri_type(const struct heif_image_handle* handle, - heif_item_id metadata_id) -{ - for (auto& metadata : handle->image->get_metadata()) { - if (metadata->item_id == metadata_id) { - return metadata->item_uri_type.c_str(); - } - } - - return nullptr; -} - - -size_t heif_image_handle_get_metadata_size(const struct heif_image_handle* handle, - heif_item_id metadata_id) -{ - for (auto& metadata : handle->image->get_metadata()) { - if (metadata->item_id == metadata_id) { - return metadata->m_data.size(); - } - } - - return 0; -} - - -struct heif_error heif_image_handle_get_metadata(const struct heif_image_handle* handle, - heif_item_id metadata_id, - void* out_data) -{ - for (auto& metadata : handle->image->get_metadata()) { - if (metadata->item_id == metadata_id) { - - if (!metadata->m_data.empty()) { - if (out_data == nullptr) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(handle->image.get()); - } - - memcpy(out_data, - metadata->m_data.data(), - metadata->m_data.size()); - } - - return Error::Ok.error_struct(handle->image.get()); - } - } - - Error err(heif_error_Usage_error, - heif_suberror_Nonexisting_item_referenced); - return err.error_struct(handle->image.get()); -} - -heif_color_profile_type heif_image_handle_get_color_profile_type(const struct heif_image_handle* handle) -{ - auto profile_icc = handle->image->get_color_profile_icc(); - if (profile_icc) { - return (heif_color_profile_type) profile_icc->get_type(); - } - - auto profile_nclx = handle->image->get_color_profile_nclx(); - if (profile_nclx) { - return (heif_color_profile_type) profile_nclx->get_type(); - } - else { - return heif_color_profile_type_not_present; - } -} - -size_t heif_image_handle_get_raw_color_profile_size(const struct heif_image_handle* handle) -{ - auto profile_icc = handle->image->get_color_profile_icc(); - if (profile_icc) { - return profile_icc->get_data().size(); - } - else { - return 0; - } -} - - -static const std::set::type> known_color_primaries{ - heif_color_primaries_ITU_R_BT_709_5, - heif_color_primaries_unspecified, - heif_color_primaries_ITU_R_BT_470_6_System_M, - heif_color_primaries_ITU_R_BT_470_6_System_B_G, - heif_color_primaries_ITU_R_BT_601_6, - heif_color_primaries_SMPTE_240M, - heif_color_primaries_generic_film, - heif_color_primaries_ITU_R_BT_2020_2_and_2100_0, - heif_color_primaries_SMPTE_ST_428_1, - heif_color_primaries_SMPTE_RP_431_2, - heif_color_primaries_SMPTE_EG_432_1, - heif_color_primaries_EBU_Tech_3213_E, -}; - -struct heif_error heif_nclx_color_profile_set_color_primaries(heif_color_profile_nclx* nclx, uint16_t cp) -{ - if (static_cast::type>(cp) > std::numeric_limits::type>::max()) { - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_color_primaries).error_struct(nullptr); - } - - auto n = static_cast::type>(cp); - if (known_color_primaries.find(n) != known_color_primaries.end()) { - nclx->color_primaries = static_cast(n); - } - else { - nclx->color_primaries = heif_color_primaries_unspecified; - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_color_primaries).error_struct(nullptr); - } - - return Error::Ok.error_struct(nullptr); -} - - -static const std::set::type> known_transfer_characteristics{ - heif_transfer_characteristic_ITU_R_BT_709_5, - heif_transfer_characteristic_unspecified, - heif_transfer_characteristic_ITU_R_BT_470_6_System_M, - heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G, - heif_transfer_characteristic_ITU_R_BT_601_6, - heif_transfer_characteristic_SMPTE_240M, - heif_transfer_characteristic_linear, - heif_transfer_characteristic_logarithmic_100, - heif_transfer_characteristic_logarithmic_100_sqrt10, - heif_transfer_characteristic_IEC_61966_2_4, - heif_transfer_characteristic_ITU_R_BT_1361, - heif_transfer_characteristic_IEC_61966_2_1, - heif_transfer_characteristic_ITU_R_BT_2020_2_10bit, - heif_transfer_characteristic_ITU_R_BT_2020_2_12bit, - heif_transfer_characteristic_ITU_R_BT_2100_0_PQ, - heif_transfer_characteristic_SMPTE_ST_428_1, - heif_transfer_characteristic_ITU_R_BT_2100_0_HLG -}; - - -struct heif_error heif_nclx_color_profile_set_transfer_characteristics(struct heif_color_profile_nclx* nclx, uint16_t tc) -{ - if (static_cast::type>(tc) > std::numeric_limits::type>::max()) { - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_transfer_characteristics).error_struct(nullptr); - } - - auto n = static_cast::type>(tc); - if (known_transfer_characteristics.find(n) != known_transfer_characteristics.end()) { - nclx->transfer_characteristics = static_cast(n); - } - else { - nclx->transfer_characteristics = heif_transfer_characteristic_unspecified; - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_transfer_characteristics).error_struct(nullptr); - } - - return Error::Ok.error_struct(nullptr); -} - - -static const std::set::type> known_matrix_coefficients{ - heif_matrix_coefficients_RGB_GBR, - heif_matrix_coefficients_ITU_R_BT_709_5, - heif_matrix_coefficients_unspecified, - heif_matrix_coefficients_US_FCC_T47, - heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G, - heif_matrix_coefficients_ITU_R_BT_601_6, - heif_matrix_coefficients_SMPTE_240M, - heif_matrix_coefficients_YCgCo, - heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance, - heif_matrix_coefficients_ITU_R_BT_2020_2_constant_luminance, - heif_matrix_coefficients_SMPTE_ST_2085, - heif_matrix_coefficients_chromaticity_derived_non_constant_luminance, - heif_matrix_coefficients_chromaticity_derived_constant_luminance, - heif_matrix_coefficients_ICtCp -}; - -struct heif_error heif_nclx_color_profile_set_matrix_coefficients(struct heif_color_profile_nclx* nclx, uint16_t mc) -{ - if (static_cast::type>(mc) > std::numeric_limits::type>::max()) { - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_matrix_coefficients).error_struct(nullptr); - } - - auto n = static_cast::type>(mc); - if (known_matrix_coefficients.find(n) != known_matrix_coefficients.end()) { - nclx->matrix_coefficients = static_cast(n);; - } - else { - nclx->matrix_coefficients = heif_matrix_coefficients_unspecified; - return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_matrix_coefficients).error_struct(nullptr); - } - - return Error::Ok.error_struct(nullptr); -} - - -struct heif_error heif_image_handle_get_nclx_color_profile(const struct heif_image_handle* handle, - struct heif_color_profile_nclx** out_data) -{ - if (!out_data) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(handle->image.get()); - } - - auto nclx_profile = handle->image->get_color_profile_nclx(); - if (!nclx_profile) { - Error err(heif_error_Color_profile_does_not_exist, - heif_suberror_Unspecified); - return err.error_struct(handle->image.get()); - } - - Error err = nclx_profile->get_nclx_color_profile(out_data); - - return err.error_struct(handle->image.get()); -} - - -struct heif_error heif_image_handle_get_raw_color_profile(const struct heif_image_handle* handle, - void* out_data) -{ - if (out_data == nullptr) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(handle->image.get()); - } - - auto raw_profile = handle->image->get_color_profile_icc(); - if (raw_profile) { - memcpy(out_data, - raw_profile->get_data().data(), - raw_profile->get_data().size()); - } - else { - Error err(heif_error_Color_profile_does_not_exist, - heif_suberror_Unspecified); - return err.error_struct(handle->image.get()); - } - - return Error::Ok.error_struct(handle->image.get()); -} - - -enum heif_color_profile_type heif_image_get_color_profile_type(const struct heif_image* image) -{ - std::shared_ptr profile; - - profile = image->image->get_color_profile_icc(); - if (!profile) { - profile = image->image->get_color_profile_nclx(); - } - - if (!profile) { - return heif_color_profile_type_not_present; - } - else { - return (heif_color_profile_type) profile->get_type(); - } -} - - -size_t heif_image_get_raw_color_profile_size(const struct heif_image* image) -{ - auto raw_profile = image->image->get_color_profile_icc(); - if (raw_profile) { - return raw_profile->get_data().size(); - } - else { - return 0; - } -} - - -struct heif_error heif_image_get_raw_color_profile(const struct heif_image* image, - void* out_data) -{ - if (out_data == nullptr) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(image->image.get()); - } - - auto raw_profile = image->image->get_color_profile_icc(); - if (raw_profile) { - memcpy(out_data, - raw_profile->get_data().data(), - raw_profile->get_data().size()); - } - else { - Error err(heif_error_Color_profile_does_not_exist, - heif_suberror_Unspecified); - return err.error_struct(image->image.get()); - } - - return Error::Ok.error_struct(image->image.get()); -} - - -struct heif_error heif_image_get_nclx_color_profile(const struct heif_image* image, - struct heif_color_profile_nclx** out_data) -{ - if (!out_data) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(image->image.get()); - } - - auto nclx_profile = image->image->get_color_profile_nclx(); - - if (!nclx_profile) { - Error err(heif_error_Color_profile_does_not_exist, - heif_suberror_Unspecified); - return err.error_struct(image->image.get()); - } - - Error err = nclx_profile->get_nclx_color_profile(out_data); - - return err.error_struct(image->image.get()); -} - - -struct heif_color_profile_nclx* heif_nclx_color_profile_alloc() -{ - return color_profile_nclx::alloc_nclx_color_profile(); -} - - -void heif_nclx_color_profile_free(struct heif_color_profile_nclx* nclx_profile) -{ - color_profile_nclx::free_nclx_color_profile(nclx_profile); -} - -int heif_image_handle_has_camera_intrinsic_matrix(const struct heif_image_handle* handle) -{ - if (!handle) { - return false; - } - - return handle->image->has_intrinsic_matrix(); -} - -struct heif_error heif_image_handle_get_camera_intrinsic_matrix(const struct heif_image_handle* handle, - struct heif_camera_intrinsic_matrix* out_matrix) -{ - if (handle == nullptr || out_matrix == nullptr) { - return heif_error{heif_error_Usage_error, - heif_suberror_Null_pointer_argument}; - } - - if (!handle->image->has_intrinsic_matrix()) { - Error err(heif_error_Usage_error, - heif_suberror_Camera_intrinsic_matrix_undefined); - return err.error_struct(handle->image.get()); - } - - const auto& m = handle->image->get_intrinsic_matrix(); - out_matrix->focal_length_x = m.focal_length_x; - out_matrix->focal_length_y = m.focal_length_y; - out_matrix->principal_point_x = m.principal_point_x; - out_matrix->principal_point_y = m.principal_point_y; - out_matrix->skew = m.skew; - - return heif_error_success; -} - -int heif_image_handle_has_camera_extrinsic_matrix(const struct heif_image_handle* handle) -{ - if (!handle) { - return false; - } - - return handle->image->has_extrinsic_matrix(); -} - -struct heif_camera_extrinsic_matrix -{ - Box_cmex::ExtrinsicMatrix matrix; -}; - -struct heif_error heif_image_handle_get_camera_extrinsic_matrix(const struct heif_image_handle* handle, - struct heif_camera_extrinsic_matrix** out_matrix) -{ - if (handle == nullptr || out_matrix == nullptr) { - return heif_error{heif_error_Usage_error, - heif_suberror_Null_pointer_argument}; - } - - if (!handle->image->has_extrinsic_matrix()) { - Error err(heif_error_Usage_error, - heif_suberror_Camera_extrinsic_matrix_undefined); - return err.error_struct(handle->image.get()); - } - - *out_matrix = new heif_camera_extrinsic_matrix; - (*out_matrix)->matrix = handle->image->get_extrinsic_matrix(); - - return heif_error_success; -} - -void heif_camera_extrinsic_matrix_release(struct heif_camera_extrinsic_matrix* matrix) -{ - delete matrix; -} - -struct heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const struct heif_camera_extrinsic_matrix* matrix, - double* out_matrix_row_major) -{ - if (matrix == nullptr || out_matrix_row_major == nullptr) { - return heif_error{heif_error_Usage_error, - heif_suberror_Null_pointer_argument}; - } - - auto m3x3 = matrix->matrix.calculate_rotation_matrix(); - - for (int i=0;i<9;i++) { - out_matrix_row_major[i] = m3x3[i]; - } - - return heif_error_success; -} - - - -// DEPRECATED -struct heif_error heif_register_decoder(heif_context* heif, const heif_decoder_plugin* decoder_plugin) -{ - return heif_register_decoder_plugin(decoder_plugin); -} - - -struct heif_error heif_register_decoder_plugin(const heif_decoder_plugin* decoder_plugin) -{ - if (!decoder_plugin) { - return error_null_parameter; - } - else if (decoder_plugin->plugin_api_version > 3) { - return error_unsupported_plugin_version; - } - - register_decoder(decoder_plugin); - return heif_error_success; -} - -struct heif_error heif_register_encoder_plugin(const heif_encoder_plugin* encoder_plugin) -{ - if (!encoder_plugin) { - return error_null_parameter; - } - else if (encoder_plugin->plugin_api_version > 3) { - return error_unsupported_plugin_version; - } - - register_encoder(encoder_plugin); - return heif_error_success; -} - - -/* -int heif_image_get_number_of_data_chunks(heif_image* img); - -void heif_image_get_data_chunk(heif_image* img, int chunk_index, - uint8_t const*const* dataptr, - int const* data_size); - -void heif_image_free_data_chunk(heif_image* img, int chunk_index); -*/ - - -/* -void heif_context_reset(struct heif_context* ctx) -{ - ctx->context->reset_to_empty_heif(); -} -*/ - -static struct heif_error heif_file_writer_write(struct heif_context* ctx, - const void* data, size_t size, void* userdata) -{ - const char* filename = static_cast(userdata); - -#if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER) - std::ofstream ostr(HeifFile::convert_utf8_path_to_utf16(filename).c_str(), std::ios_base::binary); -#else - std::ofstream ostr(filename, std::ios_base::binary); -#endif - ostr.write(static_cast(data), size); - // TODO: handle write errors - return Error::Ok.error_struct(ctx->context.get()); -} - - -struct heif_error heif_context_write_to_file(struct heif_context* ctx, - const char* filename) -{ - heif_writer writer; - writer.writer_api_version = 1; - writer.write = heif_file_writer_write; - return heif_context_write(ctx, &writer, (void*) filename); -} - - -struct heif_error heif_context_write(struct heif_context* ctx, - struct heif_writer* writer, - void* userdata) -{ - if (!writer) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); - } - else if (writer->writer_api_version != 1) { - Error err(heif_error_Usage_error, heif_suberror_Unsupported_writer_version); - return err.error_struct(ctx->context.get()); - } - - StreamWriter swriter; - ctx->context->write(swriter); - - const auto& data = swriter.get_data(); - heif_error writer_error = writer->write(ctx, data.data(), data.size(), userdata); - if (!writer_error.message) { - // It is now allowed to return a NULL error message on success. It will be replaced by "Success". An error message is still required when there is an error. - if (writer_error.code == heif_error_Ok) { - writer_error.message = Error::kSuccess; - return writer_error; - } - else { - return heif_error{heif_error_Usage_error, heif_suberror_Null_pointer_argument, "heif_writer callback returned a null error text"}; - } - } - else { - return writer_error; - } -} - - -void heif_context_add_compatible_brand(struct heif_context* ctx, - heif_brand2 compatible_brand) -{ - ctx->context->get_heif_file()->get_ftyp_box()->add_compatible_brand(compatible_brand); -} - - -int heif_context_get_encoder_descriptors(struct heif_context* ctx, - enum heif_compression_format format, - const char* name, - const struct heif_encoder_descriptor** out_encoder_descriptors, - int count) -{ - return heif_get_encoder_descriptors(format, name, out_encoder_descriptors, count); -} - - -int heif_get_encoder_descriptors(enum heif_compression_format format, - const char* name, - const struct heif_encoder_descriptor** out_encoder_descriptors, - int count) -{ - if (out_encoder_descriptors != nullptr && count <= 0) { - return 0; - } - - std::vector descriptors; - descriptors = get_filtered_encoder_descriptors(format, name); - - if (out_encoder_descriptors == nullptr) { - return static_cast(descriptors.size()); - } - - int i; - for (i = 0; i < count && static_cast(i) < descriptors.size(); i++) { - out_encoder_descriptors[i] = descriptors[i]; - } - - return i; -} - - -const char* heif_encoder_descriptor_get_name(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->get_plugin_name(); -} - - -const char* heif_encoder_descriptor_get_id_name(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->id_name; -} - - -int heif_get_decoder_descriptors(enum heif_compression_format format_filter, - const struct heif_decoder_descriptor** out_decoders, - int count) -{ - struct decoder_with_priority - { - const heif_decoder_plugin* plugin; - int priority; - }; - - std::vector plugins; - std::vector formats; - if (format_filter == heif_compression_undefined) { - formats = {heif_compression_HEVC, heif_compression_AV1, heif_compression_JPEG, heif_compression_JPEG2000, heif_compression_HTJ2K, heif_compression_VVC}; - } - else { - formats.emplace_back(format_filter); - } - - for (const auto* plugin : get_decoder_plugins()) { - for (auto& format : formats) { - int priority = plugin->does_support_format(format); - if (priority) { - plugins.push_back({plugin, priority}); - break; - } - } - } - - if (out_decoders == nullptr) { - return (int) plugins.size(); - } - - std::sort(plugins.begin(), plugins.end(), [](const decoder_with_priority& a, const decoder_with_priority& b) { - return a.priority > b.priority; - }); - - int nDecodersReturned = std::min(count, (int) plugins.size()); - - for (int i = 0; i < nDecodersReturned; i++) { - out_decoders[i] = (heif_decoder_descriptor*) (plugins[i].plugin); - } - - return nDecodersReturned; -} - - -const char* heif_decoder_descriptor_get_name(const struct heif_decoder_descriptor* descriptor) -{ - auto decoder = (heif_decoder_plugin*) descriptor; - return decoder->get_plugin_name(); -} - - -const char* heif_decoder_descriptor_get_id_name(const struct heif_decoder_descriptor* descriptor) -{ - auto decoder = (heif_decoder_plugin*) descriptor; - if (decoder->plugin_api_version < 3) { - return nullptr; - } - else { - return decoder->id_name; - } -} - - -enum heif_compression_format -heif_encoder_descriptor_get_compression_format(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->compression_format; -} - - -int heif_encoder_descriptor_supports_lossy_compression(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->supports_lossy_compression; -} - - -int heif_encoder_descriptor_supports_lossless_compression(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->supports_lossless_compression; -} - - -// DEPRECATED: typo in function name -int heif_encoder_descriptor_supportes_lossy_compression(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->supports_lossy_compression; -} - - -// DEPRECATED: typo in function name -int heif_encoder_descriptor_supportes_lossless_compression(const struct heif_encoder_descriptor* descriptor) -{ - return descriptor->plugin->supports_lossless_compression; -} - - -const char* heif_encoder_get_name(const struct heif_encoder* encoder) -{ - return encoder->plugin->get_plugin_name(); -} - - -struct heif_error heif_context_get_encoder(struct heif_context* context, - const struct heif_encoder_descriptor* descriptor, - struct heif_encoder** encoder) -{ - // Note: be aware that context may be NULL as we explicitly allowed that in an earlier documentation. - - if (!descriptor || !encoder) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(context ? context->context.get() : nullptr); - } - - *encoder = new struct heif_encoder(descriptor->plugin); - return (*encoder)->alloc(); -} - - -int heif_have_decoder_for_format(enum heif_compression_format format) -{ - auto plugin = get_decoder(format, nullptr); - return plugin != nullptr; -} - - -int heif_have_encoder_for_format(enum heif_compression_format format) -{ - auto plugin = get_encoder(format); - return plugin != nullptr; -} - - -struct heif_error heif_context_get_encoder_for_format(struct heif_context* context, - enum heif_compression_format format, - struct heif_encoder** encoder) -{ - // Note: be aware that context may be NULL as we explicitly allowed that in an earlier documentation. - - if (!encoder) { - Error err(heif_error_Usage_error, - heif_suberror_Null_pointer_argument); - return err.error_struct(context ? context->context.get() : nullptr); - } - - std::vector descriptors; - descriptors = get_filtered_encoder_descriptors(format, nullptr); - - if (descriptors.size() > 0) { - *encoder = new struct heif_encoder(descriptors[0]->plugin); - return (*encoder)->alloc(); - } - else { - *encoder = nullptr; - Error err(heif_error_Unsupported_filetype, // TODO: is this the right error code? - heif_suberror_Unspecified); - return err.error_struct(context ? context->context.get() : nullptr); - } -} - - -void heif_encoder_release(struct heif_encoder* encoder) -{ - if (encoder) { - delete encoder; - } -} - - -//struct heif_encoder_param* heif_encoder_get_param(struct heif_encoder* encoder) -//{ -// return nullptr; -//} - - -//void heif_encoder_release_param(struct heif_encoder_param* param) -//{ -//} - - -// Set a 'quality' factor (0-100). How this is mapped to actual encoding parameters is -// encoder dependent. -struct heif_error heif_encoder_set_lossy_quality(struct heif_encoder* encoder, - int quality) -{ - if (!encoder) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(nullptr); - } - - return encoder->plugin->set_parameter_quality(encoder->encoder, quality); -} - - -struct heif_error heif_encoder_set_lossless(struct heif_encoder* encoder, int enable) -{ - if (!encoder) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(nullptr); - } - - return encoder->plugin->set_parameter_lossless(encoder->encoder, enable); -} - - -struct heif_error heif_encoder_set_logging_level(struct heif_encoder* encoder, int level) -{ - if (!encoder) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(nullptr); - } - - if (encoder->plugin->set_parameter_logging_level) { - return encoder->plugin->set_parameter_logging_level(encoder->encoder, level); - } - - return heif_error_success; -} - - -const struct heif_encoder_parameter* const* heif_encoder_list_parameters(struct heif_encoder* encoder) -{ - return encoder->plugin->list_parameters(encoder->encoder); -} - - -const char* heif_encoder_parameter_get_name(const struct heif_encoder_parameter* param) -{ - return param->name; -} - -enum heif_encoder_parameter_type -heif_encoder_parameter_get_type(const struct heif_encoder_parameter* param) -{ - return param->type; -} - - -struct heif_error heif_encoder_set_parameter_integer(struct heif_encoder* encoder, - const char* parameter_name, - int value) -{ - // --- check if parameter is valid - - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - - int have_minimum = 0, have_maximum = 0, minimum = 0, maximum = 0, num_valid_values = 0; - const int* valid_values = nullptr; - heif_error err = heif_encoder_parameter_get_valid_integer_values((*params), &have_minimum, &have_maximum, - &minimum, &maximum, - &num_valid_values, - &valid_values); - if (err.code) { - return err; - } - - if ((have_minimum && value < minimum) || - (have_maximum && value > maximum)) { - return error_invalid_parameter_value; - } - - if (num_valid_values > 0) { - bool found = false; - for (int i = 0; i < num_valid_values; i++) { - if (valid_values[i] == value) { - found = true; - break; - } - } - - if (!found) { - return error_invalid_parameter_value; - } - } - } - } - - - // --- parameter is ok, pass it to the encoder plugin - - return encoder->plugin->set_parameter_integer(encoder->encoder, parameter_name, value); -} - -struct heif_error heif_encoder_get_parameter_integer(struct heif_encoder* encoder, - const char* parameter_name, - int* value_ptr) -{ - return encoder->plugin->get_parameter_integer(encoder->encoder, parameter_name, value_ptr); -} - -struct heif_error -heif_encoder_parameter_get_valid_integer_range(const struct heif_encoder_parameter* param, - int* have_minimum_maximum, - int* minimum, int* maximum) -{ - if (param->type != heif_encoder_parameter_type_integer) { - return error_unsupported_parameter; // TODO: correct error ? - } - - if (param->integer.have_minimum_maximum) { - if (minimum) { - *minimum = param->integer.minimum; - } - - if (maximum) { - *maximum = param->integer.maximum; - } - } - - if (have_minimum_maximum) { - *have_minimum_maximum = param->integer.have_minimum_maximum; - } - - return heif_error_success; -} - - -struct heif_error heif_encoder_parameter_get_valid_integer_values(const struct heif_encoder_parameter* param, - int* have_minimum, int* have_maximum, - int* minimum, int* maximum, - int* num_valid_values, - const int** out_integer_array) -{ - if (param->type != heif_encoder_parameter_type_integer) { - return error_unsupported_parameter; // TODO: correct error ? - } - - - // --- range of values - - if (param->integer.have_minimum_maximum) { - if (minimum) { - *minimum = param->integer.minimum; - } - - if (maximum) { - *maximum = param->integer.maximum; - } - } - - if (have_minimum) { - *have_minimum = param->integer.have_minimum_maximum; - } - - if (have_maximum) { - *have_maximum = param->integer.have_minimum_maximum; - } - - - // --- set of valid values - - if (param->integer.num_valid_values > 0) { - if (out_integer_array) { - *out_integer_array = param->integer.valid_values; - } - } - - if (num_valid_values) { - *num_valid_values = param->integer.num_valid_values; - } - - return heif_error_success; -} - - -struct heif_error -heif_encoder_parameter_get_valid_string_values(const struct heif_encoder_parameter* param, - const char* const** out_stringarray) -{ - if (param->type != heif_encoder_parameter_type_string) { - return error_unsupported_parameter; // TODO: correct error ? - } - - if (out_stringarray) { - *out_stringarray = param->string.valid_values; - } - - return heif_error_success; -} - -struct heif_error heif_encoder_parameter_integer_valid_range(struct heif_encoder* encoder, - const char* parameter_name, - int* have_minimum_maximum, - int* minimum, int* maximum) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - return heif_encoder_parameter_get_valid_integer_range(*params, have_minimum_maximum, - minimum, maximum); - } - } - - return error_unsupported_parameter; -} - -struct heif_error heif_encoder_set_parameter_boolean(struct heif_encoder* encoder, - const char* parameter_name, - int value) -{ - return encoder->plugin->set_parameter_boolean(encoder->encoder, parameter_name, value); -} - -struct heif_error heif_encoder_get_parameter_boolean(struct heif_encoder* encoder, - const char* parameter_name, - int* value_ptr) -{ - return encoder->plugin->get_parameter_boolean(encoder->encoder, parameter_name, value_ptr); -} - -struct heif_error heif_encoder_set_parameter_string(struct heif_encoder* encoder, - const char* parameter_name, - const char* value) -{ - return encoder->plugin->set_parameter_string(encoder->encoder, parameter_name, value); -} - -struct heif_error heif_encoder_get_parameter_string(struct heif_encoder* encoder, - const char* parameter_name, - char* value_ptr, int value_size) -{ - return encoder->plugin->get_parameter_string(encoder->encoder, parameter_name, - value_ptr, value_size); -} - -struct heif_error heif_encoder_parameter_string_valid_values(struct heif_encoder* encoder, - const char* parameter_name, - const char* const** out_stringarray) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - return heif_encoder_parameter_get_valid_string_values(*params, out_stringarray); - } - } - - return error_unsupported_parameter; -} - -struct heif_error heif_encoder_parameter_integer_valid_values(struct heif_encoder* encoder, - const char* parameter_name, - int* have_minimum, int* have_maximum, - int* minimum, int* maximum, - int* num_valid_values, - const int** out_integer_array) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - return heif_encoder_parameter_get_valid_integer_values(*params, have_minimum, have_maximum, minimum, maximum, - num_valid_values, out_integer_array); - } - } - - return error_unsupported_parameter; -} - - -static bool parse_boolean(const char* value) -{ - if (strcmp(value, "true") == 0) { - return true; - } - else if (strcmp(value, "false") == 0) { - return false; - } - else if (strcmp(value, "1") == 0) { - return true; - } - else if (strcmp(value, "0") == 0) { - return false; - } - - return false; -} - - -struct heif_error heif_encoder_set_parameter(struct heif_encoder* encoder, - const char* parameter_name, - const char* value) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - switch ((*params)->type) { - case heif_encoder_parameter_type_integer: - return heif_encoder_set_parameter_integer(encoder, parameter_name, atoi(value)); - - case heif_encoder_parameter_type_boolean: - return heif_encoder_set_parameter_boolean(encoder, parameter_name, parse_boolean(value)); - - case heif_encoder_parameter_type_string: - return heif_encoder_set_parameter_string(encoder, parameter_name, value); - break; - } - - return heif_error_success; - } - } - - return heif_encoder_set_parameter_string(encoder, parameter_name, value); - - //return error_unsupported_parameter; -} - - -struct heif_error heif_encoder_get_parameter(struct heif_encoder* encoder, - const char* parameter_name, - char* value_ptr, int value_size) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - switch ((*params)->type) { - case heif_encoder_parameter_type_integer: { - int value; - struct heif_error error = heif_encoder_get_parameter_integer(encoder, parameter_name, &value); - if (error.code) { - return error; - } - else { - snprintf(value_ptr, value_size, "%d", value); - } - } - break; - - case heif_encoder_parameter_type_boolean: { - int value; - struct heif_error error = heif_encoder_get_parameter_boolean(encoder, parameter_name, &value); - if (error.code) { - return error; - } - else { - snprintf(value_ptr, value_size, "%d", value); - } - } - break; - - case heif_encoder_parameter_type_string: { - struct heif_error error = heif_encoder_get_parameter_string(encoder, parameter_name, - value_ptr, value_size); - if (error.code) { - return error; - } - } - break; - } - - return heif_error_success; - } - } - - return error_unsupported_parameter; -} - - -int heif_encoder_has_default(struct heif_encoder* encoder, - const char* parameter_name) -{ - for (const struct heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); - *params; - params++) { - if (strcmp((*params)->name, parameter_name) == 0) { - - if ((*params)->version >= 2) { - return (*params)->has_default; - } - else { - return true; - } - } - } - - return false; -} - - -void set_default_encoding_options(heif_encoding_options& options) -{ - options.version = 7; - - options.save_alpha_channel = true; - options.macOS_compatibility_workaround = false; - options.save_two_colr_boxes_when_ICC_and_nclx_available = false; - options.output_nclx_profile = nullptr; - options.macOS_compatibility_workaround_no_nclx_profile = false; - options.image_orientation = heif_orientation_normal; - - options.color_conversion_options.version = 1; - options.color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; - options.color_conversion_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; - options.color_conversion_options.only_use_preferred_chroma_algorithm = false; - - options.prefer_uncC_short_form = true; -} - -static void copy_options(heif_encoding_options& options, const heif_encoding_options& input_options) -{ - switch (input_options.version) { - case 7: - options.prefer_uncC_short_form = input_options.prefer_uncC_short_form; - // fallthrough - case 6: - options.color_conversion_options = input_options.color_conversion_options; - // fallthrough - case 5: - options.image_orientation = input_options.image_orientation; - // fallthrough - case 4: - options.output_nclx_profile = input_options.output_nclx_profile; - options.macOS_compatibility_workaround_no_nclx_profile = input_options.macOS_compatibility_workaround_no_nclx_profile; - // fallthrough - case 3: - options.save_two_colr_boxes_when_ICC_and_nclx_available = input_options.save_two_colr_boxes_when_ICC_and_nclx_available; - // fallthrough - case 2: - options.macOS_compatibility_workaround = input_options.macOS_compatibility_workaround; - // fallthrough - case 1: - options.save_alpha_channel = input_options.save_alpha_channel; - } -} - - -heif_encoding_options* heif_encoding_options_alloc() -{ - auto options = new heif_encoding_options; - - set_default_encoding_options(*options); - - return options; -} - - -void heif_encoding_options_free(heif_encoding_options* options) -{ - delete options; -} - -struct heif_error heif_context_encode_image(struct heif_context* ctx, - const struct heif_image* input_image, - struct heif_encoder* encoder, - const struct heif_encoding_options* input_options, - struct heif_image_handle** out_image_handle) -{ - if (!encoder) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); - } - - if (out_image_handle) { - *out_image_handle = nullptr; - } - - heif_encoding_options options; - heif_color_profile_nclx nclx; - set_default_encoding_options(options); - if (input_options) { - copy_options(options, *input_options); - - if (options.output_nclx_profile == nullptr) { - auto input_nclx = input_image->image->get_color_profile_nclx(); - if (input_nclx) { - options.output_nclx_profile = &nclx; - nclx.version = 1; - nclx.color_primaries = (enum heif_color_primaries) input_nclx->get_colour_primaries(); - nclx.transfer_characteristics = (enum heif_transfer_characteristics) input_nclx->get_transfer_characteristics(); - nclx.matrix_coefficients = (enum heif_matrix_coefficients) input_nclx->get_matrix_coefficients(); - nclx.full_range_flag = input_nclx->get_full_range_flag(); - } - } - } - - auto encodingResult = ctx->context->encode_image(input_image->image, - encoder, - options, - heif_image_input_class_normal); - if (encodingResult.error != Error::Ok) { - return encodingResult.error.error_struct(ctx->context.get()); - } - - std::shared_ptr image = *encodingResult; - - // mark the new image as primary image - - if (ctx->context->is_primary_image_set() == false) { - ctx->context->set_primary_image(image); - } - - if (out_image_handle) { - *out_image_handle = new heif_image_handle; - (*out_image_handle)->image = std::move(image); - (*out_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_encode_grid(struct heif_context* ctx, - struct heif_image** tiles, - uint16_t columns, - uint16_t rows, - struct heif_encoder* encoder, - const struct heif_encoding_options* input_options, - struct heif_image_handle** out_image_handle) -{ - if (!encoder || !tiles) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); - } - else if (rows == 0 || columns == 0) { - return Error(heif_error_Usage_error, - heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); - } - - // TODO: Don't repeat this code from heif_context_encode_image() - heif_encoding_options options; - heif_color_profile_nclx nclx; - set_default_encoding_options(options); - if (input_options) { - copy_options(options, *input_options); - - if (options.output_nclx_profile == nullptr) { - auto input_nclx = tiles[0]->image->get_color_profile_nclx(); - if (input_nclx) { - options.output_nclx_profile = &nclx; - nclx.version = 1; - nclx.color_primaries = (enum heif_color_primaries) input_nclx->get_colour_primaries(); - nclx.transfer_characteristics = (enum heif_transfer_characteristics) input_nclx->get_transfer_characteristics(); - nclx.matrix_coefficients = (enum heif_matrix_coefficients) input_nclx->get_matrix_coefficients(); - nclx.full_range_flag = input_nclx->get_full_range_flag(); - } - } - } - - // Convert heif_images to a vector of HeifPixelImages - std::vector> pixel_tiles; - for (int i=0; iimage); - } - - // Encode Grid - std::shared_ptr out_grid; - auto addGridResult = ImageItem_Grid::add_and_encode_full_grid(ctx->context.get(), - pixel_tiles, - rows, columns, - encoder, - options); - if (addGridResult.error) { - return addGridResult.error.error_struct(ctx->context.get()); - } - - out_grid = addGridResult.value; - - // Mark as primary image - if (ctx->context->is_primary_image_set() == false) { - ctx->context->set_primary_image(out_grid); - } - - if (out_image_handle) { - *out_image_handle = new heif_image_handle; - (*out_image_handle)->image = std::move(out_grid); - (*out_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_add_grid_image(struct heif_context* ctx, - uint32_t image_width, - uint32_t image_height, - uint32_t tile_columns, - uint32_t tile_rows, - const struct heif_encoding_options* encoding_options, - struct heif_image_handle** out_grid_image_handle) -{ - if (tile_rows == 0 || tile_columns == 0) { - return Error(heif_error_Usage_error, - heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); - } - else if (tile_rows > 0xFFFF || tile_columns > 0xFFFF) { - return heif_error{heif_error_Usage_error, - heif_suberror_Invalid_image_size, - "Number of tile rows/columns may not exceed 65535"}; - } - - auto generateGridItemResult = ImageItem_Grid::add_new_grid_item(ctx->context.get(), - image_width, - image_height, - static_cast(tile_rows), - static_cast(tile_columns), - encoding_options); - if (generateGridItemResult.error) { - return generateGridItemResult.error.error_struct(ctx->context.get()); - } - - if (out_grid_image_handle) { - *out_grid_image_handle = new heif_image_handle; - (*out_grid_image_handle)->image = generateGridItemResult.value; - (*out_grid_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_add_overlay_image(struct heif_context* ctx, - uint32_t image_width, - uint32_t image_height, - uint16_t nImages, - const heif_item_id* image_ids, - int32_t* offsets, - const uint16_t background_rgba[4], - struct heif_image_handle** out_iovl_image_handle) -{ - if (!image_ids) { - return Error(heif_error_Usage_error, - heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); - } - else if (nImages == 0) { - return Error(heif_error_Usage_error, - heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); - } - - - std::vector refs; - refs.insert(refs.end(), image_ids, image_ids + nImages); - - ImageOverlay overlay; - overlay.set_canvas_size(image_width, image_height); - - if (background_rgba) { - overlay.set_background_color(background_rgba); - } - - for (uint16_t i=0;i> addImageResult = ImageItem_Overlay::add_new_overlay_item(ctx->context.get(), overlay); - - if (addImageResult.error != Error::Ok) { - return addImageResult.error.error_struct(ctx->context.get()); - } - - std::shared_ptr iovlimage = addImageResult.value; - - - if (out_iovl_image_handle) { - *out_iovl_image_handle = new heif_image_handle; - (*out_iovl_image_handle)->image = std::move(iovlimage); - (*out_iovl_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_add_tiled_image(struct heif_context* ctx, - const struct heif_tiled_image_parameters* parameters, - const struct heif_encoding_options* options, // TODO: do we need this? - const struct heif_encoder* encoder, - struct heif_image_handle** out_grid_image_handle) -{ - if (out_grid_image_handle) { - *out_grid_image_handle = nullptr; - } - - Result> gridImageResult; - gridImageResult = ImageItem_Tiled::add_new_tiled_item(ctx->context.get(), parameters, encoder); - - if (gridImageResult.error != Error::Ok) { - return gridImageResult.error.error_struct(ctx->context.get()); - } - - if (out_grid_image_handle) { - *out_grid_image_handle = new heif_image_handle; - (*out_grid_image_handle)->image = gridImageResult.value; - (*out_grid_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_add_image_tile(struct heif_context* ctx, - struct heif_image_handle* tiled_image, - uint32_t tile_x, uint32_t tile_y, - const struct heif_image* image, - struct heif_encoder* encoder) -{ - if (auto tili_image = std::dynamic_pointer_cast(tiled_image->image)) { - Error err = tili_image->add_image_tile(tile_x, tile_y, image->image, encoder); - return err.error_struct(ctx->context.get()); - } -#if WITH_UNCOMPRESSED_CODEC - else if (auto unci = std::dynamic_pointer_cast(tiled_image->image)) { - Error err = unci->add_image_tile(tile_x, tile_y, image->image); - return err.error_struct(ctx->context.get()); - } -#endif - else if (auto grid_item = std::dynamic_pointer_cast(tiled_image->image)) { - Error err = grid_item->add_image_tile(tile_x, tile_y, image->image, encoder); - return err.error_struct(ctx->context.get()); - } - else { - return { - heif_error_Usage_error, - heif_suberror_Unspecified, - "Cannot add tile to a non-tiled image" - }; - } -} - - -struct heif_error heif_context_add_unci_image(struct heif_context* ctx, - const struct heif_unci_image_parameters* parameters, - const struct heif_encoding_options* encoding_options, - const heif_image* prototype, - struct heif_image_handle** out_unci_image_handle) -{ -#if WITH_UNCOMPRESSED_CODEC - Result> unciImageResult; - unciImageResult = ImageItem_uncompressed::add_unci_item(ctx->context.get(), parameters, encoding_options, prototype->image); - - if (unciImageResult.error != Error::Ok) { - return unciImageResult.error.error_struct(ctx->context.get()); - } - - if (out_unci_image_handle) { - *out_unci_image_handle = new heif_image_handle; - (*out_unci_image_handle)->image = unciImageResult.value; - (*out_unci_image_handle)->context = ctx->context; - } - - return heif_error_success; -#else - return {heif_error_Unsupported_feature, - heif_suberror_Unspecified, - "support for uncompressed images (ISO23001-17) has been disabled."}; -#endif -} - - - -struct heif_error heif_context_assign_thumbnail(struct heif_context* ctx, - const struct heif_image_handle* master_image, - const struct heif_image_handle* thumbnail_image) -{ - Error error = ctx->context->assign_thumbnail(thumbnail_image->image, master_image->image); - return error.error_struct(ctx->context.get()); -} - - -struct heif_error heif_context_encode_thumbnail(struct heif_context* ctx, - const struct heif_image* image, - const struct heif_image_handle* image_handle, - struct heif_encoder* encoder, - const struct heif_encoding_options* input_options, - int bbox_size, - struct heif_image_handle** out_image_handle) -{ - heif_encoding_options options; - set_default_encoding_options(options); - - if (input_options != nullptr) { - copy_options(options, *input_options); - } - - auto encodingResult = ctx->context->encode_thumbnail(image->image, - encoder, - options, - bbox_size); - if (encodingResult.error != Error::Ok) { - return encodingResult.error.error_struct(ctx->context.get()); - } - - std::shared_ptr thumbnail_image = *encodingResult; - - if (!thumbnail_image) { - Error err(heif_error_Usage_error, - heif_suberror_Invalid_parameter_value, - "Thumbnail images must be smaller than the original image."); - return err.error_struct(ctx->context.get()); - } - - Error error = ctx->context->assign_thumbnail(image_handle->image, thumbnail_image); - if (error != Error::Ok) { - return error.error_struct(ctx->context.get()); - } - - - if (out_image_handle) { - *out_image_handle = new heif_image_handle; - (*out_image_handle)->image = thumbnail_image; - (*out_image_handle)->context = ctx->context; - } - - return heif_error_success; -} - - -struct heif_error heif_context_set_primary_image(struct heif_context* ctx, - struct heif_image_handle* image_handle) -{ - ctx->context->set_primary_image(image_handle->image); - - return heif_error_success; -} - - -struct heif_error heif_context_add_exif_metadata(struct heif_context* ctx, - const struct heif_image_handle* image_handle, - const void* data, int size) -{ - Error error = ctx->context->add_exif_metadata(image_handle->image, data, size); - if (error != Error::Ok) { - return error.error_struct(ctx->context.get()); - } - else { - return heif_error_success; - } -} - - -struct heif_error heif_context_add_XMP_metadata(struct heif_context* ctx, - const struct heif_image_handle* image_handle, - const void* data, int size) -{ - return heif_context_add_XMP_metadata2(ctx, image_handle, data, size, - heif_metadata_compression_off); -} - - -struct heif_error heif_context_add_XMP_metadata2(struct heif_context* ctx, - const struct heif_image_handle* image_handle, - const void* data, int size, - heif_metadata_compression compression) -{ - Error error = ctx->context->add_XMP_metadata(image_handle->image, data, size, compression); - if (error != Error::Ok) { - return error.error_struct(ctx->context.get()); - } - else { - return heif_error_success; - } -} - - -struct heif_error heif_context_add_generic_metadata(struct heif_context* ctx, - const struct heif_image_handle* image_handle, - const void* data, int size, - const char* item_type, const char* content_type) -{ - if (item_type == nullptr || strlen(item_type) != 4) { - return {heif_error_Usage_error, - heif_suberror_Invalid_parameter_value, - "called heif_context_add_generic_metadata() with invalid 'item_type'."}; - } - - Error error = ctx->context->add_generic_metadata(image_handle->image, data, size, - fourcc(item_type), content_type, nullptr, heif_metadata_compression_off, nullptr); - if (error != Error::Ok) { - return error.error_struct(ctx->context.get()); - } - else { - return heif_error_success; - } -} - - -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) -{ - Error error = ctx->context->add_generic_metadata(image_handle->image, data, size, - fourcc("uri "), nullptr, item_uri_type, heif_metadata_compression_off, out_item_id); - if (error != Error::Ok) { - return error.error_struct(ctx->context.get()); - } - else { - return heif_error_success; - } -} - - -void heif_context_set_maximum_image_size_limit(struct heif_context* ctx, int maximum_width) -{ - ctx->context->get_security_limits()->max_image_size_pixels = static_cast(maximum_width) * maximum_width; -} +#include "error.h" -void heif_context_set_max_decoding_threads(struct heif_context* ctx, int max_threads) -{ - ctx->context->set_max_decoding_threads(max_threads); -} +const heif_error heif_error_success = {heif_error_Ok, heif_suberror_Unspecified, Error::kSuccess}; diff -Nru libheif-1.19.8/libheif/api/libheif/heif.h libheif-1.23.4/libheif/api/libheif/heif.h --- libheif-1.19.8/libheif/api/libheif/heif.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif.h 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,6 @@ /* * HEIF codec. - * Copyright (c) 2017-2023 Dirk Farin + * Copyright (c) 2017-2025 Dirk Farin * * This file is part of libheif. * @@ -21,2608 +21,23 @@ #ifndef LIBHEIF_HEIF_H #define LIBHEIF_HEIF_H -#ifdef __cplusplus -extern "C" { -#endif - -/*! \file heif.h - * - * Public API for libheif. -*/ - -#include -#include - +#include #include - - -// API versions table -// -// release dec.options enc.options heif_reader heif_writer depth.rep col.profile -// ------------------------------------------------------------------------------------------ -// 1.0 1 N/A N/A N/A 1 N/A -// 1.1 1 N/A N/A 1 1 N/A -// 1.3 1 1 1 1 1 N/A -// 1.4 1 1 1 1 1 1 -// 1.7 2 1 1 1 1 1 -// 1.9.2 2 2 1 1 1 1 -// 1.10 2 3 1 1 1 1 -// 1.11 2 4 1 1 1 1 -// 1.13 3 4 1 1 1 1 -// 1.14 3 5 1 1 1 1 -// 1.15 4 5 1 1 1 1 -// 1.16 5 6 1 1 1 1 -// 1.18 5 7 1 1 1 1 -// 1.19 6 7 2 1 1 1 - -#if (defined(_WIN32) || defined __CYGWIN__) && !defined(LIBHEIF_STATIC_BUILD) -#ifdef LIBHEIF_EXPORTS -#define LIBHEIF_API __declspec(dllexport) -#else -#define LIBHEIF_API __declspec(dllimport) -#endif -#elif defined(HAVE_VISIBILITY) && HAVE_VISIBILITY -#ifdef LIBHEIF_EXPORTS -#define LIBHEIF_API __attribute__((__visibility__("default"))) -#else -#define LIBHEIF_API -#endif -#else -#define LIBHEIF_API -#endif - -#define heif_fourcc(a, b, c, d) ((uint32_t)((a<<24) | (b<<16) | (c<<8) | d)) - - -/* === version numbers === */ - -// Version string of linked libheif library. -LIBHEIF_API const char* heif_get_version(void); - -// Numeric version of linked libheif library, encoded as 0xHHMMLL00 = hh.mm.ll, where hh, mm, ll is the decimal representation of HH, MM, LL. -// For example: 0x02150300 is version 2.21.3 -LIBHEIF_API uint32_t heif_get_version_number(void); - -// Numeric part "HH" from above. Returned as a decimal number. -LIBHEIF_API int heif_get_version_number_major(void); -// Numeric part "MM" from above. Returned as a decimal number. -LIBHEIF_API int heif_get_version_number_minor(void); -// Numeric part "LL" from above. Returned as a decimal number. -LIBHEIF_API int heif_get_version_number_maintenance(void); - -// Helper macros to check for given versions of libheif at compile time. -#define LIBHEIF_MAKE_VERSION(h, m, l) ((h) << 24 | (m) << 16 | (l) << 8) -#define LIBHEIF_HAVE_VERSION(h, m, l) (LIBHEIF_NUMERIC_VERSION >= LIBHEIF_MAKE_VERSION(h, m, l)) - -struct heif_context; -struct heif_image_handle; -struct heif_image; - - -enum heif_error_code -{ - // Everything ok, no error occurred. - heif_error_Ok = 0, - - // Input file does not exist. - heif_error_Input_does_not_exist = 1, - - // Error in input file. Corrupted or invalid content. - heif_error_Invalid_input = 2, - - // Input file type is not supported. - heif_error_Unsupported_filetype = 3, - - // Image requires an unsupported decoder feature. - heif_error_Unsupported_feature = 4, - - // Library API has been used in an invalid way. - heif_error_Usage_error = 5, - - // Could not allocate enough memory. - heif_error_Memory_allocation_error = 6, - - // The decoder plugin generated an error - heif_error_Decoder_plugin_error = 7, - - // The encoder plugin generated an error - heif_error_Encoder_plugin_error = 8, - - // Error during encoding or when writing to the output - heif_error_Encoding_error = 9, - - // Application has asked for a color profile type that does not exist - heif_error_Color_profile_does_not_exist = 10, - - // Error loading a dynamic plugin - heif_error_Plugin_loading_error = 11, - - // Operation has been canceled - heif_error_Canceled = 12 -}; - - -enum heif_suberror_code -{ - // no further information available - heif_suberror_Unspecified = 0, - - // --- Invalid_input --- - - // End of data reached unexpectedly. - heif_suberror_End_of_data = 100, - - // Size of box (defined in header) is wrong - heif_suberror_Invalid_box_size = 101, - - // Mandatory 'ftyp' box is missing - heif_suberror_No_ftyp_box = 102, - - heif_suberror_No_idat_box = 103, - - heif_suberror_No_meta_box = 104, - - heif_suberror_No_hdlr_box = 105, - - heif_suberror_No_hvcC_box = 106, - - heif_suberror_No_pitm_box = 107, - - heif_suberror_No_ipco_box = 108, - - heif_suberror_No_ipma_box = 109, - - heif_suberror_No_iloc_box = 110, - - heif_suberror_No_iinf_box = 111, - - heif_suberror_No_iprp_box = 112, - - heif_suberror_No_iref_box = 113, - - heif_suberror_No_pict_handler = 114, - - // An item property referenced in the 'ipma' box is not existing in the 'ipco' container. - heif_suberror_Ipma_box_references_nonexisting_property = 115, - - // No properties have been assigned to an item. - heif_suberror_No_properties_assigned_to_item = 116, - - // Image has no (compressed) data - heif_suberror_No_item_data = 117, - - // Invalid specification of image grid (tiled image) - heif_suberror_Invalid_grid_data = 118, - - // Tile-images in a grid image are missing - heif_suberror_Missing_grid_images = 119, - - heif_suberror_Invalid_clean_aperture = 120, - - // Invalid specification of overlay image - heif_suberror_Invalid_overlay_data = 121, - - // Overlay image completely outside of visible canvas area - heif_suberror_Overlay_image_outside_of_canvas = 122, - - heif_suberror_Auxiliary_image_type_unspecified = 123, - - heif_suberror_No_or_invalid_primary_item = 124, - - heif_suberror_No_infe_box = 125, - - heif_suberror_Unknown_color_profile_type = 126, - - heif_suberror_Wrong_tile_image_chroma_format = 127, - - heif_suberror_Invalid_fractional_number = 128, - - heif_suberror_Invalid_image_size = 129, - - heif_suberror_Invalid_pixi_box = 130, - - heif_suberror_No_av1C_box = 131, - - heif_suberror_Wrong_tile_image_pixel_depth = 132, - - heif_suberror_Unknown_NCLX_color_primaries = 133, - - heif_suberror_Unknown_NCLX_transfer_characteristics = 134, - - heif_suberror_Unknown_NCLX_matrix_coefficients = 135, - - // 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, - - heif_suberror_No_avcC_box = 143, - - // we got a mini box, but could not read it properly - heif_suberror_Invalid_mini_box = 149, - - // Decompressing generic compression or header compression data failed (e.g. bitstream corruption) - heif_suberror_Decompression_invalid_data = 150, - - // --- Memory_allocation_error --- - - // A security limit preventing unreasonable memory allocations was exceeded by the input file. - // Please check whether the file is valid. If it is, contact us so that we could increase the - // 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 --- - - // An item ID was used that is not present in the file. - heif_suberror_Nonexisting_item_referenced = 2000, // also used for Invalid_input - - // An API argument was given a NULL pointer, which is not allowed for that function. - heif_suberror_Null_pointer_argument = 2001, - - // Image channel referenced that does not exist in the image - heif_suberror_Nonexisting_image_channel_referenced = 2002, - - // The version of the passed plugin is not supported. - heif_suberror_Unsupported_plugin_version = 2003, - - // The version of the passed writer is not supported. - heif_suberror_Unsupported_writer_version = 2004, - - // The given (encoder) parameter name does not exist. - heif_suberror_Unsupported_parameter = 2005, - - // The value for the given parameter is not in the valid range. - heif_suberror_Invalid_parameter_value = 2006, - - // Error in property specification - heif_suberror_Invalid_property = 2007, - - // Image reference cycle found in iref - heif_suberror_Item_reference_cycle = 2008, - - - // --- Unsupported_feature --- - - // Image was coded with an unsupported compression method. - heif_suberror_Unsupported_codec = 3000, - - // Image is specified in an unknown way, e.g. as tiled grid image (which is supported) - heif_suberror_Unsupported_image_type = 3001, - - heif_suberror_Unsupported_data_version = 3002, - - // The conversion of the source image to the requested chroma / colorspace is not supported. - heif_suberror_Unsupported_color_conversion = 3003, - - heif_suberror_Unsupported_item_construction_method = 3004, - - heif_suberror_Unsupported_header_compression_method = 3005, - - // Generically compressed data used an unsupported compression method - heif_suberror_Unsupported_generic_compression_method = 3006, - - heif_suberror_Unsupported_essential_property = 3007, - - // --- Encoder_plugin_error --- - - heif_suberror_Unsupported_bit_depth = 4000, - - - // --- Encoding_error --- - - heif_suberror_Cannot_write_output_data = 5000, - - heif_suberror_Encoder_initialization = 5001, - heif_suberror_Encoder_encoding = 5002, - heif_suberror_Encoder_cleanup = 5003, - - heif_suberror_Too_many_regions = 5004, - - - // --- 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_No_matching_decoder_installed = 6003 // no decoder found for that compression format -}; - - -struct heif_error -{ - // main error category - enum heif_error_code code; - - // more detailed error code - enum heif_suberror_code subcode; - - // textual error message (is always defined, you do not have to check for NULL) - const char* message; -}; - -// Default success return value. Intended for use in user-supplied callback functions. -LIBHEIF_API extern const struct heif_error heif_error_success; - - -typedef uint32_t heif_item_id; -typedef uint32_t heif_property_id; - - - -// ========================= enum types ====================== - -/** - * libheif known compression formats. - */ -enum heif_compression_format -{ - /** - * Unspecified / undefined compression format. - * - * This is used to mean "no match" or "any decoder" for some parts of the - * API. It does not indicate a specific compression format. - */ - heif_compression_undefined = 0, - /** - * HEVC compression, used for HEIC images. - * - * This is equivalent to H.265. - */ - heif_compression_HEVC = 1, - /** - * AVC compression. (Currently unused in libheif.) - * - * The compression is defined in ISO/IEC 14496-10. This is equivalent to H.264. - * - * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex E. - */ - heif_compression_AVC = 2, - /** - * JPEG compression. - * - * The compression format is defined in ISO/IEC 10918-1. The encapsulation - * of JPEG is specified in ISO/IEC 23008-12:2022 Annex H. - */ - heif_compression_JPEG = 3, - /** - * AV1 compression, used for AVIF images. - * - * The compression format is provided at https://aomediacodec.github.io/av1-spec/ - * - * The encapsulation is defined in https://aomediacodec.github.io/av1-avif/ - */ - heif_compression_AV1 = 4, - /** - * VVC compression. - * - * The compression format is defined in ISO/IEC 23090-3. This is equivalent to H.266. - * - * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex L. - */ - heif_compression_VVC = 5, - /** - * EVC compression. (Currently unused in libheif.) - * - * The compression format is defined in ISO/IEC 23094-1. - * - * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex M. - */ - heif_compression_EVC = 6, - /** - * JPEG 2000 compression. - * - * The encapsulation of JPEG 2000 is specified in ISO/IEC 15444-16:2021. - * The core encoding is defined in ISO/IEC 15444-1, or ITU-T T.800. - */ - heif_compression_JPEG2000 = 7, - /** - * Uncompressed encoding. - * - * This is defined in ISO/IEC 23001-17:2024. - */ - heif_compression_uncompressed = 8, - /** - * Mask image encoding. - * - * See ISO/IEC 23008-12:2022 Section 6.10.2 - */ - 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 -{ - heif_chroma_undefined = 99, - heif_chroma_monochrome = 0, - heif_chroma_420 = 1, - heif_chroma_422 = 2, - heif_chroma_444 = 3, - heif_chroma_interleaved_RGB = 10, - heif_chroma_interleaved_RGBA = 11, - heif_chroma_interleaved_RRGGBB_BE = 12, // HDR, big endian. - heif_chroma_interleaved_RRGGBBAA_BE = 13, // HDR, big endian. - heif_chroma_interleaved_RRGGBB_LE = 14, // HDR, little endian. - heif_chroma_interleaved_RRGGBBAA_LE = 15 // HDR, little endian. -}; - -// DEPRECATED ENUM NAMES -#define heif_chroma_interleaved_24bit heif_chroma_interleaved_RGB -#define heif_chroma_interleaved_32bit heif_chroma_interleaved_RGBA - - -enum heif_colorspace -{ - heif_colorspace_undefined = 99, - - // heif_colorspace_YCbCr should be used with one of these heif_chroma values: - // * heif_chroma_444 - // * heif_chroma_422 - // * heif_chroma_420 - heif_colorspace_YCbCr = 0, - - // heif_colorspace_RGB should be used with one of these heif_chroma values: - // * heif_chroma_444 (for planar RGB) - // * heif_chroma_interleaved_RGB - // * heif_chroma_interleaved_RGBA - // * heif_chroma_interleaved_RRGGBB_BE - // * heif_chroma_interleaved_RRGGBBAA_BE - // * heif_chroma_interleaved_RRGGBB_LE - // * heif_chroma_interleaved_RRGGBBAA_LE - heif_colorspace_RGB = 1, - - // heif_colorspace_monochrome should only be used with heif_chroma = heif_chroma_monochrome - heif_colorspace_monochrome = 2, - - // Indicates that this image has no visual channels. - heif_colorspace_nonvisual = 3 -}; - -enum heif_channel -{ - heif_channel_Y = 0, - heif_channel_Cb = 1, - heif_channel_Cr = 2, - heif_channel_R = 3, - heif_channel_G = 4, - heif_channel_B = 5, - heif_channel_Alpha = 6, - heif_channel_interleaved = 10, - heif_channel_filter_array = 11, - heif_channel_depth = 12, - heif_channel_disparity = 13 -}; - -enum heif_metadata_compression -{ - 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 -}; - -// ========================= library initialization ====================== - -struct heif_init_params -{ - int version; - - // currently no parameters -}; - - -/** - * Initialise library. - * - * You should call heif_init() when you start using libheif and heif_deinit() when you are finished. - * These calls are reference counted. Each call to heif_init() should be matched by one call to heif_deinit(). - * - * For backwards compatibility, it is not really necessary to call heif_init(), but some library memory objects - * will never be freed if you do not call heif_init()/heif_deinit(). - * - * heif_init() will load the external modules installed in the default plugin path. Thus, you need it when you - * want to load external plugins from the default path. - * Codec plugins that are compiled into the library directly (selected by the compile-time parameters of libheif) - * will be available even without heif_init(). - * - * Make sure that you do not have one part of your program use heif_init()/heif_deinit() and another part that does - * not use it as the latter may try to use an uninitialized library. If in doubt, enclose everything with init/deinit. - * - * You may pass nullptr to get default parameters. Currently, no parameters are supported. - */ -LIBHEIF_API -struct heif_error heif_init(struct heif_init_params*); - -/** - * Deinitialise and clean up library. - * - * You should call heif_init() when you start using libheif and heif_deinit() when you are finished. - * These calls are reference counted. Each call to heif_init() should be matched by one call to heif_deinit(). - * - * Note: heif_deinit() must not be called after exit(), for example in a global C++ object's destructor. - * If you do, global variables in libheif might have already been released when heif_deinit() is running, - * leading to a crash. - * - * \sa heif_init() - */ -LIBHEIF_API -void heif_deinit(void); - - -// --- Plugins are currently only supported on Unix platforms. - -enum heif_plugin_type -{ - heif_plugin_type_encoder, - heif_plugin_type_decoder -}; - -struct heif_plugin_info -{ - int version; // version of this info struct - enum heif_plugin_type type; - const void* plugin; - void* internal_handle; // for internal use only -}; - -LIBHEIF_API -struct heif_error heif_load_plugin(const char* filename, struct heif_plugin_info const** out_plugin); - -LIBHEIF_API -struct heif_error heif_load_plugins(const char* directory, - const struct heif_plugin_info** out_plugins, - int* out_nPluginsLoaded, - int output_array_size); - -LIBHEIF_API -struct heif_error heif_unload_plugin(const struct heif_plugin_info* plugin); - -// Get a NULL terminated array of the plugin directories that are searched by libheif. -// This includes the paths specified in the environment variable LIBHEIF_PLUGIN_PATHS and the built-in path -// (if not overridden by the environment variable). -LIBHEIF_API -const char*const* heif_get_plugin_directories(void); - -LIBHEIF_API -void heif_free_plugin_directories(const char*const*); - - -// ========================= file type check ====================== - -enum heif_filetype_result -{ - heif_filetype_no, - heif_filetype_yes_supported, // it is heif and can be read by libheif - heif_filetype_yes_unsupported, // it is heif, but cannot be read by libheif - heif_filetype_maybe // not sure whether it is an heif, try detection with more input data -}; - -// input data should be at least 12 bytes -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); - - -// DEPRECATED, use heif_brand2 and the heif_brand2_* constants below instead -enum heif_brand -{ - heif_unknown_brand, - heif_heic, // HEIF image with h265 - heif_heix, // 10bit images, or anything that uses h265 with range extension - heif_hevc, heif_hevx, // brands for image sequences - heif_heim, // multiview - heif_heis, // scalable - heif_hevm, // multiview sequence - heif_hevs, // scalable sequence - heif_mif1, // image, any coding algorithm - heif_msf1, // sequence, any coding algorithm - heif_avif, // HEIF image with AV1 - heif_avis, - heif_vvic, // VVC image - heif_vvis, // VVC sequence - heif_evbi, // EVC image - heif_evbs, // EVC sequence - heif_j2ki, // JPEG2000 image - heif_j2is, // JPEG2000 image sequence -}; - -// input data should be at least 12 bytes -// DEPRECATED, use heif_read_main_brand() instead -LIBHEIF_API -enum heif_brand heif_main_brand(const uint8_t* data, int len); - - -typedef uint32_t heif_brand2; - -/** - * HEVC image (`heic`) brand. - * - * Image conforms to HEVC (H.265) Main or Main Still profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.1. - */ -#define heif_brand2_heic heif_fourcc('h','e','i','c') - -/** - * HEVC image (`heix`) brand. - * - * Image conforms to HEVC (H.265) Main 10 profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.1. - */ -#define heif_brand2_heix heif_fourcc('h','e','i','x') - -/** - * HEVC image sequence (`hevc`) brand. - * - * Image sequence conforms to HEVC (H.265) Main profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.2. - */ -#define heif_brand2_hevc heif_fourcc('h','e','v','c') - -/** - * HEVC image sequence (`hevx`) brand. - * - * Image sequence conforms to HEVC (H.265) Main 10 profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.2. - */ -#define heif_brand2_hevx heif_fourcc('h','e','v','x') - -/** - * HEVC layered image (`heim`) brand. - * - * Image layers conform to HEVC (H.265) Main or Multiview Main profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.3. - */ -#define heif_brand2_heim heif_fourcc('h','e','i','m') - -/** - * HEVC layered image (`heis`) brand. - * - * Image layers conform to HEVC (H.265) Main, Main 10, Scalable Main - * or Scalable Main 10 profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.3. - */ -#define heif_brand2_heis heif_fourcc('h','e','i','s') - -/** - * HEVC layered image sequence (`hevm`) brand. - * - * Image sequence layers conform to HEVC (H.265) Main or Multiview Main profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.4. - */ -#define heif_brand2_hevm heif_fourcc('h','e','v','m') - -/** - * HEVC layered image sequence (`hevs`) brand. - * - * Image sequence layers conform to HEVC (H.265) Main, Main 10, Scalable Main - * or Scalable Main 10 profile. - * - * See ISO/IEC 23008-12:2022 Section B.4.4. - */ -#define heif_brand2_hevs heif_fourcc('h','e','v','s') - -/** - * AV1 image (`avif`) brand. - * - * See https://aomediacodec.github.io/av1-avif/#image-and-image-collection-brand - */ -#define heif_brand2_avif heif_fourcc('a','v','i','f') - -/** - * AV1 image sequence (`avis`) brand. - * - * See https://aomediacodec.github.io/av1-avif/#image-sequence-brand - */ -#define heif_brand2_avis heif_fourcc('a','v','i','s') // AVIF sequence - -/** - * HEIF image structural brand (`mif1`). - * - * This does not imply a specific coding algorithm. - * - * See ISO/IEC 23008-12:2022 Section 10.2.2. - */ -#define heif_brand2_mif1 heif_fourcc('m','i','f','1') - -/** - * HEIF image structural brand (`mif2`). - * - * This does not imply a specific coding algorithm. `mif2` extends - * the requirements of `mif1` to include the `rref` and `iscl` item - * properties. - * - * See ISO/IEC 23008-12:2022 Section 10.2.3. - */ -#define heif_brand2_mif2 heif_fourcc('m','i','f','2') - -/** - * HEIF image structural brand (`mif3`). - * - * This indicates the low-overhead (ftyp+mini) structure. - */ -#define heif_brand2_mif3 heif_fourcc('m','i','f','3') - -/** - * HEIF image sequence structural brand (`msf1`). - * - * This does not imply a specific coding algorithm. - * - * See ISO/IEC 23008-12:2022 Section 10.3.1. - */ -#define heif_brand2_msf1 heif_fourcc('m','s','f','1') - -/** - * VVC image (`vvic`) brand. - * - * See ISO/IEC 23008-12:2022 Section L.4.1. - */ -#define heif_brand2_vvic heif_fourcc('v','v','i','c') - -/** - * VVC image sequence (`vvis`) brand. - * - * See ISO/IEC 23008-12:2022 Section L.4.2. - */ -#define heif_brand2_vvis heif_fourcc('v','v','i','s') - -/** - * EVC baseline image (`evbi`) brand. - * - * See ISO/IEC 23008-12:2022 Section M.4.1. - */ -#define heif_brand2_evbi heif_fourcc('e','v','b','i') - -/** - * EVC main profile image (`evmi`) brand. - * - * See ISO/IEC 23008-12:2022 Section M.4.2. - */ -#define heif_brand2_evmi heif_fourcc('e','v','m','i') - -/** - * EVC baseline image sequence (`evbs`) brand. - * - * See ISO/IEC 23008-12:2022 Section M.4.3. - */ -#define heif_brand2_evbs heif_fourcc('e','v','b','s') - -/** - * EVC main profile image sequence (`evms`) brand. - * - * See ISO/IEC 23008-12:2022 Section M.4.4. - */ -#define heif_brand2_evms heif_fourcc('e','v','m','s') - -/** - * JPEG image (`jpeg`) brand. - * - * See ISO/IEC 23008-12:2022 Annex H.4 - */ -#define heif_brand2_jpeg heif_fourcc('j','p','e','g') - -/** - * JPEG image sequence (`jpgs`) brand. - * - * See ISO/IEC 23008-12:2022 Annex H.5 - */ -#define heif_brand2_jpgs heif_fourcc('j','p','g','s') - -/** - * JPEG 2000 image (`j2ki`) brand. - * - * See ISO/IEC 15444-16:2021 Section 6.5 - */ -#define heif_brand2_j2ki heif_fourcc('j','2','k','i') - -/** - * JPEG 2000 image sequence (`j2is`) brand. - * - * See ISO/IEC 15444-16:2021 Section 7.6 - */ -#define heif_brand2_j2is heif_fourcc('j','2','i','s') - -/** - * Multi-image application format (MIAF) brand. - * - * This is HEIF with additional constraints for interoperability. - * - * See ISO/IEC 23000-22. - */ -#define heif_brand2_miaf heif_fourcc('m','i','a','f') - -/** - * Single picture file brand. - * - * This is a compatible brand indicating the file contains a single intra-coded picture. - * - * See ISO/IEC 23008-12:2022 Section 10.2.5. -*/ -#define heif_brand2_1pic heif_fourcc('1','p','i','c') - -// input data should be at least 12 bytes -LIBHEIF_API -heif_brand2 heif_read_main_brand(const uint8_t* data, int len); - -// input data should be at least 16 bytes -LIBHEIF_API -heif_brand2 heif_read_minor_version_brand(const uint8_t* data, int len); - -// 'brand_fourcc' must be 4 character long, but need not be 0-terminated -LIBHEIF_API -heif_brand2 heif_fourcc_to_brand(const char* brand_fourcc); - -// the output buffer must be at least 4 bytes long -LIBHEIF_API -void heif_brand_to_fourcc(heif_brand2 brand, char* out_fourcc); - -// 'brand_fourcc' must be 4 character long, but need not be 0-terminated -// returns 1 if file includes the brand, and 0 if it does not -// returns -1 if the provided data is not sufficient -// (you should input at least as many bytes as indicated in the first 4 bytes of the file, usually ~50 bytes will do) -// returns -2 on other errors -LIBHEIF_API -int heif_has_compatible_brand(const uint8_t* data, int len, const char* brand_fourcc); - -// Returns an array of compatible brands. The array is allocated by this function and has to be freed with 'heif_free_list_of_compatible_brands()'. -// The number of entries is returned in out_size. -LIBHEIF_API -struct heif_error heif_list_compatible_brands(const uint8_t* data, int len, heif_brand2** out_brands, int* out_size); - -LIBHEIF_API -void heif_free_list_of_compatible_brands(heif_brand2* brands_list); - - -// Returns one of these MIME types: -// - image/heic HEIF file using h265 compression -// - image/heif HEIF file using any other compression -// - image/heic-sequence HEIF image sequence using h265 compression -// - image/heif-sequence HEIF image sequence using any other compression -// - image/avif AVIF image -// - image/avif-sequence AVIF sequence -// - image/jpeg JPEG image -// - image/png PNG image -// If the format could not be detected, an empty string is returned. -// -// Provide at least 12 bytes of input. With less input, its format might not -// be detected. You may also provide more input to increase detection accuracy. -// -// Note that JPEG and PNG images cannot be decoded by libheif even though the -// formats are detected by this function. -LIBHEIF_API -const char* heif_get_file_mime_type(const uint8_t* data, int len); - - - -// ========================= heif_context ========================= -// A heif_context represents a HEIF file that has been read. -// In the future, you will also be able to add pictures to a heif_context -// and write it into a file again. - - -// Allocate a new context for reading HEIF files. -// Has to be freed again with heif_context_free(). -LIBHEIF_API -struct heif_context* heif_context_alloc(void); - -// Free a previously allocated HEIF context. You should not free a context twice. -LIBHEIF_API -void heif_context_free(struct heif_context*); - - - -struct heif_reading_options; - -enum heif_reader_grow_status -{ - heif_reader_grow_status_size_reached, // requested size has been reached, we can read until this point - heif_reader_grow_status_timeout, // size has not been reached yet, but it may still grow further (deprecated) - heif_reader_grow_status_size_beyond_eof, // size has not been reached and never will. The file has grown to its full size - heif_reader_grow_status_error // an error has occurred -}; - -struct heif_reader_range_request_result -{ - enum heif_reader_grow_status status; // should not return 'heif_reader_grow_status_timeout' - - // Indicates up to what position the file has been read. - // If we cannot read the whole file range (status == 'heif_reader_grow_status_size_beyond_eof'), this is the actual end position. - // On the other hand, it may be that the reader was reading more data than requested. In that case, it should indicate the full size here - // and libheif may decide to make use of the additional data (e.g. for filling 'tili' offset tables). - uint64_t range_end; - - // for status == 'heif_reader_grow_status_error' - int reader_error_code; // a reader specific error code - const char* reader_error_msg; // libheif will call heif_reader.release_error_msg on this if it is not NULL -}; - - -struct heif_reader -{ - // API version supported by this reader - int reader_api_version; - - // --- version 1 functions --- - int64_t (* get_position)(void* userdata); - - // The functions read(), and seek() return 0 on success. - // Generally, libheif will make sure that we do not read past the file size. - int (* read)(void* data, - size_t size, - void* userdata); - - int (* seek)(int64_t position, - void* userdata); - - // When calling this function, libheif wants to make sure that it can read the file - // up to 'target_size'. This is useful when the file is currently downloaded and may - // grow with time. You may, for example, extract the image sizes even before the actual - // compressed image data has been completely downloaded. - // - // Even if your input files will not grow, you will have to implement at least - // detection whether the target_size is above the (fixed) file length - // (in this case, return 'size_beyond_eof'). - enum heif_reader_grow_status (* wait_for_file_size)(int64_t target_size, void* userdata); - - // --- version 2 functions --- - - // These two functions are for applications that want to stream HEIF files on demand. - // For example, a large HEIF file that is served over HTTPS and we only want to download - // it partially to decode individual tiles. - // If you do not have this use case, you do not have to implement these functions and - // you can set them to NULL. For simple linear loading, you may use the 'wait_for_file_size' - // function above instead. - - // If this function is defined, libheif will often request a file range before accessing it. - // The purpose of this function is that libheif will usually read very small chunks of data with the - // read() callback above. However, it is inefficient to request such a small chunk of data over a network - // and the network delay will significantly increase the decoding time. - // Thus, libheif will call request_range() with a larger block of data that should be preloaded and the - // subsequent read() calls will work within the requested ranges. - // - // Note: `end_pos` is one byte after the last position to be read. - // You should return - // - 'heif_reader_grow_status_size_reached' if the requested range is available, or - // - 'heif_reader_grow_status_size_beyond_eof' if the requested range exceeds the file size - // (the valid part of the range has been read). - struct heif_reader_range_request_result (*request_range)(uint64_t start_pos, uint64_t end_pos, void* userdata); - - // libheif might issue hints when it assumes that a file range might be needed in the future. - // This may happen, for example, when your are doing selective tile accesses and libheif proposes - // to preload offset pointer tables. - // Another difference to request_file_range() is that this call should be non-blocking. - // If you preload any data, do this in a background thread. - void (*preload_range_hint)(uint64_t start_pos, uint64_t end_pos, void* userdata); - - // If libheif does not need access to a file range anymore, it may call this function to - // give a hint to the reader that it may release the range from a cache. - // If you do not maintain a file cache that wants to reduce its size dynamically, you do not - // need to implement this function. - void (*release_file_range)(uint64_t start_pos, uint64_t end_pos, void* userdata); - - // Release an error message that was returned by heif_reader in an earlier call. - // If this function is NULL, the error message string will not be released. - // This is a viable option if you are only returning static strings. - void (*release_error_msg)(const char* msg); -}; - - -// Read a HEIF file from a named disk file. -// The heif_reading_options should currently be set to NULL. -LIBHEIF_API -struct heif_error heif_context_read_from_file(struct heif_context*, const char* filename, - const struct heif_reading_options*); - -// Read a HEIF file stored completely in memory. -// The heif_reading_options should currently be set to NULL. -// DEPRECATED: use heif_context_read_from_memory_without_copy() instead. -LIBHEIF_API -struct heif_error heif_context_read_from_memory(struct heif_context*, - const void* mem, size_t size, - const struct heif_reading_options*); - -// Same as heif_context_read_from_memory() except that the provided memory is not copied. -// That means, you will have to keep the memory area alive as long as you use the heif_context. -LIBHEIF_API -struct heif_error heif_context_read_from_memory_without_copy(struct heif_context*, - const void* mem, size_t size, - const struct heif_reading_options*); - -LIBHEIF_API -struct heif_error heif_context_read_from_reader(struct heif_context*, - const struct heif_reader* reader, - void* userdata, - const struct heif_reading_options*); - -// Number of top-level images in the HEIF file. This does not include the thumbnails or the -// tile images that are composed to an image grid. You can get access to the thumbnails via -// the main image handle. -LIBHEIF_API -int heif_context_get_number_of_top_level_images(struct heif_context* ctx); - -LIBHEIF_API -int heif_context_is_top_level_image_ID(struct heif_context* ctx, heif_item_id id); - -// Fills in image IDs into the user-supplied int-array 'ID_array', preallocated with 'count' entries. -// Function returns the total number of IDs filled into the array. -LIBHEIF_API -int heif_context_get_list_of_top_level_image_IDs(struct heif_context* ctx, - heif_item_id* ID_array, - int count); - -LIBHEIF_API -struct heif_error heif_context_get_primary_image_ID(struct heif_context* ctx, heif_item_id* id); - -// Get a handle to the primary image of the HEIF file. -// This is the image that should be displayed primarily when there are several images in the file. -LIBHEIF_API -struct heif_error heif_context_get_primary_image_handle(struct heif_context* ctx, - struct heif_image_handle**); - -// Get the image handle for a known image ID. -LIBHEIF_API -struct heif_error heif_context_get_image_handle(struct heif_context* ctx, - heif_item_id id, - struct heif_image_handle**); - -// Print information about the boxes of a HEIF file to file descriptor. -// This is for debugging and informational purposes only. You should not rely on -// the output having a specific format. At best, you should not use this at all. -LIBHEIF_API -void heif_context_debug_dump_boxes_to_file(struct heif_context* ctx, int fd); - - -// Set the maximum image size security limit. This function will set the maximum image area (number of pixels) -// to maximum_width ^ 2. Alternatively to using this function, you can also set the maximum image area -// in the security limits structure returned by heif_context_get_security_limits(). -LIBHEIF_API -void heif_context_set_maximum_image_size_limit(struct heif_context* ctx, int maximum_width); - -// If the maximum threads number is set to 0, the image tiles are decoded in the main thread. -// This is different from setting it to 1, which will generate a single background thread to decode the tiles. -// Note that this setting only affects libheif itself. The codecs itself may still use multi-threaded decoding. -// You can use it, for example, in cases where you are decoding several images in parallel anyway you thus want -// to minimize parallelism in each decoder. -LIBHEIF_API -void heif_context_set_max_decoding_threads(struct heif_context* ctx, int max_threads); - - -// --- security limits - -// If you set a limit to 0, the limit is disabled. -struct heif_security_limits { - uint8_t version; - - // --- version 1 - - // Limit on the maximum image size to avoid allocating too much memory. - // For example, setting this to 32768^2 pixels = 1 Gigapixels results - // in 1.5 GB memory need for YUV-4:2:0 or 4 GB for RGB32. - uint64_t max_image_size_pixels; - uint64_t max_number_of_tiles; - uint32_t max_bayer_pattern_pixels; - uint32_t max_items; - - uint32_t max_color_profile_size; - uint64_t max_memory_block_size; - - uint32_t max_components; - - uint32_t max_iloc_extents_per_item; - uint32_t max_size_entity_group; - - uint32_t max_children_per_box; // for all boxes that are not covered by other limits -}; - -// The global security limits are the default for new heif_contexts. -// These global limits cannot be changed, but you can override the limits for a specific heif_context. -LIBHEIF_API -const struct heif_security_limits* heif_get_global_security_limits(); - -// Returns a set of fully disabled security limits. Use with care and only after user confirmation. -LIBHEIF_API -const struct heif_security_limits* heif_get_disabled_security_limits(); - -// Returns the security limits for a heif_context. -// By default, the limits are set to the global limits, but you can change them in the returned object. -LIBHEIF_API -struct heif_security_limits* heif_context_get_security_limits(const struct heif_context*); - -// Overwrites the security limits of a heif_context. -// This is a convenience function to easily copy limits. -LIBHEIF_API -struct heif_error heif_context_set_security_limits(struct heif_context*, const struct heif_security_limits*); - - -// ========================= heif_image_handle ========================= - -// An heif_image_handle is a handle to a logical image in the HEIF file. -// To get the actual pixel data, you have to decode the handle to an heif_image. -// An heif_image_handle also gives you access to the thumbnails and Exif data -// associated with an image. - -// Once you obtained an heif_image_handle, you can already release the heif_context, -// since it is internally ref-counted. - -// Release image handle. -LIBHEIF_API -void heif_image_handle_release(const struct heif_image_handle*); - -// Check whether the given image_handle is the primary image of the file. -LIBHEIF_API -int heif_image_handle_is_primary_image(const struct heif_image_handle* handle); - -LIBHEIF_API -heif_item_id heif_image_handle_get_item_id(const struct heif_image_handle* handle); - -// Get the resolution of an image. -LIBHEIF_API -int heif_image_handle_get_width(const struct heif_image_handle* handle); - -LIBHEIF_API -int heif_image_handle_get_height(const struct heif_image_handle* handle); - -LIBHEIF_API -int heif_image_handle_has_alpha_channel(const struct heif_image_handle*); - -LIBHEIF_API -int heif_image_handle_is_premultiplied_alpha(const struct heif_image_handle*); - -// Returns -1 on error, e.g. if this information is not present in the image. -// Only defined for images coded in the YCbCr or monochrome colorspace. -LIBHEIF_API -int heif_image_handle_get_luma_bits_per_pixel(const struct heif_image_handle*); - -// Returns -1 on error, e.g. if this information is not present in the image. -// Only defined for images coded in the YCbCr colorspace. -LIBHEIF_API -int heif_image_handle_get_chroma_bits_per_pixel(const struct heif_image_handle*); - -// Return the colorspace that libheif proposes to use for decoding. -// Usually, these will be either YCbCr or Monochrome, but it may also propose RGB for images -// encoded with matrix_coefficients=0 or for images coded natively in RGB. -// It may also return *_undefined if the file misses relevant information to determine this without decoding. -// These are only proposed values that avoid colorspace conversions as much as possible. -// You can still request the output in your preferred colorspace, but this may involve an internal conversion. -LIBHEIF_API -struct heif_error heif_image_handle_get_preferred_decoding_colorspace(const struct heif_image_handle* image_handle, - enum heif_colorspace* out_colorspace, - enum heif_chroma* out_chroma); - -// Get the image width from the 'ispe' box. This is the original image size without -// any transformations applied to it. Do not use this unless you know exactly what -// you are doing. -LIBHEIF_API -int heif_image_handle_get_ispe_width(const struct heif_image_handle* handle); - -LIBHEIF_API -int heif_image_handle_get_ispe_height(const struct heif_image_handle* handle); - -// This gets the context associated with the image handle. -// Note that you have to release the returned context with heif_context_free() in any case. -// -// This means: when you have several image-handles that originate from the same file and you get the -// context of each of them, the returned pointer may be different even though it refers to the same -// logical context. You have to call heif_context_free() on all those context pointers. -// After you freed a context pointer, you can still use the context through a different pointer that you -// might have acquired from elsewhere. -LIBHEIF_API -struct heif_context* heif_image_handle_get_context(const struct heif_image_handle* handle); - - -struct heif_image_tiling -{ - int version; - - // --- version 1 - - uint32_t num_columns; - uint32_t num_rows; - uint32_t tile_width; - uint32_t tile_height; - - uint32_t image_width; - uint32_t image_height; - - // Position of the top left tile. - // Usually, this is (0;0), but if a tiled image is rotated or cropped, it may be that the top left tile should be placed at a negative position. - // The offsets define this negative shift. - uint32_t top_offset; - uint32_t left_offset; - - uint8_t number_of_extra_dimensions; // 0 for normal images, 1 for volumetric (3D), ... - uint32_t extra_dimension_size[8]; // size of extra dimensions (first 8 dimensions) -}; - - -// If 'process_image_transformations' is true, this returns modified sizes. -// If it is false, the top_offset and left_offset will always be (0;0). -LIBHEIF_API -struct heif_error heif_image_handle_get_image_tiling(const struct heif_image_handle* handle, int process_image_transformations, struct heif_image_tiling* out_tiling); - - -// For grid images, return the image item ID of a specific grid tile. -// If 'process_image_transformations' is true, the tile positions are given in the transformed image coordinate system and -// are internally mapped to the original image tile positions. -LIBHEIF_API -struct heif_error heif_image_handle_get_grid_image_tile_id(const struct heif_image_handle* handle, - int process_image_transformations, - uint32_t tile_x, uint32_t tile_y, - heif_item_id* out_tile_item_id); - - -struct heif_decoding_options; - -// The tile position is given in tile indices, not in pixel coordinates. -// If the image transformations are processed (option->ignore_image_transformations==false), the tile position -// is given in the transformed coordinates. -LIBHEIF_API -struct heif_error heif_image_handle_decode_image_tile(const struct heif_image_handle* in_handle, - struct heif_image** out_img, - enum heif_colorspace colorspace, - enum heif_chroma chroma, - const struct heif_decoding_options* options, - uint32_t tile_x, uint32_t tile_y); - - -// ------------------------- entity groups ------------------------ - -typedef uint32_t heif_entity_group_id; - -struct heif_entity_group -{ - heif_entity_group_id entity_group_id; - uint32_t entity_group_type; // this is a FourCC constant - heif_item_id* entities; - uint32_t num_entities; -}; - -// Use 0 for `type_filter` or `item_filter` to disable the filter. -// Returns an array of heif_entity_group structs with *out_num_groups entries. -LIBHEIF_API -struct heif_entity_group* heif_context_get_entity_groups(const struct heif_context*, - uint32_t type_filter, - heif_item_id item_filter, - int* out_num_groups); - -// Release an array of entity groups returned by heif_context_get_entity_groups(). -LIBHEIF_API -void heif_entity_groups_release(struct heif_entity_group*, int num_groups); - - -// ------------------------- depth images ------------------------- - -LIBHEIF_API -int heif_image_handle_has_depth_image(const struct heif_image_handle*); - -LIBHEIF_API -int heif_image_handle_get_number_of_depth_images(const struct heif_image_handle* handle); - -LIBHEIF_API -int heif_image_handle_get_list_of_depth_image_IDs(const struct heif_image_handle* handle, - heif_item_id* ids, int count); - -LIBHEIF_API -struct heif_error heif_image_handle_get_depth_image_handle(const struct heif_image_handle* handle, - heif_item_id depth_image_id, - struct heif_image_handle** out_depth_handle); - - -enum heif_depth_representation_type -{ - heif_depth_representation_type_uniform_inverse_Z = 0, - heif_depth_representation_type_uniform_disparity = 1, - heif_depth_representation_type_uniform_Z = 2, - heif_depth_representation_type_nonuniform_disparity = 3 -}; - -struct heif_depth_representation_info -{ - uint8_t version; - - // version 1 fields - - uint8_t has_z_near; - uint8_t has_z_far; - uint8_t has_d_min; - uint8_t has_d_max; - - double z_near; - double z_far; - double d_min; - double d_max; - - enum heif_depth_representation_type depth_representation_type; - uint32_t disparity_reference_view; - - uint32_t depth_nonlinear_representation_model_size; - uint8_t* depth_nonlinear_representation_model; - - // version 2 fields below -}; - - -LIBHEIF_API -void heif_depth_representation_info_free(const struct heif_depth_representation_info* info); - -// Returns true when there is depth_representation_info available -// Note 1: depth_image_id is currently unused because we support only one depth channel per image, but -// you should still provide the correct ID for future compatibility. -// Note 2: Because of an API bug before v1.11.0, the function also works when 'handle' is the handle of the depth image. -// However, you should pass the handle of the main image. Please adapt your code if needed. -LIBHEIF_API -int heif_image_handle_get_depth_image_representation_info(const struct heif_image_handle* handle, - heif_item_id depth_image_id, - const struct heif_depth_representation_info** out); - - - -// ------------------------- thumbnails ------------------------- - -// List the number of thumbnails assigned to this image handle. Usually 0 or 1. -LIBHEIF_API -int heif_image_handle_get_number_of_thumbnails(const struct heif_image_handle* handle); - -LIBHEIF_API -int heif_image_handle_get_list_of_thumbnail_IDs(const struct heif_image_handle* handle, - heif_item_id* ids, int count); - -// Get the image handle of a thumbnail image. -LIBHEIF_API -struct heif_error heif_image_handle_get_thumbnail(const struct heif_image_handle* main_image_handle, - heif_item_id thumbnail_id, - struct heif_image_handle** out_thumbnail_handle); - - -// ------------------------- auxiliary images ------------------------- - -#define LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA (1UL<<1) -#define LIBHEIF_AUX_IMAGE_FILTER_OMIT_DEPTH (2UL<<1) - -// List the number of auxiliary images assigned to this image handle. -LIBHEIF_API -int heif_image_handle_get_number_of_auxiliary_images(const struct heif_image_handle* handle, - int aux_filter); - -LIBHEIF_API -int heif_image_handle_get_list_of_auxiliary_image_IDs(const struct heif_image_handle* handle, - int aux_filter, - heif_item_id* ids, int count); - -// You are responsible to deallocate the returned buffer with heif_image_handle_release_auxiliary_type(). -LIBHEIF_API -struct heif_error heif_image_handle_get_auxiliary_type(const struct heif_image_handle* handle, - const char** out_type); - -LIBHEIF_API -void heif_image_handle_release_auxiliary_type(const struct heif_image_handle* handle, - const char** out_type); - -// DEPRECATED (because typo in function name). Use heif_image_handle_release_auxiliary_type() instead. -LIBHEIF_API -void heif_image_handle_free_auxiliary_types(const struct heif_image_handle* handle, - const char** out_type); - -// Get the image handle of an auxiliary image. -LIBHEIF_API -struct heif_error heif_image_handle_get_auxiliary_image_handle(const struct heif_image_handle* main_image_handle, - heif_item_id auxiliary_id, - struct heif_image_handle** out_auxiliary_handle); - - -// ------------------------- metadata (Exif / XMP) ------------------------- - -// How many metadata blocks are attached to an image. If you only want to get EXIF data, -// set the type_filter to "Exif". Otherwise, set the type_filter to NULL. -LIBHEIF_API -int heif_image_handle_get_number_of_metadata_blocks(const struct heif_image_handle* handle, - const char* type_filter); - -// 'type_filter' can be used to get only metadata of specific types, like "Exif". -// If 'type_filter' is NULL, it will return all types of metadata IDs. -LIBHEIF_API -int heif_image_handle_get_list_of_metadata_block_IDs(const struct heif_image_handle* handle, - const char* type_filter, - heif_item_id* ids, int count); - -// Return a string indicating the type of the metadata, as specified in the HEIF file. -// Exif data will have the type string "Exif". -// This string will be valid until the next call to a libheif function. -// You do not have to free this string. -LIBHEIF_API -const char* heif_image_handle_get_metadata_type(const struct heif_image_handle* handle, - heif_item_id metadata_id); - -// For EXIF, the content type is empty. -// For XMP, the content type is "application/rdf+xml". -LIBHEIF_API -const char* heif_image_handle_get_metadata_content_type(const struct heif_image_handle* handle, - heif_item_id metadata_id); - -// Get the size of the raw metadata, as stored in the HEIF file. -LIBHEIF_API -size_t heif_image_handle_get_metadata_size(const struct heif_image_handle* handle, - heif_item_id metadata_id); - -// 'out_data' must point to a memory area of the size reported by heif_image_handle_get_metadata_size(). -// The data is returned exactly as stored in the HEIF file. -// For Exif data, you probably have to skip the first four bytes of the data, since they -// indicate the offset to the start of the TIFF header of the Exif data. -LIBHEIF_API -struct heif_error heif_image_handle_get_metadata(const struct heif_image_handle* handle, - heif_item_id metadata_id, - void* out_data); - -// Only valid for item type == "uri ", an absolute URI -LIBHEIF_API -const char* heif_image_handle_get_metadata_item_uri_type(const struct heif_image_handle* handle, - heif_item_id metadata_id); - -// ------------------------- color profiles ------------------------- - -enum heif_color_profile_type -{ - heif_color_profile_type_not_present = 0, - heif_color_profile_type_nclx = heif_fourcc('n', 'c', 'l', 'x'), - heif_color_profile_type_rICC = heif_fourcc('r', 'I', 'C', 'C'), - heif_color_profile_type_prof = heif_fourcc('p', 'r', 'o', 'f') -}; - - -// Returns 'heif_color_profile_type_not_present' if there is no color profile. -// If there is an ICC profile and an NCLX profile, the ICC profile is returned. -// TODO: we need a new API for this function as images can contain both NCLX and ICC at the same time. -// However, you can still use heif_image_handle_get_raw_color_profile() and -// heif_image_handle_get_nclx_color_profile() to access both profiles. -LIBHEIF_API -enum heif_color_profile_type heif_image_handle_get_color_profile_type(const struct heif_image_handle* handle); - -LIBHEIF_API -size_t heif_image_handle_get_raw_color_profile_size(const struct heif_image_handle* handle); - -// Returns 'heif_error_Color_profile_does_not_exist' when there is no ICC profile. -LIBHEIF_API -struct heif_error heif_image_handle_get_raw_color_profile(const struct heif_image_handle* handle, - void* out_data); - - -enum heif_color_primaries -{ - heif_color_primaries_ITU_R_BT_709_5 = 1, // g=0.3;0.6, b=0.15;0.06, r=0.64;0.33, w=0.3127,0.3290 - heif_color_primaries_unspecified = 2, - heif_color_primaries_ITU_R_BT_470_6_System_M = 4, - heif_color_primaries_ITU_R_BT_470_6_System_B_G = 5, - heif_color_primaries_ITU_R_BT_601_6 = 6, - heif_color_primaries_SMPTE_240M = 7, - heif_color_primaries_generic_film = 8, - heif_color_primaries_ITU_R_BT_2020_2_and_2100_0 = 9, - heif_color_primaries_SMPTE_ST_428_1 = 10, - heif_color_primaries_SMPTE_RP_431_2 = 11, - heif_color_primaries_SMPTE_EG_432_1 = 12, - heif_color_primaries_EBU_Tech_3213_E = 22 -}; - -enum heif_transfer_characteristics -{ - heif_transfer_characteristic_ITU_R_BT_709_5 = 1, - heif_transfer_characteristic_unspecified = 2, - heif_transfer_characteristic_ITU_R_BT_470_6_System_M = 4, - heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G = 5, - heif_transfer_characteristic_ITU_R_BT_601_6 = 6, - heif_transfer_characteristic_SMPTE_240M = 7, - heif_transfer_characteristic_linear = 8, - heif_transfer_characteristic_logarithmic_100 = 9, - heif_transfer_characteristic_logarithmic_100_sqrt10 = 10, - heif_transfer_characteristic_IEC_61966_2_4 = 11, - heif_transfer_characteristic_ITU_R_BT_1361 = 12, - heif_transfer_characteristic_IEC_61966_2_1 = 13, - heif_transfer_characteristic_ITU_R_BT_2020_2_10bit = 14, - heif_transfer_characteristic_ITU_R_BT_2020_2_12bit = 15, - heif_transfer_characteristic_ITU_R_BT_2100_0_PQ = 16, - heif_transfer_characteristic_SMPTE_ST_428_1 = 17, - heif_transfer_characteristic_ITU_R_BT_2100_0_HLG = 18 -}; - -enum heif_matrix_coefficients -{ - heif_matrix_coefficients_RGB_GBR = 0, - heif_matrix_coefficients_ITU_R_BT_709_5 = 1, // TODO: or 709-6 according to h.273 - heif_matrix_coefficients_unspecified = 2, - heif_matrix_coefficients_US_FCC_T47 = 4, - heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G = 5, - heif_matrix_coefficients_ITU_R_BT_601_6 = 6, // TODO: or 601-7 according to h.273 - heif_matrix_coefficients_SMPTE_240M = 7, - heif_matrix_coefficients_YCgCo = 8, - heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance = 9, - heif_matrix_coefficients_ITU_R_BT_2020_2_constant_luminance = 10, - heif_matrix_coefficients_SMPTE_ST_2085 = 11, - heif_matrix_coefficients_chromaticity_derived_non_constant_luminance = 12, - heif_matrix_coefficients_chromaticity_derived_constant_luminance = 13, - heif_matrix_coefficients_ICtCp = 14 -}; - -struct heif_color_profile_nclx -{ - // === version 1 fields - - uint8_t version; - - enum heif_color_primaries color_primaries; - enum heif_transfer_characteristics transfer_characteristics; - enum heif_matrix_coefficients matrix_coefficients; - uint8_t full_range_flag; - - // --- decoded values (not used when saving nclx) - - float color_primary_red_x, color_primary_red_y; - float color_primary_green_x, color_primary_green_y; - float color_primary_blue_x, color_primary_blue_y; - float color_primary_white_x, color_primary_white_y; -}; - -LIBHEIF_API -struct heif_error heif_nclx_color_profile_set_color_primaries(struct heif_color_profile_nclx* nclx, uint16_t cp); - -LIBHEIF_API -struct heif_error heif_nclx_color_profile_set_transfer_characteristics(struct heif_color_profile_nclx* nclx, uint16_t transfer_characteristics); - -LIBHEIF_API -struct heif_error heif_nclx_color_profile_set_matrix_coefficients(struct heif_color_profile_nclx* nclx, uint16_t matrix_coefficients); - -// Returns 'heif_error_Color_profile_does_not_exist' when there is no NCLX profile. -// TODO: This function does currently not return an NCLX profile if it is stored in the image bitstream. -// Only NCLX profiles stored as colr boxes are returned. This may change in the future. -LIBHEIF_API -struct heif_error heif_image_handle_get_nclx_color_profile(const struct heif_image_handle* handle, - struct heif_color_profile_nclx** out_data); - -// Returned color profile has 'version' field set to the maximum allowed. -// Do not fill values for higher versions as these might be outside the allocated structure size. -// May return NULL. -LIBHEIF_API -struct heif_color_profile_nclx* heif_nclx_color_profile_alloc(void); - -LIBHEIF_API -void heif_nclx_color_profile_free(struct heif_color_profile_nclx* nclx_profile); - -// Note: in early versions of HEIF, there could only be one color profile per image. However, this has been changed. -// This function will now return ICC if one is present and NCLX only if there is no ICC. -// You may better avoid this function and simply query for NCLX and ICC directly. -LIBHEIF_API -enum heif_color_profile_type heif_image_get_color_profile_type(const struct heif_image* image); - -// Returns the size of the ICC profile if one is assigned to the image. Otherwise, it returns 0. -LIBHEIF_API -size_t heif_image_get_raw_color_profile_size(const struct heif_image* image); - -// Returns the ICC profile if one is assigned to the image. Otherwise, it returns an error. -LIBHEIF_API -struct heif_error heif_image_get_raw_color_profile(const struct heif_image* image, - void* out_data); - -LIBHEIF_API -struct heif_error heif_image_get_nclx_color_profile(const struct heif_image* image, - 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, -// and bit depths. - -// Note: when converting images to an interleaved chroma format, the resulting -// image contains only a single channel of type channel_interleaved with, e.g., 3 bytes per pixel, -// containing the interleaved R,G,B values. - -// Planar RGB images are specified as heif_colorspace_RGB / heif_chroma_444. - -enum heif_progress_step -{ - heif_progress_step_total = 0, - heif_progress_step_load_tile = 1 -}; - - -enum heif_chroma_downsampling_algorithm -{ - heif_chroma_downsampling_nearest_neighbor = 1, - heif_chroma_downsampling_average = 2, - - // Combine with 'heif_chroma_upsampling_bilinear' for best quality. - // Makes edges look sharper when using YUV 420 with bilinear chroma upsampling. - heif_chroma_downsampling_sharp_yuv = 3 -}; - -enum heif_chroma_upsampling_algorithm -{ - heif_chroma_upsampling_nearest_neighbor = 1, - heif_chroma_upsampling_bilinear = 2 -}; - - -struct heif_color_conversion_options -{ - // 'version' must be 1. - uint8_t version; - - // --- version 1 options - - enum heif_chroma_downsampling_algorithm preferred_chroma_downsampling_algorithm; - enum heif_chroma_upsampling_algorithm preferred_chroma_upsampling_algorithm; - - // When set to 'false' libheif may also use a different algorithm if the preferred one is not available - // or using a different algorithm is computationally less complex. Note that currently (v1.17.0) this - // means that for RGB input it will usually choose nearest-neighbor sampling because this is computationally - // the simplest. - // Set this field to 'true' if you want to make sure that the specified algorithm is used even - // at the cost of slightly higher computation times. - uint8_t only_use_preferred_chroma_algorithm; - - // --- Note that we cannot extend this struct because it is embedded in - // other structs (heif_decoding_options and heif_encoding_options). -}; - -// Assumes that it is a version=1 struct. -LIBHEIF_API -void heif_color_conversion_options_set_defaults(struct heif_color_conversion_options*); - - -struct heif_decoding_options -{ - uint8_t version; - - // version 1 options - - // Ignore geometric transformations like cropping, rotation, mirroring. - // Default: false (do not ignore). - uint8_t ignore_transformations; - - // Any of the progress functions may be called from background threads. - void (* start_progress)(enum heif_progress_step step, int max_progress, void* progress_user_data); - - void (* on_progress)(enum heif_progress_step step, int progress, void* progress_user_data); - - void (* end_progress)(enum heif_progress_step step, void* progress_user_data); - - void* progress_user_data; - - // version 2 options - - uint8_t convert_hdr_to_8bit; - - // version 3 options - - // When enabled, an error is returned for invalid input. Otherwise, it will try its best and - // add decoding warnings to the decoded heif_image. Default is non-strict. - uint8_t strict_decoding; - - // version 4 options - - // name_id of the decoder to use for the decoding. - // If set to NULL (default), the highest priority decoder is chosen. - // The priority is defined in the plugin. - const char* decoder_id; - - // version 5 options - - struct heif_color_conversion_options color_conversion_options; - - // version 6 options - - int (* cancel_decoding)(void* progress_user_data); -}; - - -// Allocate decoding options and fill with default values. -// Note: you should always get the decoding options through this function since the -// option structure may grow in size in future versions. -LIBHEIF_API -struct heif_decoding_options* heif_decoding_options_alloc(void); - -LIBHEIF_API -void heif_decoding_options_free(struct heif_decoding_options*); - -// Decode an heif_image_handle into the actual pixel image and also carry out -// all geometric transformations specified in the HEIF file (rotation, cropping, mirroring). -// -// If colorspace or chroma is set to heif_colorspace_undefined or heif_chroma_undefined, -// respectively, the original colorspace is taken. -// Decoding options may be NULL. If you want to supply options, always use -// heif_decoding_options_alloc() to get the structure. -LIBHEIF_API -struct heif_error heif_decode_image(const struct heif_image_handle* in_handle, - struct heif_image** out_img, - enum heif_colorspace colorspace, - enum heif_chroma chroma, - const struct heif_decoding_options* options); - -// Get the colorspace format of the image. -LIBHEIF_API -enum heif_colorspace heif_image_get_colorspace(const struct heif_image*); - -// Get the chroma format of the image. -LIBHEIF_API -enum heif_chroma heif_image_get_chroma_format(const struct heif_image*); - -/** - * Get the width of a specified image channel. - * - * @param img the image to get the width for - * @param channel the channel to select - * @return the width of the channel in pixels, or -1 the channel does not exist in the image - */ -LIBHEIF_API -int heif_image_get_width(const struct heif_image* img, enum heif_channel channel); - -/** - * Get the height of a specified image channel. - * - * @param img the image to get the height for - * @param channel the channel to select - * @return the height of the channel in pixels, or -1 the channel does not exist in the image - */ -LIBHEIF_API -int heif_image_get_height(const struct heif_image* img, enum heif_channel channel); - -/** - * Get the width of the main channel. - * - * This is the Y channel in YCbCr or mono, or any in RGB. - * - * @param img the image to get the primary width for - * @return the width in pixels - */ -LIBHEIF_API -int heif_image_get_primary_width(const struct heif_image* img); - -/** - * Get the height of the main channel. - * - * This is the Y channel in YCbCr or mono, or any in RGB. - * - * @param img the image to get the primary height for - * @return the height in pixels - */ -LIBHEIF_API -int heif_image_get_primary_height(const struct heif_image* img); - -LIBHEIF_API -struct heif_error heif_image_crop(struct heif_image* img, - int left, int right, int top, int bottom); - -// Get the number of bits per pixel in the given image channel. Returns -1 if -// a non-existing channel was given. -// Note that the number of bits per pixel may be different for each color channel. -// This function returns the number of bits used for storage of each pixel. -// Especially for HDR images, this is probably not what you want. Have a look at -// heif_image_get_bits_per_pixel_range() instead. -LIBHEIF_API -int heif_image_get_bits_per_pixel(const struct heif_image*, enum heif_channel channel); - -// Get the number of bits per pixel in the given image channel. This function returns -// the number of bits used for representing the pixel value, which might be smaller -// than the number of bits used in memory. -// For example, in 12bit HDR images, this function returns '12', while still 16 bits -// are reserved for storage. For interleaved RGBA with 12 bit, this function also returns -// '12', not '48' or '64' (heif_image_get_bits_per_pixel returns 64 in this case). -LIBHEIF_API -int heif_image_get_bits_per_pixel_range(const struct heif_image*, enum heif_channel channel); - -LIBHEIF_API -int heif_image_has_channel(const struct heif_image*, enum heif_channel channel); - -// Get a pointer to the actual pixel data. -// The 'out_stride' is returned as "bytes per line". -// When out_stride is NULL, no value will be written. -// Returns NULL if a non-existing channel was given. -// TODO: it would be better if the 'stride' parameter would be size_t to prevent integer overflows when this value is multiplicated with large y coordinates. -LIBHEIF_API -const uint8_t* heif_image_get_plane_readonly(const struct heif_image*, - enum heif_channel channel, - int* out_stride); - -LIBHEIF_API -uint8_t* heif_image_get_plane(struct heif_image*, - enum heif_channel channel, - int* out_stride); - - - -struct heif_scaling_options; - -// Currently, heif_scaling_options is not defined yet. Pass a NULL pointer. -LIBHEIF_API -struct heif_error heif_image_scale_image(const struct heif_image* input, - struct heif_image** output, - int width, int height, - const struct heif_scaling_options* options); - -// Extends the image size to match the given size by extending the right and bottom borders. -// The border areas are filled with zero. -LIBHEIF_API -struct heif_error heif_image_extend_to_size_fill_with_zero(struct heif_image* image, - uint32_t width, uint32_t height); - -// The color profile is not attached to the image handle because we might need it -// for color space transform and encoding. -LIBHEIF_API -struct heif_error heif_image_set_raw_color_profile(struct heif_image* image, - const char* profile_type_fourcc_string, - const void* profile_data, - const size_t profile_size); - -LIBHEIF_API -struct heif_error heif_image_set_nclx_color_profile(struct heif_image* image, - const struct heif_color_profile_nclx* color_profile); - - -// TODO: this function does not make any sense yet, since we currently cannot modify existing HEIF files. -//LIBHEIF_API -//void heif_image_remove_color_profile(struct heif_image* image); - -// Fills the image decoding warnings into the provided 'out_warnings' array. -// The size of the array has to be provided in max_output_buffer_entries. -// If max_output_buffer_entries==0, the number of decoder warnings is returned. -// The function fills the warnings into the provided buffer, starting with 'first_warning_idx'. -// It returns the number of warnings filled into the buffer. -// Note: you can iterate through all warnings by using 'max_output_buffer_entries=1' and iterate 'first_warning_idx'. -LIBHEIF_API -int heif_image_get_decoding_warnings(struct heif_image* image, - int first_warning_idx, - struct heif_error* out_warnings, - int max_output_buffer_entries); - -// This function is only for decoder plugin implementors. -LIBHEIF_API -void heif_image_add_decoding_warning(struct heif_image* image, - struct heif_error err); - -// Release heif_image. -LIBHEIF_API -void heif_image_release(const struct heif_image*); - - -// Note: a value of 0 for any of these values indicates that the value is undefined. -// The unit of these values is Candelas per square meter. -struct heif_content_light_level -{ - uint16_t max_content_light_level; - uint16_t max_pic_average_light_level; -}; - -LIBHEIF_API -int heif_image_has_content_light_level(const struct heif_image*); - -LIBHEIF_API -void heif_image_get_content_light_level(const struct heif_image*, struct heif_content_light_level* out); - -// Returns whether the image has 'content light level' information. If 0 is returned, the output is not filled. -LIBHEIF_API -int heif_image_handle_get_content_light_level(const struct heif_image_handle*, struct heif_content_light_level* out); - -LIBHEIF_API -void heif_image_set_content_light_level(const struct heif_image*, const struct heif_content_light_level* in); - - -// Note: color coordinates are defined according to the CIE 1931 definition of x as specified in ISO 11664-1 (see also ISO 11664-3 and CIE 15). -struct heif_mastering_display_colour_volume -{ - uint16_t display_primaries_x[3]; - uint16_t display_primaries_y[3]; - uint16_t white_point_x; - uint16_t white_point_y; - uint32_t max_display_mastering_luminance; - uint32_t min_display_mastering_luminance; -}; - -// The units for max_display_mastering_luminance and min_display_mastering_luminance is Candelas per square meter. -struct heif_decoded_mastering_display_colour_volume -{ - float display_primaries_x[3]; - float display_primaries_y[3]; - float white_point_x; - float white_point_y; - double max_display_mastering_luminance; - double min_display_mastering_luminance; -}; - -struct heif_ambient_viewing_environment -{ - uint32_t ambient_illumination; - uint16_t ambient_light_x; - uint16_t ambient_light_y; -}; - -LIBHEIF_API -int heif_image_has_mastering_display_colour_volume(const struct heif_image*); - -LIBHEIF_API -void heif_image_get_mastering_display_colour_volume(const struct heif_image*, struct heif_mastering_display_colour_volume* out); - -// Returns whether the image has 'mastering display colour volume' information. If 0 is returned, the output is not filled. -LIBHEIF_API -int heif_image_handle_get_mastering_display_colour_volume(const struct heif_image_handle*, struct heif_mastering_display_colour_volume* out); - -LIBHEIF_API -void heif_image_set_mastering_display_colour_volume(const struct heif_image*, const struct heif_mastering_display_colour_volume* in); - - -// Converts the internal numeric representation of heif_mastering_display_colour_volume to the -// normalized values, collected in heif_decoded_mastering_display_colour_volume. -// Values that are out-of-range are decoded to 0, indicating an undefined value (as specified in ISO/IEC 23008-2). -LIBHEIF_API -struct heif_error heif_mastering_display_colour_volume_decode(const struct heif_mastering_display_colour_volume* in, - struct heif_decoded_mastering_display_colour_volume* out); - -LIBHEIF_API -void heif_image_get_pixel_aspect_ratio(const struct heif_image*, uint32_t* aspect_h, uint32_t* aspect_v); - -// Returns whether the image has 'pixel aspect ratio information' information. If 0 is returned, the output is filled with the 1:1 default. -LIBHEIF_API -int heif_image_handle_get_pixel_aspect_ratio(const struct heif_image_handle*, uint32_t* aspect_h, uint32_t* aspect_v); - -LIBHEIF_API -void heif_image_set_pixel_aspect_ratio(struct heif_image*, uint32_t aspect_h, uint32_t aspect_v); - -// ==================================================================================================== -// Encoding API - -LIBHEIF_API -struct heif_error heif_context_write_to_file(struct heif_context*, - const char* filename); - -struct heif_writer -{ - // API version supported by this writer - int writer_api_version; - - // --- version 1 functions --- - - // On success, the returned heif_error may have a NULL message. It will automatically be replaced with a "Success" string. - struct heif_error (* write)(struct heif_context* ctx, // TODO: why do we need this parameter? - const void* data, - size_t size, - void* userdata); -}; - -LIBHEIF_API -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 ----- - -// The encoder used for actually encoding an image. -struct heif_encoder; - -// A description of the encoder's capabilities and name. -struct heif_encoder_descriptor; - -// A configuration parameter of the encoder. Each encoder implementation may have a different -// set of parameters. For the most common settings (e.q. quality), special functions to set -// the parameters are provided. -struct heif_encoder_parameter; - -struct heif_decoder_descriptor; - -// Get a list of available decoders. You can filter the encoders by compression format. -// Use format_filter==heif_compression_undefined to get all available decoders. -// The returned list of decoders is sorted by their priority (which is a plugin property). -// The number of decoders is returned, which are not more than 'count' if (out_decoders != nullptr). -// By setting out_decoders==nullptr, you can query the number of decoders, 'count' is ignored. -LIBHEIF_API -int heif_get_decoder_descriptors(enum heif_compression_format format_filter, - const struct heif_decoder_descriptor** out_decoders, - int count); - -// Return a long, descriptive name of the decoder (including version information). -LIBHEIF_API -const char* heif_decoder_descriptor_get_name(const struct heif_decoder_descriptor*); - -// Return a short, symbolic name for identifying the decoder. -// This name should stay constant over different decoder versions. -// Note: the returned ID may be NULL for old plugins that don't support this yet. -LIBHEIF_API -const char* heif_decoder_descriptor_get_id_name(const struct heif_decoder_descriptor*); - -// DEPRECATED: use heif_get_encoder_descriptors() instead. -// Get a list of available encoders. You can filter the encoders by compression format and name. -// Use format_filter==heif_compression_undefined and name_filter==NULL as wildcards. -// The returned list of encoders is sorted by their priority (which is a plugin property). -// The number of encoders is returned, which are not more than 'count' if (out_encoders != nullptr). -// By setting out_encoders==nullptr, you can query the number of encoders, 'count' is ignored. -// Note: to get the actual encoder from the descriptors returned here, use heif_context_get_encoder(). -LIBHEIF_API -int heif_context_get_encoder_descriptors(struct heif_context*, // TODO: why do we need this parameter? - enum heif_compression_format format_filter, - const char* name_filter, - const struct heif_encoder_descriptor** out_encoders, - int count); - -// Get a list of available encoders. You can filter the encoders by compression format and name. -// Use format_filter==heif_compression_undefined and name_filter==NULL as wildcards. -// The returned list of encoders is sorted by their priority (which is a plugin property). -// The number of encoders is returned, which are not more than 'count' if (out_encoders != nullptr). -// By setting out_encoders==nullptr, you can query the number of encoders, 'count' is ignored. -// Note: to get the actual encoder from the descriptors returned here, use heif_context_get_encoder(). -LIBHEIF_API -int heif_get_encoder_descriptors(enum heif_compression_format format_filter, - const char* name_filter, - const struct heif_encoder_descriptor** out_encoders, - int count); - -// Return a long, descriptive name of the encoder (including version information). -LIBHEIF_API -const char* heif_encoder_descriptor_get_name(const struct heif_encoder_descriptor*); - -// Return a short, symbolic name for identifying the encoder. -// This name should stay constant over different encoder versions. -LIBHEIF_API -const char* heif_encoder_descriptor_get_id_name(const struct heif_encoder_descriptor*); - -LIBHEIF_API -enum heif_compression_format -heif_encoder_descriptor_get_compression_format(const struct heif_encoder_descriptor*); - -LIBHEIF_API -int heif_encoder_descriptor_supports_lossy_compression(const struct heif_encoder_descriptor*); - -LIBHEIF_API -int heif_encoder_descriptor_supports_lossless_compression(const struct heif_encoder_descriptor*); - - -// Get an encoder instance that can be used to actually encode images from a descriptor. -LIBHEIF_API -struct heif_error heif_context_get_encoder(struct heif_context* context, - const struct heif_encoder_descriptor*, - struct heif_encoder** out_encoder); - -// Quick check whether there is a decoder available for the given format. -// Note that the decoder still may not be able to decode all variants of that format. -// You will have to query that further (todo) or just try to decode and check the returned error. -LIBHEIF_API -int heif_have_decoder_for_format(enum heif_compression_format format); - -// Quick check whether there is an enoder available for the given format. -// Note that the encoder may be limited to a certain subset of features (e.g. only 8 bit, only lossy). -// You will have to query the specific capabilities further. -LIBHEIF_API -int heif_have_encoder_for_format(enum heif_compression_format format); - -// Get an encoder for the given compression format. If there are several encoder plugins -// for this format, the encoder with the highest plugin priority will be returned. -LIBHEIF_API -struct heif_error heif_context_get_encoder_for_format(struct heif_context* context, - enum heif_compression_format format, - struct heif_encoder**); - -// You have to release the encoder after use. -LIBHEIF_API -void heif_encoder_release(struct heif_encoder*); - -// Get the encoder name from the encoder itself. -LIBHEIF_API -const char* heif_encoder_get_name(const struct heif_encoder*); - - -// --- Encoder Parameters --- - -// Libheif supports settings parameters through specialized functions and through -// generic functions by parameter name. Sometimes, the same parameter can be set -// in both ways. -// We consider it best practice to use the generic parameter functions only in -// dynamically generated user interfaces, as no guarantees are made that some specific -// parameter names are supported by all plugins. - - -// Set a 'quality' factor (0-100). How this is mapped to actual encoding parameters is -// encoder dependent. -LIBHEIF_API -struct heif_error heif_encoder_set_lossy_quality(struct heif_encoder*, int quality); - -LIBHEIF_API -struct heif_error heif_encoder_set_lossless(struct heif_encoder*, int enable); - -// level should be between 0 (= none) to 4 (= full) -LIBHEIF_API -struct heif_error heif_encoder_set_logging_level(struct heif_encoder*, int level); - -// Get a generic list of encoder parameters. -// Each encoder may define its own, additional set of parameters. -// You do not have to free the returned list. -LIBHEIF_API -const struct heif_encoder_parameter* const* heif_encoder_list_parameters(struct heif_encoder*); - -// Return the parameter name. -LIBHEIF_API -const char* heif_encoder_parameter_get_name(const struct heif_encoder_parameter*); - - -enum heif_encoder_parameter_type -{ - heif_encoder_parameter_type_integer = 1, - heif_encoder_parameter_type_boolean = 2, - heif_encoder_parameter_type_string = 3 -}; - -// Return the parameter type. -LIBHEIF_API -enum heif_encoder_parameter_type heif_encoder_parameter_get_type(const struct heif_encoder_parameter*); - -// DEPRECATED. Use heif_encoder_parameter_get_valid_integer_values() instead. -LIBHEIF_API -struct heif_error heif_encoder_parameter_get_valid_integer_range(const struct heif_encoder_parameter*, - int* have_minimum_maximum, - int* minimum, int* maximum); - -// If integer is limited by a range, have_minimum and/or have_maximum will be != 0 and *minimum, *maximum is set. -// If integer is limited by a fixed set of values, *num_valid_values will be >0 and *out_integer_array is set. -LIBHEIF_API -struct heif_error heif_encoder_parameter_get_valid_integer_values(const struct heif_encoder_parameter*, - int* have_minimum, int* have_maximum, - int* minimum, int* maximum, - int* num_valid_values, - const int** out_integer_array); - -LIBHEIF_API -struct heif_error heif_encoder_parameter_get_valid_string_values(const struct heif_encoder_parameter*, - const char* const** out_stringarray); - - -LIBHEIF_API -struct heif_error heif_encoder_set_parameter_integer(struct heif_encoder*, - const char* parameter_name, - int value); - -LIBHEIF_API -struct heif_error heif_encoder_get_parameter_integer(struct heif_encoder*, - const char* parameter_name, - int* value); - -// TODO: 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, - int* have_minimum_maximum, - int* minimum, int* maximum); - -LIBHEIF_API -struct heif_error heif_encoder_set_parameter_boolean(struct heif_encoder*, - const char* parameter_name, - int value); - -LIBHEIF_API -struct heif_error heif_encoder_get_parameter_boolean(struct heif_encoder*, - const char* parameter_name, - int* value); - -LIBHEIF_API -struct heif_error heif_encoder_set_parameter_string(struct heif_encoder*, - const char* parameter_name, - const char* value); - -LIBHEIF_API -struct heif_error heif_encoder_get_parameter_string(struct heif_encoder*, - const char* parameter_name, - char* value, int value_size); - -// returns a NULL-terminated list of valid strings or NULL if all values are allowed -LIBHEIF_API -struct heif_error heif_encoder_parameter_string_valid_values(struct heif_encoder*, - const char* parameter_name, - const char* const** out_stringarray); - -LIBHEIF_API -struct heif_error heif_encoder_parameter_integer_valid_values(struct heif_encoder*, - const char* parameter_name, - int* have_minimum, int* have_maximum, - int* minimum, int* maximum, - int* num_valid_values, - const int** out_integer_array); - -// Set a parameter of any type to the string value. -// Integer values are parsed from the string. -// Boolean values can be "true"/"false"/"1"/"0" -// -// x265 encoder specific note: -// When using the x265 encoder, you may pass any of its parameters by -// prefixing the parameter name with 'x265:'. Hence, to set the 'ctu' parameter, -// you will have to set 'x265:ctu' in libheif. -// Note that there is no checking for valid parameters when using the prefix. -LIBHEIF_API -struct heif_error heif_encoder_set_parameter(struct heif_encoder*, - const char* parameter_name, - const char* value); - -// Get the current value of a parameter of any type as a human readable string. -// The returned string is compatible with heif_encoder_set_parameter(). -LIBHEIF_API -struct heif_error heif_encoder_get_parameter(struct heif_encoder*, - const char* parameter_name, - char* value_ptr, int value_size); - -// Query whether a specific parameter has a default value. -LIBHEIF_API -int heif_encoder_has_default(struct heif_encoder*, - const char* parameter_name); - - -// The orientation values are defined equal to the EXIF Orientation tag. -enum heif_orientation -{ - heif_orientation_normal = 1, - heif_orientation_flip_horizontally = 2, - heif_orientation_rotate_180 = 3, - heif_orientation_flip_vertically = 4, - heif_orientation_rotate_90_cw_then_flip_horizontally = 5, - heif_orientation_rotate_90_cw = 6, - heif_orientation_rotate_90_cw_then_flip_vertically = 7, - heif_orientation_rotate_270_cw = 8 -}; - - -struct heif_encoding_options -{ - uint8_t version; - - // version 1 options - - uint8_t save_alpha_channel; // default: true - - // version 2 options - - // DEPRECATED. This option is not required anymore. Its value will be ignored. - uint8_t macOS_compatibility_workaround; - - // version 3 options - - uint8_t save_two_colr_boxes_when_ICC_and_nclx_available; // default: false - - // version 4 options - - // Set this to the NCLX parameters to be used in the output image or set to NULL - // when the same parameters as in the input image should be used. - struct heif_color_profile_nclx* output_nclx_profile; - - uint8_t macOS_compatibility_workaround_no_nclx_profile; - - // version 5 options - - // libheif will generate irot/imir boxes to match these orientations - enum heif_orientation image_orientation; - - // 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; - - // TODO: we should add a flag to force MIAF compatible outputs. E.g. this will put restrictions on grid tile sizes and - // might add a clap box when the grid output size does not match the color subsampling factors. - // Since some of these constraints have to be known before actually encoding the image, "forcing MIAF compatibility" - // could also be a flag in the heif_context. -}; - -LIBHEIF_API -struct heif_encoding_options* heif_encoding_options_alloc(void); - -LIBHEIF_API -void heif_encoding_options_free(struct heif_encoding_options*); - - -// Compress the input image. -// Returns a handle to the coded image in 'out_image_handle' unless out_image_handle = NULL. -// 'options' should be NULL for now. -// The first image added to the context is also automatically set the primary image, but -// you can change the primary image later with heif_context_set_primary_image(). -LIBHEIF_API -struct heif_error heif_context_encode_image(struct heif_context*, - const struct heif_image* image, - struct heif_encoder* encoder, - 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_add_grid_image(struct heif_context* ctx, - uint32_t image_width, - uint32_t image_height, - uint32_t tile_columns, - uint32_t tile_rows, - const struct heif_encoding_options* encoding_options, - struct heif_image_handle** out_grid_image_handle); - -LIBHEIF_API -struct heif_error heif_context_add_image_tile(struct heif_context* ctx, - struct heif_image_handle* tiled_image, - uint32_t tile_x, uint32_t tile_y, - const struct heif_image* image, - struct heif_encoder* encoder); - -// offsets[] should either be NULL (all offsets==0) or an array of size 2*nImages with x;y offset pairs. -// If background_rgba is NULL, the background is transparent. -LIBHEIF_API -struct heif_error heif_context_add_overlay_image(struct heif_context* ctx, - uint32_t image_width, - uint32_t image_height, - uint16_t nImages, - const heif_item_id* image_ids, - int32_t* offsets, - const uint16_t background_rgba[4], - struct heif_image_handle** out_iovl_image_handle); - -LIBHEIF_API -struct heif_error heif_context_set_primary_image(struct heif_context*, - struct heif_image_handle* image_handle); - -// Encode the 'image' as a scaled down thumbnail image. -// The image is scaled down to fit into a square area of width 'bbox_size'. -// If the input image is already so small that it fits into this bounding box, no thumbnail -// image is encoded and NULL is returned in 'out_thumb_image_handle'. -// No error is returned in this case. -// The encoded thumbnail is automatically assigned to the 'master_image_handle'. Hence, you -// do not have to call heif_context_assign_thumbnail(). -LIBHEIF_API -struct heif_error heif_context_encode_thumbnail(struct heif_context*, - const struct heif_image* image, - const struct heif_image_handle* master_image_handle, - struct heif_encoder* encoder, - const struct heif_encoding_options* options, - int bbox_size, - struct heif_image_handle** out_thumb_image_handle); - -// Assign 'thumbnail_image' as the thumbnail image of 'master_image'. -LIBHEIF_API -struct heif_error heif_context_assign_thumbnail(struct heif_context*, - const struct heif_image_handle* master_image, - const struct heif_image_handle* thumbnail_image); - -// Add EXIF metadata to an image. -LIBHEIF_API -struct heif_error heif_context_add_exif_metadata(struct heif_context*, - const struct heif_image_handle* image_handle, - const void* data, int size); - -// Add XMP metadata to an image. -LIBHEIF_API -struct heif_error heif_context_add_XMP_metadata(struct heif_context*, - const struct heif_image_handle* image_handle, - const void* data, int size); - -// New version of heif_context_add_XMP_metadata() with data compression (experimental). -LIBHEIF_API -struct heif_error heif_context_add_XMP_metadata2(struct heif_context*, - const struct heif_image_handle* image_handle, - const void* data, int size, - enum heif_metadata_compression compression); - -// Add generic, proprietary metadata to an image. You have to specify an 'item_type' that will -// identify your metadata. 'content_type' can be an additional type, or it can be NULL. -// For example, this function can be used to add IPTC metadata (IIM stream, not XMP) to an image. -// Although not standard, we propose to store IPTC data with item type="iptc", content_type=NULL. -LIBHEIF_API -struct heif_error heif_context_add_generic_metadata(struct heif_context* ctx, - const struct heif_image_handle* image_handle, - 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 - -/** - * Create a new image of the specified resolution and colorspace. - * - *

This does not allocate memory for the image data. Use {@link heif_image_add_plane} to - * add the corresponding planes to match the specified {@code colorspace} and {@code chroma}. - * - * @param width the width of the image in pixels - * @param height the height of the image in pixels - * @param colorspace the colorspace of the image - * @param chroma the chroma of the image - * @param out_image pointer to pointer of the resulting image - * @return whether the creation succeeded or there was an error -*/ -LIBHEIF_API -struct heif_error heif_image_create(int width, int height, - enum heif_colorspace colorspace, - enum heif_chroma chroma, - struct heif_image** out_image); - -/** - * Add an image plane to the image. - * - *

The image plane needs to match the colorspace and chroma of the image. Note - * that this does not need to be a single "planar" format - interleaved pixel channels - * can also be used if the chroma is interleaved. - * - *

The indicated bit_depth corresponds to the bit depth per channel. For example, - * with an interleaved format like RRGGBB where each color is represented by 10 bits, - * the {@code bit_depth} would be {@code 10} rather than {@code 30}. - * - *

For backward compatibility, one can also specify 24bits for RGB and 32bits for RGBA, - * instead of the preferred 8 bits. However, this use is deprecated. - * - * @param image the parent image to add the channel plane to - * @param channel the channel of the plane to add - * @param width the width of the plane - * @param height the height of the plane - * @param bit_depth the bit depth per color channel - * @return whether the addition succeeded or there was an error - * - * @note The width and height are usually the same as the parent image, but can be - * less for subsampling. - * - * @note The specified width can differ from the row stride of the resulting image plane. - * Always use the result of {@link heif_image_get_plane} or {@link heif_image_get_plane_readonly} - * to determine row stride. - */ -LIBHEIF_API -struct heif_error heif_image_add_plane(struct heif_image* image, - enum heif_channel channel, - int width, int height, int bit_depth); - -// Signal that the image is premultiplied by the alpha pixel values. -LIBHEIF_API -void heif_image_set_premultiplied_alpha(struct heif_image* image, - int is_premultiplied_alpha); - -LIBHEIF_API -int heif_image_is_premultiplied_alpha(struct heif_image* image); - -// This function extends the padding of the image so that it has at least the given physical size. -// The padding border is filled with the pixels along the right/bottom border. -// This function may be useful if you want to process the image, but have some external padding requirements. -// The image size will not be modified if it is already larger/equal than the given physical size. -// I.e. you cannot assume that after calling this function, the stride will be equal to min_physical_width. -LIBHEIF_API -struct heif_error heif_image_extend_padding_to_size(struct heif_image* image, int min_physical_width, int min_physical_height); - - - -// --- register plugins - -struct heif_decoder_plugin; -struct heif_encoder_plugin; - -// DEPRECATED. Use heif_register_decoder_plugin(const struct heif_decoder_plugin*) instead. -LIBHEIF_API -struct heif_error heif_register_decoder(struct heif_context* heif, const struct heif_decoder_plugin*); - -LIBHEIF_API -struct heif_error heif_register_decoder_plugin(const struct heif_decoder_plugin*); - -LIBHEIF_API -struct heif_error heif_register_encoder_plugin(const struct heif_encoder_plugin*); - -// DEPRECATED, typo in function name -LIBHEIF_API -int heif_encoder_descriptor_supportes_lossy_compression(const struct heif_encoder_descriptor*); - -// DEPRECATED, typo in function name -LIBHEIF_API -int heif_encoder_descriptor_supportes_lossless_compression(const struct heif_encoder_descriptor*); - - -#ifdef __cplusplus -} -#endif +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_aux_images.cc libheif-1.23.4/libheif/api/libheif/heif_aux_images.cc --- libheif-1.19.8/libheif/api/libheif/heif_aux_images.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_aux_images.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,351 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_aux_images.h" +#include "api_structs.h" + +#include +#include +#include +#include + + +// ------------------------- depth images ------------------------- + + +int heif_image_handle_has_depth_image(const heif_image_handle* handle) +{ + return handle->image->get_depth_channel() != nullptr; +} + + +int heif_image_handle_get_number_of_depth_images(const heif_image_handle* handle) +{ + auto depth_image = handle->image->get_depth_channel(); + + if (depth_image) { + return 1; + } + else { + return 0; + } +} + + +int heif_image_handle_get_list_of_depth_image_IDs(const heif_image_handle* handle, + heif_item_id* ids, int count) +{ + auto depth_image = handle->image->get_depth_channel(); + + if (count == 0) { + return 0; + } + + if (depth_image) { + ids[0] = depth_image->get_id(); + return 1; + } + else { + return 0; + } +} + + +heif_error heif_image_handle_get_depth_image_handle(const heif_image_handle* handle, + heif_item_id depth_id, + heif_image_handle** out_depth_handle) +{ + if (out_depth_handle == nullptr) { + return heif_error_null_pointer_argument; + } + + auto depth_image = handle->image->get_depth_channel(); + + if (!depth_image || depth_image->get_id() != depth_id) { + *out_depth_handle = nullptr; + + Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); + return err.error_struct(handle->image.get()); + } + + // Reject an item that could not be parsed (e.g. an unsupported codec) here, + // like heif_context_get_image_handle() does, rather than handing out a handle + // that can only fail at decode. + if (Error error = depth_image->get_item_error()) { + *out_depth_handle = nullptr; + return error.error_struct(handle->image.get()); + } + + *out_depth_handle = new heif_image_handle(); + (*out_depth_handle)->image = depth_image; + (*out_depth_handle)->context = handle->context; + + return Error::Ok.error_struct(handle->image.get()); +} + + +void heif_depth_representation_info_free(const heif_depth_representation_info* info) +{ + delete info; +} + + +int heif_image_handle_get_depth_image_representation_info(const heif_image_handle* handle, + heif_item_id depth_image_id, + const heif_depth_representation_info** out) +{ + std::shared_ptr depth_image; + + if (out) { + if (handle->image->is_depth_channel()) { + // Because of an API bug before v1.11.0, the input handle may be the depth image (#422). + depth_image = handle->image; + } + else { + depth_image = handle->image->get_depth_channel(); + } + + if (depth_image && depth_image->has_depth_representation_info()) { + auto info = new heif_depth_representation_info; + *info = depth_image->get_depth_representation_info(); + *out = info; + return true; + } + else { + *out = nullptr; + } + } + + return false; +} + + +// ------------------------- thumbnails ------------------------- + + +int heif_image_handle_get_number_of_thumbnails(const heif_image_handle* handle) +{ + return (int) handle->image->get_thumbnails().size(); +} + + +int heif_image_handle_get_list_of_thumbnail_IDs(const heif_image_handle* handle, + heif_item_id* ids, int count) +{ + if (ids == nullptr) { + return 0; + } + + auto thumbnails = handle->image->get_thumbnails(); + int n = (int) std::min(count, (int) thumbnails.size()); + + for (int i = 0; i < n; i++) { + ids[i] = thumbnails[i]->get_id(); + } + + return n; +} + + +heif_error heif_image_handle_get_thumbnail(const heif_image_handle* handle, + heif_item_id thumbnail_id, + heif_image_handle** out_thumbnail_handle) +{ + if (!out_thumbnail_handle) { + return heif_error_null_pointer_argument; + } + + auto thumbnails = handle->image->get_thumbnails(); + for (const auto& thumb : thumbnails) { + if (thumb->get_id() == thumbnail_id) { + if (Error error = thumb->get_item_error()) { + *out_thumbnail_handle = nullptr; + return error.error_struct(handle->image.get()); + } + *out_thumbnail_handle = new heif_image_handle(); + (*out_thumbnail_handle)->image = thumb; + (*out_thumbnail_handle)->context = handle->context; + + return Error::Ok.error_struct(handle->image.get()); + } + } + + Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); + return err.error_struct(handle->image.get()); +} + + +heif_error heif_context_encode_thumbnail(heif_context* ctx, + const heif_image* image, + const heif_image_handle* image_handle, + heif_encoder* encoder, + const heif_encoding_options* input_options, + int bbox_size, + heif_image_handle** out_image_handle) +{ + heif_encoding_options* options = heif_encoding_options_alloc(); + heif_encoding_options_copy(options, input_options); + + auto encodingResult = ctx->context->encode_thumbnail(image->image, + encoder, + *options, + bbox_size); + heif_encoding_options_free(options); + + if (!encodingResult) { + return encodingResult.error_struct(ctx->context.get()); + } + + std::shared_ptr thumbnail_image = *encodingResult; + + if (!thumbnail_image) { + Error err(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Thumbnail images must be smaller than the original image."); + return err.error_struct(ctx->context.get()); + } + + Error error = ctx->context->assign_thumbnail(image_handle->image, thumbnail_image); + if (error != Error::Ok) { + return error.error_struct(ctx->context.get()); + } + + + if (out_image_handle) { + *out_image_handle = new heif_image_handle; + (*out_image_handle)->image = thumbnail_image; + (*out_image_handle)->context = ctx->context; + } + + return heif_error_success; +} + + +heif_error heif_context_assign_thumbnail(heif_context* ctx, + const heif_image_handle* master_image, + const heif_image_handle* thumbnail_image) +{ + Error error = ctx->context->assign_thumbnail(thumbnail_image->image, master_image->image); + return error.error_struct(ctx->context.get()); +} + + +// ------------------------- auxiliary images ------------------------- + + +int heif_image_handle_get_number_of_auxiliary_images(const heif_image_handle* handle, + int include_alpha_image) +{ + return (int) handle->image->get_aux_images(include_alpha_image).size(); +} + + +int heif_image_handle_get_list_of_auxiliary_image_IDs(const heif_image_handle* handle, + int include_alpha_image, + heif_item_id* ids, int count) +{ + if (ids == nullptr) { + return 0; + } + + auto auxImages = handle->image->get_aux_images(include_alpha_image); + int n = (int) std::min(count, (int) auxImages.size()); + + for (int i = 0; i < n; i++) { + ids[i] = auxImages[i]->get_id(); + } + + return n; +} + + +heif_error heif_image_handle_get_auxiliary_type(const heif_image_handle* handle, + const char** out_type) +{ + if (out_type == nullptr) { + return heif_error_null_pointer_argument; + } + + *out_type = nullptr; + + const auto& auxType = handle->image->get_aux_type(); + + char* buf = (char*) malloc(auxType.length() + 1); + + if (buf == nullptr) { + return Error(heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + "Failed to allocate memory for the type string").error_struct(handle->image.get()); + } + + strcpy(buf, auxType.c_str()); + *out_type = buf; + + return heif_error_success; +} + + +void heif_image_handle_release_auxiliary_type(const heif_image_handle* handle, + const char** out_type) +{ + if (out_type && *out_type) { + free((void*) *out_type); + *out_type = nullptr; + } +} + + +heif_error heif_image_handle_get_auxiliary_image_handle(const heif_image_handle* main_image_handle, + heif_item_id auxiliary_id, + heif_image_handle** out_auxiliary_handle) +{ + if (!out_auxiliary_handle) { + return heif_error_null_pointer_argument; + } + + *out_auxiliary_handle = nullptr; + + auto auxImages = main_image_handle->image->get_aux_images(); + for (const auto& aux : auxImages) { + if (aux->get_id() == auxiliary_id) { + if (Error error = aux->get_item_error()) { + return error.error_struct(main_image_handle->image.get()); + } + *out_auxiliary_handle = new heif_image_handle(); + (*out_auxiliary_handle)->image = aux; + (*out_auxiliary_handle)->context = main_image_handle->context; + + return Error::Ok.error_struct(main_image_handle->image.get()); + } + } + + Error err(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); + return err.error_struct(main_image_handle->image.get()); +} + + +// ===================== DEPRECATED ===================== + +// DEPRECATED (typo) +void heif_image_handle_free_auxiliary_types(const heif_image_handle* handle, + const char** out_type) +{ + heif_image_handle_release_auxiliary_type(handle, out_type); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_aux_images.h libheif-1.23.4/libheif/api/libheif/heif_aux_images.h --- libheif-1.19.8/libheif/api/libheif/heif_aux_images.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_aux_images.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,182 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_AUX_IMAGES_H +#define LIBHEIF_HEIF_AUX_IMAGES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef struct heif_encoder heif_encoder; +typedef struct heif_encoding_options heif_encoding_options; + + +// ------------------------- depth images ------------------------- + +LIBHEIF_API +int heif_image_handle_has_depth_image(const heif_image_handle*); + +LIBHEIF_API +int heif_image_handle_get_number_of_depth_images(const heif_image_handle* handle); + +LIBHEIF_API +int heif_image_handle_get_list_of_depth_image_IDs(const heif_image_handle* handle, + heif_item_id* ids, int count); + +LIBHEIF_API +heif_error heif_image_handle_get_depth_image_handle(const heif_image_handle* handle, + heif_item_id depth_image_id, + heif_image_handle** out_depth_handle); + + +typedef enum heif_depth_representation_type { + heif_depth_representation_type_uniform_inverse_Z = 0, + heif_depth_representation_type_uniform_disparity = 1, + heif_depth_representation_type_uniform_Z = 2, + heif_depth_representation_type_nonuniform_disparity = 3 +} heif_depth_representation_type; + +typedef struct heif_depth_representation_info { + uint8_t version; + + // version 1 fields + + uint8_t has_z_near; + uint8_t has_z_far; + uint8_t has_d_min; + uint8_t has_d_max; + + double z_near; + double z_far; + double d_min; + double d_max; + + enum heif_depth_representation_type depth_representation_type; + uint32_t disparity_reference_view; + + uint32_t depth_nonlinear_representation_model_size; + uint8_t* depth_nonlinear_representation_model; + + // version 2 fields below +} heif_depth_representation_info; + + +LIBHEIF_API +void heif_depth_representation_info_free(const heif_depth_representation_info* info); + +// Returns true when there is depth_representation_info available +// Note 1: depth_image_id is currently unused because we support only one depth channel per image, but +// you should still provide the correct ID for future compatibility. +// Note 2: Because of an API bug before v1.11.0, the function also works when 'handle' is the handle of the depth image. +// However, you should pass the handle of the main image. Please adapt your code if needed. +LIBHEIF_API +int heif_image_handle_get_depth_image_representation_info(const heif_image_handle* handle, + heif_item_id depth_image_id, + const heif_depth_representation_info** out); + + + +// ------------------------- thumbnails ------------------------- + +// List the number of thumbnails assigned to this image handle. Usually 0 or 1. +LIBHEIF_API +int heif_image_handle_get_number_of_thumbnails(const heif_image_handle* handle); + +LIBHEIF_API +int heif_image_handle_get_list_of_thumbnail_IDs(const heif_image_handle* handle, + heif_item_id* ids, int count); + +// Get the image handle of a thumbnail image. +LIBHEIF_API +heif_error heif_image_handle_get_thumbnail(const heif_image_handle* main_image_handle, + heif_item_id thumbnail_id, + heif_image_handle** out_thumbnail_handle); + + +// Encode the 'image' as a scaled down thumbnail image. +// The image is scaled down to fit into a square area of width 'bbox_size'. +// If the input image is already so small that it fits into this bounding box, no thumbnail +// image is encoded and NULL is returned in 'out_thumb_image_handle'. +// No error is returned in this case. +// The encoded thumbnail is automatically assigned to the 'master_image_handle'. Hence, you +// do not have to call heif_context_assign_thumbnail(). +LIBHEIF_API +heif_error heif_context_encode_thumbnail(heif_context*, + const heif_image* image, + const heif_image_handle* master_image_handle, + heif_encoder* encoder, + const heif_encoding_options* options, + int bbox_size, + heif_image_handle** out_thumb_image_handle); + +// Assign 'thumbnail_image' as the thumbnail image of 'master_image'. +LIBHEIF_API +heif_error heif_context_assign_thumbnail(struct heif_context*, + const heif_image_handle* master_image, + const heif_image_handle* thumbnail_image); + + +// ------------------------- auxiliary images ------------------------- + +#define LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA (1UL<<1) +#define LIBHEIF_AUX_IMAGE_FILTER_OMIT_DEPTH (2UL<<1) + +// List the number of auxiliary images assigned to this image handle. +LIBHEIF_API +int heif_image_handle_get_number_of_auxiliary_images(const heif_image_handle* handle, + int aux_filter); + +LIBHEIF_API +int heif_image_handle_get_list_of_auxiliary_image_IDs(const heif_image_handle* handle, + int aux_filter, + heif_item_id* ids, int count); + +// You are responsible to deallocate the returned buffer with heif_image_handle_release_auxiliary_type(). +LIBHEIF_API +heif_error heif_image_handle_get_auxiliary_type(const heif_image_handle* handle, + const char** out_type); + +LIBHEIF_API +void heif_image_handle_release_auxiliary_type(const heif_image_handle* handle, + const char** out_type); + +// Get the image handle of an auxiliary image. +LIBHEIF_API +heif_error heif_image_handle_get_auxiliary_image_handle(const heif_image_handle* main_image_handle, + heif_item_id auxiliary_id, + heif_image_handle** out_auxiliary_handle); + +// ===================== DEPRECATED ===================== + +// DEPRECATED (because typo in function name). Use heif_image_handle_release_auxiliary_type() instead. +LIBHEIF_API +void heif_image_handle_free_auxiliary_types(const heif_image_handle* handle, + const char** out_type); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_brands.cc libheif-1.23.4/libheif/api/libheif/heif_brands.cc --- libheif-1.19.8/libheif/api/libheif/heif_brands.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_brands.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,469 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_brands.h" +#include "heif_error.h" +#include "error.h" +#include "common_utils.h" +#include "bitstream.h" +#include "box.h" + +#include +#include +#include + + + +heif_brand2 heif_read_main_brand(const uint8_t* data, int len) +{ + if (len < 12) { + return heif_unknown_brand; + } + + return heif_fourcc_to_brand((char*) (data + 8)); +} + + +heif_brand2 heif_read_minor_version_brand(const uint8_t* data, int len) +{ + if (len < 16) { + return heif_unknown_brand; + } + return heif_fourcc_to_brand((char*) (data + 12)); +} + + +heif_brand2 heif_fourcc_to_brand(const char* fourcc_string) +{ + if (fourcc_string == nullptr || !fourcc_string[0] || !fourcc_string[1] || !fourcc_string[2] || !fourcc_string[3]) { + return 0; + } + + return fourcc(fourcc_string); +} + +void heif_brand_to_fourcc(heif_brand2 brand, char* out_fourcc) +{ + if (out_fourcc) { + out_fourcc[0] = (char) ((brand >> 24) & 0xFF); + out_fourcc[1] = (char) ((brand >> 16) & 0xFF); + out_fourcc[2] = (char) ((brand >> 8) & 0xFF); + out_fourcc[3] = (char) ((brand >> 0) & 0xFF); + } +} + + +int heif_has_compatible_brand(const uint8_t* data, int len, const char* brand_fourcc) +{ + if (data == nullptr || len <= 0 || brand_fourcc == nullptr || !brand_fourcc[0] || !brand_fourcc[1] || !brand_fourcc[2] || !brand_fourcc[3]) { + return -1; + } + + auto stream = std::make_shared(data, len, false); + BitstreamRange range(stream, len); + + std::shared_ptr box; + Error err = Box::read(range, &box, heif_get_global_security_limits()); + if (err) { + if (err.sub_error_code == heif_suberror_End_of_data) { + return -1; + } + + return -2; + } + + auto ftyp = std::dynamic_pointer_cast(box); + if (!ftyp) { + return -2; + } + + return ftyp->has_compatible_brand(fourcc(brand_fourcc)) ? 1 : 0; +} + + +heif_error heif_list_compatible_brands(const uint8_t* data, int len, heif_brand2** out_brands, int* out_size) +{ + if (data == nullptr || out_brands == nullptr || out_size == nullptr) { + return heif_error_null_pointer_argument; + } + + if (len <= 0) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "data length must be positive"}; + } + + const heif_security_limits* security_limits = heif_get_global_security_limits(); + + + // --- check that file begins with ftyp box + + { + auto stream = std::make_shared(data, len, false); + BitstreamRange range(stream, len); + + BoxHeader hdr; + Error err = hdr.parse_header(range); + if (err) { + return {err.error_code, err.sub_error_code, "error reading ftype box header"}; + } + + if (hdr.get_short_type() != fourcc("ftyp")) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "File does not begin with 'ftyp' box." + }; + } + } + + auto stream = std::make_shared(data, len, false); + BitstreamRange range(stream, len); + + std::shared_ptr box; + Error err = Box::read(range, &box, security_limits); + if (err) { + if (err.sub_error_code == heif_suberror_End_of_data) { + return {err.error_code, err.sub_error_code, "insufficient input data"}; + } + + return {err.error_code, err.sub_error_code, "error reading ftyp box"}; + } + + auto ftyp = std::dynamic_pointer_cast(box); + if (!ftyp) { + return {heif_error_Invalid_input, heif_suberror_No_ftyp_box, "input is not a ftyp box"}; + } + + auto brands = ftyp->list_brands(); + size_t nBrands = brands.size(); + + if (security_limits->max_number_of_file_brands && nBrands > security_limits->max_number_of_file_brands) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "File contains more brands than allowed by security limits." + }; + } + + *out_brands = (heif_brand2*) malloc(sizeof(heif_brand2) * nBrands); + *out_size = (int)nBrands; + + for (size_t i = 0; i < nBrands; i++) { + (*out_brands)[i] = brands[i]; + } + + return heif_error_success; +} + + +void heif_free_list_of_compatible_brands(heif_brand2* brands_list) +{ + if (brands_list) { + free(brands_list); + } +} + + +enum class TriBool +{ + No, Yes, Unknown +}; + +static TriBool is_jpeg(const uint8_t* data, int len) +{ + if (len < 12) { + return TriBool::Unknown; + } + + if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF && data[3] == 0xE0 && + data[4] == 0x00 && data[5] == 0x10 && data[6] == 0x4A && data[7] == 0x46 && + data[8] == 0x49 && data[9] == 0x46 && data[10] == 0x00 && data[11] == 0x01) { + return TriBool::Yes; + } + if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF && data[3] == 0xE1 && + data[6] == 0x45 && data[7] == 0x78 && data[8] == 0x69 && data[9] == 0x66 && + data[10] == 0x00 && data[11] == 0x00) { + return TriBool::Yes; + } + else { + return TriBool::No; + } +} + + +static TriBool is_png(const uint8_t* data, int len) +{ + if (len < 8) { + return TriBool::Unknown; + } + + if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 && + data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A) { + return TriBool::Yes; + } + else { + return TriBool::No; + } +} + + +const char* heif_get_file_mime_type(const uint8_t* data, int len) +{ + heif_brand2 mainBrand = heif_read_main_brand(data, len); + + if (mainBrand == heif_brand2_heic || + mainBrand == heif_brand2_heix || + mainBrand == heif_brand2_heim || + mainBrand == heif_brand2_heis) { + return "image/heic"; + } + else if (mainBrand == heif_brand2_mif1) { + return "image/heif"; + } + else if (mainBrand == heif_brand2_hevc || + mainBrand == heif_brand2_hevx || + mainBrand == heif_brand2_hevm || + mainBrand == heif_brand2_hevs) { + return "image/heic-sequence"; + } + else if (mainBrand == heif_brand2_msf1) { + return "image/heif-sequence"; + } + else if (mainBrand == heif_brand2_avif) { + return "image/avif"; + } + else if (mainBrand == heif_brand2_avis) { + return "image/avif-sequence"; + } + else if (mainBrand == heif_brand2_avci) { + return "image/avci"; + } + else if (mainBrand == heif_brand2_avcs) { + return "image/avcs"; + } + else if (mainBrand == heif_brand2_mif3) { + heif_brand2 minorBrand = heif_read_minor_version_brand(data, len); + if (minorBrand == heif_brand2_avif) { + return "image/avif"; + } + if (minorBrand == heif_brand2_heic || + minorBrand == heif_brand2_heix || + minorBrand == heif_brand2_heim || + minorBrand == heif_brand2_heis) { + return "image/heic"; + } + // There could be other options in here, like VVC or J2K + return "image/heif"; + } + else if (mainBrand == heif_brand2_j2ki) { + return "image/hej2k"; + } + else if (mainBrand == heif_brand2_j2is) { + return "image/j2is"; + } + else if (is_jpeg(data, len) == TriBool::Yes) { + return "image/jpeg"; + } + else if (is_png(data, len) == TriBool::Yes) { + return "image/png"; + } + else { + return ""; + } +} + +heif_filetype_result heif_check_filetype(const uint8_t* data, int len) +{ + if (len < 8) { + return heif_filetype_maybe; + } + + if (data[4] != 'f' || + data[5] != 't' || + data[6] != 'y' || + data[7] != 'p') { + return heif_filetype_no; + } + + if (len >= 12) { + heif_brand2 brand = heif_read_main_brand(data, len); + + if (brand == heif_brand2_heic) { + return heif_filetype_yes_supported; + } + else if (brand == heif_brand2_heix) { + return heif_filetype_yes_supported; + } + else if (brand == heif_brand2_avif) { + return heif_filetype_yes_supported; + } + else if (brand == heif_brand2_jpeg) { + return heif_filetype_yes_supported; + } + else if (brand == heif_brand2_j2ki) { + return heif_filetype_yes_supported; + } + else if (brand == heif_brand2_mif1) { + return heif_filetype_maybe; + } + else if (brand == heif_brand2_mif2) { + return heif_filetype_maybe; + } + else { + return heif_filetype_yes_unsupported; + } + } + + return heif_filetype_maybe; +} + + +heif_error heif_has_compatible_filetype(const uint8_t* data, int len) +{ + // Get compatible brands first, because that does validity checks + heif_brand2* compatible_brands = nullptr; + int nBrands = 0; + struct heif_error err = heif_list_compatible_brands(data, len, &compatible_brands, &nBrands); + if (err.code) { + assert(compatible_brands == nullptr); // NOLINT(clang-analyzer-unix.Malloc) + return err; + } + + heif_brand2 main_brand = heif_read_main_brand(data, len); + + std::set supported_brands{ + heif_brand2_avif, + heif_brand2_heic, + heif_brand2_heix, + heif_brand2_j2ki, + heif_brand2_jpeg, + heif_brand2_miaf, + heif_brand2_mif1, + heif_brand2_mif2, + heif_brand2_mif3, + heif_brand2_msf1, + heif_brand2_isom, + heif_brand2_mp41, + heif_brand2_mp42 + }; + + auto it = supported_brands.find(main_brand); + if (it != supported_brands.end()) { + heif_free_list_of_compatible_brands(compatible_brands); + return heif_error_success; + } + + for (int i = 0; i < nBrands; i++) { + heif_brand2 compatible_brand = compatible_brands[i]; + it = supported_brands.find(compatible_brand); + if (it != supported_brands.end()) { + heif_free_list_of_compatible_brands(compatible_brands); + return heif_error_success; + } + } + heif_free_list_of_compatible_brands(compatible_brands); + return {heif_error_Invalid_input, heif_suberror_Unsupported_image_type, "No supported brands found."};; +} + + +int heif_check_jpeg_filetype(const uint8_t* data, int len) +{ + if (len < 4 || data == nullptr) { + return -1; + } + + return (data[0] == 0xFF && + data[1] == 0xD8 && + data[2] == 0xFF && + (data[3] & 0xF0) == 0xE0); +} + + +static heif_brand heif_fourcc_to_brand_enum(const char* fourcc) +{ + if (fourcc == nullptr || !fourcc[0] || !fourcc[1] || !fourcc[2] || !fourcc[3]) { + return heif_unknown_brand; + } + + char brand[5]; + brand[0] = fourcc[0]; + brand[1] = fourcc[1]; + brand[2] = fourcc[2]; + brand[3] = fourcc[3]; + brand[4] = 0; + + if (strcmp(brand, "heic") == 0) { + return heif_heic; + } + else if (strcmp(brand, "heix") == 0) { + return heif_heix; + } + else if (strcmp(brand, "hevc") == 0) { + return heif_hevc; + } + else if (strcmp(brand, "hevx") == 0) { + return heif_hevx; + } + else if (strcmp(brand, "heim") == 0) { + return heif_heim; + } + else if (strcmp(brand, "heis") == 0) { + return heif_heis; + } + else if (strcmp(brand, "hevm") == 0) { + return heif_hevm; + } + else if (strcmp(brand, "hevs") == 0) { + return heif_hevs; + } + else if (strcmp(brand, "mif1") == 0) { + return heif_mif1; + } + else if (strcmp(brand, "msf1") == 0) { + return heif_msf1; + } + else if (strcmp(brand, "avif") == 0) { + return heif_avif; + } + else if (strcmp(brand, "avis") == 0) { + return heif_avis; + } + else if (strcmp(brand, "vvic") == 0) { + return heif_vvic; + } + else if (strcmp(brand, "j2ki") == 0) { + return heif_j2ki; + } + else if (strcmp(brand, "j2is") == 0) { + return heif_j2is; + } + else { + return heif_unknown_brand; + } +} + + +enum heif_brand heif_main_brand(const uint8_t* data, int len) +{ + if (len < 12) { + return heif_unknown_brand; + } + + return heif_fourcc_to_brand_enum((char*) (data + 8)); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_brands.h libheif-1.23.4/libheif/api/libheif/heif_brands.h --- libheif-1.19.8/libheif/api/libheif/heif_brands.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_brands.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,385 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_BRANDS_H +#define LIBHEIF_HEIF_BRANDS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + + + +typedef uint32_t heif_brand2; + +/** + * HEVC image (`heic`) brand. + * + * Image conforms to HEVC (H.265) Main or Main Still profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.1. + */ +#define heif_brand2_heic heif_fourcc('h','e','i','c') + +/** + * HEVC image (`heix`) brand. + * + * Image conforms to HEVC (H.265) Main 10 profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.1. + */ +#define heif_brand2_heix heif_fourcc('h','e','i','x') + +/** + * HEVC image sequence (`hevc`) brand. + * + * Image sequence conforms to HEVC (H.265) Main profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.2. + */ +#define heif_brand2_hevc heif_fourcc('h','e','v','c') + +/** + * HEVC image sequence (`hevx`) brand. + * + * Image sequence conforms to HEVC (H.265) Main 10 profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.2. + */ +#define heif_brand2_hevx heif_fourcc('h','e','v','x') + +/** + * HEVC layered image (`heim`) brand. + * + * Image layers conform to HEVC (H.265) Main or Multiview Main profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.3. + */ +#define heif_brand2_heim heif_fourcc('h','e','i','m') + +/** + * HEVC layered image (`heis`) brand. + * + * Image layers conform to HEVC (H.265) Main, Main 10, Scalable Main + * or Scalable Main 10 profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.3. + */ +#define heif_brand2_heis heif_fourcc('h','e','i','s') + +/** + * HEVC layered image sequence (`hevm`) brand. + * + * Image sequence layers conform to HEVC (H.265) Main or Multiview Main profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.4. + */ +#define heif_brand2_hevm heif_fourcc('h','e','v','m') + +/** + * HEVC layered image sequence (`hevs`) brand. + * + * Image sequence layers conform to HEVC (H.265) Main, Main 10, Scalable Main + * or Scalable Main 10 profile. + * + * See ISO/IEC 23008-12:2022 Section B.4.4. + */ +#define heif_brand2_hevs heif_fourcc('h','e','v','s') + +/** + * AV1 image (`avif`) brand. + * + * See https://aomediacodec.github.io/av1-avif/#image-and-image-collection-brand + */ +#define heif_brand2_avif heif_fourcc('a','v','i','f') + +/** + * AV1 image sequence (`avis`) brand. + * + * See https://aomediacodec.github.io/av1-avif/#image-sequence-brand + */ +#define heif_brand2_avis heif_fourcc('a','v','i','s') // AVIF sequence + +/** + * HEIF image structural brand (`mif1`). + * + * This does not imply a specific coding algorithm. + * + * See ISO/IEC 23008-12:2022 Section 10.2.2. + */ +#define heif_brand2_mif1 heif_fourcc('m','i','f','1') + +/** + * HEIF image structural brand (`mif2`). + * + * This does not imply a specific coding algorithm. `mif2` extends + * the requirements of `mif1` to include the `rref` and `iscl` item + * properties. + * + * See ISO/IEC 23008-12:2022 Section 10.2.3. + */ +#define heif_brand2_mif2 heif_fourcc('m','i','f','2') + +/** + * HEIF image structural brand (`mif3`). + * + * This indicates the low-overhead (ftyp+mini) structure. + */ +#define heif_brand2_mif3 heif_fourcc('m','i','f','3') + +/** + * HEIF image sequence structural brand (`msf1`). + * + * This does not imply a specific coding algorithm. + * + * See ISO/IEC 23008-12:2022 Section 10.3.1. + */ +#define heif_brand2_msf1 heif_fourcc('m','s','f','1') + +/** + * VVC image (`vvic`) brand. + * + * See ISO/IEC 23008-12:2022 Section L.4.1. + */ +#define heif_brand2_vvic heif_fourcc('v','v','i','c') + +/** + * VVC image sequence (`vvis`) brand. + * + * See ISO/IEC 23008-12:2022 Section L.4.2. + */ +#define heif_brand2_vvis heif_fourcc('v','v','i','s') + +/** + * EVC baseline image (`evbi`) brand. + * + * See ISO/IEC 23008-12:2022 Section M.4.1. + */ +#define heif_brand2_evbi heif_fourcc('e','v','b','i') + +/** + * EVC main profile image (`evmi`) brand. + * + * See ISO/IEC 23008-12:2022 Section M.4.2. + */ +#define heif_brand2_evmi heif_fourcc('e','v','m','i') + +/** + * EVC baseline image sequence (`evbs`) brand. + * + * See ISO/IEC 23008-12:2022 Section M.4.3. + */ +#define heif_brand2_evbs heif_fourcc('e','v','b','s') + +/** + * EVC main profile image sequence (`evms`) brand. + * + * See ISO/IEC 23008-12:2022 Section M.4.4. + */ +#define heif_brand2_evms heif_fourcc('e','v','m','s') + +/** + * JPEG image (`jpeg`) brand. + * + * See ISO/IEC 23008-12:2022 Annex H.4 + */ +#define heif_brand2_jpeg heif_fourcc('j','p','e','g') + +/** + * JPEG image sequence (`jpgs`) brand. + * + * See ISO/IEC 23008-12:2022 Annex H.5 + */ +#define heif_brand2_jpgs heif_fourcc('j','p','g','s') + +/** + * JPEG 2000 image (`j2ki`) brand. + * + * See ISO/IEC 15444-16:2021 Section 6.5 + */ +#define heif_brand2_j2ki heif_fourcc('j','2','k','i') + +/** + * JPEG 2000 image sequence (`j2is`) brand. + * + * See ISO/IEC 15444-16:2021 Section 7.6 + */ +#define heif_brand2_j2is heif_fourcc('j','2','i','s') + +/** + * Multi-image application format (MIAF) brand. + * + * This is HEIF with additional constraints for interoperability. + * + * See ISO/IEC 23000-22. + */ +#define heif_brand2_miaf heif_fourcc('m','i','a','f') + +/** + * Single picture file brand. + * + * This is a compatible brand indicating the file contains a single intra-coded picture. + * + * See ISO/IEC 23008-12:2022 Section 10.2.5. +*/ +#define heif_brand2_1pic heif_fourcc('1','p','i','c') + +// H.264 +#define heif_brand2_avci heif_fourcc('a','v','c','i') +#define heif_brand2_avcs heif_fourcc('a','v','c','s') + +/** + * Unified ID namespace (`unif`) brand. + * + * All IDs (item, track, entity group) share a single global namespace. + * + * See ISO/IEC 14496-12. + */ +#define heif_brand2_unif heif_fourcc('u','n','i','f') + +#define heif_brand2_iso8 heif_fourcc('i','s','o','8') +#define heif_brand2_isom heif_fourcc('i','s','o','m') +#define heif_brand2_mp41 heif_fourcc('m','p','4','1') +#define heif_brand2_mp42 heif_fourcc('m','p','4','2') + +// input data should be at least 12 bytes +LIBHEIF_API +heif_brand2 heif_read_main_brand(const uint8_t* data, int len); + +// input data should be at least 16 bytes +LIBHEIF_API +heif_brand2 heif_read_minor_version_brand(const uint8_t* data, int len); + +// 'brand_fourcc' must be 4 character long, but need not be 0-terminated +LIBHEIF_API +heif_brand2 heif_fourcc_to_brand(const char* brand_fourcc); + +// the output buffer must be at least 4 bytes long +LIBHEIF_API +void heif_brand_to_fourcc(heif_brand2 brand, char* out_fourcc); + +// 'brand_fourcc' must be 4 character long, but need not be 0-terminated +// returns 1 if file includes the brand, and 0 if it does not +// returns -1 if the provided data is not sufficient +// (you should input at least as many bytes as indicated in the first 4 bytes of the file, usually ~50 bytes will do) +// returns -2 on other errors +LIBHEIF_API +int heif_has_compatible_brand(const uint8_t* data, int len, const char* brand_fourcc); + +// Returns an array of compatible brands. The array is allocated by this function and has to be freed with 'heif_free_list_of_compatible_brands()'. +// The number of entries is returned in out_size. +LIBHEIF_API +struct heif_error heif_list_compatible_brands(const uint8_t* data, int len, heif_brand2** out_brands, int* out_size); + +LIBHEIF_API +void heif_free_list_of_compatible_brands(heif_brand2* brands_list); + + + +// Returns one of these MIME types: +// - image/heic HEIF file using h265 compression +// - image/heif HEIF file using any other compression +// - image/heic-sequence HEIF image sequence using h265 compression +// - image/heif-sequence HEIF image sequence using any other compression +// - image/avif AVIF image +// - image/avif-sequence AVIF sequence +// - image/jpeg JPEG image +// - image/png PNG image +// If the format could not be detected, an empty string is returned. +// +// Provide at least 12 bytes of input. With less input, its format might not +// be detected. You may also provide more input to increase detection accuracy. +// +// Note that JPEG and PNG images cannot be decoded by libheif even though the +// formats are detected by this function. +LIBHEIF_API +const char* heif_get_file_mime_type(const uint8_t* data, int len); + + +// ========================= file type check ====================== + +typedef enum heif_filetype_result +{ + heif_filetype_no, + heif_filetype_yes_supported, // it is heif and can be read by libheif + heif_filetype_yes_unsupported, // it is heif, but cannot be read by libheif + heif_filetype_maybe // not sure whether it is an heif, try detection with more input data +} heif_filetype_result; + +// input data should be at least 12 bytes +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 +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); + + +// ===================== DEPRECATED ===================== + +// DEPRECATED, use heif_brand2 and the heif_brand2_* constants instead +typedef enum heif_brand +{ + heif_unknown_brand, + heif_heic, // HEIF image with h265 + heif_heix, // 10bit images, or anything that uses h265 with range extension + heif_hevc, heif_hevx, // brands for image sequences + heif_heim, // multiview + heif_heis, // scalable + heif_hevm, // multiview sequence + heif_hevs, // scalable sequence + heif_mif1, // image, any coding algorithm + heif_msf1, // sequence, any coding algorithm + heif_avif, // HEIF image with AV1 + heif_avis, + heif_vvic, // VVC image + heif_vvis, // VVC sequence + heif_evbi, // EVC image + heif_evbs, // EVC sequence + heif_j2ki, // JPEG2000 image + heif_j2is, // JPEG2000 image sequence +} heif_brand; + +// input data should be at least 12 bytes +// DEPRECATED, use heif_read_main_brand() instead +LIBHEIF_API +enum heif_brand heif_main_brand(const uint8_t* data, int len); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_color.cc libheif-1.23.4/libheif/api/libheif/heif_color.cc --- libheif-1.19.8/libheif/api/libheif/heif_color.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_color.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,684 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include + +#include "common_utils.h" +#include +#include "heif.h" +#include "image/pixelimage.h" +#include "api_structs.h" +#include "error.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +void heif_color_conversion_options_set_defaults(heif_color_conversion_options* options) +{ + options->version = 1; +#if HAVE_LIBSHARPYUV + options->preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_sharp_yuv; +#else + options->preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; +#endif + + options->preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + options->only_use_preferred_chroma_algorithm = true; +} + + +static void fill_default_color_conversion_options_ext(heif_color_conversion_options_ext& options) +{ + options.version = 1; + options.alpha_composition_mode = heif_alpha_composition_mode_none; + options.background_red = options.background_green = options.background_blue = 0xFFFF; + options.secondary_background_red = options.secondary_background_green = options.secondary_background_blue = 0xCCCC; + options.checkerboard_square_size = 16; +} + + +heif_color_conversion_options_ext* heif_color_conversion_options_ext_alloc() +{ + auto options = new heif_color_conversion_options_ext; + + fill_default_color_conversion_options_ext(*options); + + return options; +} + + +void heif_color_conversion_options_ext_copy(heif_color_conversion_options_ext* dst, + const heif_color_conversion_options_ext* src) +{ + if (src == nullptr) { + return; + } + + int min_version = std::min(dst->version, src->version); + + switch (min_version) { + case 1: + dst->alpha_composition_mode = src->alpha_composition_mode; + dst->background_red = src->background_red; + dst->background_green = src->background_green; + dst->background_blue = src->background_blue; + dst->secondary_background_red = src->secondary_background_red; + dst->secondary_background_green = src->secondary_background_green; + dst->secondary_background_blue = src->secondary_background_blue; + dst->checkerboard_square_size = src->checkerboard_square_size; + } +} + + +void heif_color_conversion_options_ext_free(heif_color_conversion_options_ext* options) +{ + delete options; +} + + +heif_color_profile_type heif_image_handle_get_color_profile_type(const heif_image_handle* handle) +{ + auto profile_icc = handle->image->get_color_profile_icc(); + if (profile_icc) { + return (heif_color_profile_type) profile_icc->get_type(); + } + + if (handle->image->has_nclx_color_profile()) { + return heif_color_profile_type_nclx; + } + + return heif_color_profile_type_not_present; +} + + +size_t heif_image_handle_get_raw_color_profile_size(const heif_image_handle* handle) +{ + auto profile_icc = handle->image->get_color_profile_icc(); + if (profile_icc) { + return profile_icc->get_data().size(); + } + else { + return 0; + } +} + + +heif_error heif_image_handle_get_raw_color_profile(const heif_image_handle* handle, + void* out_data) +{ + if (out_data == nullptr) { + return heif_error_null_pointer_argument; + } + + auto raw_profile = handle->image->get_color_profile_icc(); + if (raw_profile) { + memcpy(out_data, + raw_profile->get_data().data(), + raw_profile->get_data().size()); + } + else { + Error err(heif_error_Color_profile_does_not_exist, + heif_suberror_Unspecified); + return err.error_struct(handle->image.get()); + } + + return Error::Ok.error_struct(handle->image.get()); +} + + +static const std::set::type> known_color_primaries{ + heif_color_primaries_ITU_R_BT_709_5, + heif_color_primaries_unspecified, + heif_color_primaries_ITU_R_BT_470_6_System_M, + heif_color_primaries_ITU_R_BT_470_6_System_B_G, + heif_color_primaries_ITU_R_BT_601_6, + heif_color_primaries_SMPTE_240M, + heif_color_primaries_generic_film, + heif_color_primaries_ITU_R_BT_2020_2_and_2100_0, + heif_color_primaries_SMPTE_ST_428_1, + heif_color_primaries_SMPTE_RP_431_2, + heif_color_primaries_SMPTE_EG_432_1, + heif_color_primaries_EBU_Tech_3213_E, +}; + + +heif_error heif_nclx_color_profile_set_color_primaries(heif_color_profile_nclx* nclx, uint16_t cp) +{ + if (static_cast::type>(cp) > std::numeric_limits::type>::max()) { + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_color_primaries).error_struct(nullptr); + } + + auto n = static_cast::type>(cp); + if (known_color_primaries.find(n) != known_color_primaries.end()) { + nclx->color_primaries = static_cast(n); + } + else { + nclx->color_primaries = heif_color_primaries_unspecified; + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_color_primaries).error_struct(nullptr); + } + + return Error::Ok.error_struct(nullptr); +} + + +static const std::set::type> known_transfer_characteristics{ + heif_transfer_characteristic_ITU_R_BT_709_5, + heif_transfer_characteristic_unspecified, + heif_transfer_characteristic_ITU_R_BT_470_6_System_M, + heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G, + heif_transfer_characteristic_ITU_R_BT_601_6, + heif_transfer_characteristic_SMPTE_240M, + heif_transfer_characteristic_linear, + heif_transfer_characteristic_logarithmic_100, + heif_transfer_characteristic_logarithmic_100_sqrt10, + heif_transfer_characteristic_IEC_61966_2_4, + heif_transfer_characteristic_ITU_R_BT_1361, + heif_transfer_characteristic_IEC_61966_2_1, + heif_transfer_characteristic_ITU_R_BT_2020_2_10bit, + heif_transfer_characteristic_ITU_R_BT_2020_2_12bit, + heif_transfer_characteristic_ITU_R_BT_2100_0_PQ, + heif_transfer_characteristic_SMPTE_ST_428_1, + heif_transfer_characteristic_ITU_R_BT_2100_0_HLG +}; + + +heif_error heif_nclx_color_profile_set_transfer_characteristics(heif_color_profile_nclx* nclx, uint16_t tc) +{ + if (static_cast::type>(tc) > std::numeric_limits::type>::max()) { + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_transfer_characteristics).error_struct(nullptr); + } + + auto n = static_cast::type>(tc); + if (known_transfer_characteristics.find(n) != known_transfer_characteristics.end()) { + nclx->transfer_characteristics = static_cast(n); + } + else { + nclx->transfer_characteristics = heif_transfer_characteristic_unspecified; + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_transfer_characteristics).error_struct(nullptr); + } + + return Error::Ok.error_struct(nullptr); +} + + +static const std::set::type> known_matrix_coefficients{ + heif_matrix_coefficients_RGB_GBR, + heif_matrix_coefficients_ITU_R_BT_709_5, + heif_matrix_coefficients_unspecified, + heif_matrix_coefficients_US_FCC_T47, + heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G, + heif_matrix_coefficients_ITU_R_BT_601_6, + heif_matrix_coefficients_SMPTE_240M, + heif_matrix_coefficients_YCgCo, + heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance, + heif_matrix_coefficients_ITU_R_BT_2020_2_constant_luminance, + heif_matrix_coefficients_SMPTE_ST_2085, + heif_matrix_coefficients_chromaticity_derived_non_constant_luminance, + heif_matrix_coefficients_chromaticity_derived_constant_luminance, + heif_matrix_coefficients_ICtCp +}; + + +heif_error heif_nclx_color_profile_set_matrix_coefficients(heif_color_profile_nclx* nclx, uint16_t mc) +{ + if (static_cast::type>(mc) > std::numeric_limits::type>::max()) { + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_matrix_coefficients).error_struct(nullptr); + } + + auto n = static_cast::type>(mc); + if (known_matrix_coefficients.find(n) != known_matrix_coefficients.end()) { + nclx->matrix_coefficients = static_cast(n);; + } + else { + nclx->matrix_coefficients = heif_matrix_coefficients_unspecified; + return Error(heif_error_Invalid_input, heif_suberror_Unknown_NCLX_matrix_coefficients).error_struct(nullptr); + } + + return Error::Ok.error_struct(nullptr); +} + + +heif_error heif_image_handle_get_nclx_color_profile(const heif_image_handle* handle, + heif_color_profile_nclx** out_data) +{ + if (!out_data) { + return heif_error_null_pointer_argument; + } + + if (!handle->image->has_nclx_color_profile()) { + Error err(heif_error_Color_profile_does_not_exist, + heif_suberror_Unspecified); + return err.error_struct(handle->image.get()); + } + + auto nclx_profile = handle->image->get_color_profile_nclx(); + Error err = nclx_profile.get_nclx_color_profile(out_data); + + return err.error_struct(handle->image.get()); +} + + +heif_color_profile_nclx* heif_nclx_color_profile_alloc() +{ + auto profile = new heif_color_profile_nclx; + + if (profile) { + profile->version = 1; + + // sRGB defaults + profile->color_primaries = heif_color_primaries_ITU_R_BT_709_5; // 1 + profile->transfer_characteristics = heif_transfer_characteristic_IEC_61966_2_1; // 13 + profile->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; // 6 + profile->full_range_flag = true; + } + + return profile; +} + + +void heif_nclx_color_profile_free(heif_color_profile_nclx* nclx_profile) +{ + delete nclx_profile; +} + + +heif_color_profile_type heif_image_get_color_profile_type(const heif_image* image) +{ + std::shared_ptr profile; + + profile = image->image->get_color_profile_icc(); + if (profile) { + return (heif_color_profile_type) profile->get_type(); + } + + if (image->image->has_nclx_color_profile()) { + return heif_color_profile_type_nclx; + } + + return heif_color_profile_type_not_present; +} + + +size_t heif_image_get_raw_color_profile_size(const heif_image* image) +{ + auto raw_profile = image->image->get_color_profile_icc(); + if (raw_profile) { + return raw_profile->get_data().size(); + } + else { + return 0; + } +} + + +heif_error heif_image_get_raw_color_profile(const heif_image* image, + void* out_data) +{ + if (out_data == nullptr) { + return heif_error_null_pointer_argument; + } + + auto raw_profile = image->image->get_color_profile_icc(); + if (raw_profile) { + memcpy(out_data, + raw_profile->get_data().data(), + raw_profile->get_data().size()); + } + else { + Error err(heif_error_Color_profile_does_not_exist, + heif_suberror_Unspecified); + return err.error_struct(image->image.get()); + } + + return Error::Ok.error_struct(image->image.get()); +} + + +heif_error heif_image_get_nclx_color_profile(const heif_image* image, + heif_color_profile_nclx** out_data) +{ + if (!out_data) { + return heif_error_null_pointer_argument; + } + + if (!image->image->has_nclx_color_profile()) { + Error err(heif_error_Color_profile_does_not_exist, + heif_suberror_Unspecified); + return err.error_struct(image->image.get()); + } + + auto nclx_profile = image->image->get_color_profile_nclx(); + Error err = nclx_profile.get_nclx_color_profile(out_data); + + return err.error_struct(image->image.get()); +} + + +heif_error heif_image_set_raw_color_profile(heif_image* image, + const char* color_profile_type_fourcc, + const void* profile_data, + const size_t profile_size) +{ + if (strlen(color_profile_type_fourcc) != 4) { + heif_error err = { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Invalid color_profile_type (must be 4 characters)" + }; + return err; + } + + uint32_t color_profile_type = fourcc(color_profile_type_fourcc); + + std::vector data; + data.insert(data.end(), + (const uint8_t*) profile_data, + (const uint8_t*) profile_data + profile_size); + + auto color_profile = std::make_shared(color_profile_type, data); + + image->image->set_color_profile_icc(color_profile); + + return heif_error_success; +} + + +heif_error heif_image_set_nclx_color_profile(heif_image* image, + const heif_color_profile_nclx* color_profile) +{ + nclx_profile nclx; + nclx.set_colour_primaries(color_profile->color_primaries); + nclx.set_transfer_characteristics(color_profile->transfer_characteristics); + nclx.set_matrix_coefficients(color_profile->matrix_coefficients); + nclx.set_full_range_flag(color_profile->full_range_flag); + + image->image->set_color_profile_nclx(nclx); + + return heif_error_success; +} + + +// --- content light level --- + +int heif_image_has_content_light_level(const heif_image* image) +{ + return image->image->has_clli(); +} + + +int heif_image_handle_has_content_light_level(const heif_image_handle* handle) +{ + return handle->image->get_property() ? 1 : 0; +} + + +void heif_image_get_content_light_level(const heif_image* image, heif_content_light_level* out) +{ + if (out) { + *out = image->image->get_clli(); + } +} + + +int heif_image_handle_get_content_light_level(const heif_image_handle* handle, heif_content_light_level* out) +{ + auto clli = handle->image->get_property(); + if (out && clli) { + *out = clli->clli; + } + + return clli ? 1 : 0; +} + + +void heif_image_set_content_light_level(const heif_image* image, const heif_content_light_level* in) +{ + if (in == nullptr) { + return; + } + + image->image->set_clli(*in); +} + + +void heif_image_handle_set_content_light_level(const heif_image_handle* handle, const heif_content_light_level* in) +{ + if (in == nullptr) { + return; + } + + handle->image->set_clli(*in); +} + + +// --- mastering display colour volume --- + + +int heif_image_has_mastering_display_colour_volume(const heif_image* image) +{ + return image->image->has_mdcv(); +} + + +int heif_image_handle_has_mastering_display_colour_volume(const heif_image_handle* handle) +{ + return handle->image->get_property() ? 1 : 0; +} + + +void heif_image_get_mastering_display_colour_volume(const heif_image* image, heif_mastering_display_colour_volume* out) +{ + *out = image->image->get_mdcv(); +} + + +int heif_image_handle_get_mastering_display_colour_volume(const heif_image_handle* handle, heif_mastering_display_colour_volume* out) +{ + auto mdcv = handle->image->get_property(); + if (out && mdcv) { + *out = mdcv->mdcv; + } + + return mdcv ? 1 : 0; +} + + +void heif_image_set_mastering_display_colour_volume(const heif_image* image, const heif_mastering_display_colour_volume* in) +{ + if (in == nullptr) { + return; + } + + image->image->set_mdcv(*in); +} + + +void heif_image_handle_set_mastering_display_colour_volume(const heif_image_handle* handle, const heif_mastering_display_colour_volume* in) +{ + if (in == nullptr) { + return; + } + + handle->image->set_mdcv(*in); +} + + +// --- ambient viewing environment --- + +int heif_image_has_ambient_viewing_environment(const heif_image* image) +{ + return image->image->has_amve(); +} + + +int heif_image_handle_has_ambient_viewing_environment(const heif_image_handle* handle) +{ + return handle->image->get_property() ? 1 : 0; +} + + +int heif_image_get_ambient_viewing_environment(const heif_image* image, heif_ambient_viewing_environment* out) +{ + if (!image->image->has_amve()) { + return 0; + } + + if (out) { + *out = image->image->get_amve(); + } + + return 1; +} + + +int heif_image_handle_get_ambient_viewing_environment(const heif_image_handle* handle, heif_ambient_viewing_environment* out) +{ + auto amve = handle->image->get_property(); + if (out && amve) { + *out = amve->amve; + } + + return amve ? 1 : 0; +} + + +void heif_image_set_ambient_viewing_environment(const heif_image* image, const heif_ambient_viewing_environment* in) +{ + if (in == nullptr) { + return; + } + + image->image->set_amve(*in); +} + + +void heif_image_handle_set_ambient_viewing_environment(const heif_image_handle* handle, const heif_ambient_viewing_environment* in) +{ + if (in == nullptr) { + return; + } + + handle->image->set_amve(*in); +} + + +// --- nominal diffuse white --- + + +int heif_image_has_nominal_diffuse_white_luminance(const heif_image* image) +{ + return image->image->has_nominal_diffuse_white(); +} + + +uint32_t heif_image_get_nominal_diffuse_white_luminance(const heif_image* image) +{ + return image->image->get_nominal_diffuse_white_luminance(); +} + + +void heif_image_set_nominal_diffuse_white_luminance(const heif_image* image, uint32_t luminance) +{ + image->image->set_nominal_diffuse_white_luminance(luminance); +} + + +int heif_image_handle_has_nominal_diffuse_white_luminance(const heif_image_handle* handle) +{ + return handle->image->get_property() ? 1 : 0; +} + + +uint32_t heif_image_handle_get_nominal_diffuse_white_luminance(const heif_image_handle* handle) +{ + auto ndwt = handle->image->get_property(); + return ndwt ? ndwt->get_diffuse_white_luminance() : 0; +} + + +void heif_image_handle_set_nominal_diffuse_white_luminance(const heif_image_handle* handle, uint32_t luminance) +{ + handle->image->set_nominal_diffuse_white_luminance(luminance); +} + + +float mdcv_coord_decode_x(uint16_t coord) +{ + // check for unspecified value + if (coord < 5 || coord > 37000) { + return 0.0f; + } + + return (float) (coord * 0.00002); +} + + +float mdcv_coord_decode_y(uint16_t coord) +{ + // check for unspecified value + if (coord < 5 || coord > 42000) { + return 0.0f; + } + + return (float) (coord * 0.00002); +} + + +heif_error heif_mastering_display_colour_volume_decode(const heif_mastering_display_colour_volume* in, + heif_decoded_mastering_display_colour_volume* out) +{ + if (in == nullptr || out == nullptr) { + return heif_error_null_pointer_argument; + } + + for (int c = 0; c < 3; c++) { + out->display_primaries_x[c] = mdcv_coord_decode_x(in->display_primaries_x[c]); + out->display_primaries_y[c] = mdcv_coord_decode_y(in->display_primaries_y[c]); + } + + out->white_point_x = mdcv_coord_decode_x(in->white_point_x); + out->white_point_y = mdcv_coord_decode_y(in->white_point_y); + + if (in->max_display_mastering_luminance < 50000 || in->max_display_mastering_luminance > 100000000) { + out->max_display_mastering_luminance = 0; + } + else { + out->max_display_mastering_luminance = in->max_display_mastering_luminance * 0.0001; + } + + if (in->min_display_mastering_luminance < 1 || in->min_display_mastering_luminance > 50000) { + out->min_display_mastering_luminance = 0; + } + else { + out->min_display_mastering_luminance = in->min_display_mastering_luminance * 0.0001; + } + + return heif_error_success; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_color.h libheif-1.23.4/libheif/api/libheif/heif_color.h --- libheif-1.19.8/libheif/api/libheif/heif_color.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_color.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,479 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_COLOR_H +#define LIBHEIF_HEIF_COLOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// forward declaration from other header files +typedef struct heif_image heif_image; + + +typedef enum heif_chroma_downsampling_algorithm +{ + heif_chroma_downsampling_nearest_neighbor = 1, + heif_chroma_downsampling_average = 2, + + // Combine with 'heif_chroma_upsampling_bilinear' for best quality. + // Makes edges look sharper when using YUV 420 with bilinear chroma upsampling. + heif_chroma_downsampling_sharp_yuv = 3 +} heif_chroma_downsampling_algorithm; + +typedef enum heif_chroma_upsampling_algorithm +{ + heif_chroma_upsampling_nearest_neighbor = 1, + heif_chroma_upsampling_bilinear = 2 +} heif_chroma_upsampling_algorithm; + + +typedef struct heif_color_conversion_options +{ + // 'version' must be 1. + uint8_t version; + + // --- version 1 options + + heif_chroma_downsampling_algorithm preferred_chroma_downsampling_algorithm; + heif_chroma_upsampling_algorithm preferred_chroma_upsampling_algorithm; + + // When set to 'false' libheif may also use a different algorithm if the preferred one is not available + // or using a different algorithm is computationally less complex. Note that currently (v1.17.0) this + // means that for RGB input it will usually choose nearest-neighbor sampling because this is computationally + // the simplest. + // Set this field to 'true' if you want to make sure that the specified algorithm is used even + // at the cost of slightly higher computation times. + uint8_t only_use_preferred_chroma_algorithm; + + // --- Note that we cannot extend this struct because it is embedded in + // other structs (heif_decoding_options and heif_encoding_options). +} heif_color_conversion_options; + + +typedef enum heif_alpha_composition_mode +{ + heif_alpha_composition_mode_none, + heif_alpha_composition_mode_solid_color, + heif_alpha_composition_mode_checkerboard, +} heif_alpha_composition_mode; + + +typedef struct heif_color_conversion_options_ext +{ + uint8_t version; + + // --- version 1 options + + heif_alpha_composition_mode alpha_composition_mode; + + // color values should be specified in the range [0, 65535] + uint16_t background_red, background_green, background_blue; + uint16_t secondary_background_red, secondary_background_green, secondary_background_blue; + uint16_t checkerboard_square_size; +} heif_color_conversion_options_ext; + + +// Assumes that it is a version=1 struct. +LIBHEIF_API +void heif_color_conversion_options_set_defaults(heif_color_conversion_options*); + +LIBHEIF_API +heif_color_conversion_options_ext* heif_color_conversion_options_ext_alloc(void); + +LIBHEIF_API +void heif_color_conversion_options_ext_copy(heif_color_conversion_options_ext* dst, + const heif_color_conversion_options_ext* src); + +LIBHEIF_API +void heif_color_conversion_options_ext_free(heif_color_conversion_options_ext*); + + +// ------------------------- color profiles ------------------------- + +typedef enum heif_color_profile_type +{ + heif_color_profile_type_not_present = 0, + heif_color_profile_type_nclx = heif_fourcc('n', 'c', 'l', 'x'), + heif_color_profile_type_rICC = heif_fourcc('r', 'I', 'C', 'C'), + heif_color_profile_type_prof = heif_fourcc('p', 'r', 'o', 'f') +} heif_color_profile_type; + + +// Returns 'heif_color_profile_type_not_present' if there is no color profile. +// If there is an ICC profile and an NCLX profile, the ICC profile is returned. +// TODO: we need a new API for this function as images can contain both NCLX and ICC at the same time. +// However, you can still use heif_image_handle_get_raw_color_profile() and +// heif_image_handle_get_nclx_color_profile() to access both profiles. +LIBHEIF_API +heif_color_profile_type heif_image_handle_get_color_profile_type(const heif_image_handle* handle); + +LIBHEIF_API +size_t heif_image_handle_get_raw_color_profile_size(const heif_image_handle* handle); + +// Returns 'heif_error_Color_profile_does_not_exist' when there is no ICC profile. +LIBHEIF_API +struct heif_error heif_image_handle_get_raw_color_profile(const heif_image_handle* handle, + void* out_data); + + +typedef enum heif_color_primaries +{ + heif_color_primaries_ITU_R_BT_709_5 = 1, // g=0.3;0.6, b=0.15;0.06, r=0.64;0.33, w=0.3127,0.3290 + heif_color_primaries_unspecified = 2, + heif_color_primaries_ITU_R_BT_470_6_System_M = 4, + heif_color_primaries_ITU_R_BT_470_6_System_B_G = 5, + heif_color_primaries_ITU_R_BT_601_6 = 6, + heif_color_primaries_SMPTE_240M = 7, + heif_color_primaries_generic_film = 8, + heif_color_primaries_ITU_R_BT_2020_2_and_2100_0 = 9, + heif_color_primaries_SMPTE_ST_428_1 = 10, + heif_color_primaries_SMPTE_RP_431_2 = 11, + heif_color_primaries_SMPTE_EG_432_1 = 12, + heif_color_primaries_EBU_Tech_3213_E = 22 +} heif_color_primaries; + +typedef enum heif_transfer_characteristics +{ + heif_transfer_characteristic_ITU_R_BT_709_5 = 1, + heif_transfer_characteristic_unspecified = 2, + heif_transfer_characteristic_ITU_R_BT_470_6_System_M = 4, + heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G = 5, + heif_transfer_characteristic_ITU_R_BT_601_6 = 6, + heif_transfer_characteristic_SMPTE_240M = 7, + heif_transfer_characteristic_linear = 8, + heif_transfer_characteristic_logarithmic_100 = 9, + heif_transfer_characteristic_logarithmic_100_sqrt10 = 10, + heif_transfer_characteristic_IEC_61966_2_4 = 11, + heif_transfer_characteristic_ITU_R_BT_1361 = 12, + heif_transfer_characteristic_IEC_61966_2_1 = 13, + heif_transfer_characteristic_ITU_R_BT_2020_2_10bit = 14, + heif_transfer_characteristic_ITU_R_BT_2020_2_12bit = 15, + heif_transfer_characteristic_ITU_R_BT_2100_0_PQ = 16, + heif_transfer_characteristic_SMPTE_ST_428_1 = 17, + heif_transfer_characteristic_ITU_R_BT_2100_0_HLG = 18 +} heif_transfer_characteristics; + +typedef enum heif_matrix_coefficients +{ + heif_matrix_coefficients_RGB_GBR = 0, + heif_matrix_coefficients_ITU_R_BT_709_5 = 1, // TODO: or 709-6 according to h.273 + heif_matrix_coefficients_unspecified = 2, + heif_matrix_coefficients_US_FCC_T47 = 4, + heif_matrix_coefficients_ITU_R_BT_470_6_System_B_G = 5, + heif_matrix_coefficients_ITU_R_BT_601_6 = 6, // TODO: or 601-7 according to h.273 + heif_matrix_coefficients_SMPTE_240M = 7, + heif_matrix_coefficients_YCgCo = 8, + heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance = 9, + heif_matrix_coefficients_ITU_R_BT_2020_2_constant_luminance = 10, + heif_matrix_coefficients_SMPTE_ST_2085 = 11, + heif_matrix_coefficients_chromaticity_derived_non_constant_luminance = 12, + heif_matrix_coefficients_chromaticity_derived_constant_luminance = 13, + heif_matrix_coefficients_ICtCp = 14 +} heif_matrix_coefficients; + +typedef struct heif_color_profile_nclx +{ + // === version 1 fields + + uint8_t version; + + heif_color_primaries color_primaries; + heif_transfer_characteristics transfer_characteristics; + heif_matrix_coefficients matrix_coefficients; + uint8_t full_range_flag; + + // --- decoded values (not used when saving nclx) + + float color_primary_red_x, color_primary_red_y; + float color_primary_green_x, color_primary_green_y; + float color_primary_blue_x, color_primary_blue_y; + float color_primary_white_x, color_primary_white_y; +} heif_color_profile_nclx; + +LIBHEIF_API +heif_error heif_nclx_color_profile_set_color_primaries(heif_color_profile_nclx* nclx, uint16_t cp); + +LIBHEIF_API +heif_error heif_nclx_color_profile_set_transfer_characteristics(heif_color_profile_nclx* nclx, uint16_t transfer_characteristics); + +LIBHEIF_API +heif_error heif_nclx_color_profile_set_matrix_coefficients(heif_color_profile_nclx* nclx, uint16_t matrix_coefficients); + +// Returns 'heif_error_Color_profile_does_not_exist' when there is no NCLX profile. +// TODO: This function does currently not return an NCLX profile if it is stored in the image bitstream. +// Only NCLX profiles stored as colr boxes are returned. This may change in the future. +LIBHEIF_API +heif_error heif_image_handle_get_nclx_color_profile(const heif_image_handle* handle, + heif_color_profile_nclx** out_data); + +// Returned color profile has 'version' field set to the maximum allowed. +// Do not fill values for higher versions as these might be outside the allocated structure size. +// May return NULL. +LIBHEIF_API +heif_color_profile_nclx* heif_nclx_color_profile_alloc(void); + +LIBHEIF_API +void heif_nclx_color_profile_free(heif_color_profile_nclx* nclx_profile); + +// Note: in early versions of HEIF, there could only be one color profile per image. However, this has been changed. +// This function will now return ICC if one is present and NCLX only if there is no ICC. +// You may better avoid this function and simply query for NCLX and ICC directly. +LIBHEIF_API +heif_color_profile_type heif_image_get_color_profile_type(const heif_image* image); + +// Returns the size of the ICC profile if one is assigned to the image. Otherwise, it returns 0. +LIBHEIF_API +size_t heif_image_get_raw_color_profile_size(const heif_image* image); + +// Returns the ICC profile if one is assigned to the image. Otherwise, it returns an error. +LIBHEIF_API +heif_error heif_image_get_raw_color_profile(const heif_image* image, + void* out_data); + +LIBHEIF_API +heif_error heif_image_get_nclx_color_profile(const heif_image* image, + heif_color_profile_nclx** out_data); + + +// The color profile is not attached to the image handle because we might need it +// for color space transform and encoding. +LIBHEIF_API +heif_error heif_image_set_raw_color_profile(heif_image* image, + const char* profile_type_fourcc_string, + const void* profile_data, + const size_t profile_size); + +LIBHEIF_API +heif_error heif_image_set_nclx_color_profile(heif_image* image, + const heif_color_profile_nclx* color_profile); + + +// TODO: this function does not make any sense yet, since we currently cannot modify existing HEIF files. +//LIBHEIF_API +//void heif_image_remove_color_profile(struct heif_image* image); + + +// --- content light level --- + +// Semantics follow the H.265 'Content light level information' SEI message +// (ITU-T H.265 Annex D.3.35). Identifies upper bounds for the nominal +// target brightness light level of the pictures. The bounds are defined +// against samples in a 4:4:4 RGB representation in the linear light domain, +// in units of candelas per square metre (cd/m^2 = nits). A field value +// of 0 means "no upper bound indicated" (i.e. undefined). +typedef struct heif_content_light_level +{ + // Upper bound on the maximum light level among all individual linear-light + // 4:4:4 RGB samples across the sequence, in cd/m^2 (= field). + // Also called MaxCLL. + uint16_t max_content_light_level; + + // Upper bound on the maximum per-picture average light level among the + // linear-light 4:4:4 RGB samples (averaged over any individual picture), + // in cd/m^2 (= field). Also called MaxFALL (or MaxPALL). + // For letterboxed content the average is expected to be taken only over + // the visually relevant region (H.265 D.3.35 NOTE 3). + uint16_t max_pic_average_light_level; +} heif_content_light_level; + +LIBHEIF_API +int heif_image_has_content_light_level(const heif_image*); + +LIBHEIF_API +int heif_image_handle_has_content_light_level(const heif_image_handle*); + +// TODO: this function should also return 'int' to be consistent to heif_image_handle_get_content_light_level. +LIBHEIF_API +void heif_image_get_content_light_level(const heif_image*, heif_content_light_level* out); + +// Returns whether the image has 'content light level' information. If 0 is returned, the output is not filled. +LIBHEIF_API +int heif_image_handle_get_content_light_level(const heif_image_handle*, heif_content_light_level* out); + +LIBHEIF_API +void heif_image_set_content_light_level(const heif_image*, const heif_content_light_level* in); + +LIBHEIF_API +void heif_image_handle_set_content_light_level(const heif_image_handle*, const heif_content_light_level* in); + + +// --- mastering display colour volume --- + +// Semantics follow the H.265 'Mastering display colour volume' SEI message +// (ITU-T H.265 Annex D.3.28) and SMPTE ST 2086. Identifies the colour +// volume (primaries, white point, luminance range) of the mastering display +// for the associated content. CIE 1931 chromaticity coordinates are defined +// by ISO 11664-1 (see also ISO 11664-3 and CIE 15). +// +// For RGB mastering displays, H.265 suggests the index order +// [0]=green, [1]=blue, [2]=red +// (see H.265 Annex E, Table E.3). +typedef struct heif_mastering_display_colour_volume +{ + // CIE 1931 chromaticity coordinates of the three colour primaries, in + // normalized increments of 0.00002 (CIE value = field / 50000). + // Valid ranges per H.265: x in 5..37000, y in 5..42000 inclusive; values + // outside indicate "unknown / unspecified". + // Note: SMPTE ST 2086 uses four-decimal-place coordinates, i.e. multiples + // of 5 in this encoding. + uint16_t display_primaries_x[3]; + uint16_t display_primaries_y[3]; + + // CIE 1931 chromaticity coordinates of the mastering display's white + // point, in normalized increments of 0.00002. Valid ranges as for the + // primaries (x in 5..37000, y in 5..42000). + // Note: ANSI/CTA 861-G uses (0,0) to indicate that the white point + // chromaticity is unknown. + uint16_t white_point_x; + uint16_t white_point_y; + + // Nominal maximum / minimum display luminance of the mastering display, + // in units of 0.0001 candelas per square metre (cd/m^2 = field / 10000). + // Valid ranges per H.265: + // max in 50000..100000000 (5..10000 cd/m^2) + // min in 1..50000 (0.0001..5 cd/m^2) + // When max == 50000, min shall not equal 50000. + // Note: ANSI/CTA 861-G uses 0 to indicate "unknown". + // Note: SMPTE ST 2086 (2018) specifies max as a multiple of 1 cd/m^2, i.e. + // a multiple of 10000 in this encoding. + uint32_t max_display_mastering_luminance; + uint32_t min_display_mastering_luminance; +} heif_mastering_display_colour_volume; + +// The units for max_display_mastering_luminance and min_display_mastering_luminance is Candelas per square meter. +typedef struct heif_decoded_mastering_display_colour_volume +{ + float display_primaries_x[3]; + float display_primaries_y[3]; + float white_point_x; + float white_point_y; + double max_display_mastering_luminance; + double min_display_mastering_luminance; +} heif_decoded_mastering_display_colour_volume; + +// Semantics follow the H.265 'Ambient viewing environment' SEI message +// (ITU-T H.265 Annex D.3.39). Identifies the nominal ambient viewing +// environment intended for display of the associated content. +typedef struct heif_ambient_viewing_environment +{ + // Environmental illuminance of the ambient viewing environment, in units + // of 0.0001 lux (i.e. lux = value / 10000). In H.265 this field is called + // 'ambient_illuminance' and is required to be non-zero; 0 is treated here + // as "undefined". + uint32_t ambient_illumination; + + // Normalized CIE 1931 chromaticity coordinates of the environmental + // ambient light, in units of 0.00002 (i.e. CIE x = value / 50000). + // Valid range per H.265 is 0..50000 inclusive. CIE 1931 x/y are defined + // by ISO 11664-1 (see also ISO 11664-3 and CIE 15). + // + // Example (Rec. ITU-R BT.2035, D65 background): ambient_illumination=100000, + // ambient_light_x=15635, ambient_light_y=16450. + uint16_t ambient_light_x; + uint16_t ambient_light_y; +} heif_ambient_viewing_environment; + +LIBHEIF_API +int heif_image_has_mastering_display_colour_volume(const heif_image*); + +LIBHEIF_API +int heif_image_handle_has_mastering_display_colour_volume(const heif_image_handle*); + +LIBHEIF_API +void heif_image_get_mastering_display_colour_volume(const heif_image*, heif_mastering_display_colour_volume* out); + +// Returns whether the image has 'mastering display colour volume' information. If 0 is returned, the output is not filled. +LIBHEIF_API +int heif_image_handle_get_mastering_display_colour_volume(const heif_image_handle*, heif_mastering_display_colour_volume* out); + +LIBHEIF_API +void heif_image_set_mastering_display_colour_volume(const heif_image*, const heif_mastering_display_colour_volume* in); + +LIBHEIF_API +void heif_image_handle_set_mastering_display_colour_volume(const heif_image_handle*, const heif_mastering_display_colour_volume* in); + + +// --- ambient viewing environment --- + +LIBHEIF_API +int heif_image_has_ambient_viewing_environment(const heif_image*); + +LIBHEIF_API +int heif_image_handle_has_ambient_viewing_environment(const heif_image_handle*); + +// Returns whether the image has 'ambient viewing environment' information. If 0 is returned, the output is not filled. +LIBHEIF_API +int heif_image_get_ambient_viewing_environment(const heif_image*, heif_ambient_viewing_environment* out); + +// Returns whether the image has 'ambient viewing environment' information. If 0 is returned, the output is not filled. +LIBHEIF_API +int heif_image_handle_get_ambient_viewing_environment(const heif_image_handle*, heif_ambient_viewing_environment* out); + +LIBHEIF_API +void heif_image_set_ambient_viewing_environment(const heif_image*, const heif_ambient_viewing_environment* in); + +LIBHEIF_API +void heif_image_handle_set_ambient_viewing_environment(const heif_image_handle*, const heif_ambient_viewing_environment* in); + + +// --- nominal diffuse white --- + +// Nominal diffuse white luminance (ISO/IEC 23008-12 'ndwt' box). The luminance +// is given in units of 0.0001 candelas per square metre. A value of 0 is valid +// and selects the default definition of ISO/TS 22028-5. + +// If the image has no 'nominal diffuse white' information, the getter returns 0, +// which is indistinguishable from a stored luminance of 0. Use the has_ function +// to disambiguate. +LIBHEIF_API +int heif_image_has_nominal_diffuse_white_luminance(const heif_image*); + +LIBHEIF_API +uint32_t heif_image_get_nominal_diffuse_white_luminance(const heif_image*); + +LIBHEIF_API +void heif_image_set_nominal_diffuse_white_luminance(const heif_image*, uint32_t luminance); + +LIBHEIF_API +int heif_image_handle_has_nominal_diffuse_white_luminance(const heif_image_handle*); + +LIBHEIF_API +uint32_t heif_image_handle_get_nominal_diffuse_white_luminance(const heif_image_handle*); + +LIBHEIF_API +void heif_image_handle_set_nominal_diffuse_white_luminance(const heif_image_handle*, uint32_t luminance); + + +// Converts the internal numeric representation of heif_mastering_display_colour_volume to the +// normalized values, collected in heif_decoded_mastering_display_colour_volume. +// Values that are out-of-range are decoded to 0, indicating an undefined value (as specified in ISO/IEC 23008-2). +LIBHEIF_API +struct heif_error heif_mastering_display_colour_volume_decode(const heif_mastering_display_colour_volume* in, + heif_decoded_mastering_display_colour_volume* out); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_components.cc libheif-1.23.4/libheif/api/libheif/heif_components.cc --- libheif-1.19.8/libheif/api/libheif/heif_components.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_components.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,279 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_components.h" +#include "api_structs.h" +#include "image/pixelimage.h" + + +// --- id-based component access on a decoded heif_image + +uint32_t heif_image_get_number_of_used_components(const heif_image* image) +{ + if (!image || !image->image) { + return 0; + } + return image->image->get_number_of_used_components(); +} + + +void heif_image_get_used_component_ids(const heif_image* image, uint32_t* out_component_ids) +{ + if (!image || !image->image || !out_component_ids) { + return; + } + + auto indices = image->image->get_used_component_ids(); + for (size_t i = 0; i < indices.size(); i++) { + out_component_ids[i] = indices[i]; + } +} + + +heif_channel heif_image_get_component_channel(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return heif_channel_Y; + } + return image->image->get_component_channel(component_id); +} + + +uint32_t heif_image_get_component_width(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return 0; + } + return image->image->get_component_width(component_id); +} + + +uint32_t heif_image_get_component_height(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return 0; + } + return image->image->get_component_height(component_id); +} + + +int heif_image_get_component_bits_per_pixel(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return 0; + } + return image->image->get_component_bits_per_pixel(component_id); +} + + +uint16_t heif_image_get_component_type(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return 0; + } + return image->image->get_component_type(component_id); +} + + +heif_component_datatype heif_image_get_component_datatype(const heif_image* image, uint32_t component_id) +{ + if (!image || !image->image) { + return heif_component_datatype_undefined; + } + return image->image->get_component_datatype(component_id); +} + + +// --- handle-side per-component getters +// +// These read from ImageItem::m_components (populated at parse time by +// ImageItem::populate_component_descriptions). For unci images, the ids are +// the same numerical values that heif_image_get_used_component_ids() will +// report after decoding. + +uint32_t heif_image_handle_get_number_of_components(const heif_image_handle* handle) +{ + if (!handle) { + return 0; + } + return static_cast(handle->image->get_component_descriptions().size()); +} + + +void heif_image_handle_get_used_component_ids(const heif_image_handle* handle, uint32_t* out_component_ids) +{ + if (!handle || !out_component_ids) { + return; + } + const auto& comps = handle->image->get_component_descriptions(); + for (size_t i = 0; i < comps.size(); i++) { + out_component_ids[i] = comps[i].component_id; + } +} + + +uint16_t heif_image_handle_get_component_type(const heif_image_handle* handle, uint32_t component_id) +{ + if (!handle) { + return 0; + } + if (auto* desc = handle->image->find_component_description(component_id)) { + return desc->component_type; + } + return 0; +} + + +int heif_image_handle_get_component_bits_per_pixel(const heif_image_handle* handle, uint32_t component_id) +{ + if (!handle) { + return -1; + } + if (auto* desc = handle->image->find_component_description(component_id)) { + return static_cast(desc->bit_depth); + } + return -1; +} + + +heif_component_datatype heif_image_handle_get_component_datatype(const heif_image_handle* handle, uint32_t component_id) +{ + if (!handle) { + return heif_component_datatype_undefined; + } + if (auto* desc = handle->image->find_component_description(component_id)) { + return desc->datatype; + } + return heif_component_datatype_undefined; +} + + +// --- adding components + +heif_error heif_image_add_component(heif_image* image, + int width, int height, + uint16_t component_type, + heif_component_datatype datatype, + int bit_depth, + uint32_t* out_component_id) +{ + if (!image || !image->image) { + return heif_error_null_pointer_argument; + } + + auto result = image->image->add_component(width, height, component_type, datatype, bit_depth, nullptr); + if (!result) { + return result.error_struct(image->image.get()); + } + + if (out_component_id) { + *out_component_id = *result; + } + + return heif_error_success; +} + + +// --- untyped uint8 accessors + +const uint8_t* heif_image_get_component_readonly(const heif_image* image, uint32_t component_id, size_t* out_stride) +{ + if (!image || !image->image) { + if (out_stride) *out_stride = 0; + return nullptr; + } + return image->image->get_component(component_id, out_stride); +} + + +uint8_t* heif_image_get_component(heif_image* image, uint32_t component_id, size_t* out_stride) +{ + if (!image || !image->image) { + if (out_stride) *out_stride = 0; + return nullptr; + } + return image->image->get_component(component_id, out_stride); +} + + +// Typed accessors: convert the internal byte stride to element count for the +// public API. libheif's allocator pads rows to a 16-byte boundary (see +// pixelimage.cc), which is a multiple of sizeof(T) for every supported T +// (<= 16 bytes), so the division is always exact. +#define heif_image_get_component_X(name, type) \ +const type* heif_image_get_component_ ## name ## _readonly(const struct heif_image* image, \ + uint32_t component_id, \ + size_t* out_row_elements) \ +{ \ + if (!image || !image->image) { \ + if (out_row_elements) *out_row_elements = 0; \ + return nullptr; \ + } \ + size_t byte_stride = 0; \ + const type* p = image->image->get_component_memory(component_id, &byte_stride); \ + if (out_row_elements) *out_row_elements = byte_stride / sizeof(type); \ + return p; \ +} \ + \ +type* heif_image_get_component_ ## name (struct heif_image* image, \ + uint32_t component_id, \ + size_t* out_row_elements) \ +{ \ + if (!image || !image->image) { \ + if (out_row_elements) *out_row_elements = 0; \ + return nullptr; \ + } \ + size_t byte_stride = 0; \ + type* p = image->image->get_component_memory(component_id, &byte_stride); \ + if (out_row_elements) *out_row_elements = byte_stride / sizeof(type); \ + return p; \ +} + +heif_image_get_component_X(uint16, uint16_t) +heif_image_get_component_X(uint32, uint32_t) +heif_image_get_component_X(uint64, uint64_t) +heif_image_get_component_X(int8, int8_t) +heif_image_get_component_X(int16, int16_t) +heif_image_get_component_X(int32, int32_t) +heif_image_get_component_X(int64, int64_t) +heif_image_get_component_X(float32, float) +heif_image_get_component_X(float64, double) +heif_image_get_component_X(complex32, heif_complex32) +heif_image_get_component_X(complex64, heif_complex64) + + +heif_error heif_image_set_gimi_component_content_id(heif_image* image, + uint32_t component_id, + const char* content_id) +{ + if (image == nullptr || content_id == nullptr) { + return heif_error_null_pointer_argument; + } + + auto* desc = image->image->find_component_description(component_id); + if (!desc) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "No component with the requested component_id exists."}; + } + desc->gimi_content_id = content_id; + + return heif_error_success; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_components.h libheif-1.23.4/libheif/api/libheif/heif_components.h --- libheif-1.19.8/libheif/api/libheif/heif_components.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_components.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,264 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_COMPONENTS_H +#define LIBHEIF_HEIF_COMPONENTS_H + +#include "libheif/heif_library.h" +#include "libheif/heif_image.h" +#include "libheif/heif_image_handle.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* @file heif_components.h + * @brief Multi-component image API. + * + * The component model (id-addressed pixel planes with per-component type, + * datatype and bit-depth) is codec-agnostic. It was introduced for ISO 23001-17 + * (uncompressed) images but is also expected to be reused by other codecs that + * carry multiple typed components (e.g. JPEG-2000). + */ + + +// --- pixel datatype support +// +// The numeric values are aligned with the ISO/IEC 23001-17 Table 2 +// component_format byte (used by the uncC box of the uncompressed codec). +// This is an internal convenience and should not be relied upon. + +typedef enum heif_component_datatype +{ + heif_component_datatype_unsigned_integer = 0, + heif_component_datatype_floating_point = 1, + heif_component_datatype_complex_number = 2, + heif_component_datatype_signed_integer = 3, + heif_component_datatype_undefined = 0xFF +} heif_component_datatype; + +typedef struct heif_complex32 +{ + float real, imaginary; +} heif_complex32; + +typedef struct heif_complex64 +{ + double real, imaginary; +} heif_complex64; + + +// --- component types (ISO/IEC 23001-17 Table 1, used in the cmpd box) + +typedef enum heif_cmpd_component_type +{ + heif_cmpd_component_type_monochrome = 0, + heif_cmpd_component_type_Y = 1, + heif_cmpd_component_type_Cb = 2, + heif_cmpd_component_type_Cr = 3, + heif_cmpd_component_type_red = 4, + heif_cmpd_component_type_green = 5, + heif_cmpd_component_type_blue = 6, + heif_cmpd_component_type_alpha = 7, + heif_cmpd_component_type_depth = 8, + heif_cmpd_component_type_disparity = 9, + heif_cmpd_component_type_palette = 10, + heif_cmpd_component_type_filter_array = 11, + heif_cmpd_component_type_padded = 12, + heif_cmpd_component_type_cyan = 13, + heif_cmpd_component_type_magenta = 14, + heif_cmpd_component_type_yellow = 15, + heif_cmpd_component_type_key_black = 16 +} heif_cmpd_component_type; + + +// --- ID-based component access (operates on a decoded heif_image) + +// Returns the number of components that have pixel data (planes) in this image. +LIBHEIF_API +uint32_t heif_image_get_number_of_used_components(const heif_image*); + +// Fills `out_component_ids` with the valid component IDs. +// The caller must allocate an array of at least heif_image_get_number_of_used_components() elements. +LIBHEIF_API +void heif_image_get_used_component_ids(const heif_image*, uint32_t* out_component_ids); + +LIBHEIF_API +heif_channel heif_image_get_component_channel(const heif_image*, uint32_t component_id); + +LIBHEIF_API +uint32_t heif_image_get_component_width(const heif_image*, uint32_t component_id); + +LIBHEIF_API +uint32_t heif_image_get_component_height(const heif_image*, uint32_t component_id); + +LIBHEIF_API +int heif_image_get_component_bits_per_pixel(const heif_image*, uint32_t component_id); + +LIBHEIF_API +uint16_t heif_image_get_component_type(const heif_image*, uint32_t component_id); + +// Returns the datatype (unsigned/signed integer, floating point, or complex +// number) of the given component. +LIBHEIF_API +heif_component_datatype heif_image_get_component_datatype(const heif_image*, uint32_t component_id); + + +// --- ID-based component access via heif_image_handle (before decoding) +// +// These let the caller introspect a multi-component image's components without +// decoding any tile. They return 0 / -1 / heif_component_datatype_undefined for +// images that do not expose a component model or for unknown component IDs. +// +// The component IDs returned here match the IDs that +// heif_image_get_used_component_ids() will report after the same image is +// decoded, so the same numerical id can be used to address a component on +// either side of the API. + +LIBHEIF_API +uint32_t heif_image_handle_get_number_of_components(const heif_image_handle*); + +// Fills `out_component_ids` with the valid component IDs. +// The caller must allocate an array of at least +// heif_image_handle_get_number_of_components() elements. +LIBHEIF_API +void heif_image_handle_get_used_component_ids(const heif_image_handle*, uint32_t* out_component_ids); + +LIBHEIF_API +uint16_t heif_image_handle_get_component_type(const heif_image_handle*, uint32_t component_id); + +LIBHEIF_API +int heif_image_handle_get_component_bits_per_pixel(const heif_image_handle*, uint32_t component_id); + +LIBHEIF_API +heif_component_datatype heif_image_handle_get_component_datatype(const heif_image_handle*, uint32_t component_id); + + +// --- adding components to a heif_image (encoder path) + +LIBHEIF_API +heif_error heif_image_add_component(heif_image* image, + int width, int height, + uint16_t component_type, + heif_component_datatype datatype, + int bit_depth, + uint32_t* out_component_id); + + +// --- untyped uint8 component data getters: stride is in BYTES per row. + +LIBHEIF_API +const uint8_t* heif_image_get_component_readonly(const heif_image*, uint32_t component_id, size_t* out_stride); + +LIBHEIF_API +uint8_t* heif_image_get_component(heif_image*, uint32_t component_id, size_t* out_stride); + + +// --- typed component data getters: `out_row_elements` is the number of T +// elements per row, not bytes. Index with `data[y * row_elements + x]` (no +// casts, no sizeof). libheif allocates rows with element-aligned padding, so +// this count is always exact for the named type T. + +LIBHEIF_API +const uint16_t* heif_image_get_component_uint16_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +uint16_t* heif_image_get_component_uint16(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const uint32_t* heif_image_get_component_uint32_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +uint32_t* heif_image_get_component_uint32(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const uint64_t* heif_image_get_component_uint64_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +uint64_t* heif_image_get_component_uint64(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const int8_t* heif_image_get_component_int8_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +int8_t* heif_image_get_component_int8(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const int16_t* heif_image_get_component_int16_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +int16_t* heif_image_get_component_int16(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const int32_t* heif_image_get_component_int32_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +int32_t* heif_image_get_component_int32(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const int64_t* heif_image_get_component_int64_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +int64_t* heif_image_get_component_int64(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const float* heif_image_get_component_float32_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +float* heif_image_get_component_float32(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const double* heif_image_get_component_float64_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +double* heif_image_get_component_float64(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const heif_complex32* heif_image_get_component_complex32_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +heif_complex32* heif_image_get_component_complex32(heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +const heif_complex64* heif_image_get_component_complex64_readonly(const heif_image*, uint32_t component_id, size_t* out_row_elements); + +LIBHEIF_API +heif_complex64* heif_image_get_component_complex64(heif_image*, uint32_t component_id, size_t* out_row_elements); + + +// --- GIMI component content IDs (set before encoding) + +// Set a GIMI component content ID for the component with the given +// component_id (as minted by heif_image_add_component / returned via the +// component access API). Pass an empty string to clear a previously set id. +// Returns an error if no component with this id exists on the image. +// The collected ids are written into an ItemComponentContentIDProperty box +// during encoding. +LIBHEIF_API +heif_error heif_image_set_gimi_component_content_id(heif_image*, + uint32_t component_id, + const char* content_id); + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_context.cc libheif-1.23.4/libheif/api/libheif/heif_context.cc --- libheif-1.19.8/libheif/api/libheif/heif_context.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_context.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,312 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_context.h" +#include "api_structs.h" +#include "context.h" +#include "init.h" +#include "file.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +// for _write +#include +#else +#include +#endif + + +heif_context* heif_context_alloc() +{ + load_plugins_if_not_initialized_yet(); + + heif_context* ctx = new heif_context; + ctx->context = std::make_shared(); + + return ctx; +} + +void heif_context_free(heif_context* ctx) +{ + delete ctx; +} + +heif_error heif_context_read_from_file(heif_context* ctx, const char* filename, + const heif_reading_options*) +{ + return exception_guard([&]() -> heif_error { + Error err = ctx->context->read_from_file(filename); + return err.error_struct(ctx->context.get()); + }); +} + +heif_error heif_context_read_from_memory(heif_context* ctx, const void* mem, size_t size, + const heif_reading_options*) +{ + return exception_guard([&]() -> heif_error { + Error err = ctx->context->read_from_memory(mem, size, true); + return err.error_struct(ctx->context.get()); + }); +} + +heif_error heif_context_read_from_memory_without_copy(heif_context* ctx, const void* mem, size_t size, + const heif_reading_options*) +{ + return exception_guard([&]() -> heif_error { + Error err = ctx->context->read_from_memory(mem, size, false); + return err.error_struct(ctx->context.get()); + }); +} + +heif_error heif_context_read_from_reader(heif_context* ctx, + const heif_reader* reader_func_table, + void* userdata, + const heif_reading_options*) +{ + return exception_guard([&]() -> heif_error { + auto reader = std::make_shared(reader_func_table, userdata); + + Error err = ctx->context->read(reader); + return err.error_struct(ctx->context.get()); + }); +} + +// TODO: heif_error heif_context_read_from_file_descriptor(heif_context*, int fd); + +int heif_context_get_number_of_top_level_images(heif_context* ctx) +{ + return (int) ctx->context->get_top_level_images(true).size(); +} + + +int heif_context_is_top_level_image_ID(heif_context* ctx, heif_item_id id) +{ + const std::vector > images = ctx->context->get_top_level_images(true); + + for (const auto& img : images) { + if (img->get_id() == id) { + return true; + } + } + + return false; +} + + +int heif_context_get_list_of_top_level_image_IDs(heif_context* ctx, + heif_item_id* ID_array, + int count) +{ + if (ID_array == nullptr || count == 0 || ctx == nullptr) { + return 0; + } + + + // fill in ID values into output array + + const std::vector > imgs = ctx->context->get_top_level_images(true); + int n = (int) std::min(count, (int) imgs.size()); + for (int i = 0; i < n; i++) { + ID_array[i] = imgs[i]->get_id(); + } + + return n; +} + + +heif_error heif_context_get_primary_image_ID(heif_context* ctx, heif_item_id* id) +{ + if (!id) { + return Error(heif_error_Usage_error, + heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); + } + + std::shared_ptr primary = ctx->context->get_primary_image(true); + if (!primary) { + return Error(heif_error_Invalid_input, + heif_suberror_No_or_invalid_primary_item).error_struct(ctx->context.get()); + } + + *id = primary->get_id(); + + return Error::Ok.error_struct(ctx->context.get()); +} + + +heif_error heif_context_get_primary_image_handle(heif_context* ctx, heif_image_handle** img) +{ + if (!img) { + Error err(heif_error_Usage_error, + heif_suberror_Null_pointer_argument); + return err.error_struct(ctx->context.get()); + } + + std::shared_ptr primary_image = ctx->context->get_primary_image(true); + + // It is a requirement of an HEIF file there is always a primary image. + // If there is none, an error is generated when loading the file. + if (!primary_image) { + Error err(heif_error_Invalid_input, + heif_suberror_No_or_invalid_primary_item); + return err.error_struct(ctx->context.get()); + } + + if (Error error = primary_image->get_item_error()) { + return error.error_struct(ctx->context.get()); + } + + *img = new heif_image_handle(); + (*img)->image = std::move(primary_image); + (*img)->context = ctx->context; + + return Error::Ok.error_struct(ctx->context.get()); +} + + +heif_error heif_context_get_image_handle(heif_context* ctx, + heif_item_id id, + heif_image_handle** imgHdl) +{ + if (!imgHdl) { + return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, ""}; + } + + auto image = ctx->context->get_image(id, true); + + if (!image) { + *imgHdl = nullptr; + + return {heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced, ""}; + } + + if (Error error = image->get_item_error()) { + return error.error_struct(ctx->context.get()); + } + + *imgHdl = new heif_image_handle(); + (*imgHdl)->image = std::move(image); + (*imgHdl)->context = ctx->context; + + return heif_error_success; +} + + +void heif_context_debug_dump_boxes_to_file(heif_context* ctx, int fd) +{ + if (!ctx) { + return; + } + + std::string dump = ctx->context->debug_dump_boxes(); + + std::string item_dump = ctx->context->debug_dump_item_data(); + if (!item_dump.empty()) { + dump += "\n"; + dump += item_dump; + } + + // TODO(fancycode): Should we return an error if writing fails? +#ifdef _WIN32 + auto written = _write(fd, dump.c_str(), static_cast(dump.size())); +#else + auto written = write(fd, dump.c_str(), dump.size()); +#endif + (void) written; +} + + +// ==================================================================================================== +// Write the heif_context to a HEIF file + + +static heif_error heif_file_writer_write(heif_context* ctx, + const void* data, size_t size, void* userdata) +{ + const char* filename = static_cast(userdata); + +#if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER) + std::ofstream ostr(HeifFile::convert_utf8_path_to_utf16(filename).c_str(), std::ios_base::binary); +#else + std::ofstream ostr(filename, std::ios_base::binary); +#endif + ostr.write(static_cast(data), size); + // TODO: handle write errors + return Error::Ok.error_struct(ctx->context.get()); +} + + +void heif_context_set_write_mini_format(heif_context* ctx, int enable) +{ + ctx->context->set_write_mini_format(enable != 0); +} + + +heif_error heif_context_write_to_file(heif_context* ctx, + const char* filename) +{ + heif_writer writer; + writer.writer_api_version = 1; + writer.write = heif_file_writer_write; + return heif_context_write(ctx, &writer, (void*) filename); +} + + +heif_error heif_context_write(heif_context* ctx, + heif_writer* writer, + void* userdata) +{ + if (!writer) { + return Error(heif_error_Usage_error, + heif_suberror_Null_pointer_argument).error_struct(ctx->context.get()); + } + + if (writer->writer_api_version != 1) { + Error err(heif_error_Usage_error, heif_suberror_Unsupported_writer_version); + return err.error_struct(ctx->context.get()); + } + + StreamWriter swriter; + Error err = ctx->context->write(swriter); + if (err) { + return err.error_struct(ctx->context.get()); + } + + const auto& data = swriter.get_data(); + heif_error writer_error = writer->write(ctx, data.data(), data.size(), userdata); + if (!writer_error.message) { + // It is now allowed to return a NULL error message on success. It will be replaced by "Success". An error message is still required when there is an error. + if (writer_error.code == heif_error_Ok) { + writer_error.message = Error::kSuccess; + return writer_error; + } + else { + return heif_error{heif_error_Usage_error, heif_suberror_Null_pointer_argument, "heif_writer callback returned a null error text"}; + } + } + else { + return writer_error; + } +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_context.h libheif-1.23.4/libheif/api/libheif/heif_context.h --- libheif-1.19.8/libheif/api/libheif/heif_context.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_context.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,343 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_CONTEXT_H +#define LIBHEIF_HEIF_CONTEXT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + + +/** + * libheif known compression formats. + */ +typedef enum heif_compression_format +{ + /** + * Unspecified / undefined compression format. + * + * This is used to mean "no match" or "any decoder" for some parts of the + * API. It does not indicate a specific compression format. + */ + heif_compression_undefined = 0, + /** + * HEVC compression, used for HEIC images. + * + * This is equivalent to H.265. + */ + heif_compression_HEVC = 1, + /** + * AVC compression. (Currently unused in libheif.) + * + * The compression is defined in ISO/IEC 14496-10. This is equivalent to H.264. + * + * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex E. + */ + heif_compression_AVC = 2, + /** + * JPEG compression. + * + * The compression format is defined in ISO/IEC 10918-1. The encapsulation + * of JPEG is specified in ISO/IEC 23008-12:2022 Annex H. + */ + heif_compression_JPEG = 3, + /** + * AV1 compression, used for AVIF images. + * + * The compression format is provided at https://aomediacodec.github.io/av1-spec/ + * + * The encapsulation is defined in https://aomediacodec.github.io/av1-avif/ + */ + heif_compression_AV1 = 4, + /** + * VVC compression. + * + * The compression format is defined in ISO/IEC 23090-3. This is equivalent to H.266. + * + * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex L. + */ + heif_compression_VVC = 5, + /** + * EVC compression. (Currently unused in libheif.) + * + * The compression format is defined in ISO/IEC 23094-1. + * + * The encapsulation is defined in ISO/IEC 23008-12:2022 Annex M. + */ + heif_compression_EVC = 6, + /** + * JPEG 2000 compression. + * + * The encapsulation of JPEG 2000 is specified in ISO/IEC 15444-16:2021. + * The core encoding is defined in ISO/IEC 15444-1, or ITU-T T.800. + */ + heif_compression_JPEG2000 = 7, + /** + * Uncompressed encoding. + * + * This is defined in ISO/IEC 23001-17:2024. + */ + heif_compression_uncompressed = 8, + /** + * Mask image encoding. + * + * See ISO/IEC 23008-12:2022 Section 6.10.2 + */ + 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 +} heif_compression_format; + + +// ========================= heif_context ========================= +// A heif_context represents a HEIF file that has been read. +// In the future, you will also be able to add pictures to a heif_context +// and write it into a file again. + + +// Allocate a new context for reading HEIF files. +// Has to be freed again with heif_context_free(). +LIBHEIF_API +heif_context* heif_context_alloc(void); + +// Free a previously allocated HEIF context. You should not free a context twice. +LIBHEIF_API +void heif_context_free(heif_context*); + + +typedef struct heif_reading_options heif_reading_options; + +typedef enum heif_reader_grow_status +{ + heif_reader_grow_status_size_reached, // requested size has been reached, we can read until this point + heif_reader_grow_status_timeout, // size has not been reached yet, but it may still grow further (deprecated) + heif_reader_grow_status_size_beyond_eof, // size has not been reached and never will. The file has grown to its full size + heif_reader_grow_status_error // an error has occurred +} heif_reader_grow_status; + + +typedef struct heif_reader_range_request_result +{ + enum heif_reader_grow_status status; // should not return 'heif_reader_grow_status_timeout' + + // Indicates up to what position the file has been read. + // If we cannot read the whole file range (status == 'heif_reader_grow_status_size_beyond_eof'), this is the actual end position. + // On the other hand, it may be that the reader was reading more data than requested. In that case, it should indicate the full size here + // and libheif may decide to make use of the additional data (e.g. for filling 'tili' offset tables). + uint64_t range_end; + + // for status == 'heif_reader_grow_status_error' + int reader_error_code; // a reader specific error code + const char* reader_error_msg; // libheif will call heif_reader.release_error_msg on this if it is not NULL +} heif_reader_range_request_result; + + +typedef struct heif_reader +{ + // API version supported by this reader + int reader_api_version; + + // --- version 1 functions --- + int64_t (* get_position)(void* userdata); + + // The functions read(), and seek() return 0 on success. + // Generally, libheif will make sure that we do not read past the file size. + int (* read)(void* data, + size_t size, + void* userdata); + + int (* seek)(int64_t position, + void* userdata); + + // When calling this function, libheif wants to make sure that it can read the file + // up to 'target_size'. This is useful when the file is currently downloaded and may + // grow with time. You may, for example, extract the image sizes even before the actual + // compressed image data has been completely downloaded. + // + // Even if your input files will not grow, you will have to implement at least + // detection whether the target_size is above the (fixed) file length + // (in this case, return 'size_beyond_eof'). + enum heif_reader_grow_status (* wait_for_file_size)(int64_t target_size, void* userdata); + + // --- version 2 functions --- + + // These two functions are for applications that want to stream HEIF files on demand. + // For example, a large HEIF file that is served over HTTPS and we only want to download + // it partially to decode individual tiles. + // If you do not have this use case, you do not have to implement these functions and + // you can set them to NULL. For simple linear loading, you may use the 'wait_for_file_size' + // function above instead. + + // If this function is defined, libheif will often request a file range before accessing it. + // The purpose of this function is that libheif will usually read very small chunks of data with the + // read() callback above. However, it is inefficient to request such a small chunk of data over a network + // and the network delay will significantly increase the decoding time. + // Thus, libheif will call request_range() with a larger block of data that should be preloaded and the + // subsequent read() calls will work within the requested ranges. + // + // Note: `end_pos` is one byte after the last position to be read. + // You should return + // - 'heif_reader_grow_status_size_reached' if the requested range is available, or + // - 'heif_reader_grow_status_size_beyond_eof' if the requested range exceeds the file size + // (the valid part of the range has been read). + heif_reader_range_request_result (* request_range)(uint64_t start_pos, uint64_t end_pos, void* userdata); + + // libheif might issue hints when it assumes that a file range might be needed in the future. + // This may happen, for example, when your are doing selective tile accesses and libheif proposes + // to preload offset pointer tables. + // Another difference to request_file_range() is that this call should be non-blocking. + // If you preload any data, do this in a background thread. + void (* preload_range_hint)(uint64_t start_pos, uint64_t end_pos, void* userdata); + + // If libheif does not need access to a file range anymore, it may call this function to + // give a hint to the reader that it may release the range from a cache. + // If you do not maintain a file cache that wants to reduce its size dynamically, you do not + // need to implement this function. + void (* release_file_range)(uint64_t start_pos, uint64_t end_pos, void* userdata); + + // Release an error message that was returned by heif_reader in an earlier call. + // If this function is NULL, the error message string will not be released. + // This is a viable option if you are only returning static strings. + void (* release_error_msg)(const char* msg); +} heif_reader; + + +// Read a HEIF file from a named disk file. +// The heif_reading_options should currently be set to NULL. +LIBHEIF_API +heif_error heif_context_read_from_file(heif_context*, const char* filename, + const heif_reading_options*); + +// Read a HEIF file stored completely in memory. +// The heif_reading_options should currently be set to NULL. +// DEPRECATED: use heif_context_read_from_memory_without_copy() instead. +LIBHEIF_API +heif_error heif_context_read_from_memory(heif_context*, + const void* mem, size_t size, + const heif_reading_options*); + +// Same as heif_context_read_from_memory() except that the provided memory is not copied. +// That means, you will have to keep the memory area alive as long as you use the heif_context. +LIBHEIF_API +heif_error heif_context_read_from_memory_without_copy(heif_context*, + const void* mem, size_t size, + const heif_reading_options*); + +LIBHEIF_API +heif_error heif_context_read_from_reader(heif_context*, + const heif_reader* reader, + void* userdata, + const heif_reading_options*); + +// Number of top-level images in the HEIF file. This does not include the thumbnails or the +// tile images that are composed to an image grid. You can get access to the thumbnails via +// the main image handle. +LIBHEIF_API +int heif_context_get_number_of_top_level_images(heif_context* ctx); + +LIBHEIF_API +int heif_context_is_top_level_image_ID(heif_context* ctx, heif_item_id id); + +// Fills in image IDs into the user-supplied int-array 'ID_array', preallocated with 'count' entries. +// Function returns the total number of IDs filled into the array. +LIBHEIF_API +int heif_context_get_list_of_top_level_image_IDs(heif_context* ctx, + heif_item_id* ID_array, + int count); + +LIBHEIF_API +heif_error heif_context_get_primary_image_ID(heif_context* ctx, heif_item_id* id); + +// Get a handle to the primary image of the HEIF file. +// This is the image that should be displayed primarily when there are several images in the file. +LIBHEIF_API +heif_error heif_context_get_primary_image_handle(heif_context* ctx, + heif_image_handle**); + +// Get the image handle for a known image ID. +LIBHEIF_API +heif_error heif_context_get_image_handle(heif_context* ctx, + heif_item_id id, + heif_image_handle**); + +// Print information about the boxes of a HEIF file to file descriptor. +// This is for debugging and informational purposes only. You should not rely on +// the output having a specific format. At best, you should not use this at all. +LIBHEIF_API +void heif_context_debug_dump_boxes_to_file(heif_context* ctx, int fd); + +// ==================================================================================================== +// Mini format (experimental) + +// Enable writing in the compact 'mini' box format (ISO/IEC 23008-12 DAmd2). +// When enabled, the output file will use a single 'mini' box instead of the standard +// meta+mdat box structure, if the file content is compatible with the mini format. +// If the content cannot be represented as a mini box, the standard format is used as fallback. +// Note: the mini box format is defined by a draft amendment to ISO/IEC 23008-12 +// and may have reduced interoperability with other readers. +// Default: disabled. +LIBHEIF_API +void heif_context_set_write_mini_format(heif_context*, int enable); + + +// ==================================================================================================== +// Write the heif_context to a HEIF file + +LIBHEIF_API +heif_error heif_context_write_to_file(heif_context*, + const char* filename); + +typedef struct heif_writer +{ + // API version supported by this writer + int writer_api_version; + + // --- version 1 functions --- + + // On success, the returned heif_error may have a NULL message. It will automatically be replaced with a "Success" string. + heif_error (* write)(heif_context* ctx, // TODO: why do we need this parameter? + const void* data, + size_t size, + void* userdata); +} heif_writer; + + +LIBHEIF_API +heif_error heif_context_write(heif_context*, + heif_writer* writer, + void* userdata); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_cxx.h libheif-1.23.4/libheif/api/libheif/heif_cxx.h --- libheif-1.19.8/libheif/api/libheif/heif_cxx.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_cxx.h 2026-09-06 14:45:17.000000000 +0000 @@ -232,6 +232,22 @@ int get_ispe_height() const noexcept; + // ------------------------- HDR metadata ------------------------- + + bool has_content_light_level() const noexcept; + + // Returns a zero-initialized struct if the image has no such information. + heif_content_light_level get_content_light_level() const noexcept; + + void set_content_light_level(const heif_content_light_level&) noexcept; + + bool has_mastering_display_colour_volume() const noexcept; + + // Returns a zero-initialized struct if the image has no such information. + heif_mastering_display_colour_volume get_mastering_display_colour_volume() const noexcept; + + void set_mastering_display_colour_volume(const heif_mastering_display_colour_volume&) noexcept; + // ------------------------- depth images ------------------------- // TODO @@ -283,7 +299,7 @@ public: ColorProfile_nclx(); - ~ColorProfile_nclx(); + ~ColorProfile_nclx() = default; heif_color_primaries get_color_primaries() const; @@ -305,10 +321,9 @@ void set_full_range_flag(bool is_full_range); private: - ColorProfile_nclx(heif_color_profile_nclx* nclx) - { mProfile = nclx; } + ColorProfile_nclx(heif_color_profile_nclx* nclx); - heif_color_profile_nclx* mProfile; + std::shared_ptr mProfile; friend class Image; }; @@ -324,8 +339,8 @@ // throws Error void create(int width, int height, - enum heif_colorspace colorspace, - enum heif_chroma chroma); + heif_colorspace colorspace, + heif_chroma chroma); // throws Error void add_plane(enum heif_channel channel, @@ -345,10 +360,16 @@ bool has_channel(enum heif_channel channel) const noexcept; + // DEPRECATED const uint8_t* get_plane(enum heif_channel channel, int* out_stride) const noexcept; + // DEPRECATED uint8_t* get_plane(enum heif_channel channel, int* out_stride) noexcept; + const uint8_t* get_plane2(enum heif_channel channel, size_t* out_stride) const noexcept; + + uint8_t* get_plane2(enum heif_channel channel, size_t* out_stride) noexcept; + // throws Error void set_nclx_color_profile(const ColorProfile_nclx&); @@ -363,6 +384,22 @@ void set_raw_color_profile(heif_color_profile_type type, const std::vector& data); + // ------------------------- HDR metadata ------------------------- + + bool has_content_light_level() const noexcept; + + // Returns a zero-initialized struct if the image has no such information. + heif_content_light_level get_content_light_level() const noexcept; + + void set_content_light_level(const heif_content_light_level&) noexcept; + + bool has_mastering_display_colour_volume() const noexcept; + + // Returns a zero-initialized struct if the image has no such information. + heif_mastering_display_colour_volume get_mastering_display_colour_volume() const noexcept; + + void set_mastering_display_colour_volume(const heif_mastering_display_colour_volume&) noexcept; + bool is_premultiplied_alpha() const noexcept; void set_premultiplied_alpha(bool is_premultiplied_alpha) noexcept; @@ -386,14 +423,14 @@ { public: static std::vector - get_encoder_descriptors(enum heif_compression_format format_filter, + get_encoder_descriptors(heif_compression_format format_filter, const char* name_filter) noexcept; std::string get_name() const noexcept; std::string get_id_name() const noexcept; - enum heif_compression_format get_compression_format() const noexcept; + heif_compression_format get_compression_format() const noexcept; // DEPRECATED: typo in function name bool supportes_lossy_compression() const noexcept; @@ -448,7 +485,7 @@ { public: // throws Error - Encoder(enum heif_compression_format format); + Encoder(heif_compression_format format); // throws Error void set_lossy_quality(int quality); @@ -552,7 +589,11 @@ heif_reader_trampoline_get_position, heif_reader_trampoline_read, heif_reader_trampoline_seek, - heif_reader_trampoline_wait_for_file_size + heif_reader_trampoline_wait_for_file_size, + NULL, + NULL, + NULL, + NULL, }; inline void Context::read_from_reader(Reader& reader, const ReadingOptions& /*opts*/) @@ -713,6 +754,40 @@ return heif_image_handle_get_ispe_height(m_image_handle.get()); } + inline bool ImageHandle::has_content_light_level() const noexcept + { + return heif_image_handle_has_content_light_level(m_image_handle.get()) != 0; + } + + inline heif_content_light_level ImageHandle::get_content_light_level() const noexcept + { + heif_content_light_level clli{}; + heif_image_handle_get_content_light_level(m_image_handle.get(), &clli); + return clli; + } + + inline void ImageHandle::set_content_light_level(const heif_content_light_level& clli) noexcept + { + heif_image_handle_set_content_light_level(m_image_handle.get(), &clli); + } + + inline bool ImageHandle::has_mastering_display_colour_volume() const noexcept + { + return heif_image_handle_has_mastering_display_colour_volume(m_image_handle.get()) != 0; + } + + inline heif_mastering_display_colour_volume ImageHandle::get_mastering_display_colour_volume() const noexcept + { + heif_mastering_display_colour_volume mdcv{}; + heif_image_handle_get_mastering_display_colour_volume(m_image_handle.get(), &mdcv); + return mdcv; + } + + inline void ImageHandle::set_mastering_display_colour_volume(const heif_mastering_display_colour_volume& mdcv) noexcept + { + heif_image_handle_set_mastering_display_colour_volume(m_image_handle.get(), &mdcv); + } + // ------------------------- depth images ------------------------- // TODO @@ -803,12 +878,15 @@ inline ColorProfile_nclx::ColorProfile_nclx() { - mProfile = heif_nclx_color_profile_alloc(); + auto profile = heif_nclx_color_profile_alloc(); + mProfile = std::shared_ptr(profile, + [](heif_color_profile_nclx* p) { heif_nclx_color_profile_free(p); }); } - inline ColorProfile_nclx::~ColorProfile_nclx() + inline ColorProfile_nclx::ColorProfile_nclx(heif_color_profile_nclx* nclx) { - heif_nclx_color_profile_free(mProfile); + mProfile = std::shared_ptr(nclx, + [](heif_color_profile_nclx* p) { heif_nclx_color_profile_free(p); }); } inline heif_color_primaries ColorProfile_nclx::get_color_primaries() const @@ -847,8 +925,8 @@ inline void Image::create(int width, int height, - enum heif_colorspace colorspace, - enum heif_chroma chroma) + heif_colorspace colorspace, + heif_chroma chroma) { heif_image* image; Error err = Error(heif_image_create(width, height, colorspace, chroma, &image)); @@ -916,9 +994,19 @@ return heif_image_get_plane(m_image.get(), channel, out_stride); } + inline const uint8_t* Image::get_plane2(enum heif_channel channel, size_t* out_stride) const noexcept + { + return heif_image_get_plane_readonly2(m_image.get(), channel, out_stride); + } + + inline uint8_t* Image::get_plane2(enum heif_channel channel, size_t* out_stride) noexcept + { + return heif_image_get_plane2(m_image.get(), channel, out_stride); + } + inline void Image::set_nclx_color_profile(const ColorProfile_nclx& nclx) { - Error err = Error(heif_image_set_nclx_color_profile(m_image.get(), nclx.mProfile)); + Error err = Error(heif_image_set_nclx_color_profile(m_image.get(), nclx.mProfile.get())); if (err) { throw err; } @@ -976,6 +1064,40 @@ } } + inline bool Image::has_content_light_level() const noexcept + { + return heif_image_has_content_light_level(m_image.get()) != 0; + } + + inline heif_content_light_level Image::get_content_light_level() const noexcept + { + heif_content_light_level clli{}; + heif_image_get_content_light_level(m_image.get(), &clli); + return clli; + } + + inline void Image::set_content_light_level(const heif_content_light_level& clli) noexcept + { + heif_image_set_content_light_level(m_image.get(), &clli); + } + + inline bool Image::has_mastering_display_colour_volume() const noexcept + { + return heif_image_has_mastering_display_colour_volume(m_image.get()) != 0; + } + + inline heif_mastering_display_colour_volume Image::get_mastering_display_colour_volume() const noexcept + { + heif_mastering_display_colour_volume mdcv{}; + heif_image_get_mastering_display_colour_volume(m_image.get(), &mdcv); + return mdcv; + } + + inline void Image::set_mastering_display_colour_volume(const heif_mastering_display_colour_volume& mdcv) noexcept + { + heif_image_set_mastering_display_colour_volume(m_image.get(), &mdcv); + } + inline bool Image::is_premultiplied_alpha() const noexcept { return heif_image_is_premultiplied_alpha(m_image.get()) != 0; @@ -1001,7 +1123,7 @@ inline std::vector - EncoderDescriptor::get_encoder_descriptors(enum heif_compression_format format_filter, + EncoderDescriptor::get_encoder_descriptors(heif_compression_format format_filter, const char* name_filter) noexcept { int maxDescriptors = 10; @@ -1044,7 +1166,7 @@ return heif_encoder_descriptor_get_id_name(m_descriptor); } - inline enum heif_compression_format EncoderDescriptor::get_compression_format() const noexcept + inline heif_compression_format EncoderDescriptor::get_compression_format() const noexcept { return heif_encoder_descriptor_get_compression_format(m_descriptor); } @@ -1081,7 +1203,7 @@ } - inline Encoder::Encoder(enum heif_compression_format format) + inline Encoder::Encoder(heif_compression_format format) { heif_encoder* encoder; Error err = Error(heif_context_get_encoder_for_format(nullptr, format, &encoder)); diff -Nru libheif-1.19.8/libheif/api/libheif/heif_decoding.cc libheif-1.23.4/libheif/api/libheif/heif_decoding.cc --- libheif-1.19.8/libheif/api/libheif/heif_decoding.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_decoding.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,277 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_decoding.h" +#include "api_structs.h" +#include "plugin_registry.h" + +#include +#include +#include +#include + + +void heif_context_set_max_decoding_threads(heif_context* ctx, int max_threads) +{ + ctx->context->set_max_decoding_threads(max_threads); +} + + +int heif_context_get_max_decoding_threads(const heif_context* ctx) +{ + if (!ctx) { + return HeifContext::default_max_decoding_threads; + } + return ctx->context->get_max_decoding_threads(); +} + + +int heif_have_decoder_for_format(heif_compression_format format) +{ + auto plugin = get_decoder(format, nullptr); + return plugin != nullptr; +} + + +static void fill_default_decoding_options(heif_decoding_options& options) +{ + options.version = 10; + + options.ignore_transformations = false; + + options.start_progress = nullptr; + options.on_progress = nullptr; + options.end_progress = nullptr; + options.progress_user_data = nullptr; + + // version 2 + + options.convert_hdr_to_8bit = false; + + // version 3 + + options.strict_decoding = false; + + // version 4 + + options.decoder_id = nullptr; + + // version 5 + + options.color_conversion_options.version = 1; + options.color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; + options.color_conversion_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + options.color_conversion_options.only_use_preferred_chroma_algorithm = false; + + // version 6 + + options.cancel_decoding = nullptr; + + // version 7 + + options.color_conversion_options_ext = nullptr; + + // version 8 + + options.ignore_sequence_editlist = false; + options.output_image_nclx_profile = nullptr; + options.num_codec_threads = 0; + options.num_library_threads = 0; + + // version 9 + + options.autocorrect_broken_input = false; + + // version 10 + + options.output_image_nclx_profile_passthrough = false; +} + + +heif_decoding_options* heif_decoding_options_alloc() +{ + auto options = new heif_decoding_options; + + fill_default_decoding_options(*options); + + return options; +} + + +// overwrite the (possibly lower version) input options over the default options +void heif_decoding_options_copy(heif_decoding_options* dst, + const heif_decoding_options* src) +{ + if (src == nullptr) { + return; + } + + int min_version = std::min(dst->version, src->version); + + switch (min_version) { + case 10: + dst->output_image_nclx_profile_passthrough = src->output_image_nclx_profile_passthrough; + [[fallthrough]]; + case 9: + dst->autocorrect_broken_input = src->autocorrect_broken_input; + [[fallthrough]]; + case 8: + dst->num_library_threads = src->num_library_threads; + dst->num_codec_threads = src->num_codec_threads; + dst->output_image_nclx_profile = src->output_image_nclx_profile; + dst->ignore_sequence_editlist = src->ignore_sequence_editlist; + [[fallthrough]]; + case 7: + dst->color_conversion_options_ext = src->color_conversion_options_ext; + [[fallthrough]]; + case 6: + dst->cancel_decoding = src->cancel_decoding; + [[fallthrough]]; + case 5: + dst->color_conversion_options = src->color_conversion_options; + [[fallthrough]]; + case 4: + dst->decoder_id = src->decoder_id; + [[fallthrough]]; + case 3: + dst->strict_decoding = src->strict_decoding; + [[fallthrough]]; + case 2: + dst->convert_hdr_to_8bit = src->convert_hdr_to_8bit; + [[fallthrough]]; + case 1: + dst->ignore_transformations = src->ignore_transformations; + dst->start_progress = src->start_progress; + dst->on_progress = src->on_progress; + dst->end_progress = src->end_progress; + dst->progress_user_data = src->progress_user_data; + } +} + + +void heif_decoding_options_free(heif_decoding_options* options) +{ + delete options; +} + + +int heif_get_decoder_descriptors(heif_compression_format format_filter, + const heif_decoder_descriptor** out_decoders, + int count) +{ + struct decoder_with_priority + { + const heif_decoder_plugin* plugin; + int priority; + }; + + std::vector plugins; + std::vector formats; + if (format_filter == heif_compression_undefined) { + formats = {heif_compression_HEVC, heif_compression_AV1, heif_compression_JPEG, heif_compression_JPEG2000, heif_compression_HTJ2K, heif_compression_VVC}; + } + else { + formats.emplace_back(format_filter); + } + + for (const auto* plugin : get_decoder_plugins()) { + for (auto& format : formats) { + int priority = plugin->does_support_format(format); + if (priority) { + plugins.push_back({plugin, priority}); + break; + } + } + } + + if (out_decoders == nullptr) { + return (int) plugins.size(); + } + + std::sort(plugins.begin(), plugins.end(), [](const decoder_with_priority& a, const decoder_with_priority& b) { + return a.priority > b.priority; + }); + + int nDecodersReturned = std::min(count, (int) plugins.size()); + + for (int i = 0; i < nDecodersReturned; i++) { + out_decoders[i] = (heif_decoder_descriptor*) (plugins[i].plugin); + } + + return nDecodersReturned; +} + + +const char* heif_decoder_descriptor_get_name(const heif_decoder_descriptor* descriptor) +{ + auto decoder = (heif_decoder_plugin*) descriptor; + return decoder->get_plugin_name(); +} + + +const char* heif_decoder_descriptor_get_id_name(const heif_decoder_descriptor* descriptor) +{ + auto decoder = (heif_decoder_plugin*) descriptor; + if (decoder->plugin_api_version < 3) { + return nullptr; + } + else { + return decoder->id_name; + } +} + + +heif_error heif_decode_image(const heif_image_handle* in_handle, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* input_options) +{ + if (out_img == nullptr || in_handle == nullptr) { + return heif_error_null_pointer_argument; + } + + *out_img = nullptr; + + return exception_guard([&]() -> heif_error { + heif_item_id id = in_handle->image->get_id(); + + heif_decoding_options dec_options; + fill_default_decoding_options(dec_options); + heif_decoding_options_copy(&dec_options, input_options); + + Result > decodingResult = in_handle->context->decode_image(id, + colorspace, + chroma, + dec_options, + false, 0, 0, {}); + + if (!decodingResult) { + return decodingResult.error_struct(in_handle->image.get()); + } + + std::shared_ptr img = *decodingResult; + + *out_img = new heif_image(); + (*out_img)->image = std::move(img); + + return Error::Ok.error_struct(in_handle->image.get()); + }); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_decoding.h libheif-1.23.4/libheif/api/libheif/heif_decoding.h --- libheif-1.19.8/libheif/api/libheif/heif_decoding.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_decoding.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,219 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_DECODING_H +#define LIBHEIF_HEIF_DECODING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + + +// If the maximum threads number is set to 0, the image tiles are decoded in the main thread. +// This is different from setting it to 1, which will generate a single background thread to decode the tiles. +// Note that this setting only affects libheif itself. The codecs itself may still use multi-threaded decoding. +// You can use it, for example, in cases where you are decoding several images in parallel anyway you thus want +// to minimize parallelism in each decoder. +LIBHEIF_API +void heif_context_set_max_decoding_threads(heif_context* ctx, int max_threads); + +// Returns the current maximum number of background threads used for parallel tile decoding. +// This reflects either the library default or the value last passed to +// heif_context_set_max_decoding_threads(). +// If 'ctx' is NULL, the library default is returned. +LIBHEIF_API +int heif_context_get_max_decoding_threads(const heif_context* ctx); + +// Quick check whether there is a decoder available for the given format. +// Note that the decoder still may not be able to decode all variants of that format. +// You will have to query that further (todo) or just try to decode and check the returned error. +LIBHEIF_API +int heif_have_decoder_for_format(heif_compression_format format); + + +typedef enum heif_progress_step +{ + heif_progress_step_total = 0, + heif_progress_step_load_tile = 1 +} heif_progress_step; + + +typedef struct heif_decoding_options +{ + uint8_t version; + + // version 1 options + + // Ignore geometric transformations like cropping, rotation, mirroring. + // Default: false (do not ignore). + uint8_t ignore_transformations; + + // Any of the progress functions may be called from background threads. + void (* start_progress)(heif_progress_step step, int max_progress, void* progress_user_data); + + void (* on_progress)(heif_progress_step step, int progress, void* progress_user_data); + + void (* end_progress)(heif_progress_step step, void* progress_user_data); + + void* progress_user_data; + + // version 2 options + + uint8_t convert_hdr_to_8bit; + + // version 3 options + + // When enabled, an error is returned for invalid input. Otherwise, it will try its best and + // add decoding warnings to the decoded heif_image. Default is non-strict. + uint8_t strict_decoding; + + // version 4 options + + // name_id of the decoder to use for the decoding. + // If set to NULL (default), the highest priority decoder is chosen. + // The priority is defined in the plugin. + const char* decoder_id; + + // version 5 options + + heif_color_conversion_options color_conversion_options; + + // version 6 options + + int (* cancel_decoding)(void* progress_user_data); + + // version 7 options + + // When set to NULL, default options will be used + heif_color_conversion_options_ext* color_conversion_options_ext; + + // version 8 options (v1.21.0) + + // If enabled, it will decode the media timeline, ignoring the sequence tracks edit-list. + int ignore_sequence_editlist; // bool + + // Requested NCLX color profile of the decoded output image. If the input + // image's NCLX differs, libheif will color-convert the pixels accordingly + // (e.g. YCbCr matrix, primaries, range) so the result matches what is + // requested here. + // + // When set to NULL, the behavior depends on the flag + // output_image_nclx_profile_passthrough below: by default NULL means + // "convert to sRGB"; with the passthrough flag enabled, NULL means + // "keep the input image's NCLX". + heif_color_profile_nclx* output_image_nclx_profile; + + int num_library_threads; // 0 = let libheif decide (TODO, currently ignored) + int num_codec_threads; // 0 = use decoder default + + // version 9 options + + // If enabled, libheif will attempt to work around known broken-input quirks + // (e.g. Sony HIF files where the NCLX `colr` box disagrees with the HEVC VUI + // on the YCbCr range flag). Default: false (strict spec-conformant behavior). + uint8_t autocorrect_broken_input; + + // version 10 options + + // Controls the meaning of output_image_nclx_profile == NULL. + // + // When false (default), a NULL output_image_nclx_profile means "convert the + // decoded image to sRGB" (BT.709 primaries, sRGB transfer, BT.601 matrix, + // full-range). For HDR inputs (e.g. BT.2100 PQ) this silently discards the + // original color volume. + // + // When true, a NULL output_image_nclx_profile means "keep the input image's + // NCLX". The decoded image carries the input file's primaries / transfer / + // matrix / range, and no extra color-space conversion is performed solely + // because the output NCLX was unspecified. If a YCbCr<->RGB colorspace + // conversion fires for another reason, the input NCLX is used to drive that + // conversion (so the result is tagged consistently with the source). + // + // Although this flag is off by default to preserve historical behavior, new + // code that wants to preserve HDR through decode should generally enable it. + // Setting output_image_nclx_profile to a non-NULL value overrides this flag. + uint8_t output_image_nclx_profile_passthrough; +} heif_decoding_options; + + +// Allocate decoding options and fill with default values. +// Note: you should always get the decoding options through this function since the +// option structure may grow in size in future versions. +LIBHEIF_API +heif_decoding_options* heif_decoding_options_alloc(void); + +LIBHEIF_API +void heif_decoding_options_copy(heif_decoding_options* dst, + const heif_decoding_options* src); + +LIBHEIF_API +void heif_decoding_options_free(heif_decoding_options*); + + +typedef struct heif_decoder_descriptor heif_decoder_descriptor; + +// Get a list of available decoders. You can filter the encoders by compression format. +// Use format_filter==heif_compression_undefined to get all available decoders. +// The returned list of decoders is sorted by their priority (which is a plugin property). +// The number of decoders is returned, which are not more than 'count' if (out_decoders != nullptr). +// By setting out_decoders==nullptr, you can query the number of decoders, 'count' is ignored. +LIBHEIF_API +int heif_get_decoder_descriptors(heif_compression_format format_filter, + const heif_decoder_descriptor** out_decoders, + int count); + +// Return a long, descriptive name of the decoder (including version information). +LIBHEIF_API +const char* heif_decoder_descriptor_get_name(const heif_decoder_descriptor*); + +// Return a short, symbolic name for identifying the decoder. +// This name should stay constant over different decoder versions. +// Note: the returned ID may be NULL for old plugins that don't support this yet. +LIBHEIF_API +const char* heif_decoder_descriptor_get_id_name(const heif_decoder_descriptor*); + + +// Decode an heif_image_handle into the actual pixel image and also carry out +// all geometric transformations specified in the HEIF file (rotation, cropping, mirroring). +// +// If colorspace or chroma is set to heif_colorspace_undefined or heif_chroma_undefined, +// respectively, the original colorspace is taken. +// Note: for images with matrix_coefficients=0 the "original colorspace" is YCbCr +// (tagged with matrix_coefficients=0); this is not specified and may change in a future +// version. See heif_image_handle_get_preferred_decoding_colorspace(). +// Decoding options may be NULL. If you want to supply options, always use +// heif_decoding_options_alloc() to get the structure. +LIBHEIF_API +heif_error heif_decode_image(const heif_image_handle* in_handle, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* options); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_emscripten.h libheif-1.23.4/libheif/api/libheif/heif_emscripten.h --- libheif-1.19.8/libheif/api/libheif/heif_emscripten.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_emscripten.h 2026-09-06 14:45:17.000000000 +0000 @@ -2,6 +2,7 @@ #define LIBHEIF_BOX_EMSCRIPTEN_H #include +#include #include #include @@ -12,13 +13,14 @@ #include #include "heif.h" +#include "heif_items.h" -static std::string _heif_get_version() +static std::string heif_js_get_version() { return heif_get_version(); } -static struct heif_error _heif_context_read_from_memory( +static struct heif_error heif_js_context_read_from_memory( struct heif_context* context, const std::string& data) { return heif_context_read_from_memory(context, data.data(), data.size(), nullptr); @@ -43,7 +45,13 @@ return emscripten::val(err); } +#if __EMSCRIPTEN_major__ > 4 || \ + (__EMSCRIPTEN_major__ == 4 && \ + (__EMSCRIPTEN_minor__ > 0 || __EMSCRIPTEN_tiny__ >= 9)) + return emscripten::val(handle, emscripten::allow_raw_pointers()); +#else return emscripten::val(handle); +#endif } static emscripten::val heif_js_context_get_primary_image_handle( @@ -61,7 +69,13 @@ return emscripten::val(err); } +#if __EMSCRIPTEN_major__ > 4 || \ + (__EMSCRIPTEN_major__ == 4 && \ + (__EMSCRIPTEN_minor__ > 0 || __EMSCRIPTEN_tiny__ >= 9)) + return emscripten::val(handle, emscripten::allow_raw_pointers()); +#else return emscripten::val(handle); +#endif } @@ -78,27 +92,99 @@ return result; } - heif_item_id* ids = (heif_item_id*) malloc(count * sizeof(heif_item_id)); - if (!ids) { - struct heif_error err; - err.code = heif_error_Memory_allocation_error; - err.subcode = heif_suberror_Security_limit_exceeded; - return emscripten::val(err); + std::vector ids(static_cast(count)); + + int received = heif_context_get_list_of_top_level_image_IDs(context, ids.data(), count); + + for (int i = 0; i < received; i++) { + result.set(i, ids[i]); + } + return result; +} + + +static emscripten::val heif_js_context_get_list_of_item_IDs( + struct heif_context* context) +{ + emscripten::val result = emscripten::val::array(); + if (!context) { + return result; } - int received = heif_context_get_list_of_top_level_image_IDs(context, ids, count); - if (!received) { - free(ids); + int count = heif_context_get_number_of_items(context); + if (count <= 0) { return result; } - for (int i = 0; i < received; i++) { + std::vector ids(static_cast(count)); + + int num_ids_received = heif_context_get_list_of_item_IDs(context, ids.data(), count); + + for (int i = 0; i < num_ids_received; i++) { result.set(i, ids[i]); } - free(ids); + return result; } + +static emscripten::val heif_js_item_get_item_type( + const struct heif_context* ctx, heif_item_id id) +{ + uint32_t type = heif_item_get_item_type(ctx, id); + std::string type_string = fourcc_to_string(type); + return emscripten::val(type_string); +} + + +static emscripten::val heif_js_item_get_mime_item_content_type( + const struct heif_context* ctx, heif_item_id id) +{ + std::string content_type = ""; + const char* cstring = heif_item_get_mime_item_content_type(ctx, id); + if (cstring) { + content_type = cstring; + } + return emscripten::val(content_type); +} + + +static emscripten::val heif_js_item_get_mime_item_content_encoding( + const struct heif_context* ctx, heif_item_id id) +{ + std::string content_encoding = ""; + const char* cstring = heif_item_get_mime_item_content_encoding(ctx, id); + if (cstring) { + content_encoding = cstring; + } + return emscripten::val(content_encoding); +} + + +static emscripten::val heif_js_item_get_uri_item_uri_type( + const struct heif_context* ctx, heif_item_id id) +{ + std::string uri_type = ""; + const char* cstring = heif_item_get_uri_item_uri_type(ctx, id); + if (cstring) { + uri_type = cstring; + } + return emscripten::val(uri_type); +} + + +static emscripten::val heif_js_item_get_item_name( + const struct heif_context* ctx, heif_item_id id) +{ + std::string item_name = ""; + const char* cstring = heif_item_get_item_name(ctx, id); + if (cstring) { + item_name = cstring; + } + return emscripten::val(item_name); +} + + #if 0 static void strided_copy(void* dest, const void* src, int width, int height, int stride) @@ -120,7 +206,7 @@ } static emscripten::val heif_js_decode_image(struct heif_image_handle* handle, - enum heif_colorspace colorspace, enum heif_chroma chroma) + heif_colorspace colorspace, heif_chroma chroma) { emscripten::val result = emscripten::val::object(); if (!handle) { @@ -139,20 +225,20 @@ result.set("width", width); int height = heif_image_handle_get_height(handle); result.set("height", height); - std::basic_string data; + std::vector data; result.set("chroma", heif_image_get_chroma_format(image)); result.set("colorspace", heif_image_get_colorspace(image)); switch (heif_image_get_colorspace(image)) { case heif_colorspace_YCbCr: { - int stride_y; - const uint8_t* plane_y = heif_image_get_plane_readonly(image, - heif_channel_Y, &stride_y); - int stride_u; - const uint8_t* plane_u = heif_image_get_plane_readonly(image, - heif_channel_Cb, &stride_u); - int stride_v; - const uint8_t* plane_v = heif_image_get_plane_readonly(image, - heif_channel_Cr, &stride_v); + size_t stride_y; + const uint8_t* plane_y = heif_image_get_plane_readonly2(image, + heif_channel_Y, &stride_y); + size_t stride_u; + const uint8_t* plane_u = heif_image_get_plane_readonly2(image, + heif_channel_Cb, &stride_u); + size_t stride_v; + const uint8_t* plane_v = heif_image_get_plane_readonly2(image, + heif_channel_Cr, &stride_v); data.resize((width * height) + (2 * round_odd(width) * round_odd(height))); unsigned char* dest = const_cast(data.data()); strided_copy(dest, plane_y, width, height, stride_y); @@ -164,17 +250,17 @@ break; case heif_colorspace_RGB: { if(heif_image_get_chroma_format(image) == heif_chroma_interleaved_RGB) { - int stride_rgb; - const uint8_t* plane_rgb = heif_image_get_plane_readonly(image, - heif_channel_interleaved, &stride_rgb); + size_t stride_rgb; + const uint8_t* plane_rgb = heif_image_get_plane_readonly2(image, + heif_channel_interleaved, &stride_rgb); data.resize(width * height * 3); unsigned char* dest = const_cast(data.data()); strided_copy(dest, plane_rgb, width * 3, height, stride_rgb); } else if (heif_image_get_chroma_format(image) == heif_chroma_interleaved_RGBA) { - int stride_rgba; - const uint8_t* plane_rgba = heif_image_get_plane_readonly(image, - heif_channel_interleaved, &stride_rgba); + size_t stride_rgba; + const uint8_t* plane_rgba = heif_image_get_plane_readonly2(image, + heif_channel_interleaved, &stride_rgba); data.resize(width * height * 4); unsigned char* dest = const_cast(data.data()); strided_copy(dest, plane_rgba, width * 4, height, stride_rgba); @@ -187,9 +273,9 @@ case heif_colorspace_monochrome: { assert(heif_image_get_chroma_format(image) == heif_chroma_monochrome); - int stride_grey; - const uint8_t* plane_grey = heif_image_get_plane_readonly(image, - heif_channel_Y, &stride_grey); + size_t stride_grey; + const uint8_t* plane_grey = heif_image_get_plane_readonly2(image, + heif_channel_Y, &stride_grey); data.resize(width * height); unsigned char* dest = const_cast(data.data()); strided_copy(dest, plane_grey, width, height, stride_grey); @@ -202,10 +288,9 @@ result.set("data", std::move(data)); if (heif_image_has_channel(image, heif_channel_Alpha)) { - std::basic_string alpha; - int stride_alpha; - const uint8_t* plane_alpha = heif_image_get_plane_readonly(image, - heif_channel_Alpha, &stride_alpha); + std::vector alpha; + size_t stride_alpha; + const uint8_t* plane_alpha = heif_image_get_plane_readonly2(image, heif_channel_Alpha, &stride_alpha); alpha.resize(width * height); unsigned char* dest = const_cast(alpha.data()); strided_copy(dest, plane_alpha, width, height, stride_alpha); @@ -222,7 +307,7 @@ * This image has to be released after the image data has been read (copied) with heif_image_release(). */ static emscripten::val heif_js_decode_image2(struct heif_image_handle* handle, - enum heif_colorspace colorspace, enum heif_chroma chroma) + heif_colorspace colorspace, heif_chroma chroma) { emscripten::val result = emscripten::val::object(); if (!handle) { @@ -235,7 +320,13 @@ return emscripten::val(err); } +#if __EMSCRIPTEN_major__ > 4 || \ + (__EMSCRIPTEN_major__ == 4 && \ + (__EMSCRIPTEN_minor__ > 0 || __EMSCRIPTEN_tiny__ >= 9)) + result.set("image", image, emscripten::allow_raw_pointers()); +#else result.set("image", image); +#endif int width = heif_image_handle_get_width(handle); result.set("width", width); @@ -265,8 +356,8 @@ emscripten::val val_channel_info = emscripten::val::object(); val_channel_info.set("id", channel); - int stride; - const uint8_t* plane = heif_image_get_plane_readonly(image, channel, &stride); + size_t stride; + const uint8_t* plane = heif_image_get_plane_readonly2(image, channel, &stride); val_channel_info.set("stride", stride); val_channel_info.set("data", emscripten::val(emscripten::typed_memory_view(stride * height, plane))); @@ -290,32 +381,42 @@ emscripten::function(#name, &name, emscripten::allow_raw_pointers()) EMSCRIPTEN_BINDINGS(libheif) { - emscripten::function("heif_get_version", &_heif_get_version, - emscripten::allow_raw_pointers()); + + // heif.h + emscripten::function("heif_get_version", &heif_js_get_version, emscripten::allow_raw_pointers()); + emscripten::function("heif_context_read_from_memory", &heif_js_context_read_from_memory, emscripten::allow_raw_pointers()); + emscripten::function("heif_check_filetype", &heif_js_check_filetype, emscripten::allow_raw_pointers()); + emscripten::function("heif_context_get_list_of_top_level_image_IDs", &heif_js_context_get_list_of_top_level_image_IDs, emscripten::allow_raw_pointers()); + emscripten::function("heif_context_get_image_handle", &heif_js_context_get_image_handle, emscripten::allow_raw_pointers()); + emscripten::function("heif_context_get_primary_image_handle", &heif_js_context_get_primary_image_handle, emscripten::allow_raw_pointers()); + emscripten::function("heif_js_decode_image2", &heif_js_decode_image2, emscripten::allow_raw_pointers()); EXPORT_HEIF_FUNCTION(heif_get_version_number); - EXPORT_HEIF_FUNCTION(heif_context_alloc); EXPORT_HEIF_FUNCTION(heif_context_free); - emscripten::function("heif_context_read_from_memory", - &_heif_context_read_from_memory, emscripten::allow_raw_pointers()); - emscripten::function("heif_js_check_filetype", - &heif_js_check_filetype, emscripten::allow_raw_pointers()); EXPORT_HEIF_FUNCTION(heif_context_get_number_of_top_level_images); - emscripten::function("heif_js_context_get_list_of_top_level_image_IDs", - &heif_js_context_get_list_of_top_level_image_IDs, emscripten::allow_raw_pointers()); - emscripten::function("heif_js_context_get_image_handle", - &heif_js_context_get_image_handle, emscripten::allow_raw_pointers()); - emscripten::function("heif_js_context_get_primary_image_handle", - &heif_js_context_get_primary_image_handle, emscripten::allow_raw_pointers()); - //emscripten::function("heif_js_decode_image", - //&heif_js_decode_image, emscripten::allow_raw_pointers()); - emscripten::function("heif_js_decode_image2", - &heif_js_decode_image2, emscripten::allow_raw_pointers()); EXPORT_HEIF_FUNCTION(heif_image_handle_release); EXPORT_HEIF_FUNCTION(heif_image_handle_get_width); EXPORT_HEIF_FUNCTION(heif_image_handle_get_height); EXPORT_HEIF_FUNCTION(heif_image_handle_is_primary_image); EXPORT_HEIF_FUNCTION(heif_image_release); + EXPORT_HEIF_FUNCTION(heif_image_handle_has_alpha_channel); + EXPORT_HEIF_FUNCTION(heif_image_handle_is_premultiplied_alpha); + + // heif_items.h + emscripten::function("heif_context_get_list_of_item_IDs", &heif_js_context_get_list_of_item_IDs, emscripten::allow_raw_pointers()); + emscripten::function("heif_item_get_item_type", heif_js_item_get_item_type, emscripten::allow_raw_pointers()); + emscripten::function("heif_item_get_mime_item_content_type", heif_js_item_get_mime_item_content_type, emscripten::allow_raw_pointers()); + emscripten::function("heif_item_get_mime_item_content_encoding", heif_js_item_get_mime_item_content_encoding, emscripten::allow_raw_pointers()); + emscripten::function("heif_item_get_uri_item_uri_type", heif_js_item_get_uri_item_uri_type, emscripten::allow_raw_pointers()); + emscripten::function("heif_item_get_item_name", heif_js_item_get_item_name, emscripten::allow_raw_pointers()); + EXPORT_HEIF_FUNCTION(heif_context_get_number_of_items); + EXPORT_HEIF_FUNCTION(heif_item_is_item_hidden); + + // DEPRECATED, use functions without the 'js' prefix. + emscripten::function("heif_js_check_filetype", &heif_js_check_filetype, emscripten::allow_raw_pointers()); + emscripten::function("heif_js_context_get_list_of_top_level_image_IDs", &heif_js_context_get_list_of_top_level_image_IDs, emscripten::allow_raw_pointers()); + emscripten::function("heif_js_context_get_image_handle", &heif_js_context_get_image_handle, emscripten::allow_raw_pointers()); + emscripten::function("heif_js_context_get_primary_image_handle", &heif_js_context_get_primary_image_handle, emscripten::allow_raw_pointers()); emscripten::enum_("heif_error_code") .value("heif_error_Ok", heif_error_Ok) @@ -329,6 +430,7 @@ .value("heif_error_Decoder_plugin_error", heif_error_Decoder_plugin_error) .value("heif_error_Encoder_plugin_error", heif_error_Encoder_plugin_error) .value("heif_error_Encoding_error", heif_error_Encoding_error) + .value("heif_error_End_of_sequence", heif_error_End_of_sequence) .value("heif_error_Color_profile_does_not_exist", heif_error_Color_profile_does_not_exist) .value("heif_error_Canceled", heif_error_Canceled); emscripten::enum_("heif_suberror_code") @@ -345,6 +447,7 @@ .value("heif_suberror_No_ftyp_box", heif_suberror_No_ftyp_box) .value("heif_suberror_No_idat_box", heif_suberror_No_idat_box) .value("heif_suberror_No_meta_box", heif_suberror_No_meta_box) + .value("heif_suberror_No_moov_box", heif_suberror_No_moov_box) .value("heif_suberror_No_hdlr_box", heif_suberror_No_hdlr_box) .value("heif_suberror_No_hvcC_box", heif_suberror_No_hvcC_box) .value("heif_suberror_No_vvcC_box", heif_suberror_No_vvcC_box) @@ -361,6 +464,7 @@ .value("heif_suberror_No_item_data", heif_suberror_No_item_data) .value("heif_suberror_Invalid_grid_data", heif_suberror_Invalid_grid_data) .value("heif_suberror_Missing_grid_images", heif_suberror_Missing_grid_images) + .value("heif_suberror_NCLX_colr_VUI_mismatch", heif_suberror_NCLX_colr_VUI_mismatch) .value("heif_suberror_No_av1C_box", heif_suberror_No_av1C_box) .value("heif_suberror_No_avcC_box", heif_suberror_No_avcC_box) .value("heif_suberror_Invalid_mini_box", heif_suberror_Invalid_mini_box) @@ -399,6 +503,7 @@ .value("heif_suberror_Unsupported_item_construction_method", heif_suberror_Unsupported_item_construction_method) .value("heif_suberror_Unsupported_header_compression_method", heif_suberror_Unsupported_header_compression_method) .value("heif_suberror_Unsupported_bit_depth", heif_suberror_Unsupported_bit_depth) + .value("heif_suberror_Unsupported_track_type", heif_suberror_Unsupported_track_type) .value("heif_suberror_Wrong_tile_image_pixel_depth", heif_suberror_Wrong_tile_image_pixel_depth) .value("heif_suberror_Unknown_NCLX_color_primaries", heif_suberror_Unknown_NCLX_color_primaries) .value("heif_suberror_Unknown_NCLX_transfer_characteristics", heif_suberror_Unknown_NCLX_transfer_characteristics) @@ -433,6 +538,7 @@ .value("heif_chroma_interleaved_RRGGBBAA_BE", heif_chroma_interleaved_RRGGBBAA_BE) .value("heif_chroma_interleaved_RRGGBB_LE", heif_chroma_interleaved_RRGGBB_LE) .value("heif_chroma_interleaved_RRGGBBAA_LE", heif_chroma_interleaved_RRGGBBAA_LE) + .value("heif_chroma_planar", heif_chroma_planar) // Aliases .value("heif_chroma_interleaved_24bit", heif_chroma_interleaved_24bit) .value("heif_chroma_interleaved_32bit", heif_chroma_interleaved_32bit); @@ -448,6 +554,8 @@ .value("heif_colorspace_YCbCr", heif_colorspace_YCbCr) .value("heif_colorspace_RGB", heif_colorspace_RGB) .value("heif_colorspace_monochrome", heif_colorspace_monochrome) + .value("heif_colorspace_filter_array", heif_colorspace_filter_array) + .value("heif_colorspace_custom", heif_colorspace_custom) .value("heif_colorspace_nonvisual", heif_colorspace_nonvisual); emscripten::enum_("heif_channel") .value("heif_channel_Y", heif_channel_Y) @@ -460,7 +568,8 @@ .value("heif_channel_interleaved", heif_channel_interleaved) .value("heif_channel_filter_array", heif_channel_filter_array) .value("heif_channel_depth", heif_channel_depth) - .value("heif_channel_disparity", heif_channel_disparity); + .value("heif_channel_disparity", heif_channel_disparity) + .value("heif_channel_unknown", heif_channel_unknown); emscripten::enum_("heif_filetype_result") .value("heif_filetype_no", heif_filetype_no) diff -Nru libheif-1.19.8/libheif/api/libheif/heif_encoding.cc libheif-1.23.4/libheif/api/libheif/heif_encoding.cc --- libheif-1.19.8/libheif/api/libheif/heif_encoding.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_encoding.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,845 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_encoding.h" +#include "api_structs.h" +#include "context.h" +#include "init.h" +#include "plugin_registry.h" +#include "image-items/overlay.h" +#include "image-items/tiled.h" +#include "image-items/grid.h" + +#include + +#include +#include +#include +#include +#include + + +static heif_error error_unsupported_parameter = { + heif_error_Usage_error, + heif_suberror_Unsupported_parameter, + "Unsupported encoder parameter" +}; +static heif_error error_invalid_parameter_value = { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Invalid parameter value" +}; + + +int heif_have_encoder_for_format(heif_compression_format format) +{ + auto plugin = get_encoder(format); + return plugin != nullptr; +} + + +int heif_get_encoder_descriptors(heif_compression_format format, + const char* name, + const heif_encoder_descriptor** out_encoder_descriptors, + int count) +{ + if (out_encoder_descriptors != nullptr && count <= 0) { + return 0; + } + + std::vector descriptors; + descriptors = get_filtered_encoder_descriptors(format, name); + + if (out_encoder_descriptors == nullptr) { + return static_cast(descriptors.size()); + } + + int i; + for (i = 0; i < count && static_cast(i) < descriptors.size(); i++) { + out_encoder_descriptors[i] = descriptors[i]; + } + + return i; +} + + +const char* heif_encoder_descriptor_get_name(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->get_plugin_name(); +} + + +const char* heif_encoder_descriptor_get_id_name(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->id_name; +} + + +heif_compression_format +heif_encoder_descriptor_get_compression_format(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->compression_format; +} + + +int heif_encoder_descriptor_supports_lossy_compression(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->supports_lossy_compression; +} + + +int heif_encoder_descriptor_supports_lossless_compression(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->supports_lossless_compression; +} + + +heif_error heif_context_get_encoder(heif_context* context, + const heif_encoder_descriptor* descriptor, + heif_encoder** encoder) +{ + // Note: be aware that context may be NULL as we explicitly allowed that in an earlier documentation. + + if (!descriptor || !encoder) { + return heif_error_null_pointer_argument; + } + + *encoder = new heif_encoder(descriptor->plugin); + return (*encoder)->alloc(); +} + + +heif_error heif_context_get_encoder_for_format(heif_context* context, + heif_compression_format format, + heif_encoder** encoder) +{ + // Note: be aware that context may be NULL as we explicitly allowed that in an earlier documentation. + + if (!encoder) { + return heif_error_null_pointer_argument; + } + + std::vector descriptors; + descriptors = get_filtered_encoder_descriptors(format, nullptr); + + if (descriptors.size() > 0) { + *encoder = new heif_encoder(descriptors[0]->plugin); + return (*encoder)->alloc(); + } + else { + *encoder = nullptr; + Error err(heif_error_Unsupported_filetype, // TODO: is this the right error code? + heif_suberror_Unspecified); + return err.error_struct(context ? context->context.get() : nullptr); + } +} + + +void heif_encoder_release(heif_encoder* encoder) +{ + if (encoder) { + delete encoder; + } +} + + +const char* heif_encoder_get_name(const heif_encoder* encoder) +{ + return encoder->plugin->get_plugin_name(); +} + + +// Set a 'quality' factor (0-100). How this is mapped to actual encoding parameters is +// encoder dependent. +heif_error heif_encoder_set_lossy_quality(heif_encoder* encoder, + int quality) +{ + if (!encoder) { + return heif_error_null_pointer_argument; + } + + return encoder->plugin->set_parameter_quality(encoder->encoder, quality); +} + + +heif_error heif_encoder_set_lossless(heif_encoder* encoder, int enable) +{ + if (!encoder) { + return heif_error_null_pointer_argument; + } + + return encoder->plugin->set_parameter_lossless(encoder->encoder, enable); +} + + +heif_error heif_encoder_set_logging_level(heif_encoder* encoder, int level) +{ + if (!encoder) { + return heif_error_null_pointer_argument; + } + + if (encoder->plugin->set_parameter_logging_level) { + return encoder->plugin->set_parameter_logging_level(encoder->encoder, level); + } + + return heif_error_success; +} + + +const heif_encoder_parameter* const* heif_encoder_list_parameters(heif_encoder* encoder) +{ + return encoder->plugin->list_parameters(encoder->encoder); +} + + +const char* heif_encoder_parameter_get_name(const heif_encoder_parameter* param) +{ + return param->name; +} + +heif_encoder_parameter_type +heif_encoder_parameter_get_type(const heif_encoder_parameter* param) +{ + return param->type; +} + + +heif_error +heif_encoder_parameter_get_valid_integer_range(const heif_encoder_parameter* param, + int* have_minimum_maximum, + int* minimum, int* maximum) +{ + if (param->type != heif_encoder_parameter_type_integer) { + return error_unsupported_parameter; // TODO: correct error ? + } + + if (param->integer.have_minimum_maximum) { + if (minimum) { + *minimum = param->integer.minimum; + } + + if (maximum) { + *maximum = param->integer.maximum; + } + } + + if (have_minimum_maximum) { + *have_minimum_maximum = param->integer.have_minimum_maximum; + } + + return heif_error_success; +} + + +heif_error heif_encoder_parameter_get_valid_integer_values(const heif_encoder_parameter* param, + int* have_minimum, int* have_maximum, + int* minimum, int* maximum, + int* num_valid_values, + const int** out_integer_array) +{ + if (param->type != heif_encoder_parameter_type_integer) { + return error_unsupported_parameter; // TODO: correct error ? + } + + + // --- range of values + + if (param->integer.have_minimum_maximum) { + if (minimum) { + *minimum = param->integer.minimum; + } + + if (maximum) { + *maximum = param->integer.maximum; + } + } + + if (have_minimum) { + *have_minimum = param->integer.have_minimum_maximum; + } + + if (have_maximum) { + *have_maximum = param->integer.have_minimum_maximum; + } + + + // --- set of valid values + + if (param->integer.num_valid_values > 0) { + if (out_integer_array) { + *out_integer_array = param->integer.valid_values; + } + } + + if (num_valid_values) { + *num_valid_values = param->integer.num_valid_values; + } + + return heif_error_success; +} + + +heif_error +heif_encoder_parameter_get_valid_string_values(const heif_encoder_parameter* param, + const char* const** out_stringarray) +{ + if (param->type != heif_encoder_parameter_type_string) { + return error_unsupported_parameter; // TODO: correct error ? + } + + if (out_stringarray) { + *out_stringarray = param->string.valid_values; + } + + return heif_error_success; +} + + +heif_error heif_encoder_set_parameter_integer(heif_encoder* encoder, + const char* parameter_name, + int value) +{ + // --- check if parameter is valid + + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + int have_minimum = 0, have_maximum = 0, minimum = 0, maximum = 0, num_valid_values = 0; + const int* valid_values = nullptr; + heif_error err = heif_encoder_parameter_get_valid_integer_values((*params), &have_minimum, &have_maximum, + &minimum, &maximum, + &num_valid_values, + &valid_values); + if (err.code) { + return err; + } + + if ((have_minimum && value < minimum) || + (have_maximum && value > maximum)) { + return error_invalid_parameter_value; + } + + if (num_valid_values > 0) { + bool found = false; + for (int i = 0; i < num_valid_values; i++) { + if (valid_values[i] == value) { + found = true; + break; + } + } + + if (!found) { + return error_invalid_parameter_value; + } + } + } + } + + + // --- parameter is ok, pass it to the encoder plugin + + return encoder->plugin->set_parameter_integer(encoder->encoder, parameter_name, value); +} + +heif_error heif_encoder_get_parameter_integer(heif_encoder* encoder, + const char* parameter_name, + int* value_ptr) +{ + return encoder->plugin->get_parameter_integer(encoder->encoder, parameter_name, value_ptr); +} + + +heif_error heif_encoder_parameter_integer_valid_range(heif_encoder* encoder, + const char* parameter_name, + int* have_minimum_maximum, + int* minimum, int* maximum) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + return heif_encoder_parameter_get_valid_integer_range(*params, have_minimum_maximum, + minimum, maximum); + } + } + + return error_unsupported_parameter; +} + +heif_error heif_encoder_set_parameter_boolean(heif_encoder* encoder, + const char* parameter_name, + int value) +{ + return encoder->plugin->set_parameter_boolean(encoder->encoder, parameter_name, value); +} + +heif_error heif_encoder_get_parameter_boolean(heif_encoder* encoder, + const char* parameter_name, + int* value_ptr) +{ + return encoder->plugin->get_parameter_boolean(encoder->encoder, parameter_name, value_ptr); +} + +heif_error heif_encoder_set_parameter_string(heif_encoder* encoder, + const char* parameter_name, + const char* value) +{ + return encoder->plugin->set_parameter_string(encoder->encoder, parameter_name, value); +} + +heif_error heif_encoder_get_parameter_string(heif_encoder* encoder, + const char* parameter_name, + char* value_ptr, int value_size) +{ + return encoder->plugin->get_parameter_string(encoder->encoder, parameter_name, + value_ptr, value_size); +} + +heif_error heif_encoder_parameter_string_valid_values(heif_encoder* encoder, + const char* parameter_name, + const char* const** out_stringarray) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + return heif_encoder_parameter_get_valid_string_values(*params, out_stringarray); + } + } + + return error_unsupported_parameter; +} + +heif_error heif_encoder_parameter_integer_valid_values(heif_encoder* encoder, + const char* parameter_name, + int* have_minimum, int* have_maximum, + int* minimum, int* maximum, + int* num_valid_values, + const int** out_integer_array) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + return heif_encoder_parameter_get_valid_integer_values(*params, have_minimum, have_maximum, minimum, maximum, + num_valid_values, out_integer_array); + } + } + + return error_unsupported_parameter; +} + + +static bool parse_boolean(const char* value) +{ + if (strcmp(value, "true") == 0) { + return true; + } + else if (strcmp(value, "false") == 0) { + return false; + } + else if (strcmp(value, "1") == 0) { + return true; + } + else if (strcmp(value, "0") == 0) { + return false; + } + + return false; +} + + +heif_error heif_encoder_set_parameter(heif_encoder* encoder, + const char* parameter_name, + const char* value) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + switch ((*params)->type) { + case heif_encoder_parameter_type_integer: + return heif_encoder_set_parameter_integer(encoder, parameter_name, atoi(value)); + + case heif_encoder_parameter_type_boolean: + return heif_encoder_set_parameter_boolean(encoder, parameter_name, parse_boolean(value)); + + case heif_encoder_parameter_type_string: + return heif_encoder_set_parameter_string(encoder, parameter_name, value); + break; + } + + return heif_error_success; + } + } + + return heif_encoder_set_parameter_string(encoder, parameter_name, value); + + //return error_unsupported_parameter; +} + + +heif_error heif_encoder_get_parameter(heif_encoder* encoder, + const char* parameter_name, + char* value_ptr, int value_size) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + switch ((*params)->type) { + case heif_encoder_parameter_type_integer: { + int value; + heif_error error = heif_encoder_get_parameter_integer(encoder, parameter_name, &value); + if (error.code) { + return error; + } + else { + snprintf(value_ptr, value_size, "%d", value); + } + } + break; + + case heif_encoder_parameter_type_boolean: { + int value; + heif_error error = heif_encoder_get_parameter_boolean(encoder, parameter_name, &value); + if (error.code) { + return error; + } + else { + snprintf(value_ptr, value_size, "%d", value); + } + } + break; + + case heif_encoder_parameter_type_string: { + heif_error error = heif_encoder_get_parameter_string(encoder, parameter_name, + value_ptr, value_size); + if (error.code) { + return error; + } + } + break; + } + + return heif_error_success; + } + } + + return error_unsupported_parameter; +} + + +int heif_encoder_has_default(heif_encoder* encoder, + const char* parameter_name) +{ + for (const heif_encoder_parameter* const* params = heif_encoder_list_parameters(encoder); + *params; + params++) { + if (strcmp((*params)->name, parameter_name) == 0) { + if ((*params)->version >= 2) { + return (*params)->has_default; + } + else { + return true; + } + } + } + + return false; +} + + +// 1 = heif_orientation_normal +// 2 = heif_orientation_flip_horizontally +// 3 = heif_orientation_rotate_180 +// 4 = heif_orientation_flip_vertically +// 5 = heif_orientation_rotate_90_cw_then_flip_horizontally +// 6 = heif_orientation_rotate_90_cw +// 7 = heif_orientation_rotate_90_cw_then_flip_vertically +// 8 = heif_orientation_rotate_270_cw +static int orientation_concat[8][8] { + // second: 1 2 3 4 5 6 7 8 + /* first=1 */ {1, 2, 3, 4, 5, 6, 7, 8}, + /* first=2 */ {2, 1, 4, 3, 8, 7, 6, 5}, + /* first=3 */ {3, 4, 1, 2, 7, 8, 5, 6}, + /* first=4 */ {4, 3, 2, 1, 6, 5, 8, 7}, + /* first=5 */ {5, 6, 7, 8, 1, 2, 3, 4}, + /* first=6 */ {6, 5, 8, 7, 4, 3, 2, 1}, + /* first=7 */ {7, 8, 5, 6, 3, 4, 1, 2}, + /* first=8 */ {8, 7, 6, 5, 2, 1, 4, 3} +}; + + +heif_orientation heif_orientation_concat(heif_orientation first, heif_orientation second) +{ + // handle invalid input + if (first < 1 || first > 8 || second < 1 || second > 8) { + return heif_orientation_normal; + } + + return static_cast(orientation_concat[first - 1][second - 1]); +} + + +static void set_default_encoding_options(heif_encoding_options& options) +{ + options.version = 8; + + options.save_alpha_channel = true; + options.macOS_compatibility_workaround = false; + options.save_two_colr_boxes_when_ICC_and_nclx_available = false; + options.output_nclx_profile = nullptr; + options.macOS_compatibility_workaround_no_nclx_profile = false; + options.image_orientation = heif_orientation_normal; + + options.color_conversion_options.version = 1; + options.color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; + options.color_conversion_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + options.color_conversion_options.only_use_preferred_chroma_algorithm = false; + + options.prefer_uncC_short_form = true; + + options.unci_parameters = nullptr; +} + + +heif_encoding_options* heif_encoding_options_alloc() +{ + auto options = new heif_encoding_options; + + set_default_encoding_options(*options); + + return options; +} + + +void heif_encoding_options_copy(heif_encoding_options* dst, const heif_encoding_options* src) +{ + if (src == nullptr) { + return; + } + + int min_version = std::min(dst->version, src->version); + + switch (min_version) { + case 8: + dst->unci_parameters = src->unci_parameters; + [[fallthrough]]; + case 7: + dst->prefer_uncC_short_form = src->prefer_uncC_short_form; + [[fallthrough]]; + case 6: + dst->color_conversion_options = src->color_conversion_options; + [[fallthrough]]; + case 5: + dst->image_orientation = src->image_orientation; + [[fallthrough]]; + case 4: + dst->output_nclx_profile = src->output_nclx_profile; + dst->macOS_compatibility_workaround_no_nclx_profile = src->macOS_compatibility_workaround_no_nclx_profile; + [[fallthrough]]; + case 3: + dst->save_two_colr_boxes_when_ICC_and_nclx_available = src->save_two_colr_boxes_when_ICC_and_nclx_available; + [[fallthrough]]; + case 2: + dst->macOS_compatibility_workaround = src->macOS_compatibility_workaround; + [[fallthrough]]; + case 1: + dst->save_alpha_channel = src->save_alpha_channel; + } +} + + +void heif_encoding_options_free(heif_encoding_options* options) +{ + delete options; +} + +heif_error heif_context_encode_image(heif_context* ctx, + const heif_image* input_image, + heif_encoder* encoder, + const heif_encoding_options* input_options, + heif_image_handle** out_image_handle) +{ + if (!encoder) { + return heif_error_null_pointer_argument; + } + + if (out_image_handle) { + *out_image_handle = nullptr; + } + + heif_encoding_options options; + heif_color_profile_nclx nclx; + set_default_encoding_options(options); + if (input_options) { + heif_encoding_options_copy(&options, input_options); + + if (options.output_nclx_profile == nullptr) { + if (input_image->image->has_nclx_color_profile()) { + nclx_profile input_nclx = input_image->image->get_color_profile_nclx(); + + options.output_nclx_profile = &nclx; + nclx.version = 1; + nclx.color_primaries = input_nclx.get_colour_primaries(); + nclx.transfer_characteristics = input_nclx.get_transfer_characteristics(); + nclx.matrix_coefficients = input_nclx.get_matrix_coefficients(); + nclx.full_range_flag = input_nclx.get_full_range_flag(); + } + } + } + + auto encodingResult = ctx->context->encode_image(input_image->image, + encoder, + options, + heif_image_input_class_normal); + if (!encodingResult) { + return encodingResult.error_struct(ctx->context.get()); + } + + std::shared_ptr image = *encodingResult; + + // mark the new image as primary image + + if (ctx->context->is_primary_image_set() == false) { + ctx->context->set_primary_image(image); + } + + if (out_image_handle) { + *out_image_handle = new heif_image_handle; + (*out_image_handle)->image = std::move(image); + (*out_image_handle)->context = ctx->context; + } + + return heif_error_success; +} + + +heif_error heif_context_add_overlay_image(heif_context* ctx, + uint32_t image_width, + uint32_t image_height, + uint16_t nImages, + const heif_item_id* image_ids, + int32_t* offsets, + const uint16_t background_rgba[4], + heif_image_handle** out_iovl_image_handle) +{ + if (!image_ids) { + return heif_error_null_pointer_argument; + } + else if (nImages == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); + } + + + std::vector refs; + refs.insert(refs.end(), image_ids, image_ids + nImages); + + ImageOverlay overlay; + overlay.set_canvas_size(image_width, image_height); + + if (background_rgba) { + overlay.set_background_color(background_rgba); + } + + for (uint16_t i = 0; i < nImages; i++) { + overlay.add_image_on_top(image_ids[i], + offsets ? offsets[2 * i] : 0, + offsets ? offsets[2 * i + 1] : 0); + } + + Result > addImageResult = ImageItem_Overlay::add_new_overlay_item(ctx->context.get(), overlay); + + if (!addImageResult) { + return addImageResult.error_struct(ctx->context.get()); + } + + std::shared_ptr iovlimage = *addImageResult; + + + if (out_iovl_image_handle) { + *out_iovl_image_handle = new heif_image_handle; + (*out_iovl_image_handle)->image = std::move(iovlimage); + (*out_iovl_image_handle)->context = ctx->context; + } + + return heif_error_success; +} + + +heif_error heif_context_set_primary_image(heif_context* ctx, + heif_image_handle* image_handle) +{ + ctx->context->set_primary_image(image_handle->image); + + return heif_error_success; +} + + +void heif_context_set_major_brand(heif_context* ctx, + heif_brand2 major_brand) +{ + auto ftyp = ctx->context->get_heif_file()->get_ftyp_box(); + ftyp->set_major_brand(major_brand); + ftyp->add_compatible_brand(major_brand); +} + + +void heif_context_add_compatible_brand(heif_context* ctx, + heif_brand2 compatible_brand) +{ + ctx->context->get_heif_file()->get_ftyp_box()->add_compatible_brand(compatible_brand); +} + + +void heif_context_set_unif(heif_context* ctx, int flag) +{ + ctx->context->set_unif(flag != 0); +} + + +// === DEPRECATED === + +// DEPRECATED: typo in function name +int heif_encoder_descriptor_supportes_lossy_compression(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->supports_lossy_compression; +} + + +// DEPRECATED: typo in function name +int heif_encoder_descriptor_supportes_lossless_compression(const heif_encoder_descriptor* descriptor) +{ + return descriptor->plugin->supports_lossless_compression; +} + + +// DEPRECATED +int heif_context_get_encoder_descriptors(heif_context* ctx, + heif_compression_format format, + const char* name, + const heif_encoder_descriptor** out_encoder_descriptors, + int count) +{ + return heif_get_encoder_descriptors(format, name, out_encoder_descriptors, count); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_encoding.h libheif-1.23.4/libheif/api/libheif/heif_encoding.h --- libheif-1.19.8/libheif/api/libheif/heif_encoding.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_encoding.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,425 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_ENCODING_H +#define LIBHEIF_HEIF_ENCODING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include +#include +#include + +// Forward declaration. The full definition lives in heif_uncompressed.h. +// heif_encoding_options only stores a pointer, so a forward typedef is enough here +// and avoids a circular include (heif_uncompressed.h pulls in heif.h, which pulls in this header). +typedef struct heif_unci_image_parameters heif_unci_image_parameters; + + +// ----- encoder ----- + +/** + * Opaque object that represents the encoder used to code the images. + */ +typedef struct heif_encoder heif_encoder; + +// A description of the encoder's capabilities and name. +typedef struct heif_encoder_descriptor heif_encoder_descriptor; + +// A configuration parameter of the encoder. Each encoder implementation may have a different +// set of parameters. For the most common settings (e.q. quality), special functions to set +// the parameters are provided. +typedef struct heif_encoder_parameter heif_encoder_parameter; + + +// Quick check whether there is an enoder available for the given format. +// Note that the encoder may be limited to a certain subset of features (e.g. only 8 bit, only lossy). +// You will have to query the specific capabilities further. +LIBHEIF_API +int heif_have_encoder_for_format(heif_compression_format format); + +// Get a list of available encoders. You can filter the encoders by compression format and name. +// Use format_filter==heif_compression_undefined and name_filter==NULL as wildcards. +// The returned list of encoders is sorted by their priority (which is a plugin property). +// The number of encoders is returned, which are not more than 'count' if (out_encoders != nullptr). +// By setting out_encoders==nullptr, you can query the number of encoders, 'count' is ignored. +// Note: to get the actual encoder from the descriptors returned here, use heif_context_get_encoder(). +LIBHEIF_API +int heif_get_encoder_descriptors(heif_compression_format format_filter, + const char* name_filter, + const heif_encoder_descriptor** out_encoders, + int count); + +// Return a long, descriptive name of the encoder (including version information). +LIBHEIF_API +const char* heif_encoder_descriptor_get_name(const heif_encoder_descriptor*); + +// Return a short, symbolic name for identifying the encoder. +// This name should stay constant over different encoder versions. +LIBHEIF_API +const char* heif_encoder_descriptor_get_id_name(const heif_encoder_descriptor*); + +LIBHEIF_API +heif_compression_format +heif_encoder_descriptor_get_compression_format(const heif_encoder_descriptor*); + +LIBHEIF_API +int heif_encoder_descriptor_supports_lossy_compression(const heif_encoder_descriptor*); + +LIBHEIF_API +int heif_encoder_descriptor_supports_lossless_compression(const heif_encoder_descriptor*); + + +// Get an encoder instance that can be used to actually encode images from a descriptor. +LIBHEIF_API +heif_error heif_context_get_encoder(heif_context* context, + const heif_encoder_descriptor*, + heif_encoder** out_encoder); + +// Get an encoder for the given compression format. If there are several encoder plugins +// for this format, the encoder with the highest plugin priority will be returned. +LIBHEIF_API +heif_error heif_context_get_encoder_for_format(heif_context* context, + heif_compression_format format, + heif_encoder**); + +/** + * Release the encoder object after use. + */ +LIBHEIF_API +void heif_encoder_release(heif_encoder*); + +// Get the encoder name from the encoder itself. +LIBHEIF_API +const char* heif_encoder_get_name(const heif_encoder*); + + +// --- Encoder Parameters --- + +// Libheif supports settings parameters through specialized functions and through +// generic functions by parameter name. Sometimes, the same parameter can be set +// in both ways. +// We consider it best practice to use the generic parameter functions only in +// dynamically generated user interfaces, as no guarantees are made that some specific +// parameter names are supported by all plugins. + + +// Set a 'quality' factor (0-100). How this is mapped to actual encoding parameters is +// encoder dependent. +LIBHEIF_API +heif_error heif_encoder_set_lossy_quality(heif_encoder*, int quality); + +LIBHEIF_API +heif_error heif_encoder_set_lossless(heif_encoder*, int enable); + +// level should be between 0 (= none) to 4 (= full) +LIBHEIF_API +heif_error heif_encoder_set_logging_level(heif_encoder*, int level); + +// Get a generic list of encoder parameters. +// Each encoder may define its own, additional set of parameters. +// You do not have to free the returned list. +LIBHEIF_API +const heif_encoder_parameter* const* heif_encoder_list_parameters(heif_encoder*); + +// Return the parameter name. +LIBHEIF_API +const char* heif_encoder_parameter_get_name(const heif_encoder_parameter*); + + +typedef enum heif_encoder_parameter_type +{ + heif_encoder_parameter_type_integer = 1, + heif_encoder_parameter_type_boolean = 2, + heif_encoder_parameter_type_string = 3 +} heif_encoder_parameter_type; + +// Return the parameter type. +LIBHEIF_API +enum heif_encoder_parameter_type heif_encoder_parameter_get_type(const heif_encoder_parameter*); + +// DEPRECATED. Use heif_encoder_parameter_get_valid_integer_values() instead. +LIBHEIF_API +heif_error heif_encoder_parameter_get_valid_integer_range(const heif_encoder_parameter*, + int* have_minimum_maximum, + int* minimum, int* maximum); + +// If integer is limited by a range, have_minimum and/or have_maximum will be != 0 and *minimum, *maximum is set. +// If integer is limited by a fixed set of values, *num_valid_values will be >0 and *out_integer_array is set. +LIBHEIF_API +heif_error heif_encoder_parameter_get_valid_integer_values(const heif_encoder_parameter*, + int* have_minimum, int* have_maximum, + int* minimum, int* maximum, + int* num_valid_values, + const int** out_integer_array); + +LIBHEIF_API +heif_error heif_encoder_parameter_get_valid_string_values(const heif_encoder_parameter*, + const char* const** out_stringarray); + + +LIBHEIF_API +heif_error heif_encoder_set_parameter_integer(heif_encoder*, + const char* parameter_name, + int value); + +LIBHEIF_API +heif_error heif_encoder_get_parameter_integer(heif_encoder*, + const char* parameter_name, + int* value); + +// TODO: name should be changed to heif_encoder_get_valid_integer_parameter_range +LIBHEIF_API // DEPRECATED. +heif_error heif_encoder_parameter_integer_valid_range(heif_encoder*, + const char* parameter_name, + int* have_minimum_maximum, + int* minimum, int* maximum); + +LIBHEIF_API +heif_error heif_encoder_set_parameter_boolean(heif_encoder*, + const char* parameter_name, + int value); + +LIBHEIF_API +heif_error heif_encoder_get_parameter_boolean(heif_encoder*, + const char* parameter_name, + int* value); + +LIBHEIF_API +heif_error heif_encoder_set_parameter_string(heif_encoder*, + const char* parameter_name, + const char* value); + +LIBHEIF_API +heif_error heif_encoder_get_parameter_string(heif_encoder*, + const char* parameter_name, + char* value, int value_size); + +// returns a NULL-terminated list of valid strings or NULL if all values are allowed +LIBHEIF_API +heif_error heif_encoder_parameter_string_valid_values(heif_encoder*, + const char* parameter_name, + const char* const** out_stringarray); + +LIBHEIF_API +heif_error heif_encoder_parameter_integer_valid_values(heif_encoder*, + const char* parameter_name, + int* have_minimum, int* have_maximum, + int* minimum, int* maximum, + int* num_valid_values, + const int** out_integer_array); + +// Set a parameter of any type to the string value. +// Integer values are parsed from the string. +// Boolean values can be "true"/"false"/"1"/"0" +// +// x265 encoder specific note: +// When using the x265 encoder, you may pass any of its parameters by +// prefixing the parameter name with 'x265:'. Hence, to set the 'ctu' parameter, +// you will have to set 'x265:ctu' in libheif. +// Note that there is no checking for valid parameters when using the prefix. +LIBHEIF_API +heif_error heif_encoder_set_parameter(heif_encoder*, + const char* parameter_name, + const char* value); + +// Get the current value of a parameter of any type as a human readable string. +// The returned string is compatible with heif_encoder_set_parameter(). +LIBHEIF_API +heif_error heif_encoder_get_parameter(heif_encoder*, + const char* parameter_name, + char* value_ptr, int value_size); + +// Query whether a specific parameter has a default value. +LIBHEIF_API +int heif_encoder_has_default(heif_encoder*, + const char* parameter_name); + + +// The orientation values are defined equal to the EXIF Orientation tag. +typedef enum heif_orientation +{ + heif_orientation_normal = 1, + heif_orientation_flip_horizontally = 2, + heif_orientation_rotate_180 = 3, + heif_orientation_flip_vertically = 4, + heif_orientation_rotate_90_cw_then_flip_horizontally = 5, + heif_orientation_rotate_90_cw = 6, + heif_orientation_rotate_90_cw_then_flip_vertically = 7, + heif_orientation_rotate_270_cw = 8 +} heif_orientation; + + +LIBHEIF_API +heif_orientation heif_orientation_concat(heif_orientation first, heif_orientation second); + + +typedef struct heif_encoding_options +{ + uint8_t version; + + // version 1 options + + uint8_t save_alpha_channel; // default: true + + // version 2 options + + // DEPRECATED. This option is not required anymore. Its value will be ignored. + uint8_t macOS_compatibility_workaround; + + // version 3 options + + uint8_t save_two_colr_boxes_when_ICC_and_nclx_available; // default: false + + // version 4 options + + // Set this to the NCLX parameters to be used in the output image or set to NULL + // when the same parameters as in the input image should be used. + heif_color_profile_nclx* output_nclx_profile; + + uint8_t macOS_compatibility_workaround_no_nclx_profile; + + // version 5 options + + // libheif will generate irot/imir boxes to match these orientations + heif_orientation image_orientation; + + // version 6 options + + 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; + + // version 8 options + + // Optional 'unci'-specific encoding parameters (compression method, and future fields + // such as interleave type and padding). + // + // Default: nullptr + const heif_unci_image_parameters* unci_parameters; + + // TODO: we should add a flag to force MIAF compatible outputs. E.g. this will put restrictions on grid tile sizes and + // might add a clap box when the grid output size does not match the color subsampling factors. + // Since some of these constraints have to be known before actually encoding the image, "forcing MIAF compatibility" + // could also be a flag in the heif_context. +} heif_encoding_options; + +LIBHEIF_API +heif_encoding_options* heif_encoding_options_alloc(void); + +LIBHEIF_API +void heif_encoding_options_copy(heif_encoding_options* dst, const heif_encoding_options* src); + +LIBHEIF_API +void heif_encoding_options_free(heif_encoding_options*); + + +// Compress the input image. +// Returns a handle to the coded image in 'out_image_handle' unless out_image_handle = NULL. +// 'options' should be NULL for now. +// The first image added to the context is also automatically set the primary image, but +// you can change the primary image later with heif_context_set_primary_image(). +LIBHEIF_API +heif_error heif_context_encode_image(heif_context*, + const heif_image* image, + heif_encoder* encoder, + const heif_encoding_options* options, + heif_image_handle** out_image_handle); + +// offsets[] should either be NULL (all offsets==0) or an array of size 2*nImages with x;y offset pairs. +// If background_rgba is NULL, the background is transparent. +LIBHEIF_API +heif_error heif_context_add_overlay_image(heif_context* ctx, + uint32_t image_width, + uint32_t image_height, + uint16_t nImages, + const heif_item_id* image_ids, + int32_t* offsets, + const uint16_t background_rgba[4], + heif_image_handle** out_iovl_image_handle); + +LIBHEIF_API +heif_error heif_context_set_primary_image(heif_context*, + heif_image_handle* image_handle); + +// Set the major brand of the file. +// If this function is not called, the major brand is determined automatically from +// the image or sequence content. +LIBHEIF_API +void heif_context_set_major_brand(heif_context* ctx, + heif_brand2 major_brand); + +// 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(heif_context* ctx, + heif_brand2 compatible_brand); + +/** + * Enable the unified ID namespace ('unif' brand). + * + * When enabled, item IDs, track IDs, and entity group IDs share a single + * global counter so no ID value is reused across categories. The 'unif' + * compatible brand is automatically added to the output file. + * + * @param ctx the encoding context + * @param flag non-zero to enable, zero to disable + */ +LIBHEIF_API +void heif_context_set_unif(heif_context* ctx, int flag); + +// --- deprecated functions --- + +// DEPRECATED, typo in function name +LIBHEIF_API +int heif_encoder_descriptor_supportes_lossy_compression(const heif_encoder_descriptor*); + +// DEPRECATED, typo in function name +LIBHEIF_API +int heif_encoder_descriptor_supportes_lossless_compression(const heif_encoder_descriptor*); + +// DEPRECATED: use heif_get_encoder_descriptors() instead. +// Get a list of available encoders. You can filter the encoders by compression format and name. +// Use format_filter==heif_compression_undefined and name_filter==NULL as wildcards. +// The returned list of encoders is sorted by their priority (which is a plugin property). +// The number of encoders is returned, which are not more than 'count' if (out_encoders != nullptr). +// By setting out_encoders==nullptr, you can query the number of encoders, 'count' is ignored. +// Note: to get the actual encoder from the descriptors returned here, use heif_context_get_encoder(). +LIBHEIF_API +int heif_context_get_encoder_descriptors(heif_context*, // TODO: why do we need this parameter? + heif_compression_format format_filter, + const char* name_filter, + const heif_encoder_descriptor** out_encoders, + int count); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_entity_groups.cc libheif-1.23.4/libheif/api/libheif/heif_entity_groups.cc --- libheif-1.19.8/libheif/api/libheif/heif_entity_groups.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_entity_groups.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,100 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_entity_groups.h" +#include "box.h" +#include "api_structs.h" +#include "file.h" + +#include +#include + + +heif_entity_group* heif_context_get_entity_groups(const heif_context* ctx, + uint32_t type_filter, + heif_item_id item_filter, + int* out_num_groups) +{ + std::shared_ptr grplBox = ctx->context->get_heif_file()->get_grpl_box(); + if (!grplBox) { + *out_num_groups = 0; + return nullptr; + } + + std::vector > all_entity_group_boxes = grplBox->get_all_child_boxes(); + if (all_entity_group_boxes.empty()) { + *out_num_groups = 0; + return nullptr; + } + + // --- filter groups + + std::vector > entity_group_boxes; + for (auto& group : all_entity_group_boxes) { + if (type_filter != 0 && group->get_short_type() != type_filter) { + continue; + } + + auto groupBox = std::dynamic_pointer_cast(group); + if (!groupBox) continue; + const std::vector& items = groupBox->get_item_ids(); + + if (item_filter != 0 && std::all_of(items.begin(), items.end(), [item_filter](heif_item_id item) { + return item != item_filter; + })) { + continue; + } + + entity_group_boxes.emplace_back(groupBox); + } + + // --- convert to C structs + + auto* groups = new heif_entity_group[entity_group_boxes.size()]; + for (size_t i = 0; i < entity_group_boxes.size(); i++) { + const auto& groupBox = entity_group_boxes[i]; + const std::vector& items = groupBox->get_item_ids(); + + groups[i].entity_group_id = groupBox->get_group_id(); + groups[i].entity_group_type = groupBox->get_short_type(); + groups[i].entities = (items.empty() ? nullptr : new heif_item_id[items.size()]); + groups[i].num_entities = static_cast(items.size()); + + if (groups[i].entities) { + // avoid clang static analyzer false positive + for (size_t k = 0; k < items.size(); k++) { + groups[i].entities[k] = items[k]; + } + } + } + + *out_num_groups = static_cast(entity_group_boxes.size()); + return groups; +} + + +void heif_entity_groups_release(heif_entity_group* grp, int num_groups) +{ + for (int i = 0; i < num_groups; i++) { + delete[] grp[i].entities; + } + + delete[] grp; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_entity_groups.h libheif-1.23.4/libheif/api/libheif/heif_entity_groups.h --- libheif-1.19.8/libheif/api/libheif/heif_entity_groups.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_entity_groups.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,221 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_ENTITY_GROUPS_H +#define LIBHEIF_HEIF_ENTITY_GROUPS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "libheif/heif_library.h" + +// Entity group type FourCCs. +// For the complete list of registered entity group types, see https://mp4ra.org/registered-types/entity-groups + +/** + * Alternatives. + * + * Alternative versions of the same content, listed in order of preference. + * Only one of them should be displayed. + * + * See ISO/IEC 14496-12:2026 Section 8.15.3.1. + */ +#define heif_entity_group_altr heif_fourcc('a','l','t','r') + +/** + * Multi-resolution pyramid. + * + * The same image at different resolutions, listed from lowest to highest resolution. + * + * See ISO/IEC 23008-12:2025/Amd. 1:2025 Section 6.8.12. + */ +#define heif_entity_group_pymd heif_fourcc('p','y','m','d') + +/** + * Timeline equivalence. + * + * Relates untimed image items to an equivalent position in the timeline of a track. + * + * See ISO/IEC 23008-12:2025 Section 6.8.1. + */ +#define heif_entity_group_eqiv heif_fourcc('e','q','i','v') + +/** + * Image burst. + * + * A series of burst-captured images in temporally increasing order. + * The images can be stored as image items or as a single image sequence track (the group's only entity). + * + * See ISO/IEC 23008-12:2025 Section 6.8.2. + */ +#define heif_entity_group_brst heif_fourcc('b','r','s','t') + +/** + * Time-synchronized capture. + * + * Images captured simultaneously, stored either as image items or as image sequence tracks, but not a mixture of both. + * + * See ISO/IEC 23008-12:2025 Section 6.8.3. + */ +#define heif_entity_group_tsyn heif_fourcc('t','s','y','n') + +/** + * Stereo pair. + * + * A stereoscopic pair of two image items: first the left view, then the right view. + * + * See ISO/IEC 23008-12:2025 Section 6.8.5. + */ +#define heif_entity_group_ster heif_fourcc('s','t','e','r') + +/** + * Stereo pair with monoscopic fallback. + * + * Three image items: left view, right view, and a monoscopic fallback, + * which may be the same as the left or right view. + * + * See ISO/IEC 23008-12:2025/Amd. 1:2025 Section 6.8.11. + */ +#define heif_entity_group_stem heif_fourcc('s','t','e','m') + +/** + * Auto-exposure bracketing. + * + * Images of the same scene taken with varying exposure settings. + * + * See ISO/IEC 23008-12:2025 Section 6.8.6.2. + */ +#define heif_entity_group_aebr heif_fourcc('a','e','b','r') + +/** + * White balance bracketing. + * + * Images of the same scene taken with varying white balance settings. + * + * See ISO/IEC 23008-12:2025 Section 6.8.6.3. + */ +#define heif_entity_group_wbbr heif_fourcc('w','b','b','r') + +/** + * Focus bracketing. + * + * Images of the same scene taken with varying focus settings. + * + * See ISO/IEC 23008-12:2025 Section 6.8.6.4. + */ +#define heif_entity_group_fobr heif_fourcc('f','o','b','r') + +/** + * Flash exposure bracketing. + * + * Images of the same scene taken with varying flash exposure settings. + * + * See ISO/IEC 23008-12:2025 Section 6.8.6.5. + */ +#define heif_entity_group_afbr heif_fourcc('a','f','b','r') + +/** + * Depth of field bracketing. + * + * Images of the same scene taken with varying aperture settings. + * + * See ISO/IEC 23008-12:2025 Section 6.8.6.6. + */ +#define heif_entity_group_dobr heif_fourcc('d','o','b','r') + +/** + * Album collection. + * + * A user-defined album of images. + * + * See ISO/IEC 23008-12:2025 Section 6.8.7.1. + */ +#define heif_entity_group_albc heif_fourcc('a','l','b','c') + +/** + * Favorites collection. + * + * A user-defined collection of favorite images. + * + * See ISO/IEC 23008-12:2025 Section 6.8.7.2. + */ +#define heif_entity_group_favc heif_fourcc('f','a','v','c') + +/** + * Panorama. + * + * Images captured to be composed into a panorama, listed in panorama order. + * + * See ISO/IEC 23008-12:2025 Section 6.8.8. + */ +#define heif_entity_group_pano heif_fourcc('p','a','n','o') + +/** + * Slideshow. + * + * Images intended to form a slideshow, listed in display order. + * + * See ISO/IEC 23008-12:2025 Section 6.8.9. + */ +#define heif_entity_group_slid heif_fourcc('s','l','i','d') + +/** + * Progressive rendering. + * + * The same image at different quality levels, listed from lowest to highest quality. + * These images should also be members of an 'altr' entity group. + * + * See ISO/IEC 23008-12:2025 Section 6.8.10. + */ +#define heif_entity_group_prgr heif_fourcc('p','r','g','r') + +// ------------------------- entity groups ------------------------ + +typedef uint32_t heif_entity_group_id; + +typedef struct heif_entity_group +{ + heif_entity_group_id entity_group_id; + // one of the heif_entity_group_* FourCC constants defined above + uint32_t entity_group_type; + heif_item_id* entities; + uint32_t num_entities; +} heif_entity_group; + +// `type_filter` is the wanted FourCC of the entity group type. +// Use 0 for `type_filter` or `item_filter` to disable the filter. +// Returns an array of heif_entity_group structs with *out_num_groups entries. +LIBHEIF_API +heif_entity_group* heif_context_get_entity_groups(const heif_context*, + uint32_t type_filter, + heif_item_id item_filter, + int* out_num_groups); + +// Release an array of entity groups returned by heif_context_get_entity_groups(). +LIBHEIF_API +void heif_entity_groups_release(heif_entity_group*, int num_groups); + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_error.h libheif-1.23.4/libheif/api/libheif/heif_error.h --- libheif-1.19.8/libheif/api/libheif/heif_error.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_error.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,311 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_ERROR_H +#define LIBHEIF_HEIF_ERROR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + + +typedef enum heif_error_code +{ + // Everything ok, no error occurred. + heif_error_Ok = 0, + + // Input file does not exist. + heif_error_Input_does_not_exist = 1, + + // Error in input file. Corrupted or invalid content. + heif_error_Invalid_input = 2, + + // Input file type is not supported. + heif_error_Unsupported_filetype = 3, + + // Image requires an unsupported decoder feature. + heif_error_Unsupported_feature = 4, + + // Library API has been used in an invalid way. + heif_error_Usage_error = 5, + + // Could not allocate enough memory. + heif_error_Memory_allocation_error = 6, + + // The decoder plugin generated an error + heif_error_Decoder_plugin_error = 7, + + // The encoder plugin generated an error + heif_error_Encoder_plugin_error = 8, + + // Error during encoding or when writing to the output + heif_error_Encoding_error = 9, + + // Application has asked for a color profile type that does not exist + heif_error_Color_profile_does_not_exist = 10, + + // Error loading a dynamic plugin + heif_error_Plugin_loading_error = 11, + + // Operation has been canceled + heif_error_Canceled = 12, + + heif_error_End_of_sequence = 13 +} heif_error_code; + + +typedef enum heif_suberror_code +{ + // no further information available + heif_suberror_Unspecified = 0, + + // --- Invalid_input --- + + // End of data reached unexpectedly. + heif_suberror_End_of_data = 100, + + // Size of box (defined in header) is wrong + heif_suberror_Invalid_box_size = 101, + + // Mandatory 'ftyp' box is missing + heif_suberror_No_ftyp_box = 102, + + heif_suberror_No_idat_box = 103, + + heif_suberror_No_meta_box = 104, + + heif_suberror_No_hdlr_box = 105, + + heif_suberror_No_hvcC_box = 106, + + heif_suberror_No_pitm_box = 107, + + heif_suberror_No_ipco_box = 108, + + heif_suberror_No_ipma_box = 109, + + heif_suberror_No_iloc_box = 110, + + heif_suberror_No_iinf_box = 111, + + heif_suberror_No_iprp_box = 112, + + heif_suberror_No_iref_box = 113, + + heif_suberror_No_pict_handler = 114, + + // An item property referenced in the 'ipma' box is not existing in the 'ipco' container. + heif_suberror_Ipma_box_references_nonexisting_property = 115, + + // No properties have been assigned to an item. + heif_suberror_No_properties_assigned_to_item = 116, + + // Image has no (compressed) data + heif_suberror_No_item_data = 117, + + // Invalid specification of image grid (tiled image) + heif_suberror_Invalid_grid_data = 118, + + // Tile-images in a grid image are missing + heif_suberror_Missing_grid_images = 119, + + heif_suberror_Invalid_clean_aperture = 120, + + // Invalid specification of overlay image + heif_suberror_Invalid_overlay_data = 121, + + // Overlay image completely outside of visible canvas area + heif_suberror_Overlay_image_outside_of_canvas = 122, + + heif_suberror_Auxiliary_image_type_unspecified = 123, + + heif_suberror_No_or_invalid_primary_item = 124, + + heif_suberror_No_infe_box = 125, + + heif_suberror_Unknown_color_profile_type = 126, + + heif_suberror_Wrong_tile_image_chroma_format = 127, + + heif_suberror_Invalid_fractional_number = 128, + + heif_suberror_Invalid_image_size = 129, + + heif_suberror_Invalid_pixi_box = 130, + + heif_suberror_No_av1C_box = 131, + + heif_suberror_Wrong_tile_image_pixel_depth = 132, + + heif_suberror_Unknown_NCLX_color_primaries = 133, + + heif_suberror_Unknown_NCLX_transfer_characteristics = 134, + + heif_suberror_Unknown_NCLX_matrix_coefficients = 135, + + // 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, + + heif_suberror_No_avcC_box = 143, + + // we got a mini box, but could not read it properly + heif_suberror_Invalid_mini_box = 149, + + // Decompressing generic compression or header compression data failed (e.g. bitstream corruption) + heif_suberror_Decompression_invalid_data = 150, + + heif_suberror_No_moov_box = 151, + + // The colr (NCLX) box and the codec bitstream VUI/color signalling disagree. + // Per ISO/IEC 14496-12 and ISO/IEC 23000-22 (MIAF) the colr box takes precedence, + // but the conflict is reported as a warning. + heif_suberror_NCLX_colr_VUI_mismatch = 152, + + // --- Memory_allocation_error --- + + // A security limit preventing unreasonable memory allocations was exceeded by the input file. + // Please check whether the file is valid. If it is, contact us so that we could increase the + // 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 --- + + // An item ID was used that is not present in the file. + heif_suberror_Nonexisting_item_referenced = 2000, // also used for Invalid_input + + // An API argument was given a NULL pointer, which is not allowed for that function. + heif_suberror_Null_pointer_argument = 2001, + + // Image channel referenced that does not exist in the image + heif_suberror_Nonexisting_image_channel_referenced = 2002, + + // The version of the passed plugin is not supported. + heif_suberror_Unsupported_plugin_version = 2003, + + // The version of the passed writer is not supported. + heif_suberror_Unsupported_writer_version = 2004, + + // The given (encoder) parameter name does not exist. + heif_suberror_Unsupported_parameter = 2005, + + // The value for the given parameter is not in the valid range. + heif_suberror_Invalid_parameter_value = 2006, + + // Error in property specification + heif_suberror_Invalid_property = 2007, + + // Image reference cycle found in iref + heif_suberror_Item_reference_cycle = 2008, + + + // --- Unsupported_feature --- + + // Image was coded with an unsupported compression method. + heif_suberror_Unsupported_codec = 3000, + + // Image is specified in an unknown way, e.g. as tiled grid image (which is supported) + heif_suberror_Unsupported_image_type = 3001, + + heif_suberror_Unsupported_data_version = 3002, + + // The conversion of the source image to the requested chroma / colorspace is not supported. + heif_suberror_Unsupported_color_conversion = 3003, + + heif_suberror_Unsupported_item_construction_method = 3004, + + heif_suberror_Unsupported_header_compression_method = 3005, + + // Generically compressed data used an unsupported compression method + heif_suberror_Unsupported_generic_compression_method = 3006, + + heif_suberror_Unsupported_essential_property = 3007, + + heif_suberror_Unsupported_track_type = 3008, + + // --- Encoder_plugin_error --- + + heif_suberror_Unsupported_bit_depth = 4000, + + + // --- Encoding_error --- + + heif_suberror_Cannot_write_output_data = 5000, + + heif_suberror_Encoder_initialization = 5001, + heif_suberror_Encoder_encoding = 5002, + heif_suberror_Encoder_cleanup = 5003, + + heif_suberror_Too_many_regions = 5004, + + + // --- 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_No_matching_decoder_installed = 6003 // no decoder found for that compression format +} heif_suberror_code; + + +typedef struct heif_error +{ + // main error category + heif_error_code code; + + // more detailed error code + heif_suberror_code subcode; + + // textual error message (is always defined, you do not have to check for NULL) + const char* message; +} heif_error; + +// Default success return value. Intended for use in user-supplied callback functions. +LIBHEIF_API extern const heif_error heif_error_success; + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_experimental.cc libheif-1.23.4/libheif/api/libheif/heif_experimental.cc --- libheif-1.19.8/libheif/api/libheif/heif_experimental.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_experimental.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,13 +21,14 @@ #include "heif_experimental.h" #include "context.h" #include "api_structs.h" -#include "file.h" +#include "image-items/unc_image.h" +#include "image-items/tiled.h" #include #include #include #include -#include +#include struct heif_property_camera_intrinsic_matrix @@ -35,51 +36,39 @@ Box_cmin::RelativeIntrinsicMatrix matrix; }; -struct heif_error heif_item_get_property_camera_intrinsic_matrix(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_camera_intrinsic_matrix** out_matrix) +heif_error heif_item_get_property_camera_intrinsic_matrix(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_camera_intrinsic_matrix** out_matrix) { if (!out_matrix || !context) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return err.error_struct(context->context.get()); - } - - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range"}; - } - - auto cmin = std::dynamic_pointer_cast(properties[propertyId - 1]); + auto cmin = context->context->find_property(itemId, propertyId); if (!cmin) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "wrong property type"}; + return cmin.error_struct(context->context.get()); } *out_matrix = new heif_property_camera_intrinsic_matrix; - (*out_matrix)->matrix = cmin->get_intrinsic_matrix(); + (*out_matrix)->matrix = (*cmin)->get_intrinsic_matrix(); return heif_error_success; } -void heif_property_camera_intrinsic_matrix_release(struct heif_property_camera_intrinsic_matrix* matrix) +void heif_property_camera_intrinsic_matrix_release(heif_property_camera_intrinsic_matrix* matrix) { delete matrix; } -struct heif_error heif_property_camera_intrinsic_matrix_get_focal_length(const struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double* out_focal_length_x, - double* out_focal_length_y) +heif_error heif_property_camera_intrinsic_matrix_get_focal_length(const heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double* out_focal_length_x, + double* out_focal_length_y) { if (!matrix) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed as matrix"}; + return heif_error_null_pointer_argument; } double fx, fy; @@ -92,13 +81,13 @@ } -struct heif_error heif_property_camera_intrinsic_matrix_get_principal_point(const struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double* out_principal_point_x, - double* out_principal_point_y) +heif_error heif_property_camera_intrinsic_matrix_get_principal_point(const heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double* out_principal_point_x, + double* out_principal_point_y) { if (!matrix) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed as matrix"}; + return heif_error_null_pointer_argument; } double px, py; @@ -111,11 +100,11 @@ } -struct heif_error heif_property_camera_intrinsic_matrix_get_skew(const struct heif_property_camera_intrinsic_matrix* matrix, - double* out_skew) +heif_error heif_property_camera_intrinsic_matrix_get_skew(const heif_property_camera_intrinsic_matrix* matrix, + double* out_skew) { if (!matrix || !out_skew) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } *out_skew = matrix->matrix.skew; @@ -124,14 +113,14 @@ } -struct heif_property_camera_intrinsic_matrix* heif_property_camera_intrinsic_matrix_alloc() +heif_property_camera_intrinsic_matrix* heif_property_camera_intrinsic_matrix_alloc() { return new heif_property_camera_intrinsic_matrix; } -void heif_property_camera_intrinsic_matrix_set_simple(struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double focal_length, double principal_point_x, double principal_point_y) +void heif_property_camera_intrinsic_matrix_set_simple(heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double focal_length, double principal_point_x, double principal_point_y) { if (!matrix) { return; @@ -143,12 +132,12 @@ matrix->matrix.principal_point_y = principal_point_y / image_height; } -void heif_property_camera_intrinsic_matrix_set_full(struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double focal_length_x, - double focal_length_y, - double principal_point_x, double principal_point_y, - double skew) +void heif_property_camera_intrinsic_matrix_set_full(heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double focal_length_x, + double focal_length_y, + double principal_point_x, double principal_point_y, + double skew) { if (!matrix) { return; @@ -168,22 +157,25 @@ } -struct heif_error heif_item_add_property_camera_intrinsic_matrix(const struct heif_context* context, - heif_item_id itemId, - const struct heif_property_camera_intrinsic_matrix* matrix, - heif_property_id* out_propertyId) +heif_error heif_item_add_property_camera_intrinsic_matrix(const heif_context* context, + heif_item_id itemId, + const heif_property_camera_intrinsic_matrix* matrix, + heif_property_id* out_propertyId) { if (!context || !matrix) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + return heif_error_null_pointer_argument; } auto cmin = std::make_shared(); cmin->set_intrinsic_matrix(matrix->matrix); - heif_property_id id = context->context->add_property(itemId, cmin, false); + auto id = context->context->add_property(itemId, cmin, false); + if (!id) { + return id.error_struct(context->context.get()); + } if (out_propertyId) { - *out_propertyId = id; + *out_propertyId = *id; } return heif_error_success; @@ -196,50 +188,38 @@ }; -struct heif_error heif_item_get_property_camera_extrinsic_matrix(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_camera_extrinsic_matrix** out_matrix) +heif_error heif_item_get_property_camera_extrinsic_matrix(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_camera_extrinsic_matrix** out_matrix) { if (!out_matrix || !context) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return err.error_struct(context->context.get()); - } - - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range"}; - } - - auto cmex = std::dynamic_pointer_cast(properties[propertyId - 1]); + auto cmex = context->context->find_property(itemId, propertyId); if (!cmex) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "wrong property type"}; + return cmex.error_struct(context->context.get()); } *out_matrix = new heif_property_camera_extrinsic_matrix; - (*out_matrix)->matrix = cmex->get_extrinsic_matrix(); + (*out_matrix)->matrix = (*cmex)->get_extrinsic_matrix(); return heif_error_success; } -void heif_property_camera_extrinsic_matrix_release(struct heif_property_camera_extrinsic_matrix* matrix) +void heif_property_camera_extrinsic_matrix_release(heif_property_camera_extrinsic_matrix* matrix) { delete matrix; } -struct heif_error heif_property_camera_extrinsic_matrix_get_rotation_matrix(const struct heif_property_camera_extrinsic_matrix* matrix, - double* out_matrix) +heif_error heif_property_camera_extrinsic_matrix_get_rotation_matrix(const heif_property_camera_extrinsic_matrix* matrix, + double* out_matrix) { if (!matrix || !out_matrix) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } auto rot_matrix = matrix->matrix.calculate_rotation_matrix(); @@ -251,11 +231,11 @@ } -struct heif_error heif_property_camera_extrinsic_matrix_get_position_vector(const struct heif_property_camera_extrinsic_matrix* matrix, - int32_t* out_vector) +heif_error heif_property_camera_extrinsic_matrix_get_position_vector(const heif_property_camera_extrinsic_matrix* matrix, + int32_t* out_vector) { if (!matrix || !out_vector) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } out_vector[0] = matrix->matrix.pos_x; @@ -266,11 +246,11 @@ } -struct heif_error heif_property_camera_extrinsic_matrix_get_world_coordinate_system_id(const struct heif_property_camera_extrinsic_matrix* matrix, - uint32_t* out_wcs_id) +heif_error heif_property_camera_extrinsic_matrix_get_world_coordinate_system_id(const heif_property_camera_extrinsic_matrix* matrix, + uint32_t* out_wcs_id) { if (!matrix || !out_wcs_id) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } *out_wcs_id = matrix->matrix.world_coordinate_system_id; @@ -279,20 +259,117 @@ } -struct heif_error heif_image_extract_area(const heif_image* srcimg, - uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, - const heif_security_limits* limits, - struct heif_image** out_image) +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES + +heif_error heif_context_add_pyramid_entity_group(struct heif_context* ctx, + const heif_item_id* layer_item_ids, + size_t num_layers, + /* + uint16_t tile_width, + uint16_t tile_height, + uint32_t num_layers, + const heif_pyramid_layer_info* in_layers, + */ + heif_item_id* out_group_id) +{ + if (!layer_item_ids) { + return heif_error_null_pointer_argument; + } + + if (num_layers == 0) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "Number of layers cannot be 0."}; + } + + std::vector layers(num_layers); + for (size_t i = 0; i < num_layers; i++) { + layers[i] = layer_item_ids[i]; + } + + Result result = ctx->context->add_pyramid_group(layers); + + if (result) { + if (out_group_id) { + *out_group_id = *result; + } + return heif_error_success; + } + else { + return result.error_struct(ctx->context.get()); + } +} + + +heif_pyramid_layer_info* heif_context_get_pyramid_entity_group_info(heif_context* ctx, heif_entity_group_id id, int* out_num_layers) { - auto extractResult = srcimg->image->extract_image_area(x0,y0,w,h, limits); - if (extractResult.error) { - return extractResult.error.error_struct(srcimg->image.get()); + if (!out_num_layers) { + return nullptr; } - heif_image* area = new heif_image; - area->image = extractResult.value; + std::shared_ptr groupBox = ctx->context->get_heif_file()->get_entity_group(id); + if (!groupBox) { + return nullptr; + } + + const auto pymdBox = std::dynamic_pointer_cast(groupBox); + if (!pymdBox) { + return nullptr; + } - *out_image = area; + const std::vector pymd_layers = pymdBox->get_layers(); + if (pymd_layers.empty()) { + return nullptr; + } + + auto items = pymdBox->get_item_ids(); + assert(items.size() == pymd_layers.size()); + + auto* layerInfo = new heif_pyramid_layer_info[pymd_layers.size()]; + for (size_t i = 0; i < pymd_layers.size(); i++) { + layerInfo[i].layer_image_id = items[i]; + layerInfo[i].layer_binning = pymd_layers[i].layer_binning; + layerInfo[i].tile_rows_in_layer = pymd_layers[i].tiles_in_layer_row_minus1 + 1; + layerInfo[i].tile_columns_in_layer = pymd_layers[i].tiles_in_layer_column_minus1 + 1; + } + + *out_num_layers = static_cast(pymd_layers.size()); + + return layerInfo; +} + + +void heif_pyramid_layer_info_release(heif_pyramid_layer_info* infos) +{ + delete[] infos; +} + + +#endif + + +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +heif_error heif_context_add_tiled_image(heif_context* ctx, + const heif_tiled_image_parameters* parameters, + const heif_encoding_options* options, + const heif_encoder* encoder, + heif_image_handle** out_grid_image_handle) +{ + if (out_grid_image_handle) { + *out_grid_image_handle = nullptr; + } + + Result > gridImageResult; + gridImageResult = ImageItem_Tiled::add_new_tiled_item(ctx->context.get(), parameters, encoder, options); + + if (!gridImageResult) { + return gridImageResult.error_struct(ctx->context.get()); + } + + if (out_grid_image_handle) { + *out_grid_image_handle = new heif_image_handle; + (*out_grid_image_handle)->image = *gridImageResult; + (*out_grid_image_handle)->context = ctx->context; + } return heif_error_success; } +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_experimental.h libheif-1.23.4/libheif/api/libheif/heif_experimental.h --- libheif-1.19.8/libheif/api/libheif/heif_experimental.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_experimental.h 2026-09-06 14:45:17.000000000 +0000 @@ -29,95 +29,96 @@ #if HEIF_ENABLE_EXPERIMENTAL_FEATURES - /* =================================================================================== - * This file contains candidate APIs that did not make it into the public API yet. - * =================================================================================== - */ +/* =================================================================================== + * This file contains candidate APIs that did not make it into the public API yet. + * =================================================================================== + */ - /* - heif_item_property_type_camera_intrinsic_matrix = heif_fourcc('c', 'm', 'i', 'n'), - heif_item_property_type_camera_extrinsic_matrix = heif_fourcc('c', 'm', 'e', 'x') +/* +heif_item_property_type_camera_intrinsic_matrix = heif_fourcc('c', 'm', 'i', 'n'), +heif_item_property_type_camera_extrinsic_matrix = heif_fourcc('c', 'm', 'e', 'x') */ -struct heif_property_camera_intrinsic_matrix; -struct heif_property_camera_extrinsic_matrix; +typedef struct heif_property_camera_intrinsic_matrix heif_property_camera_intrinsic_matrix; +typedef struct heif_property_camera_extrinsic_matrix heif_property_camera_extrinsic_matrix; //LIBHEIF_API -struct heif_error heif_item_get_property_camera_intrinsic_matrix(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_camera_intrinsic_matrix** out_matrix); +heif_error heif_item_get_property_camera_intrinsic_matrix(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_camera_intrinsic_matrix** out_matrix); //LIBHEIF_API -void heif_property_camera_intrinsic_matrix_release(struct heif_property_camera_intrinsic_matrix* matrix); +void heif_property_camera_intrinsic_matrix_release(heif_property_camera_intrinsic_matrix* matrix); //LIBHEIF_API -struct heif_error heif_property_camera_intrinsic_matrix_get_focal_length(const struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double* out_focal_length_x, - double* out_focal_length_y); +heif_error heif_property_camera_intrinsic_matrix_get_focal_length(const heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double* out_focal_length_x, + double* out_focal_length_y); //LIBHEIF_API -struct heif_error heif_property_camera_intrinsic_matrix_get_principal_point(const struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double* out_principal_point_x, - double* out_principal_point_y); +heif_error heif_property_camera_intrinsic_matrix_get_principal_point(const heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double* out_principal_point_x, + double* out_principal_point_y); //LIBHEIF_API -struct heif_error heif_property_camera_intrinsic_matrix_get_skew(const struct heif_property_camera_intrinsic_matrix* matrix, - double* out_skew); +heif_error heif_property_camera_intrinsic_matrix_get_skew(const heif_property_camera_intrinsic_matrix* matrix, + double* out_skew); //LIBHEIF_API -struct heif_property_camera_intrinsic_matrix* heif_property_camera_intrinsic_matrix_alloc(); +heif_property_camera_intrinsic_matrix* heif_property_camera_intrinsic_matrix_alloc(void); //LIBHEIF_API -void heif_property_camera_intrinsic_matrix_set_simple(struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double focal_length, double principal_point_x, double principal_point_y); +void heif_property_camera_intrinsic_matrix_set_simple(heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double focal_length, double principal_point_x, double principal_point_y); //LIBHEIF_API -void heif_property_camera_intrinsic_matrix_set_full(struct heif_property_camera_intrinsic_matrix* matrix, - int image_width, int image_height, - double focal_length_x, - double focal_length_y, - double principal_point_x, double principal_point_y, - double skew); +void heif_property_camera_intrinsic_matrix_set_full(heif_property_camera_intrinsic_matrix* matrix, + int image_width, int image_height, + double focal_length_x, + double focal_length_y, + double principal_point_x, double principal_point_y, + double skew); //LIBHEIF_API -struct heif_error heif_item_add_property_camera_intrinsic_matrix(const struct heif_context* context, +heif_error heif_item_add_property_camera_intrinsic_matrix(const heif_context* context, heif_item_id itemId, - const struct heif_property_camera_intrinsic_matrix* matrix, + const heif_property_camera_intrinsic_matrix* matrix, heif_property_id* out_propertyId); //LIBHEIF_API -struct heif_error heif_item_get_property_camera_extrinsic_matrix(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_camera_extrinsic_matrix** out_matrix); +heif_error heif_item_get_property_camera_extrinsic_matrix(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_camera_extrinsic_matrix** out_matrix); //LIBHEIF_API -void heif_property_camera_extrinsic_matrix_release(struct heif_property_camera_extrinsic_matrix* matrix); +void heif_property_camera_extrinsic_matrix_release(heif_property_camera_extrinsic_matrix* matrix); // `out_matrix` must point to a 9-element matrix, which will be filled in row-major order. //LIBHEIF_API -struct heif_error heif_property_camera_extrinsic_matrix_get_rotation_matrix(const struct heif_property_camera_extrinsic_matrix* matrix, - double* out_matrix); +heif_error heif_property_camera_extrinsic_matrix_get_rotation_matrix(const heif_property_camera_extrinsic_matrix* matrix, + double* out_matrix); // `out_vector` must point to a 3-element vector, which will be filled with the (X,Y,Z) coordinates (in micrometers). //LIBHEIF_API -struct heif_error heif_property_camera_extrinsic_matrix_get_position_vector(const struct heif_property_camera_extrinsic_matrix* matrix, - int32_t* out_vector); +heif_error heif_property_camera_extrinsic_matrix_get_position_vector(const heif_property_camera_extrinsic_matrix* matrix, + int32_t* out_vector); //LIBHEIF_API -struct heif_error heif_property_camera_extrinsic_matrix_get_world_coordinate_system_id(const struct heif_property_camera_extrinsic_matrix* matrix, - uint32_t* out_wcs_id); +heif_error heif_property_camera_extrinsic_matrix_get_world_coordinate_system_id(const heif_property_camera_extrinsic_matrix* matrix, + uint32_t* out_wcs_id); #endif // --- Tiled images -struct heif_tiled_image_parameters { +typedef struct heif_tiled_image_parameters +{ int version; // --- version 1 @@ -138,296 +139,44 @@ // boolean flags uint8_t tiles_are_sequential; // TODO: can we derive this automatically -}; - -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -LIBHEIF_API -struct heif_error heif_context_add_tiled_image(struct heif_context* ctx, - const struct heif_tiled_image_parameters* parameters, - const struct heif_encoding_options* options, // TODO: do we need this? - const struct heif_encoder* encoder, - struct heif_image_handle** out_tiled_image_handle); -#endif - -// --- 'unci' images - -// This is similar to heif_metadata_compression. We should try to keep the integers compatible, but each enum will just -// contain the allowed values. -enum heif_unci_compression -{ - heif_unci_compression_off = 0, - //heif_unci_compression_auto = 1, - //heif_unci_compression_unknown = 2, // only used when reading unknown method from input file - heif_unci_compression_deflate = 3, - heif_unci_compression_zlib = 4, - heif_unci_compression_brotli = 5 -}; - - -struct heif_unci_image_parameters { - int version; - - // --- version 1 - - uint32_t image_width; - uint32_t image_height; - - uint32_t tile_width; - uint32_t tile_height; - - enum heif_unci_compression compression; // TODO - - // TODO: interleave type, padding -}; +} heif_tiled_image_parameters; #if HEIF_ENABLE_EXPERIMENTAL_FEATURES LIBHEIF_API -struct heif_error heif_context_add_unci_image(struct heif_context* ctx, - const struct heif_unci_image_parameters* parameters, - const struct heif_encoding_options* encoding_options, - const struct heif_image* prototype, - struct heif_image_handle** out_unci_image_handle); +heif_error heif_context_add_tiled_image(heif_context* ctx, + const heif_tiled_image_parameters* parameters, + const heif_encoding_options* options, // TODO: do we need this? + const heif_encoder* encoder, + heif_image_handle** out_tiled_image_handle); #endif // --- 'pymd' entity group (pyramid layers) -struct heif_pyramid_layer_info { +typedef struct heif_pyramid_layer_info +{ heif_item_id layer_image_id; uint16_t layer_binning; uint32_t tile_rows_in_layer; uint32_t tile_columns_in_layer; -}; +} heif_pyramid_layer_info; #if HEIF_ENABLE_EXPERIMENTAL_FEATURES // The input images are automatically sorted according to resolution. You can provide them in any order. LIBHEIF_API -struct heif_error heif_context_add_pyramid_entity_group(struct heif_context* ctx, - const heif_item_id* layer_item_ids, - size_t num_layers, - heif_item_id* out_group_id); +heif_error heif_context_add_pyramid_entity_group(heif_context* ctx, + const heif_item_id* layer_item_ids, + size_t num_layers, + heif_item_id* out_group_id); LIBHEIF_API -struct heif_pyramid_layer_info* heif_context_get_pyramid_entity_group_info(struct heif_context*, heif_entity_group_id id, int* out_num_layers); +heif_pyramid_layer_info* heif_context_get_pyramid_entity_group_info(heif_context*, + heif_entity_group_id id, + int* out_num_layers); LIBHEIF_API -void heif_pyramid_layer_info_release(struct heif_pyramid_layer_info*); +void heif_pyramid_layer_info_release(heif_pyramid_layer_info*); #endif -// --- other pixel datatype support - -enum heif_channel_datatype -{ - heif_channel_datatype_undefined = 0, - heif_channel_datatype_unsigned_integer = 1, - heif_channel_datatype_signed_integer = 2, - heif_channel_datatype_floating_point = 3, - heif_channel_datatype_complex_number = 4 -}; - -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -LIBHEIF_API -struct heif_error heif_image_add_channel(struct heif_image* image, - enum heif_channel channel, - int width, int height, - enum heif_channel_datatype datatype, int bit_depth); - - -LIBHEIF_API -int heif_image_list_channels(struct heif_image*, - enum heif_channel** out_channels); - -LIBHEIF_API -void heif_channel_release_list(enum heif_channel** channels); -#endif - -struct heif_complex32 { - float real, imaginary; -}; - -struct heif_complex64 { - double real, imaginary; -}; - -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -LIBHEIF_API -enum heif_channel_datatype heif_image_get_datatype(const struct heif_image* img, enum heif_channel channel); - - -// The 'stride' in all of these functions are in units of the underlying datatype. -LIBHEIF_API -const uint16_t* heif_image_get_channel_uint16_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const uint32_t* heif_image_get_channel_uint32_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const uint64_t* heif_image_get_channel_uint64_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const int16_t* heif_image_get_channel_int16_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const int32_t* heif_image_get_channel_int32_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const int64_t* heif_image_get_channel_int64_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const float* heif_image_get_channel_float32_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const double* heif_image_get_channel_float64_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const struct heif_complex32* heif_image_get_channel_complex32_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -const struct heif_complex64* heif_image_get_channel_complex64_readonly(const struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -uint16_t* heif_image_get_channel_uint16(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -uint32_t* heif_image_get_channel_uint32(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -uint64_t* heif_image_get_channel_uint64(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -int16_t* heif_image_get_channel_int16(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -int32_t* heif_image_get_channel_int32(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -int64_t* heif_image_get_channel_int64(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -float* heif_image_get_channel_float32(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -double* heif_image_get_channel_float64(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -struct heif_complex32* heif_image_get_channel_complex32(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - -LIBHEIF_API -struct heif_complex64* heif_image_get_channel_complex64(struct heif_image*, - enum heif_channel channel, - size_t* out_stride); - - -// ========================= Timestamps ========================= - -LIBHEIF_API extern const uint64_t heif_tai_clock_info_unknown_time_uncertainty; -LIBHEIF_API extern const int32_t heif_tai_clock_info_unknown_drift_rate; -LIBHEIF_API extern const uint64_t heif_unknown_tai_timestamp; -#endif - -struct heif_tai_clock_info -{ - uint8_t version; - - // version 1 - - uint64_t time_uncertainty; - uint32_t clock_resolution; - int32_t clock_drift_rate; - uint8_t clock_type; -}; - - -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -int heif_is_tai_clock_info_drift_rate_undefined(int32_t drift_rate); - - -// Creates a new clock info property if it doesn't already exist. -LIBHEIF_API -struct heif_error heif_property_set_clock_info(struct heif_context* ctx, - heif_item_id itemId, - const struct heif_tai_clock_info* clock, - heif_property_id* out_propertyId); - -// The `out_clock` struct passed in needs to have the `version` field set so that this -// function knows which fields it is safe to fill. -// When the read property is a lower version, the version variable of out_clock will be reduced. -LIBHEIF_API -struct heif_error heif_property_get_clock_info(const struct heif_context* ctx, - heif_item_id itemId, - struct heif_tai_clock_info* out_clock); -#endif - -struct heif_tai_timestamp_packet -{ - uint8_t version; - - // version 1 - - uint64_t tai_timestamp; - uint8_t synchronization_state; // bool - uint8_t timestamp_generation_failure; // bool - uint8_t timestamp_is_modified; // bool -}; - -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES - -// Creates a new TAI timestamp property if one doesn't already exist for itemId. -// Creates a new clock info property if one doesn't already exist for itemId. -LIBHEIF_API -struct heif_error heif_property_set_tai_timestamp(struct heif_context* ctx, - heif_item_id itemId, - struct heif_tai_timestamp_packet* timestamp, - heif_property_id* out_propertyId); - -LIBHEIF_API -struct heif_error heif_property_get_tai_timestamp(const struct heif_context* ctx, - heif_item_id itemId, - struct heif_tai_timestamp_packet* out_timestamp); - -LIBHEIF_API -heif_error heif_image_extract_area(const heif_image*, - uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, - const heif_security_limits* limits, - struct heif_image** out_image); -#endif #ifdef __cplusplus } diff -Nru libheif-1.19.8/libheif/api/libheif/heif_export.h libheif-1.23.4/libheif/api/libheif/heif_export.h --- libheif-1.19.8/libheif/api/libheif/heif_export.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_export.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,49 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_EXPORT_H +#define LIBHEIF_HEIF_EXPORT_H + +// LIBHEIF_API marks the symbols that are exported from the shared library. +// +// This macro lives in its own tiny, dependency-free header so that every +// public header can pull it in without creating include cycles. In particular, +// heif_library.h and heif_error.h are mutually dependent (heif_library.h needs +// the heif_error type while heif_error.h needs LIBHEIF_API), so the macro +// cannot live in either of them if both are to compile standalone. + +#if (defined(_WIN32) || defined __CYGWIN__) && !defined(LIBHEIF_STATIC_BUILD) +#ifdef LIBHEIF_EXPORTS +#define LIBHEIF_API __declspec(dllexport) +#else +#define LIBHEIF_API __declspec(dllimport) +#endif +#elif (defined(__GNUC__) || defined(__clang__)) && defined(HAVE_VISIBILITY) && HAVE_VISIBILITY +// GCC-style visibility attributes. +#ifdef LIBHEIF_EXPORTS +#define LIBHEIF_API __attribute__((__visibility__("default"))) +#else +#define LIBHEIF_API +#endif +#else +#define LIBHEIF_API +#endif + +#endif // LIBHEIF_HEIF_EXPORT_H diff -Nru libheif-1.19.8/libheif/api/libheif/heif_image.cc libheif-1.23.4/libheif/api/libheif/heif_image.cc --- libheif-1.19.8/libheif/api/libheif/heif_image.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_image.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,410 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_image.h" +#include "heif.h" +#include "image/pixelimage.h" +#include "api_structs.h" +#include "error.h" +#include + +#include +#include +#include +#include +#include + + +heif_colorspace heif_image_get_colorspace(const heif_image* img) +{ + return img->image->get_colorspace(); +} + +heif_chroma heif_image_get_chroma_format(const heif_image* img) +{ + return img->image->get_chroma_format(); +} + + +static int uint32_to_int(uint32_t v) +{ + if (v == 0 || v > static_cast(std::numeric_limits::max())) { + return -1; + } + else { + return static_cast(v); + } +} + + +int heif_image_get_width(const heif_image* img, heif_channel channel) +{ + return uint32_to_int(img->image->get_width(channel)); +} + + +int heif_image_get_height(const heif_image* img, heif_channel channel) +{ + return uint32_to_int(img->image->get_height(channel)); +} + + +int heif_image_get_primary_width(const heif_image* img) +{ + return uint32_to_int(img->image->get_width()); +} + + +int heif_image_get_primary_height(const heif_image* img) +{ + return uint32_to_int(img->image->get_height()); +} + + +heif_error heif_image_crop(heif_image* img, + int left, int right, int top, int bottom) +{ + uint32_t w = img->image->get_width(); + uint32_t h = img->image->get_height(); + + // Margins must be non-negative and must not consume the entire image. + // Without this check, `left + right >= w` would underflow the unsigned + // `w - 1 - right` computation below and cause an OOB read (issue #1746). + if (left < 0 || right < 0 || top < 0 || bottom < 0 || + static_cast(left) + right >= w || + static_cast(top) + bottom >= h) { + return heif_error{ + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Invalid crop margins" + }; + } + + auto cropResult = img->image->crop(left, w - 1 - right, top, h - 1 - bottom, nullptr); + if (!cropResult) { + return cropResult.error_struct(img->image.get()); + } + + img->image = *cropResult; + + return heif_error_success; +} + + +heif_error heif_image_extract_area(const heif_image* srcimg, + uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, + const heif_security_limits* limits, + heif_image** out_image) +{ + auto extractResult = srcimg->image->extract_image_area(x0, y0, w, h, limits); + if (!extractResult) { + return extractResult.error_struct(srcimg->image.get()); + } + + heif_image* area = new heif_image; + area->image = *extractResult; + + *out_image = area; + + return heif_error_success; +} + + +int heif_image_get_bits_per_pixel(const heif_image* img, heif_channel channel) +{ + // get_storage_bits_per_pixel() returns 0 for a non-existing channel; + // the public API documents -1 for that case. + uint16_t bpp = img->image->get_storage_bits_per_pixel(channel); + return bpp == 0 ? -1 : bpp; +} + + +int heif_image_get_bits_per_pixel_range(const heif_image* img, heif_channel channel) +{ + // get_bits_per_pixel() returns 0 for a non-existing channel; + // keep the public API's -1 result for that case. + uint16_t bpp = img->image->get_bits_per_pixel(channel); + return bpp == 0 ? -1 : bpp; +} + + +int heif_image_has_channel(const heif_image* img, heif_channel channel) +{ + return img->image->has_channel(channel); +} + + +const uint8_t* heif_image_get_plane_readonly(const heif_image* image, + heif_channel channel, + int* out_stride) +{ + if (!out_stride) { + return nullptr; + } + + if (!image || !image->image) { + *out_stride = 0; + return nullptr; + } + + size_t stride; + const auto* p = image->image->get_channel_memory(channel, &stride); + + // TODO: use C++20 std::cmp_greater() + if (stride > static_cast(std::numeric_limits::max())) { + return nullptr; + } + + *out_stride = static_cast(stride); + return p; +} + + +uint8_t* heif_image_get_plane(heif_image* image, + heif_channel channel, + int* out_stride) +{ + if (!out_stride) { + return nullptr; + } + + if (!image || !image->image) { + *out_stride = 0; + return nullptr; + } + + size_t stride; + uint8_t* p = image->image->get_channel_memory(channel, &stride); + + // TODO: use C++20 std::cmp_greater() + if (stride > static_cast(std::numeric_limits::max())) { + return nullptr; + } + + *out_stride = static_cast(stride); + return p; +} + + +const uint8_t* heif_image_get_plane_readonly2(const heif_image* image, + heif_channel channel, + size_t* out_stride) +{ + if (!out_stride) { + return nullptr; + } + + if (!image || !image->image) { + *out_stride = 0; + return nullptr; + } + + return image->image->get_channel_memory(channel, out_stride); +} + + +uint8_t* heif_image_get_plane2(heif_image* image, + heif_channel channel, + size_t* out_stride) +{ + if (!out_stride) { + return nullptr; + } + + if (!image || !image->image) { + *out_stride = 0; + return nullptr; + } + + return image->image->get_channel_memory(channel, out_stride); +} + + +heif_error heif_image_scale_image(const heif_image* input, + heif_image** output, + int width, int height, + const heif_scaling_options* options) +{ + std::shared_ptr out_img; + + Error err = input->image->scale_nearest_neighbor(out_img, width, height, nullptr); + if (err) { + return err.error_struct(input->image.get()); + } + + *output = new heif_image; + (*output)->image = std::move(out_img); + + return Error::Ok.error_struct(input->image.get()); +} + + +heif_error heif_image_extend_to_size_fill_with_zero(heif_image* image, + uint32_t width, uint32_t height) +{ + Error err = image->image->extend_to_size_with_zero(width, height, nullptr); + if (err) { + return err.error_struct(image->image.get()); + } + + return heif_error_ok; +} + + +int heif_image_get_decoding_warnings(heif_image* image, + int first_warning_idx, + heif_error* out_warnings, + int max_output_buffer_entries) +{ + if (max_output_buffer_entries == 0) { + return (int) image->image->get_warnings().size(); + } + else { + const auto& warnings = image->image->get_warnings(); + int n; + for (n = 0; n + first_warning_idx < (int) warnings.size() && n < max_output_buffer_entries; n++) { + out_warnings[n] = warnings[n + first_warning_idx].error_struct(image->image.get()); + } + return n; + } +} + +void heif_image_add_decoding_warning(heif_image* image, + heif_error err) +{ + image->image->add_warning(Error(err.code, err.subcode)); +} + + +void heif_image_release(const heif_image* img) +{ + delete img; +} + + +void heif_image_get_pixel_aspect_ratio(const heif_image* image, uint32_t* aspect_h, uint32_t* aspect_v) +{ + image->image->get_pixel_ratio(aspect_h, aspect_v); +} + + +void heif_image_set_pixel_aspect_ratio(heif_image* image, uint32_t aspect_h, uint32_t aspect_v) +{ + image->image->set_pixel_ratio(aspect_h, aspect_v); +} + +void heif_image_handle_set_pixel_aspect_ratio(heif_image_handle* handle, uint32_t aspect_h, uint32_t aspect_v) +{ + handle->image->set_pixel_ratio(aspect_h, aspect_v); +} + +heif_error heif_image_create(int width, int height, + heif_colorspace colorspace, + heif_chroma chroma, + heif_image** image) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + // auto-correct colorspace_YCbCr + chroma_planar (formerly chroma_monochrome) + // to colorspace_monochrome + // TODO: this should return an error in a later version (see below) + if (chroma == heif_chroma_planar && colorspace == heif_colorspace_YCbCr) { + colorspace = heif_colorspace_monochrome; + + std::cerr << "libheif warning: heif_image_create() used with an illegal colorspace/chroma combination. This will return an error in a future version.\n"; + } + + // return error if invalid colorspace + chroma combination is used. + // (RGB + planar canonicalization happens later in HeifPixelImage::create.) + auto validChroma = get_valid_chroma_values_for_colorspace(colorspace); + if (std::find(validChroma.begin(), validChroma.end(), chroma) == validChroma.end()) { + *image = nullptr; + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "Invalid colorspace/chroma combination."}; + } + + heif_image* img = new heif_image; + img->image = std::make_shared(); + + img->image->create(width, height, colorspace, chroma); + + *image = img; + + return heif_error_success; +} + +heif_error heif_image_add_plane(heif_image* image, + heif_channel channel, int width, int height, int bit_depth) +{ + // Note: no security limit, because this is explicitly requested by the user. + if (auto err = image->image->add_channel(channel, width, height, bit_depth, nullptr)) { + return err.error_struct(image->image.get()); + } + else { + return heif_error_success; + } +} + + +heif_error heif_image_add_plane_safe(heif_image* image, + heif_channel channel, int width, int height, int bit_depth, + const heif_security_limits* limits) +{ + if (auto err = image->image->add_channel(channel, width, height, bit_depth, limits)) { + return err.error_struct(image->image.get()); + } + else { + return heif_error_success; + } +} + + +void heif_image_set_premultiplied_alpha(heif_image* image, + int is_premultiplied_alpha) +{ + if (image == nullptr) { + return; + } + + image->image->set_premultiplied_alpha(is_premultiplied_alpha); +} + + +int heif_image_is_premultiplied_alpha(heif_image* image) +{ + if (image == nullptr) { + return 0; + } + + return image->image->is_premultiplied_alpha(); +} + + +heif_error heif_image_extend_padding_to_size(heif_image* image, int min_physical_width, int min_physical_height) +{ + Error err = image->image->extend_padding_to_size(min_physical_width, min_physical_height, false, nullptr); + if (err) { + return err.error_struct(image->image.get()); + } + else { + return heif_error_success; + } +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_image.h libheif-1.23.4/libheif/api/libheif/heif_image.h --- libheif-1.19.8/libheif/api/libheif/heif_image.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_image.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,421 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_IMAGE_H +#define LIBHEIF_HEIF_IMAGE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + + +// The heif_chroma enum describes pixel layout (planar vs interleaved, and any +// YUV chroma subsampling). It does NOT describe pixel semantics; the semantic +// interpretation of the planes comes from heif_colorspace. +// +// heif_chroma_planar covers "one or more planar components, no subsampling". +// It is used for: +// - heif_colorspace_monochrome (single luma plane) +// - heif_colorspace_filter_array (single CFA mosaic plane) +// - heif_colorspace_custom (any number of planar components) +// For heif_colorspace_RGB the historical name heif_chroma_444 is the +// canonical form for planar RGB. heif_chroma_planar is accepted as a +// synonym at the C API boundary and is internally canonicalized to +// heif_chroma_444 so existing callers and internal code paths keep seeing +// heif_chroma_444 on read-back. Callers should also accept a returned +// heif_chroma_planar as a synonym. We might switch to this as the +// canonical name in the future. +// +// The YUV subsampling names (heif_chroma_420 / _422 / _444) are kept for +// heif_colorspace_YCbCr. +typedef enum heif_chroma +{ + heif_chroma_undefined = 99, + heif_chroma_planar = 0, + heif_chroma_420 = 1, + heif_chroma_422 = 2, + heif_chroma_444 = 3, + heif_chroma_interleaved_RGB = 10, + heif_chroma_interleaved_RGBA = 11, + heif_chroma_interleaved_RRGGBB_BE = 12, // HDR, big endian. + heif_chroma_interleaved_RRGGBBAA_BE = 13, // HDR, big endian. + heif_chroma_interleaved_RRGGBB_LE = 14, // HDR, little endian. + heif_chroma_interleaved_RRGGBBAA_LE = 15 // HDR, little endian. +} heif_chroma; + +// DEPRECATED ENUM NAMES +#define heif_chroma_interleaved_24bit heif_chroma_interleaved_RGB +#define heif_chroma_interleaved_32bit heif_chroma_interleaved_RGBA + +// Legacy alias for heif_chroma_planar. The name "monochrome" was misleading +// for non-grayscale planar layouts (filter_array, custom); use +// heif_chroma_planar in new code. +#define heif_chroma_monochrome heif_chroma_planar + + +typedef enum heif_colorspace +{ + heif_colorspace_undefined = 99, + + // heif_colorspace_YCbCr should be used with one of these heif_chroma values: + // * heif_chroma_444 + // * heif_chroma_422 + // * heif_chroma_420 + heif_colorspace_YCbCr = 0, + + // heif_colorspace_RGB should be used with one of these heif_chroma values: + // * heif_chroma_444 (for planar RGB) + // * heif_chroma_planar is accepted as a synonym and is internally canonicalized to heif_chroma_444. + // It is the preferred, future-proof value. + // * heif_chroma_interleaved_RGB + // * heif_chroma_interleaved_RGBA + // * heif_chroma_interleaved_RRGGBB_BE + // * heif_chroma_interleaved_RRGGBBAA_BE + // * heif_chroma_interleaved_RRGGBB_LE + // * heif_chroma_interleaved_RRGGBBAA_LE + heif_colorspace_RGB = 1, + + // heif_colorspace_monochrome should only be used with heif_chroma = heif_chroma_planar. + heif_colorspace_monochrome = 2, + + // Indicates that this image has a special, custom arrangement of components. + // For example, it can have several monochrome channels or just a depth component with no color image. + // Images of this type are always planar and use heif_chroma_planar. + heif_colorspace_custom = 3, + + // Images of this type are filter-array (CFA / Bayer) mosaics. The single + // mosaicked plane is described as heif_chroma_planar. + heif_colorspace_filter_array = 4 +} heif_colorspace; + +#define heif_colorspace_nonvisual heif_colorspace_custom + +typedef enum heif_channel +{ + heif_channel_Y = 0, + heif_channel_Cb = 1, + heif_channel_Cr = 2, + heif_channel_R = 3, + heif_channel_G = 4, + heif_channel_B = 5, + heif_channel_Alpha = 6, + heif_channel_interleaved = 10, + heif_channel_filter_array = 11, + heif_channel_depth = 12, + heif_channel_disparity = 13, + heif_channel_unknown = 65535 +} heif_channel; + +// An heif_image contains a decoded pixel image in various colorspaces, chroma formats, +// and bit depths. + +// Note: when converting images to an interleaved chroma format, the resulting +// image contains only a single channel of type channel_interleaved with, e.g., 3 bytes per pixel, +// containing the interleaved R,G,B values. + +// Planar RGB images are specified as heif_colorspace_RGB / heif_chroma_444. +// heif_chroma_planar is accepted as a synonym and is internally canonicalized +// to heif_chroma_444 so heif_image_get_chroma_format() always returns +// heif_chroma_444 for planar RGB. + +typedef struct heif_image heif_image; +typedef struct heif_image_handle heif_image_handle; +typedef struct heif_security_limits heif_security_limits; + + +// Get the colorspace format of the image. +LIBHEIF_API +heif_colorspace heif_image_get_colorspace(const heif_image*); + +// Get the chroma format of the image. +LIBHEIF_API +heif_chroma heif_image_get_chroma_format(const heif_image*); + +/** + * Get the width of a specified image channel. + * + * @param img the image to get the width for + * @param channel the channel to select + * @return the width of the channel in pixels, or -1 the channel does not exist in the image + */ +LIBHEIF_API +int heif_image_get_width(const heif_image* img, heif_channel channel); + +/** + * Get the height of a specified image channel. + * + * @param img the image to get the height for + * @param channel the channel to select + * @return the height of the channel in pixels, or -1 the channel does not exist in the image + */ +LIBHEIF_API +int heif_image_get_height(const heif_image* img, heif_channel channel); + +/** + * Get the logical width of the image. + * + * For well-formed images this equals the size of the main channel (the Y channel + * in YCbCr or mono, or the RGB channels). Subsampled chroma channels may be smaller. + * + * @param img the image to get the primary width for + * @return the width in pixels + */ +LIBHEIF_API +int heif_image_get_primary_width(const heif_image* img); + +/** + * Get the logical height of the image. + * + * For well-formed images this equals the size of the main channel (the Y channel + * in YCbCr or mono, or the RGB channels). Subsampled chroma channels may be smaller. + * + * @param img the image to get the primary height for + * @return the height in pixels + */ +LIBHEIF_API +int heif_image_get_primary_height(const heif_image* img); + +/** + * Crop the image in place by removing margins from each edge. + * + * The four parameters specify the number of pixels to remove from each side, + * not absolute pixel coordinates. For example, to crop a 100x100 image down + * to its central 80x80 region, pass `left=10, right=10, top=10, bottom=10`. + * Passing all zeros leaves the image unchanged. + * + * The resulting image has dimensions `(w - left - right) x (h - top - bottom)`, + * which must be at least 1x1. All four values must be non-negative and the + * sums `left + right` and `top + bottom` must each be strictly less than the + * corresponding image dimension; otherwise `heif_error_Usage_error` / + * `heif_suberror_Invalid_parameter_value` is returned and the image is left + * unchanged. + * + * @param img the image to crop (modified in place on success) + * @param left number of pixels to remove from the left edge + * @param right number of pixels to remove from the right edge + * @param top number of pixels to remove from the top edge + * @param bottom number of pixels to remove from the bottom edge + */ +LIBHEIF_API +heif_error heif_image_crop(heif_image* img, + int left, int right, int top, int bottom); + +LIBHEIF_API +heif_error heif_image_extract_area(const heif_image*, + uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, + const heif_security_limits* limits, + heif_image** out_image); + +// Get the number of bits per pixel in the given image channel. Returns -1 if +// a non-existing channel was given. +// Note that the number of bits per pixel may be different for each color channel. +// This function returns the number of bits used for storage of each pixel. +// Especially for HDR images, this is probably not what you want. Have a look at +// heif_image_get_bits_per_pixel_range() instead. +LIBHEIF_API +int heif_image_get_bits_per_pixel(const heif_image*, heif_channel channel); + +// Get the number of bits per pixel in the given image channel. This function returns +// the number of bits used for representing the pixel value, which might be smaller +// than the number of bits used in memory. +// For example, in 12bit HDR images, this function returns '12', while still 16 bits +// are reserved for storage. For interleaved RGBA with 12 bit, this function also returns +// '12', not '48' or '64' (heif_image_get_bits_per_pixel returns 64 in this case). +LIBHEIF_API +int heif_image_get_bits_per_pixel_range(const heif_image*, heif_channel channel); + +LIBHEIF_API +int heif_image_has_channel(const heif_image*, heif_channel channel); + +// Get a pointer to the actual pixel data. +// The 'out_stride' is returned as "bytes per line". +// When out_stride is NULL, no value will be written. +// Returns NULL if a non-existing channel was given. +// Deprecated, use the safer version heif_image_get_plane_readonly2() instead. +LIBHEIF_API +const uint8_t* heif_image_get_plane_readonly(const heif_image*, + heif_channel channel, + int* out_stride); + +// Deprecated, use the safer version heif_image_get_plane2() instead. +LIBHEIF_API +uint8_t* heif_image_get_plane(heif_image*, + heif_channel channel, + int* out_stride); + +// These are safer variants of the two functions above. +// The 'stride' parameter is often multiplied by the image height in the client application. +// For very large images, this can lead to integer overflows and, consequently, illegal memory accesses. +// The changed 'stride' parameter type eliminates this common error. +// +// Size any copy loop that reads or writes this plane from *this* channel's own +// heif_image_get_width()/heif_image_get_height() and this 'out_stride' -- never from +// heif_image_handle_get_width()/heif_image_handle_get_height() (the signalled item size, which +// is not guaranteed to match the decoded plane) and never by assuming stride equals +// width * bytes_per_pixel: the row may be padded, so require stride >= bytes_per_pixel * width +// and use the returned stride for row-to-row offsets. +LIBHEIF_API +const uint8_t* heif_image_get_plane_readonly2(const heif_image*, + heif_channel channel, + size_t* out_stride); + +LIBHEIF_API +uint8_t* heif_image_get_plane2(heif_image*, + heif_channel channel, + size_t* out_stride); + + +typedef struct heif_scaling_options heif_scaling_options; + +// Currently, heif_scaling_options is not defined yet. Pass a NULL pointer. +LIBHEIF_API +heif_error heif_image_scale_image(const heif_image* input, + heif_image** output, + int width, int height, + const heif_scaling_options* options); + +// Extends the image size to match the given size by extending the right and bottom borders. +// The border areas are filled with zero. +LIBHEIF_API +heif_error heif_image_extend_to_size_fill_with_zero(heif_image* image, + uint32_t width, uint32_t height); + +// Fills the image decoding warnings into the provided 'out_warnings' array. +// The size of the array has to be provided in max_output_buffer_entries. +// If max_output_buffer_entries==0, the number of decoder warnings is returned. +// The function fills the warnings into the provided buffer, starting with 'first_warning_idx'. +// It returns the number of warnings filled into the buffer. +// Note: you can iterate through all warnings by using 'max_output_buffer_entries=1' and iterate 'first_warning_idx'. +LIBHEIF_API +int heif_image_get_decoding_warnings(heif_image* image, + int first_warning_idx, + heif_error* out_warnings, + int max_output_buffer_entries); + +// This function is only for decoder plugin implementors. +LIBHEIF_API +void heif_image_add_decoding_warning(heif_image* image, + heif_error err); + +// Release heif_image. +LIBHEIF_API +void heif_image_release(const heif_image*); + +LIBHEIF_API +void heif_image_get_pixel_aspect_ratio(const heif_image*, uint32_t* aspect_h, uint32_t* aspect_v); + +LIBHEIF_API +void heif_image_set_pixel_aspect_ratio(heif_image*, uint32_t aspect_h, uint32_t aspect_v); + +LIBHEIF_API +void heif_image_handle_set_pixel_aspect_ratio(heif_image_handle*, uint32_t aspect_h, uint32_t aspect_v); + +// --- heif_image allocation + +/** + * Create a new image of the specified resolution and colorspace. + * + *

This does not allocate memory for the image data. Use {@link heif_image_add_plane} to + * add the corresponding planes to match the specified {@code colorspace} and {@code chroma}. + * + * @param width the width of the image in pixels + * @param height the height of the image in pixels + * @param colorspace the colorspace of the image + * @param chroma the chroma of the image + * @param out_image pointer to pointer of the resulting image + * @return whether the creation succeeded or there was an error +*/ +LIBHEIF_API +heif_error heif_image_create(int width, int height, + heif_colorspace colorspace, + heif_chroma chroma, + heif_image** out_image); + +/** + * Add an image plane to the image. + * + *

The image plane needs to match the colorspace and chroma of the image. Note + * that this does not need to be a single "planar" format - interleaved pixel channels + * can also be used if the chroma is interleaved. + * + *

The indicated bit_depth corresponds to the bit depth per channel. For example, + * with an interleaved format like RRGGBB where each color is represented by 10 bits, + * the {@code bit_depth} would be {@code 10} rather than {@code 30}. + * + *

For backward compatibility, one can also specify 24bits for RGB and 32bits for RGBA, + * instead of the preferred 8 bits. However, this use is deprecated. + * + * @param image the parent image to add the channel plane to + * @param channel the channel of the plane to add + * @param width the width of the plane + * @param height the height of the plane + * @param bit_depth the bit depth per color channel + * @return whether the addition succeeded or there was an error + * + * @note The width and height are usually the same as the parent image, but can be + * less for subsampling. + * + * @note The specified width can differ from the row stride of the resulting image plane. + * Always use the result of {@link heif_image_get_plane} or {@link heif_image_get_plane_readonly} + * to determine row stride. + */ +LIBHEIF_API +heif_error heif_image_add_plane(heif_image* image, + heif_channel channel, + int width, int height, int bit_depth); + +/* + * The security limits should preferably be the limits from a heif_context. + * The memory allocated will then be registered in the memory budget of that context. + */ +LIBHEIF_API +heif_error heif_image_add_plane_safe(heif_image* image, + heif_channel channel, + int width, int height, int bit_depth, + const heif_security_limits* limits); + +// Signal that the image is premultiplied by the alpha pixel values. +LIBHEIF_API +void heif_image_set_premultiplied_alpha(heif_image* image, + int is_premultiplied_alpha); + +LIBHEIF_API +int heif_image_is_premultiplied_alpha(heif_image* image); + +// This function extends the padding of the image so that it has at least the given physical size. +// The padding border is filled with the pixels along the right/bottom border. +// This function may be useful if you want to process the image, but have some external padding requirements. +// The image size will not be modified if it is already larger/equal than the given physical size. +// I.e. you cannot assume that after calling this function, the stride will be equal to min_physical_width. +LIBHEIF_API +heif_error heif_image_extend_padding_to_size(heif_image* image, + int min_physical_width, int min_physical_height); + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_image_handle.cc libheif-1.23.4/libheif/api/libheif/heif_image_handle.cc --- libheif-1.19.8/libheif/api/libheif/heif_image_handle.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_image_handle.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,354 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_image_handle.h" +#include "api_structs.h" +#include "box.h" +#if WITH_UNCOMPRESSED_CODEC +#include "codecs/uncompressed/unc_boxes.h" +#endif +#include +#include +#include +#include +#include + + +void heif_image_handle_release(const heif_image_handle* handle) +{ + delete handle; +} + + +int heif_image_handle_is_primary_image(const heif_image_handle* handle) +{ + return handle->image->is_primary(); +} + + +heif_item_id heif_image_handle_get_item_id(const heif_image_handle* handle) +{ + return handle->image->get_id(); +} + + +int heif_image_handle_get_width(const heif_image_handle* handle) +{ + if (handle && handle->image) { + uint32_t w = handle->image->get_width(); + if (w > INT_MAX) { + return 0; + } + else { + return static_cast(w); + } + } + else { + return 0; + } +} + + +int heif_image_handle_get_height(const heif_image_handle* handle) +{ + if (handle && handle->image) { + uint32_t h = handle->image->get_height(); + if (h > INT_MAX) { + return 0; + } + else { + return static_cast(h); + } + } + else { + return 0; + } +} + + +int heif_image_handle_has_alpha_channel(const heif_image_handle* handle) +{ + // TODO: for now, also scan the grid tiles for alpha information (issue #708), but depending about + // how the discussion about this structure goes forward, we might remove this again. + + return handle->context->has_alpha(handle->image->get_id()); // handle case in issue #708 + //return handle->image->get_alpha_channel() != nullptr; // old alpha check that fails on alpha in grid tiles +} + + +int heif_image_handle_is_premultiplied_alpha(const heif_image_handle* handle) +{ + // TODO: what about images that have the alpha in the grid tiles (issue #708) ? + return handle->image->is_premultiplied_alpha(); +} + + +int heif_image_handle_get_luma_bits_per_pixel(const heif_image_handle* handle) +{ + return handle->image->get_luma_bits_per_pixel(); +} + + +int heif_image_handle_get_chroma_bits_per_pixel(const heif_image_handle* handle) +{ + return handle->image->get_chroma_bits_per_pixel(); +} + + +heif_error heif_image_handle_get_preferred_decoding_colorspace(const heif_image_handle* image_handle, + heif_colorspace* out_colorspace, + heif_chroma* out_chroma) +{ + Error err = image_handle->image->get_coded_image_colorspace(out_colorspace, out_chroma); + if (err) { + return err.error_struct(image_handle->image.get()); + } + + return heif_error_success; +} + + +int heif_image_handle_get_ispe_width(const heif_image_handle* handle) +{ + if (handle && handle->image) { + return handle->image->get_ispe_width(); + } + else { + return 0; + } +} + + +int heif_image_handle_get_ispe_height(const heif_image_handle* handle) +{ + if (handle && handle->image) { + return handle->image->get_ispe_height(); + } + else { + return 0; + } +} + + +int heif_image_handle_get_pixel_aspect_ratio(const heif_image_handle* handle, uint32_t* aspect_h, uint32_t* aspect_v) +{ + auto pasp = handle->image->get_property(); + if (pasp) { + *aspect_h = pasp->hSpacing; + *aspect_v = pasp->vSpacing; + return 1; + } + else { + *aspect_h = 1; + *aspect_v = 1; + return 0; + } +} + + +heif_context* heif_image_handle_get_context(const heif_image_handle* handle) +{ + auto ctx = new heif_context(); + ctx->context = handle->context; + return ctx; +} + + +const char* heif_image_handle_get_gimi_content_id(const heif_image_handle* handle) +{ + if (!handle->image->has_gimi_sample_content_id()) { + return nullptr; + } + + std::string id = handle->image->get_gimi_sample_content_id(); + char* idstring = new char[id.size() + 1]; + strcpy(idstring, id.c_str()); + return idstring; +} + + +void heif_image_handle_set_gimi_content_id(heif_image_handle* handle, const char* content_id) +{ + auto gimi_box = std::make_shared(); + gimi_box->set_content_id(content_id); + handle->context->add_property(handle->image->get_id(), gimi_box, false); + handle->image->set_gimi_sample_content_id(content_id); +} + + +#if WITH_UNCOMPRESSED_CODEC +static std::shared_ptr get_effective_cmpd(const heif_image_handle* handle) +{ + // Try explicit cmpd property first. + auto cmpd = handle->image->get_property(); + if (cmpd) { + return cmpd; + } + + // For profile-based uncC (version 1), the cmpd is synthesized inside the uncC box. + auto uncC = handle->image->get_property(); + if (uncC) { + fill_uncC_and_cmpd_from_profile(uncC, cmpd); + } + return cmpd; +} + +#endif + + +uint32_t heif_image_handle_get_number_of_cmpd_components(const heif_image_handle* handle) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle) { + return 0; + } + auto cmpd = get_effective_cmpd(handle); + if (!cmpd) { + return 0; + } + return static_cast(cmpd->get_components().size()); +#else + return 0; +#endif +} + + +uint16_t heif_image_handle_get_cmpd_component_type(const heif_image_handle* handle, uint32_t component_idx) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle) { + return 0; + } + auto cmpd = get_effective_cmpd(handle); + if (!cmpd) { + return 0; + } + const auto& components = cmpd->get_components(); + if (component_idx >= components.size()) { + return 0; + } + return components[component_idx].component_type; +#else + return 0; +#endif +} + + +const char* heif_image_handle_get_cmpd_component_type_uri(const heif_image_handle* handle, uint32_t component_idx) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle) { + return nullptr; + } + auto cmpd = get_effective_cmpd(handle); + if (!cmpd) { + return nullptr; + } + const auto& components = cmpd->get_components(); + if (component_idx >= components.size()) { + return nullptr; + } + const auto& uri = components[component_idx].component_type_uri; + if (uri.empty()) { + return nullptr; + } + char* uristring = new char[uri.size() + 1]; + strcpy(uristring, uri.c_str()); + return uristring; +#else + return nullptr; +#endif +} + + +// heif_image_handle_get_number_of_components, _get_used_component_ids, +// _get_component_type, _get_component_bits_per_pixel and _get_component_datatype +// live in heif_components.cc. + + +int heif_image_handle_has_gimi_component_content_ids(const heif_image_handle* handle) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle) { + return 0; + } + auto box = handle->image->get_property(); + if (!box) { + return 0; + } + return static_cast(box->get_content_ids().size()); +#else + return 0; +#endif +} + + +const char* heif_image_handle_get_gimi_component_content_id(const heif_image_handle* handle, uint32_t component_idx) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle) { + return nullptr; + } + auto box = handle->image->get_property(); + if (!box) { + return nullptr; + } + const auto& ids = box->get_content_ids(); + if (component_idx >= ids.size()) { + return nullptr; + } + char* idstring = new char[ids[component_idx].size() + 1]; + strcpy(idstring, ids[component_idx].c_str()); + return idstring; +#else + return nullptr; +#endif +} + + +void heif_image_handle_set_gimi_component_content_id(heif_image_handle* handle, + uint32_t component_idx, + const char* content_id) +{ +#if WITH_UNCOMPRESSED_CODEC + if (!handle || !content_id) { + return; + } + + auto box = handle->image->get_property(); + if (!box) { + // Create a new box and add it as property. + auto new_box = std::make_shared(); + std::vector ids(component_idx + 1); + ids[component_idx] = content_id; + new_box->set_content_ids(ids); + handle->context->add_property(handle->image->get_id(), new_box, false); + } + else { + // Mutate the existing box in-place. + auto ids = box->get_content_ids(); + if (component_idx >= ids.size()) { + ids.resize(component_idx + 1); + } + ids[component_idx] = content_id; + box->set_content_ids(ids); + } +#endif +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_image_handle.h libheif-1.23.4/libheif/api/libheif/heif_image_handle.h --- libheif-1.19.8/libheif/api/libheif/heif_image_handle.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_image_handle.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,196 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_IMAGE_HANDLE_H +#define LIBHEIF_HEIF_IMAGE_HANDLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include + + +// ========================= heif_image_handle ========================= + +// An heif_image_handle is a handle to a logical image in the HEIF file. +// To get the actual pixel data, you have to decode the handle to an heif_image. +// An heif_image_handle also gives you access to the thumbnails and Exif data +// associated with an image. + +// Once you obtained an heif_image_handle, you can already release the heif_context, +// since it is internally ref-counted. + +// Release image handle. +LIBHEIF_API +void heif_image_handle_release(const heif_image_handle*); + +// Check whether the given image_handle is the primary image of the file. +LIBHEIF_API +int heif_image_handle_is_primary_image(const heif_image_handle* handle); + +LIBHEIF_API +heif_item_id heif_image_handle_get_item_id(const heif_image_handle* handle); + +/** Get the image width. + * + * This is the *signalled* size declared for the item, available without decoding. It is not + * guaranteed to match the size of the image you get back from heif_decode_image(): do not use + * this value to allocate or bound access to a decoded pixel buffer. Once you have decoded the + * image, use heif_image_get_width(heif_image*, heif_channel) (or heif_image_get_primary_width()) + * for the actual plane size, and size any copy loop from that together with the stride returned + * by heif_image_get_plane_readonly2() / heif_image_get_plane2() -- never from this handle value + * or from width * bytes_per_pixel. + * + * If 'handle' is invalid (NULL) or if the image size exceeds the range of `int`, 0 is returned. + */ +LIBHEIF_API +int heif_image_handle_get_width(const heif_image_handle* handle); + +/** Get the image height. + * + * This is the *signalled* size declared for the item, available without decoding. It is not + * guaranteed to match the size of the image you get back from heif_decode_image(): do not use + * this value to allocate or bound access to a decoded pixel buffer. Once you have decoded the + * image, use heif_image_get_height(heif_image*, heif_channel) (or heif_image_get_primary_height()) + * for the actual plane size, and size any copy loop from that together with the stride returned + * by heif_image_get_plane_readonly2() / heif_image_get_plane2() -- never from this handle value + * or from height * stride. + * + * If 'handle' is invalid (NULL) or if the image size exceeds the range of `int`, 0 is returned. + */ +LIBHEIF_API +int heif_image_handle_get_height(const heif_image_handle* handle); + +LIBHEIF_API +int heif_image_handle_has_alpha_channel(const heif_image_handle*); + +LIBHEIF_API +int heif_image_handle_is_premultiplied_alpha(const heif_image_handle*); + +// Returns -1 on error, e.g. if this information is not present in the image. +// Only defined for images coded in the YCbCr or monochrome colorspace. +LIBHEIF_API +int heif_image_handle_get_luma_bits_per_pixel(const heif_image_handle*); + +// Returns -1 on error, e.g. if this information is not present in the image. +// Only defined for images coded in the YCbCr colorspace. +LIBHEIF_API +int heif_image_handle_get_chroma_bits_per_pixel(const heif_image_handle*); + +// Return the colorspace that libheif proposes to use for decoding. +// Usually, these will be either YCbCr or Monochrome, but it may also propose RGB for images +// encoded with matrix_coefficients=0 or for images coded natively in RGB. +// It may also return *_undefined if the file misses relevant information to determine this without decoding. +// These are only proposed values that avoid colorspace conversions as much as possible. +// You can still request the output in your preferred colorspace, but this may involve an internal conversion. +// +// Note on matrix_coefficients=0 (H.273 identity, i.e. RGB carried in YCbCr planes): +// for such images this function proposes RGB. However, heif_decode_image() called with +// heif_colorspace_undefined currently returns the image still tagged as YCbCr (with +// matrix_coefficients=0). That tagging is self-consistent. Converting it to RGB applies +// the identity mapping and yields correct colors, so request heif_colorspace_RGB +// explicitly if you need RGB output. This exact behavior is not specified and may change. +// A later version may return RGB directly when decoding with an undefined colorspace. +// See heif_decode_image(). +LIBHEIF_API +heif_error heif_image_handle_get_preferred_decoding_colorspace(const heif_image_handle* image_handle, + heif_colorspace* out_colorspace, + heif_chroma* out_chroma); + +// Get the image width from the 'ispe' box. This is the original image size without +// any transformations applied to it. Do not use this unless you know exactly what +// you are doing. +LIBHEIF_API +int heif_image_handle_get_ispe_width(const heif_image_handle* handle); + +LIBHEIF_API +int heif_image_handle_get_ispe_height(const heif_image_handle* handle); + +// Returns whether the image has 'pixel aspect ratio information' information. If 0 is returned, the output is filled with the 1:1 default. +LIBHEIF_API +int heif_image_handle_get_pixel_aspect_ratio(const heif_image_handle*, uint32_t* aspect_h, uint32_t* aspect_v); + + +// This gets the context associated with the image handle. +// Note that you have to release the returned context with heif_context_free() in any case. +// +// This means: when you have several image-handles that originate from the same file and you get the +// context of each of them, the returned pointer may be different even though it refers to the same +// logical context. You have to call heif_context_free() on all those context pointers. +// After you freed a context pointer, you can still use the context through a different pointer that you +// might have acquired from elsewhere. +LIBHEIF_API +heif_context* heif_image_handle_get_context(const heif_image_handle* handle); + +LIBHEIF_API +const char* heif_image_handle_get_gimi_content_id(const heif_image_handle* handle); + +LIBHEIF_API +void heif_image_handle_set_gimi_content_id(heif_image_handle* handle, const char* content_id); + + +// --- cmpd component queries + +// Returns the number of components in the cmpd box, or 0 if no cmpd property exists. +LIBHEIF_API +uint32_t heif_image_handle_get_number_of_cmpd_components(const heif_image_handle*); + +// Returns the component_type for the given cmpd component index. +// Returns 0 if out of range or no cmpd property. +LIBHEIF_API +uint16_t heif_image_handle_get_cmpd_component_type(const heif_image_handle*, uint32_t component_idx); + +// Returns the component_type_uri for the given cmpd component index (component_type >= 0x8000). +// Returns NULL if the component does not have a URI. +// The returned string must be freed with heif_string_release(). +LIBHEIF_API +const char* heif_image_handle_get_cmpd_component_type_uri(const heif_image_handle*, uint32_t component_idx); + + +// --- GIMI component content IDs (handle-level) + +// Returns non-zero (count of content IDs) if an ItemComponentContentIDProperty is set, 0 otherwise. +LIBHEIF_API +int heif_image_handle_has_gimi_component_content_ids(const heif_image_handle*); + +// Returns the GIMI component content ID for the given component index. +// Returns NULL if no ItemComponentContentIDProperty is set or index is out of range. +// The returned string must be freed with heif_string_release(). +LIBHEIF_API +const char* heif_image_handle_get_gimi_component_content_id(const heif_image_handle*, uint32_t component_idx); + +// Set a GIMI component content ID for a single component. +// If an ItemComponentContentIDProperty does not yet exist, one will be created. +// The content IDs array is resized as needed (new entries default to empty). +LIBHEIF_API +void heif_image_handle_set_gimi_component_content_id(heif_image_handle*, + uint32_t component_idx, + const char* content_id); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_items.cc libheif-1.23.4/libheif/api/libheif/heif_items.cc --- libheif-1.19.8/libheif/api/libheif/heif_items.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_items.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,18 +27,18 @@ #include #include #include - +#include // ------------------------- reading ------------------------- -int heif_context_get_number_of_items(const struct heif_context* ctx) +int heif_context_get_number_of_items(const heif_context* ctx) { return (int) ctx->context->get_heif_file()->get_number_of_items(); } -int heif_context_get_list_of_item_IDs(const struct heif_context* ctx, +int heif_context_get_list_of_item_IDs(const heif_context* ctx, heif_item_id* ID_array, int count) { @@ -59,13 +59,13 @@ } -uint32_t heif_item_get_item_type(const struct heif_context* ctx, heif_item_id item_id) +uint32_t heif_item_get_item_type(const heif_context* ctx, heif_item_id item_id) { return ctx->context->get_heif_file()->get_item_type_4cc(item_id); } -int heif_item_is_item_hidden(const struct heif_context* ctx, heif_item_id item_id) +int heif_item_is_item_hidden(const heif_context* ctx, heif_item_id item_id) { auto infe = ctx->context->get_heif_file()->get_infe_box(item_id); if (infe == nullptr) { @@ -77,7 +77,7 @@ } -const char* heif_item_get_mime_item_content_type(const struct heif_context* ctx, heif_item_id item_id) +const char* heif_item_get_mime_item_content_type(const heif_context* ctx, heif_item_id item_id) { auto infe = ctx->context->get_heif_file()->get_infe_box(item_id); if (!infe) { return nullptr; } @@ -89,7 +89,8 @@ return infe->get_content_type().c_str(); } -const char* heif_item_get_mime_item_content_encoding(const struct heif_context* ctx, heif_item_id item_id) + +const char* heif_item_get_mime_item_content_encoding(const heif_context* ctx, heif_item_id item_id) { auto infe = ctx->context->get_heif_file()->get_infe_box(item_id); if (!infe) { return nullptr; } @@ -102,7 +103,7 @@ } -const char* heif_item_get_uri_item_uri_type(const struct heif_context* ctx, heif_item_id item_id) +const char* heif_item_get_uri_item_uri_type(const heif_context* ctx, heif_item_id item_id) { auto infe = ctx->context->get_heif_file()->get_infe_box(item_id); if (!infe) { return nullptr; } @@ -114,7 +115,8 @@ return infe->get_item_uri_type().c_str(); } -const char* heif_item_get_item_name(const struct heif_context* ctx, heif_item_id item_id) + +const char* heif_item_get_item_name(const heif_context* ctx, heif_item_id item_id) { auto infe = ctx->context->get_heif_file()->get_infe_box(item_id); if (!infe) { return nullptr; } @@ -123,40 +125,56 @@ } -struct heif_error heif_item_get_item_data(const struct heif_context* ctx, - heif_item_id item_id, - heif_metadata_compression* out_compression_format, - uint8_t** out_data, size_t* out_data_size) +heif_error heif_item_set_item_name(heif_context* ctx, + heif_item_id item, + const char* item_name) +{ + auto infe = ctx->context->get_heif_file()->get_infe_box(item); + if (!infe) { + return heif_error{heif_error_Input_does_not_exist, heif_suberror_Nonexisting_item_referenced, "Item does not exist"}; + } + + infe->set_item_name(item_name); + + return heif_error_success; +} + + +heif_error heif_item_get_item_data(const heif_context* ctx, + heif_item_id item_id, + heif_metadata_compression* out_compression_format, + uint8_t** out_data, size_t* out_data_size) { if (out_data && !out_data_size) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "cannot return data with out_data_size==NULL"}; + return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "cannot return data with out_data_size==NULL"}; } - std::vector data; - Error err = ctx->context->get_heif_file()->get_item_data(item_id, &data, out_compression_format); - if (err) { - *out_data_size = 0; + auto dataResult = ctx->context->get_heif_file()->get_item_data(item_id, out_compression_format); + if (!dataResult) { + if (out_data_size) { + *out_data_size = 0; + } if (out_data) { - *out_data = 0; + *out_data = nullptr; } - return err.error_struct(ctx->context.get()); + return dataResult.error_struct(ctx->context.get()); } if (out_data_size) { - *out_data_size = data.size(); + *out_data_size = dataResult->size(); } if (out_data) { - *out_data = new uint8_t[data.size()]; - memcpy(*out_data, data.data(), data.size()); + *out_data = new uint8_t[dataResult->size()]; + memcpy(*out_data, dataResult->data(), dataResult->size()); } return heif_error_success; } -void heif_release_item_data(const struct heif_context* ctx, uint8_t** item_data) +void heif_release_item_data(const heif_context* ctx, uint8_t** item_data) { (void) ctx; @@ -167,8 +185,51 @@ } +heif_error heif_item_get_property_extended_language(const heif_context* context, + heif_item_id itemId, + char** out_language) +{ + if (!out_language || !context) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + } + + auto elng = context->context->find_property(itemId); + if (!elng) { + return elng.error_struct(context->context.get()); + } + + std::string lang = (*elng)->get_extended_language(); + *out_language = new char[lang.length() + 1]; + strcpy(*out_language, lang.c_str()); + + return heif_error_success; +} -size_t heif_context_get_item_references(const struct heif_context* ctx, + +heif_error heif_item_set_property_extended_language(heif_context* context, + heif_item_id item_id, + const char* language, heif_property_id* out_optional_propertyId) +{ + if (!context || !language) { + return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + } + + Result property_id_result = context->context->add_text_property(item_id, + language); + + if (auto err = property_id_result.error()) { + return err.error_struct(context->context.get()); + } + + if (out_optional_propertyId) { + *out_optional_propertyId = *property_id_result; + } + + return heif_error_success; +} + + +size_t heif_context_get_item_references(const heif_context* ctx, heif_item_id from_item_id, int index, uint32_t* out_reference_type_4cc, @@ -204,7 +265,7 @@ } -void heif_release_item_references(const struct heif_context* ctx, heif_item_id** references) +void heif_release_item_references(const heif_context* ctx, heif_item_id** references) { (void) ctx; @@ -215,117 +276,149 @@ } +heif_error heif_context_add_item_reference(heif_context* ctx, + uint32_t reference_type, + heif_item_id from_item, + heif_item_id to_item) +{ + ctx->context->get_heif_file()->add_iref_reference(from_item, + reference_type, {to_item}); + + return heif_error_success; +} + +heif_error heif_context_add_item_references(heif_context* ctx, + uint32_t reference_type, + heif_item_id from_item, + const heif_item_id* to_item, + int num_to_items) +{ + std::vector to_refs(to_item, to_item + num_to_items); + + ctx->context->get_heif_file()->add_iref_reference(from_item, + reference_type, to_refs); + + return heif_error_success; +} + + // ------------------------- writing ------------------------- -struct heif_error heif_context_add_item(struct heif_context* ctx, - const char* item_type, - const void* data, int size, - heif_item_id* out_item_id) +// Check the (data, size) pair that all item writers take. 'size' is a public 'int' that +// is converted to size_t further down: a negative value would turn into a huge allocation +// (std::length_error through the C API), and a NULL buffer with a non-zero size would be +// dereferenced. +static heif_error check_item_data_arguments(const void* data, int size) { - if (item_type == nullptr || strlen(item_type) != 4) { + if (size < 0) { return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, - "called heif_context_add_item() with invalid 'item_type'."}; + "item data size must not be negative"}; } - Result result = ctx->context->get_heif_file()->add_infe(fourcc(item_type), (const uint8_t*) data, size); - - if (result && out_item_id) { - *out_item_id = result.value; - return heif_error_success; - } - else { - return result.error.error_struct(ctx->context.get()); + if (size > 0 && data == nullptr) { + return heif_error_null_pointer_argument; } + + return heif_error_success; } -struct heif_error heif_context_add_mime_item(struct heif_context* ctx, - const char* content_type, - heif_metadata_compression content_encoding, - const void* data, int size, - heif_item_id* out_item_id) -{ - Result result = ctx->context->get_heif_file()->add_infe_mime(content_type, content_encoding, (const uint8_t*) data, size); - if (result && out_item_id) { - *out_item_id = result.value; - return heif_error_success; +// Common tail of all item writers: report the error, or store the new item ID. +// A NULL out_item_id is allowed (the item is added, but its ID is not reported). +static heif_error return_new_item_id(heif_context* ctx, Result result, heif_item_id* out_item_id) +{ + if (!result) { + return result.error_struct(ctx->context.get()); } - else { - return result.error.error_struct(ctx->context.get()); + + if (out_item_id) { + *out_item_id = *result; } + + return heif_error_success; } -struct heif_error heif_context_add_precompressed_mime_item(struct heif_context* ctx, - const char* content_type, - const char* content_encoding, - const void* data, int size, - heif_item_id* out_item_id) +heif_error heif_context_add_item(heif_context* ctx, + const char* item_type, + const void* data, int size, + heif_item_id* out_item_id) { - Result result = ctx->context->get_heif_file()->add_precompressed_infe_mime(content_type, content_encoding, (const uint8_t*) data, size); - - if (result && out_item_id) { - *out_item_id = result.value; - return heif_error_success; + if (item_type == nullptr || strlen(item_type) != 4) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "called heif_context_add_item() with invalid 'item_type'." + }; } - else { - return result.error.error_struct(ctx->context.get()); + + if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) { + return err; } + + return exception_guard([&]() -> heif_error { + Result result = ctx->context->get_heif_file()->add_infe(fourcc(item_type), (const uint8_t*) data, size); + return return_new_item_id(ctx, std::move(result), out_item_id); + }); } -struct heif_error heif_context_add_uri_item(struct heif_context* ctx, - const char* item_uri_type, - const void* data, int size, - heif_item_id* out_item_id) +heif_error heif_context_add_mime_item(heif_context* ctx, + const char* content_type, + heif_metadata_compression content_encoding, + const void* data, int size, + heif_item_id* out_item_id) { - Result result = ctx->context->get_heif_file()->add_infe_uri(item_uri_type, (const uint8_t*) data, size); - - if (result && out_item_id) { - *out_item_id = result.value; - return heif_error_success; - } - else { - return result.error.error_struct(ctx->context.get()); + if (content_type == nullptr) { + return heif_error_null_pointer_argument; } -} - -struct heif_error heif_context_add_item_reference(struct heif_context* ctx, - uint32_t reference_type, - heif_item_id from_item, - heif_item_id to_item) -{ - ctx->context->get_heif_file()->add_iref_reference(from_item, - reference_type, {to_item}); + if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) { + return err; + } - return heif_error_success; + return exception_guard([&]() -> heif_error { + Result result = ctx->context->get_heif_file()->add_infe_mime(content_type, content_encoding, (const uint8_t*) data, size); + return return_new_item_id(ctx, std::move(result), out_item_id); + }); } -struct heif_error heif_context_add_item_references(struct heif_context* ctx, - uint32_t reference_type, - heif_item_id from_item, - const heif_item_id* to_item, - int num_to_items) + +heif_error heif_context_add_precompressed_mime_item(heif_context* ctx, + const char* content_type, + const char* content_encoding, + const void* data, int size, + heif_item_id* out_item_id) { - std::vector to_refs(to_item, to_item + num_to_items); + if (content_type == nullptr || content_encoding == nullptr) { + return heif_error_null_pointer_argument; + } - ctx->context->get_heif_file()->add_iref_reference(from_item, - reference_type, to_refs); + if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) { + return err; + } - return heif_error_success; + return exception_guard([&]() -> heif_error { + Result result = ctx->context->get_heif_file()->add_precompressed_infe_mime(content_type, content_encoding, (const uint8_t*) data, size); + return return_new_item_id(ctx, std::move(result), out_item_id); + }); } -struct heif_error heif_item_set_item_name(struct heif_context* ctx, - heif_item_id item, - const char* item_name) +heif_error heif_context_add_uri_item(heif_context* ctx, + const char* item_uri_type, + const void* data, int size, + heif_item_id* out_item_id) { - auto infe = ctx->context->get_heif_file()->get_infe_box(item); - if (!infe) { - return heif_error{heif_error_Input_does_not_exist, heif_suberror_Nonexisting_item_referenced, "Item does not exist"}; + if (item_uri_type == nullptr) { + return heif_error_null_pointer_argument; } - infe->set_item_name(item_name); + if (heif_error err = check_item_data_arguments(data, size); err.code != heif_error_Ok) { + return err; + } - return heif_error_success; + return exception_guard([&]() -> heif_error { + Result result = ctx->context->get_heif_file()->add_infe_uri(item_uri_type, (const uint8_t*) data, size); + return return_new_item_id(ctx, std::move(result), out_item_id); + }); } diff -Nru libheif-1.19.8/libheif/api/libheif/heif_items.h libheif-1.23.4/libheif/api/libheif/heif_items.h --- libheif-1.19.8/libheif/api/libheif/heif_items.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_items.h 2026-09-06 14:45:17.000000000 +0000 @@ -21,7 +21,9 @@ #ifndef LIBHEIF_HEIF_ITEMS_H #define LIBHEIF_HEIF_ITEMS_H -#include "libheif/heif.h" +#include "heif_library.h" +#include "heif_error.h" +#include "heif_metadata.h" #ifdef __cplusplus extern "C" { @@ -38,7 +40,7 @@ * @return the number of items */ LIBHEIF_API -int heif_context_get_number_of_items(const struct heif_context* ctx); +int heif_context_get_number_of_items(const heif_context* ctx); /** * Get the item identifiers. @@ -51,7 +53,7 @@ * @return the total number of IDs filled into the array, which may be less than {@code count}. */ LIBHEIF_API -int heif_context_get_list_of_item_IDs(const struct heif_context* ctx, +int heif_context_get_list_of_item_IDs(const heif_context* ctx, heif_item_id* ID_array, int count); @@ -59,7 +61,7 @@ * Gets the item type. * * Usually, this is a four character code (e.g. `mime` or `uri `), but it can theoretically be - * any 4-byte number. Thus, the type is returned as an integer. You can use {@link heif_fourcc} to map + * any 4-byte number. Thus, the type is returned as an integer. You can use #heif_fourcc to map * between the two representations. * * @param ctx the file context @@ -67,13 +69,19 @@ * @return the item type */ LIBHEIF_API -uint32_t heif_item_get_item_type(const struct heif_context* ctx, heif_item_id item_id); +uint32_t heif_item_get_item_type(const heif_context* ctx, heif_item_id item_id); #define heif_item_type_mime heif_fourcc('m','i','m','e') #define heif_item_type_uri heif_fourcc('u','r','i',' ') +#define heif_item_type_exif heif_fourcc('E','x','i','f') +#define heif_item_type_grid heif_fourcc('g','r','i','d') +#define heif_item_type_hvc1 heif_fourcc('h','v','c','1') +#define heif_item_type_unci heif_fourcc('u','n','c','i') +#define heif_item_type_av01 heif_fourcc('a','v','0','1') +#define heif_item_type_avc1 heif_fourcc('a','v','c','1') LIBHEIF_API -int heif_item_is_item_hidden(const struct heif_context* ctx, heif_item_id item_id); +int heif_item_is_item_hidden(const heif_context* ctx, heif_item_id item_id); /** @@ -87,7 +95,7 @@ * @return the item content_type */ LIBHEIF_API -const char* heif_item_get_mime_item_content_type(const struct heif_context* ctx, heif_item_id item_id); +const char* heif_item_get_mime_item_content_type(const heif_context* ctx, heif_item_id item_id); /** * Gets the content_encoding for a MIME item. @@ -102,7 +110,7 @@ * @return the item content_type */ LIBHEIF_API -const char* heif_item_get_mime_item_content_encoding(const struct heif_context* ctx, heif_item_id item_id); +const char* heif_item_get_mime_item_content_encoding(const heif_context* ctx, heif_item_id item_id); /** * Gets the item_uri_type for an item. @@ -115,15 +123,15 @@ * @return the item item_uri_type */ LIBHEIF_API -const char* heif_item_get_uri_item_uri_type(const struct heif_context* ctx, heif_item_id item_id); +const char* heif_item_get_uri_item_uri_type(const heif_context* ctx, heif_item_id item_id); LIBHEIF_API -const char* heif_item_get_item_name(const struct heif_context* ctx, heif_item_id item_id); +const char* heif_item_get_item_name(const heif_context* ctx, heif_item_id item_id); LIBHEIF_API -struct heif_error heif_item_set_item_name(struct heif_context* ctx, - heif_item_id item, - const char* item_name); +heif_error heif_item_set_item_name(heif_context* ctx, + heif_item_id item, + const char* item_name); /** @@ -149,10 +157,10 @@ * @return whether the call succeeded, or there was an error */ LIBHEIF_API -struct heif_error heif_item_get_item_data(const struct heif_context* ctx, - heif_item_id item_id, - enum heif_metadata_compression* out_compression_format, - uint8_t** out_data, size_t* out_data_size); +heif_error heif_item_get_item_data(const heif_context* ctx, + heif_item_id item_id, + enum heif_metadata_compression* out_compression_format, + uint8_t** out_data, size_t* out_data_size); /** * Free the item data. @@ -164,9 +172,30 @@ * @param item_data the data to free */ LIBHEIF_API -void heif_release_item_data(const struct heif_context* ctx, uint8_t** item_data); +void heif_release_item_data(const heif_context* ctx, uint8_t** item_data); +// ------------------------- item language ------------------------- + +/** + * Get the extended language associated with the item. + * The item is usually a text item. + * + * @param context the heif file context containg the item. + * @param itemId the identifier for the item + * @param out_language output parameter with the item's language. Free with heif_string_release(). + * @return heif_error_ok on success, or an error value indicating the problem + */ +LIBHEIF_API +heif_error heif_item_get_property_extended_language(const heif_context* context, + heif_item_id itemId, + char** out_language); + +LIBHEIF_API +heif_error heif_item_set_property_extended_language(heif_context* context, + heif_item_id item_id, + const char* language, heif_property_id* out_optional_propertyId); + // ------------------------- item references ------------------------- /** @@ -180,55 +209,117 @@ * @return the number of items that reference the given item. Returns 0 if the index exceeds the number of references. */ LIBHEIF_API -size_t heif_context_get_item_references(const struct heif_context* ctx, +size_t heif_context_get_item_references(const heif_context* ctx, heif_item_id from_item_id, int index, uint32_t* out_reference_type_4cc, heif_item_id** out_references_to); LIBHEIF_API -void heif_release_item_references(const struct heif_context* ctx, heif_item_id** references); +void heif_release_item_references(const heif_context* ctx, heif_item_id** references); LIBHEIF_API -struct heif_error heif_context_add_item_reference(struct heif_context* ctx, - uint32_t reference_type, - heif_item_id from_item, - heif_item_id to_item); +heif_error heif_context_add_item_reference(heif_context* ctx, + uint32_t reference_type, + heif_item_id from_item, + heif_item_id to_item); LIBHEIF_API -struct heif_error heif_context_add_item_references(struct heif_context* ctx, - uint32_t reference_type, - heif_item_id from_item, - const heif_item_id* to_item, - int num_to_items); +heif_error heif_context_add_item_references(heif_context* ctx, + uint32_t reference_type, + heif_item_id from_item, + const heif_item_id* to_item, + int num_to_items); // ------------------------- adding new items ------------------------- +/* + * All functions in this section add a new item to the context and return its ID in + * 'out_item_id'. The item ID is freshly allocated and does not collide with any item that + * already exists in the context, including items read from an input file. + * + * 'data' is copied into the context, the caller may release it afterwards. 'size' is the + * number of bytes in 'data'. A NULL 'data' is only allowed together with size 0. + * 'out_item_id' may be NULL, in which case the item is added but its ID is not reported. + * + * The new item is marked as hidden. Use heif_context_add_item_reference() to link it to an + * image (e.g. with a 'cdsc' reference for metadata that describes the image). + * + * Note that a file must contain at least one image or image sequence to be written; + * a context holding only items added with these functions cannot be written. + */ + +/** + * Add an item with an arbitrary four-character item type (e.g. "Exif"). + * + * @param ctx the file context + * @param item_type four-character item type + * @param data the item payload + * @param size number of bytes in 'data' + * @param out_item_id receives the ID of the new item (may be NULL) + */ +LIBHEIF_API +heif_error heif_context_add_item(heif_context* ctx, + const char* item_type, + const void* data, int size, + heif_item_id* out_item_id); + +/** + * Add a 'mime' item, optionally compressing the payload. + * + * @param ctx the file context + * @param content_type MIME content type of the payload (e.g. "application/rdf+xml") + * @param content_encoding compression to apply to the payload before storing it. + * Use heif_metadata_compression_method_supported() to check which methods this + * build of libheif can compress. An unsupported method returns an error and no + * item is added. + * @param data the (uncompressed) item payload + * @param size number of bytes in 'data' + * @param out_item_id receives the ID of the new item (may be NULL) + */ +LIBHEIF_API +heif_error heif_context_add_mime_item(heif_context* ctx, + const char* content_type, + enum heif_metadata_compression content_encoding, + const void* data, int size, + heif_item_id* out_item_id); + +/** + * Add a 'mime' item whose payload has already been compressed by the caller. + * + * @param ctx the file context + * @param content_type MIME content type of the (uncompressed) payload + * @param content_encoding the HTTP content-coding name that was applied to 'data', stored + * verbatim as the item's content_encoding. Use "" for uncompressed data. + * libheif itself can decode "deflate", "compress_zlib" and "br" (when built with + * zlib / Brotli support) and treats "identity" as uncompressed; items with any other + * content_encoding are kept, but their data can only be retrieved in compressed form + * with heif_item_get_item_data(). + * @param data the compressed item payload + * @param size number of bytes in 'data' + * @param out_item_id receives the ID of the new item (may be NULL) + */ +LIBHEIF_API +heif_error heif_context_add_precompressed_mime_item(heif_context* ctx, + const char* content_type, + const char* content_encoding, + const void* data, int size, + heif_item_id* out_item_id); + +/** + * Add a 'uri ' item. + * + * @param ctx the file context + * @param item_uri_type the URI that identifies the type of the item payload + * @param data the item payload + * @param size number of bytes in 'data' + * @param out_item_id receives the ID of the new item (may be NULL) + */ LIBHEIF_API -struct heif_error heif_context_add_item(struct heif_context* ctx, - const char* item_type, - const void* data, int size, - heif_item_id* out_item_id); - -LIBHEIF_API -struct heif_error heif_context_add_mime_item(struct heif_context* ctx, - const char* content_type, - enum heif_metadata_compression content_encoding, - const void* data, int size, - heif_item_id* out_item_id); - -LIBHEIF_API -struct heif_error heif_context_add_precompressed_mime_item(struct heif_context* ctx, - const char* content_type, - const char* content_encoding, - const void* data, int size, - heif_item_id* out_item_id); - -LIBHEIF_API -struct heif_error heif_context_add_uri_item(struct heif_context* ctx, - const char* item_uri_type, - const void* data, int size, - heif_item_id* out_item_id); +heif_error heif_context_add_uri_item(heif_context* ctx, + const char* item_uri_type, + const void* data, int size, + heif_item_id* out_item_id); #ifdef __cplusplus } diff -Nru libheif-1.19.8/libheif/api/libheif/heif_library.cc libheif-1.23.4/libheif/api/libheif/heif_library.cc --- libheif-1.19.8/libheif/api/libheif/heif_library.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_library.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,107 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_library.h" +#include "heif_plugin.h" +#include "api_structs.h" +#include "plugin_registry.h" + +#ifdef _WIN32 +// for _write +#include +#else + +#include + +#endif + +#include + + +static heif_error error_unsupported_plugin_version = {heif_error_Usage_error, + heif_suberror_Unsupported_plugin_version, + "Unsupported plugin version"}; + + +const char* heif_get_version() +{ + return (LIBHEIF_VERSION); +} + +uint32_t heif_get_version_number() +{ + return (LIBHEIF_NUMERIC_VERSION); +} + +int heif_get_version_number_major() +{ + return ((LIBHEIF_NUMERIC_VERSION) >> 24) & 0xFF; +} + +int heif_get_version_number_minor() +{ + return ((LIBHEIF_NUMERIC_VERSION) >> 16) & 0xFF; +} + +int heif_get_version_number_maintenance() +{ + return ((LIBHEIF_NUMERIC_VERSION) >> 8) & 0xFF; +} + + + +heif_error heif_register_decoder_plugin(const heif_decoder_plugin* decoder_plugin) +{ + if (!decoder_plugin) { + return heif_error_null_pointer_argument; + } + else if (decoder_plugin->plugin_api_version > heif_decoder_plugin_latest_version) { + return error_unsupported_plugin_version; + } + + register_decoder(decoder_plugin); + return heif_error_success; +} + +heif_error heif_register_encoder_plugin(const heif_encoder_plugin* encoder_plugin) +{ + if (!encoder_plugin) { + return heif_error_null_pointer_argument; + } + else if (encoder_plugin->plugin_api_version > heif_encoder_plugin_latest_version) { + return error_unsupported_plugin_version; + } + + register_encoder(encoder_plugin); + return heif_error_success; +} + + +void heif_string_release(const char* str) +{ + delete[] str; +} + + +// DEPRECATED +heif_error heif_register_decoder(heif_context* heif, const heif_decoder_plugin* decoder_plugin) +{ + return heif_register_decoder_plugin(decoder_plugin); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_library.h libheif-1.23.4/libheif/api/libheif/heif_library.h --- libheif-1.19.8/libheif/api/libheif/heif_library.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_library.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,211 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_LIBRARY_H +#define LIBHEIF_HEIF_LIBRARY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + + +// API versions table +// +// release dec.options enc.options heif_reader heif_writer depth.rep col.profile +// ------------------------------------------------------------------------------------------ +// 1.0 1 N/A N/A N/A 1 N/A +// 1.1 1 N/A N/A 1 1 N/A +// 1.3 1 1 1 1 1 N/A +// 1.4 1 1 1 1 1 1 +// 1.7 2 1 1 1 1 1 +// 1.9.2 2 2 1 1 1 1 +// 1.10 2 3 1 1 1 1 +// 1.11 2 4 1 1 1 1 +// 1.13 3 4 1 1 1 1 +// 1.14 3 5 1 1 1 1 +// 1.15 4 5 1 1 1 1 +// 1.16 5 6 1 1 1 1 +// 1.18 5 7 1 1 1 1 +// 1.19 6 7 2 1 1 1 +// 1.20 7 7 2 1 1 1 +// 1.21 8 7 2 1 1 1 +// 1.22 9 8 2 1 1 1 +// 1.23 10 8 2 1 1 1 + +// The LIBHEIF_API export macro is defined in its own dependency-free header so +// that heif_error.h (which is needed by this header) can use it without +// creating an include cycle. +#include + +/** + * Build a 32 bit integer from a 4-character code. + */ +#define heif_fourcc(a, b, c, d) ((uint32_t)((a<<24) | (b<<16) | (c<<8) | d)) + +#include +#include + + +/* === version numbers === */ + +// Version string of linked libheif library. +LIBHEIF_API const char* heif_get_version(void); + +// Numeric version of linked libheif library, encoded as 0xHHMMLL00 = hh.mm.ll, where hh, mm, ll is the decimal representation of HH, MM, LL. +// For example: 0x02150300 is version 2.21.3 +LIBHEIF_API uint32_t heif_get_version_number(void); + +// Numeric part "HH" from above. Returned as a decimal number. +LIBHEIF_API int heif_get_version_number_major(void); +// Numeric part "MM" from above. Returned as a decimal number. +LIBHEIF_API int heif_get_version_number_minor(void); +// Numeric part "LL" from above. Returned as a decimal number. +LIBHEIF_API int heif_get_version_number_maintenance(void); + +// Helper macros to check for given versions of libheif at compile time. +#define LIBHEIF_MAKE_VERSION(h, m, l) ((h) << 24 | (m) << 16 | (l) << 8) +#define LIBHEIF_HAVE_VERSION(h, m, l) (LIBHEIF_NUMERIC_VERSION >= LIBHEIF_MAKE_VERSION(h, m, l)) + +typedef struct heif_context heif_context; +typedef struct heif_image_handle heif_image_handle; + +typedef uint32_t heif_item_id; +typedef uint32_t heif_property_id; + +/** + * Free a string returned by libheif in various API functions. + * You may pass NULL. + */ +LIBHEIF_API +void heif_string_release(const char*); + + +// ========================= library initialization ====================== + +typedef struct heif_init_params +{ + int version; + + // currently no parameters +} heif_init_params; + + +/** + * Initialise library. + * + * You should call heif_init() when you start using libheif and heif_deinit() when you are finished. + * These calls are reference counted. Each call to heif_init() should be matched by one call to heif_deinit(). + * + * For backwards compatibility, it is not really necessary to call heif_init(), but some library memory objects + * will never be freed if you do not call heif_init()/heif_deinit(). + * + * heif_init() will load the external modules installed in the default plugin path. Thus, you need it when you + * want to load external plugins from the default path. + * Codec plugins that are compiled into the library directly (selected by the compile-time parameters of libheif) + * will be available even without heif_init(). + * + * Make sure that you do not have one part of your program use heif_init()/heif_deinit() and another part that does + * not use it as the latter may try to use an uninitialized library. If in doubt, enclose everything with init/deinit. + * + * You may pass nullptr to get default parameters. Currently, no parameters are supported. + */ +LIBHEIF_API +heif_error heif_init(heif_init_params*); + +/** + * Deinitialise and clean up library. + * + * You should call heif_init() when you start using libheif and heif_deinit() when you are finished. + * These calls are reference counted. Each call to heif_init() should be matched by one call to heif_deinit(). + * + * Note: heif_deinit() must not be called after exit(), for example in a global C++ object's destructor. + * If you do, global variables in libheif might have already been released when heif_deinit() is running, + * leading to a crash. + * + * \sa heif_init() + */ +LIBHEIF_API +void heif_deinit(void); + + +// --- Codec plugins --- + +// --- Plugins are currently only supported on Unix platforms. + +typedef enum heif_plugin_type +{ + heif_plugin_type_encoder, + heif_plugin_type_decoder +} heif_plugin_type; + +typedef struct heif_plugin_info +{ + int version; // version of this info struct + heif_plugin_type type; + const void* plugin; + void* internal_handle; // for internal use only +} heif_plugin_info; + +LIBHEIF_API +heif_error heif_load_plugin(const char* filename, heif_plugin_info const** out_plugin); + +LIBHEIF_API +heif_error heif_load_plugins(const char* directory, + const heif_plugin_info** out_plugins, + int* out_nPluginsLoaded, + int output_array_size); + +LIBHEIF_API +heif_error heif_unload_plugin(const heif_plugin_info* plugin); + +// Get a NULL terminated array of the plugin directories that are searched by libheif. +// This includes the paths specified in the environment variable LIBHEIF_PLUGIN_PATHS and the built-in path +// (if not overridden by the environment variable). +LIBHEIF_API +const char* const* heif_get_plugin_directories(void); + +LIBHEIF_API +void heif_free_plugin_directories(const char* const*); + + +// --- register plugins + +typedef struct heif_decoder_plugin heif_decoder_plugin; +typedef struct heif_encoder_plugin heif_encoder_plugin; + +LIBHEIF_API +heif_error heif_register_decoder_plugin(const heif_decoder_plugin*); + +LIBHEIF_API +heif_error heif_register_encoder_plugin(const heif_encoder_plugin*); + + +// DEPRECATED. Use heif_register_decoder_plugin(const struct heif_decoder_plugin*) instead. +LIBHEIF_API +heif_error heif_register_decoder(heif_context* heif, const heif_decoder_plugin*); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_metadata.cc libheif-1.23.4/libheif/api/libheif/heif_metadata.cc --- libheif-1.19.8/libheif/api/libheif/heif_metadata.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_metadata.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,252 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_metadata.h" +#include "libheif/heif.h" +#include "api_structs.h" + +#include "box.h" + +#include +#include +#include +#include + + +int heif_metadata_compression_method_supported(enum heif_metadata_compression method) +{ + switch (method) { + case heif_metadata_compression_off: + case heif_metadata_compression_auto: + return true; + + case heif_metadata_compression_deflate: + case heif_metadata_compression_zlib: +#if HAVE_ZLIB + return true; +#else + return false; +#endif + + case heif_metadata_compression_brotli: +#if HAVE_BROTLI + return true; +#else + return false; +#endif + + default: + return false; + } +} + + +int heif_image_handle_get_number_of_metadata_blocks(const heif_image_handle* handle, + const char* type_filter) +{ + int cnt = 0; + for (const auto& metadata : handle->image->get_metadata()) { + if (type_filter == nullptr || + metadata->item_type == type_filter) { + cnt++; + } + } + + return cnt; +} + + +int heif_image_handle_get_list_of_metadata_block_IDs(const heif_image_handle* handle, + const char* type_filter, + heif_item_id* ids, int count) +{ + int cnt = 0; + for (const auto& metadata : handle->image->get_metadata()) { + if (type_filter == nullptr || + metadata->item_type == type_filter) { + if (cnt < count) { + ids[cnt] = metadata->item_id; + cnt++; + } + else { + break; + } + } + } + + return cnt; +} + + +const char* heif_image_handle_get_metadata_type(const heif_image_handle* handle, + heif_item_id metadata_id) +{ + for (auto& metadata : handle->image->get_metadata()) { + if (metadata->item_id == metadata_id) { + return metadata->item_type.c_str(); + } + } + + return nullptr; +} + + +const char* heif_image_handle_get_metadata_content_type(const heif_image_handle* handle, + heif_item_id metadata_id) +{ + for (auto& metadata : handle->image->get_metadata()) { + if (metadata->item_id == metadata_id) { + return metadata->content_type.c_str(); + } + } + + return nullptr; +} + + +size_t heif_image_handle_get_metadata_size(const heif_image_handle* handle, + heif_item_id metadata_id) +{ + for (auto& metadata : handle->image->get_metadata()) { + if (metadata->item_id == metadata_id) { + return metadata->m_data.size(); + } + } + + return 0; +} + + +heif_error heif_image_handle_get_metadata(const heif_image_handle* handle, + heif_item_id metadata_id, + void* out_data) +{ + for (auto& metadata : handle->image->get_metadata()) { + if (metadata->item_id == metadata_id) { + if (!metadata->m_data.empty()) { + if (out_data == nullptr) { + return heif_error_null_pointer_argument; + } + + memcpy(out_data, + metadata->m_data.data(), + metadata->m_data.size()); + } + + return Error::Ok.error_struct(handle->image.get()); + } + } + + Error err(heif_error_Usage_error, + heif_suberror_Nonexisting_item_referenced); + return err.error_struct(handle->image.get()); +} + + +const char* heif_image_handle_get_metadata_item_uri_type(const heif_image_handle* handle, + heif_item_id metadata_id) +{ + for (auto& metadata : handle->image->get_metadata()) { + if (metadata->item_id == metadata_id) { + return metadata->item_uri_type.c_str(); + } + } + + return nullptr; +} + + +heif_error heif_context_add_exif_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size) +{ + Error error = ctx->context->add_exif_metadata(image_handle->image, data, size); + if (error != Error::Ok) { + return error.error_struct(ctx->context.get()); + } + else { + return heif_error_success; + } +} + + +heif_error heif_context_add_XMP_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size) +{ + return heif_context_add_XMP_metadata2(ctx, image_handle, data, size, + heif_metadata_compression_off); +} + + +heif_error heif_context_add_XMP_metadata2(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size, + heif_metadata_compression compression) +{ + Error error = ctx->context->add_XMP_metadata(image_handle->image, data, size, compression); + if (error != Error::Ok) { + return error.error_struct(ctx->context.get()); + } + else { + return heif_error_success; + } +} + + +heif_error heif_context_add_generic_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size, + const char* item_type, const char* content_type) +{ + if (item_type == nullptr || strlen(item_type) != 4) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "called heif_context_add_generic_metadata() with invalid 'item_type'." + }; + } + + Error error = ctx->context->add_generic_metadata(image_handle->image, data, size, + fourcc(item_type), content_type, nullptr, heif_metadata_compression_off, nullptr); + if (error != Error::Ok) { + return error.error_struct(ctx->context.get()); + } + else { + return heif_error_success; + } +} + + +heif_error heif_context_add_generic_uri_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size, + const char* item_uri_type, + heif_item_id* out_item_id) +{ + Error error = ctx->context->add_generic_metadata(image_handle->image, data, size, + fourcc("uri "), nullptr, item_uri_type, heif_metadata_compression_off, out_item_id); + if (error != Error::Ok) { + return error.error_struct(ctx->context.get()); + } + else { + return heif_error_success; + } +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_metadata.h libheif-1.23.4/libheif/api/libheif/heif_metadata.h --- libheif-1.19.8/libheif/api/libheif/heif_metadata.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_metadata.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,136 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_METADATA_H +#define LIBHEIF_HEIF_METADATA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + + +typedef enum heif_metadata_compression +{ + 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 +} heif_metadata_compression; + +LIBHEIF_API +int heif_metadata_compression_method_supported(enum heif_metadata_compression method); + +// ------------------------- metadata (Exif / XMP) ------------------------- + +// How many metadata blocks are attached to an image. If you only want to get EXIF data, +// set the type_filter to "Exif". Otherwise, set the type_filter to NULL. +LIBHEIF_API +int heif_image_handle_get_number_of_metadata_blocks(const heif_image_handle* handle, + const char* type_filter); + +// 'type_filter' can be used to get only metadata of specific types, like "Exif". +// If 'type_filter' is NULL, it will return all types of metadata IDs. +LIBHEIF_API +int heif_image_handle_get_list_of_metadata_block_IDs(const heif_image_handle* handle, + const char* type_filter, + heif_item_id* ids, int count); + +// Return a string indicating the type of the metadata, as specified in the HEIF file. +// Exif data will have the type string "Exif". +// This string will be valid until the next call to a libheif function. +// You do not have to free this string. +LIBHEIF_API +const char* heif_image_handle_get_metadata_type(const heif_image_handle* handle, + heif_item_id metadata_id); + +// For EXIF, the content type is empty. +// For XMP, the content type is "application/rdf+xml". +LIBHEIF_API +const char* heif_image_handle_get_metadata_content_type(const heif_image_handle* handle, + heif_item_id metadata_id); + +// Get the size of the raw metadata, as stored in the HEIF file. +LIBHEIF_API +size_t heif_image_handle_get_metadata_size(const heif_image_handle* handle, + heif_item_id metadata_id); + +// 'out_data' must point to a memory area of the size reported by heif_image_handle_get_metadata_size(). +// The data is returned exactly as stored in the HEIF file. +// For Exif data, you probably have to skip the first four bytes of the data, since they +// indicate the offset to the start of the TIFF header of the Exif data. +LIBHEIF_API +heif_error heif_image_handle_get_metadata(const heif_image_handle* handle, + heif_item_id metadata_id, + void* out_data); + +// Only valid for item type == "uri ", an absolute URI +LIBHEIF_API +const char* heif_image_handle_get_metadata_item_uri_type(const heif_image_handle* handle, + heif_item_id metadata_id); + +// --- writing Exif / XMP metadata --- + +// Add EXIF metadata to an image. +LIBHEIF_API +heif_error heif_context_add_exif_metadata(heif_context*, + const heif_image_handle* image_handle, + const void* data, int size); + +// Add XMP metadata to an image. +LIBHEIF_API +heif_error heif_context_add_XMP_metadata(heif_context*, + const heif_image_handle* image_handle, + const void* data, int size); + +// New version of heif_context_add_XMP_metadata() with data compression (experimental). +LIBHEIF_API +heif_error heif_context_add_XMP_metadata2(heif_context*, + const heif_image_handle* image_handle, + const void* data, int size, + enum heif_metadata_compression compression); + +// Add generic, proprietary metadata to an image. You have to specify an 'item_type' that will +// identify your metadata. 'content_type' can be an additional type, or it can be NULL. +// For example, this function can be used to add IPTC metadata (IIM stream, not XMP) to an image. +// Although not standard, we propose to store IPTC data with item type="iptc", content_type=NULL. +LIBHEIF_API +heif_error heif_context_add_generic_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + 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 +heif_error heif_context_add_generic_uri_metadata(heif_context* ctx, + const heif_image_handle* image_handle, + const void* data, int size, + const char* item_uri_type, + heif_item_id* out_item_id); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_omaf.cc libheif-1.23.4/libheif/api/libheif/heif_omaf.cc --- libheif-1.19.8/libheif/api/libheif/heif_omaf.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_omaf.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,47 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Brad Hards + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "libheif/heif_omaf.h" +#include "api_structs.h" + + +heif_omaf_image_projection heif_image_handle_get_omaf_image_projection(const heif_image_handle* handle) +{ + return handle->image->get_omaf_image_projection(); +} + +void heif_image_handle_set_omaf_image_projection(heif_image_handle* handle, + heif_omaf_image_projection image_projection) +{ + handle->image->set_omaf_image_projection(image_projection); +} + + +heif_omaf_image_projection heif_image_get_omaf_image_projection(const heif_image* image) +{ + return image->image->get_omaf_image_projection(); +} + +void heif_image_set_omaf_image_projection(heif_image* image, + heif_omaf_image_projection image_projection) +{ + image->image->set_omaf_image_projection(image_projection); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_omaf.h libheif-1.23.4/libheif/api/libheif/heif_omaf.h --- libheif-1.19.8/libheif/api/libheif/heif_omaf.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_omaf.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,104 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Brad Hards + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_OMAF_H +#define LIBHEIF_HEIF_OMAF_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------------------- OMAF projection information ------------------------- +// +// ISO/IEC 23090-2 "Omnidirectional media format" describes formats for +// representing immersive / 360-degree imagery. The 'prfr' (projection format) +// item property records the kind of projection used by the encoded image. + +/** + * OMAF Image projection. + * + * The image projection for most images is flat - it is projected as intended to be shown on + * a flat screen or print. For immersive or omnidirectional media (e.g. VR headsets, or + * equivalent), there are alternatives such as an equirectangular projection or cubemap projection. + * + * See ISO/IEC 23090-2 "Omnidirectional media format" for more information. + */ +typedef enum heif_omaf_image_projection +{ + /** + * Equirectangular projection. + */ + heif_omaf_image_projection_equirectangular = 0x00, + + /** + * Cube map. + */ + heif_omaf_image_projection_cube_map = 0x01, + + /* + * Values 2 through 31 are reserved in ISO/IEC 23090-2:2023 Table 10. + * Files may carry any of them; libheif passes the raw projection_type + * value through unchanged, so callers can log or round-trip it. + * Handle anything outside the named constants in a `default:` arm. + */ + + /** + * Flat projection. Also returned by the get-projection accessors when no + * projection information is present on the image, so callers can use + * `result == heif_omaf_image_projection_flat` to test for "no prfr box". + * 0xFF lies outside the prfr value range reserved by ISO 23090-2:2023 + * Table 10, so it cannot collide with a value read from a file. + */ + heif_omaf_image_projection_flat = 0xFF, +} heif_omaf_image_projection; + + +// To test whether projection information is present, compare the getter's +// result against heif_omaf_image_projection_flat. + +LIBHEIF_API +heif_omaf_image_projection heif_image_handle_get_omaf_image_projection(const heif_image_handle* handle); + +LIBHEIF_API +void heif_image_handle_set_omaf_image_projection(heif_image_handle* handle, + heif_omaf_image_projection image_projection); + +// Variants operating on a heif_image (a decoded or about-to-be-encoded pixel +// image). Setting the projection on a heif_image before encoding causes the +// resulting image item to carry the corresponding prfr property. + +LIBHEIF_API +heif_omaf_image_projection heif_image_get_omaf_image_projection(const heif_image* image); + +LIBHEIF_API +void heif_image_set_omaf_image_projection(heif_image* image, + heif_omaf_image_projection image_projection); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_plugin.cc libheif-1.23.4/libheif/api/libheif/heif_plugin.cc --- libheif-1.19.8/libheif/api/libheif/heif_plugin.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_plugin.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,13 +21,13 @@ #include "heif.h" #include "heif_plugin.h" // needed to avoid 'unresolved symbols' on Visual Studio compiler -struct heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; +heif_error heif_error_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; -struct heif_error heif_error_unsupported_parameter = {heif_error_Usage_error, +heif_error heif_error_unsupported_parameter = {heif_error_Usage_error, heif_suberror_Unsupported_parameter, "Unsupported encoder parameter"}; -struct heif_error heif_error_invalid_parameter_value = {heif_error_Usage_error, +heif_error heif_error_invalid_parameter_value = {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "Invalid parameter value"}; diff -Nru libheif-1.19.8/libheif/api/libheif/heif_plugin.h libheif-1.23.4/libheif/api/libheif/heif_plugin.h --- libheif-1.19.8/libheif/api/libheif/heif_plugin.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_plugin.h 2026-09-06 14:45:17.000000000 +0000 @@ -20,12 +20,13 @@ #ifndef LIBHEIF_HEIF_PLUGIN_H #define LIBHEIF_HEIF_PLUGIN_H +#include "heif_sequences.h" #ifdef __cplusplus extern "C" { #endif -#include +#include // ==================================================================================================== @@ -42,7 +43,16 @@ // 1.8 1 2 2 // 1.13 2 3 2 // 1.15 3 3 2 - +// 1.20 4 3 2 +// 1.21 5 4 2 +// 1.22 6 4 2 + +#define heif_decoder_plugin_latest_version 6 +#define heif_encoder_plugin_latest_version 4 + +// The minimum plugin versions that can be used with this libheif version. +#define heif_decoder_plugin_minimum_version 5 +#define heif_encoder_plugin_minimum_version 4 // ==================================================================================================== // Decoder plugin API @@ -50,7 +60,29 @@ // added as plugins. A plugin has to implement the functions specified in heif_decoder_plugin // and the plugin has to be registered to the libheif library using heif_register_decoder(). -struct heif_decoder_plugin +typedef struct heif_decoder_plugin_compressed_format_description +{ + heif_compression_format format; + +} heif_decoder_plugin_compressed_format_description; + + +typedef struct heif_decoder_plugin_options +{ + heif_compression_format format; + int strict_decoding; // bool + int num_threads; // 0 - undefined, use decoder default + + // --- added in plugin_api_version 6 --- + // Security limits the plugin must enforce for image allocations during decoding. + // The pointee must outlive the decoder instance (typically the context's limits). + // Only read by plugins reporting plugin_api_version >= 6. + const heif_security_limits* limits; + +} heif_decoder_plugin_options; + + +typedef struct heif_decoder_plugin { // API version supported by this plugin (see table above for supported versions) int plugin_api_version; @@ -59,38 +91,38 @@ // --- version 1 functions --- // Human-readable name of the plugin - const char* (* get_plugin_name)(); + const char* (* get_plugin_name)(void); // Global plugin initialization (may be NULL) - void (* init_plugin)(); + void (* init_plugin)(void); // Global plugin deinitialization (may be NULL) - void (* deinit_plugin)(); + void (* deinit_plugin)(void); // Query whether the plugin supports decoding of the given format // Result is a priority value. The plugin with the largest value wins. // Default priority is 100. Returning 0 indicates that the plugin cannot decode this format. - int (* does_support_format)(enum heif_compression_format format); + int (* does_support_format)(heif_compression_format format); // Create a new decoder context for decoding an image - struct heif_error (* new_decoder)(void** decoder); + heif_error (* new_decoder)(void** decoder); // Free the decoder context (heif_image can still be used after destruction) void (* free_decoder)(void* decoder); // Push more data into the decoder. This can be called multiple times. // This may not be called after any decode_*() function has been called. - struct heif_error (* push_data)(void* decoder, const void* data, size_t size); + heif_error (* push_data)(void* decoder, const void* data, size_t size); // --- After pushing the data into the decoder, the decode functions may be called only once. - struct heif_error (* decode_image)(void* decoder, struct heif_image** out_img); + heif_error (* decode_image)(void* decoder, heif_image** out_img); - // --- version 2 functions will follow below ... --- + // --- version 2 functions --- - void (*set_strict_decoding)(void* decoder, int flag); + void (* set_strict_decoding)(void* decoder, int flag); // If not NULL, this can provide a specialized function to convert YCbCr to sRGB, because // only the codec itself knows how to interpret the chroma samples and their locations. @@ -104,35 +136,60 @@ // Reset decoder, such that we can feed in new data for another image. // void (*reset_image)(void* decoder); - // --- version 3 functions will follow below ... --- + // --- version 3 functions --- const char* id_name; - // --- version 4 functions will follow below ... --- -}; + // --- version 4 functions --- + + heif_error (* decode_next_image)(void* decoder, heif_image** out_img, + const heif_security_limits* limits); + + // --- version 5 functions will follow below ... --- + uint32_t minimum_required_libheif_version; + + // Query whether the plugin supports decoding of the given format + // Result is a priority value. The plugin with the largest value wins. + // Default priority is 100. Returning 0 indicates that the plugin cannot decode this format. + int (* does_support_format2)(const heif_decoder_plugin_compressed_format_description* format); -enum heif_encoded_data_type + // Create a new decoder context for decoding an image + heif_error (* new_decoder2)(void** decoder, const heif_decoder_plugin_options*); + + heif_error (* push_data2)(void* decoder, const void* data, size_t size, uintptr_t user_data); + + heif_error (* flush_data)(void* decoder); + + heif_error (* decode_next_image2)(void* decoder, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits); + + // --- Note: when adding new versions, also update `heif_decoder_plugin_latest_version`. +} heif_decoder_plugin; + + +typedef enum heif_encoded_data_type { heif_encoded_data_type_HEVC_header = 1, heif_encoded_data_type_HEVC_image = 2, heif_encoded_data_type_HEVC_depth_SEI = 3 -}; +} heif_encoded_data_type; // Specifies the class of the input image content. // The encoder may want to encode different classes with different parameters // (e.g. always encode alpha lossless) -enum heif_image_input_class +typedef enum heif_image_input_class { heif_image_input_class_normal = 1, heif_image_input_class_alpha = 2, heif_image_input_class_depth = 3, heif_image_input_class_thumbnail = 4 -}; +} heif_image_input_class; -struct heif_encoder_plugin +typedef struct heif_encoder_plugin { // API version supported by this plugin (see table above for supported versions) int plugin_api_version; @@ -141,7 +198,7 @@ // --- version 1 functions --- // The compression format generated by this plugin. - enum heif_compression_format compression_format; + heif_compression_format compression_format; // Short name of the encoder that can be used as command line parameter when selecting an encoder. // Hence, it should stay stable and not contain any version numbers that will change. @@ -157,69 +214,69 @@ // Human-readable name of the plugin - const char* (* get_plugin_name)(); + const char* (* get_plugin_name)(void); // Global plugin initialization (may be NULL) - void (* init_plugin)(); + void (* init_plugin)(void); // Global plugin cleanup (may be NULL). // Free data that was allocated in init_plugin() - void (* cleanup_plugin)(); + void (* cleanup_plugin)(void); // Create a new decoder context for decoding an image - struct heif_error (* new_encoder)(void** encoder); + heif_error (* new_encoder)(void** encoder); // Free the decoder context (heif_image can still be used after destruction) void (* free_encoder)(void* encoder); - struct heif_error (* set_parameter_quality)(void* encoder, int quality); + heif_error (* set_parameter_quality)(void* encoder, int quality); - struct heif_error (* get_parameter_quality)(void* encoder, int* quality); + heif_error (* get_parameter_quality)(void* encoder, int* quality); - struct heif_error (* set_parameter_lossless)(void* encoder, int lossless); + heif_error (* set_parameter_lossless)(void* encoder, int lossless); - struct heif_error (* get_parameter_lossless)(void* encoder, int* lossless); + heif_error (* get_parameter_lossless)(void* encoder, int* lossless); - struct heif_error (* set_parameter_logging_level)(void* encoder, int logging); + heif_error (* set_parameter_logging_level)(void* encoder, int logging); - struct heif_error (* get_parameter_logging_level)(void* encoder, int* logging); + heif_error (* get_parameter_logging_level)(void* encoder, int* logging); - const struct heif_encoder_parameter** (* list_parameters)(void* encoder); + const heif_encoder_parameter** (* list_parameters)(void* encoder); - struct heif_error (* set_parameter_integer)(void* encoder, const char* name, int value); + heif_error (* set_parameter_integer)(void* encoder, const char* name, int value); - struct heif_error (* get_parameter_integer)(void* encoder, const char* name, int* value); + heif_error (* get_parameter_integer)(void* encoder, const char* name, int* value); - struct heif_error (* set_parameter_boolean)(void* encoder, const char* name, int value); + heif_error (* set_parameter_boolean)(void* encoder, const char* name, int value); - struct heif_error (* get_parameter_boolean)(void* encoder, const char* name, int* value); + heif_error (* get_parameter_boolean)(void* encoder, const char* name, int* value); - struct heif_error (* set_parameter_string)(void* encoder, const char* name, const char* value); + heif_error (* set_parameter_string)(void* encoder, const char* name, const char* value); - struct heif_error (* get_parameter_string)(void* encoder, const char* name, char* value, int value_size); + heif_error (* get_parameter_string)(void* encoder, const char* name, char* value, int value_size); // Replace the input colorspace/chroma with the one that is supported by the encoder and that // comes as close to the input colorspace/chroma as possible. - void (* query_input_colorspace)(enum heif_colorspace* inout_colorspace, - enum heif_chroma* inout_chroma); + void (* query_input_colorspace)(heif_colorspace* inout_colorspace, + heif_chroma* inout_chroma); // Encode an image. // After pushing an image into the encoder, you should call get_compressed_data() to // get compressed data until it returns a NULL data pointer. - struct heif_error (* encode_image)(void* encoder, const struct heif_image* image, - enum heif_image_input_class image_class); + heif_error (* encode_image)(void* encoder, const heif_image* image, + enum heif_image_input_class image_class); // Get a packet of decoded data. The data format depends on the codec. // For HEVC, each packet shall contain exactly one NAL, starting with the NAL header without startcode. - struct heif_error (* get_compressed_data)(void* encoder, uint8_t** data, int* size, + heif_error (* get_compressed_data)(void* encoder, uint8_t** data, int* size, enum heif_encoded_data_type* type); // --- version 2 --- void (* query_input_colorspace2)(void* encoder, - enum heif_colorspace* inout_colorspace, - enum heif_chroma* inout_chroma); + heif_colorspace* inout_colorspace, + heif_chroma* inout_chroma); // --- version 3 --- @@ -230,8 +287,30 @@ void (* query_encoded_size)(void* encoder, uint32_t input_width, uint32_t input_height, uint32_t* encoded_width, uint32_t* encoded_height); - // --- version 4 functions will follow below ... --- -}; + // --- version 4 --- + + uint32_t minimum_required_libheif_version; + + heif_error (* start_sequence_encoding)(void* encoder, const heif_image* image, + enum heif_image_input_class image_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options); + // TODO: is heif_sequence_encoding_options a good choice here? + + heif_error (* encode_sequence_frame)(void* encoder, const heif_image* image, uintptr_t frame_nr); + + heif_error (* end_sequence_encoding)(void* encoder); + + heif_error (* get_compressed_data2)(void* encoder, uint8_t** data, int* size, + uintptr_t* frame_nr, + int* is_keyframe, int* more_frame_packets); + + int does_indicate_keyframes; + + // --- version 5 functions will follow below ... --- + + // --- Note: when adding new versions, also update `heif_encoder_plugin_latest_version`. +} heif_encoder_plugin; // Names for standard parameters. These should only be used by the encoder plugins. @@ -241,7 +320,7 @@ // For use only by the encoder plugins. // Application programs should use the access functions. // NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) -struct heif_encoder_parameter +typedef struct heif_encoder_parameter { int version; // current version: 2 @@ -280,15 +359,15 @@ // --- version 2 fields int has_default; -}; +} heif_encoder_parameter; -extern struct heif_error heif_error_ok; -extern struct heif_error heif_error_unsupported_parameter; -extern struct heif_error heif_error_invalid_parameter_value; +extern heif_error heif_error_ok; +extern heif_error heif_error_unsupported_parameter; +extern heif_error heif_error_invalid_parameter_value; #define HEIF_WARN_OR_FAIL(strict, image, cmd, cleanupBlock) \ -{ struct heif_error e = cmd; \ +{ heif_error e = cmd; \ if (e.code != heif_error_Ok) { \ if (strict) { \ cleanupBlock \ diff -Nru libheif-1.19.8/libheif/api/libheif/heif_properties.cc libheif-1.23.4/libheif/api/libheif/heif_properties.cc --- libheif-1.19.8/libheif/api/libheif/heif_properties.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_properties.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,6 +22,7 @@ #include "context.h" #include "api_structs.h" #include "file.h" +#include "image/pixelimage.h" #include #include @@ -30,8 +31,7 @@ #include #include - -int heif_item_get_properties_of_type(const struct heif_context* context, +int heif_item_get_properties_of_type(const heif_context* context, heif_item_id id, heif_item_property_type type, heif_property_id* out_list, @@ -75,7 +75,7 @@ } -int heif_item_get_transformation_properties(const struct heif_context* context, +int heif_item_get_transformation_properties(const heif_context* context, heif_item_id id, heif_property_id* out_list, int count) @@ -113,9 +113,9 @@ return out_idx; } -enum heif_item_property_type heif_item_get_property_type(const struct heif_context* context, - heif_item_id id, - heif_property_id propertyId) +heif_item_property_type heif_item_get_property_type(const heif_context* context, + heif_item_id id, + heif_property_id propertyId) { auto file = context->context->get_heif_file(); @@ -131,11 +131,11 @@ } auto property = properties[propertyId - 1]; - return (enum heif_item_property_type) property->get_short_type(); + return (heif_item_property_type) property->get_short_type(); } -static char* create_c_string_copy(const std::string s) +static char* create_c_string_copy(const std::string& s) { char* copy = new char[s.length() + 1]; strcpy(copy, s.data()); @@ -143,38 +143,25 @@ } -struct heif_error heif_item_get_property_user_description(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_user_description** out) +heif_error heif_item_get_property_user_description(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_user_description** out) { if (!out || !context) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + return heif_error_null_pointer_argument; } - - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return err.error_struct(context->context.get()); - } - - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range"}; - } - - auto udes = std::dynamic_pointer_cast(properties[propertyId - 1]); + auto udes = context->context->find_property(itemId, propertyId); if (!udes) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "wrong property type"}; + return udes.error_struct(context->context.get()); } auto* udes_c = new heif_property_user_description(); udes_c->version = 1; - udes_c->lang = create_c_string_copy(udes->get_lang()); - udes_c->name = create_c_string_copy(udes->get_name()); - udes_c->description = create_c_string_copy(udes->get_description()); - udes_c->tags = create_c_string_copy(udes->get_tags()); + udes_c->lang = create_c_string_copy((*udes)->get_lang()); + udes_c->name = create_c_string_copy((*udes)->get_name()); + udes_c->description = create_c_string_copy((*udes)->get_description()); + udes_c->tags = create_c_string_copy((*udes)->get_tags()); *out = udes_c; @@ -182,13 +169,13 @@ } -struct heif_error heif_item_add_property_user_description(const struct heif_context* context, - heif_item_id itemId, - const struct heif_property_user_description* description, - heif_property_id* out_propertyId) +heif_error heif_item_add_property_user_description(const heif_context* context, + heif_item_id itemId, + const heif_property_user_description* description, + heif_property_id* out_propertyId) { if (!context || !description) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + return heif_error_null_pointer_argument; } auto udes = std::make_shared(); @@ -197,121 +184,99 @@ udes->set_description(description->description ? description->description : ""); udes->set_tags(description->tags ? description->tags : ""); - heif_property_id id = context->context->add_property(itemId, udes, false); + auto id = context->context->add_property(itemId, udes, false); + if (!id) { + return id.error_struct(context->context.get()); + } if (out_propertyId) { - *out_propertyId = id; + *out_propertyId = *id; } return heif_error_success; } -enum heif_transform_mirror_direction heif_item_get_property_transform_mirror(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId) +void heif_property_user_description_release(heif_property_user_description* udes) { - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return heif_transform_mirror_direction_invalid; + if (udes == nullptr) { + return; } - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return heif_transform_mirror_direction_invalid; - } + delete[] udes->lang; + delete[] udes->name; + delete[] udes->description; + delete[] udes->tags; + + delete udes; +} - auto imir = std::dynamic_pointer_cast(properties[propertyId - 1]); + +heif_transform_mirror_direction heif_item_get_property_transform_mirror(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId) +{ + auto imir = context->context->find_property(itemId, propertyId); if (!imir) { return heif_transform_mirror_direction_invalid; } - return imir->get_mirror_direction(); + return (*imir)->get_mirror_direction(); } -int heif_item_get_property_transform_rotation_ccw(const struct heif_context* context, +int heif_item_get_property_transform_rotation_ccw(const heif_context* context, heif_item_id itemId, heif_property_id propertyId) { - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return -1; - } - - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return -1; - } - - auto irot = std::dynamic_pointer_cast(properties[propertyId - 1]); + auto irot = context->context->find_property(itemId, propertyId); if (!irot) { return -1; } - return irot->get_rotation_ccw(); + return (*irot)->get_rotation_ccw(); } - -void heif_item_get_property_transform_crop_borders(const struct heif_context* context, +void heif_item_get_property_transform_crop_borders(const heif_context* context, heif_item_id itemId, heif_property_id propertyId, int image_width, int image_height, int* left, int* top, int* right, int* bottom) { - auto file = context->context->get_heif_file(); - - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return; - } - - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return; - } - - auto clap = std::dynamic_pointer_cast(properties[propertyId - 1]); + auto clap = context->context->find_property(itemId, propertyId); if (!clap) { return; } - if (left) *left = clap->left_rounded(image_width); - if (right) *right = image_width - 1 - clap->right_rounded(image_width); - if (top) *top = clap->top_rounded(image_height); - if (bottom) *bottom = image_height - 1 - clap->bottom_rounded(image_height); -} - - -void heif_property_user_description_release(struct heif_property_user_description* udes) -{ - if (udes == nullptr) { + auto crop = (*clap)->get_crop(image_width, image_height); + if (!crop) { + // The clean aperture cannot be applied to an image of this size (zero size, negative + // size, or a size beyond the supported coordinate range). This function has no error + // return, so report that nothing is cropped. + if (left) *left = 0; + if (right) *right = 0; + if (top) *top = 0; + if (bottom) *bottom = 0; return; } - delete[] udes->lang; - delete[] udes->name; - delete[] udes->description; - delete[] udes->tags; - - delete udes; + if (left) *left = crop->left; + if (right) *right = image_width - 1 - crop->right; + if (top) *top = crop->top; + if (bottom) *bottom = image_height - 1 - crop->bottom; } -struct heif_error heif_item_add_raw_property(const struct heif_context* context, - heif_item_id itemId, - uint32_t short_type, - const uint8_t* uuid_type, - const uint8_t* data, size_t size, - int is_essential, - heif_property_id* out_propertyId) +heif_error heif_item_add_raw_property(const heif_context* context, + heif_item_id itemId, + uint32_t short_type, + const uint8_t* uuid_type, + const uint8_t* data, size_t size, + int is_essential, + heif_property_id* out_propertyId) { if (!context || !data || (short_type == fourcc("uuid") && uuid_type==nullptr)) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument passed in"}; + return heif_error_null_pointer_argument; } auto raw_box = std::make_shared(short_type); @@ -324,283 +289,737 @@ std::vector data_vector(data, data + size); raw_box->set_raw_data(data_vector); - heif_property_id id = context->context->add_property(itemId, raw_box, is_essential != 0); + auto id = context->context->add_property(itemId, raw_box, is_essential != 0); + if (!id) { + return id.error_struct(context->context.get()); + } if (out_propertyId) { - *out_propertyId = id; + *out_propertyId = *id; } return heif_error_success; } -template -struct heif_error find_property(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - std::shared_ptr* box_casted) +heif_error heif_item_get_property_raw_size(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + size_t* size_out) { - auto file = context->context->get_heif_file(); + if (!context || !size_out) { + return heif_error_null_pointer_argument; + } + auto box_other = context->context->find_property(itemId, propertyId); + if (!box_other) { + return box_other.error_struct(context->context.get()); + } - std::vector> properties; - Error err = file->get_properties(itemId, properties); - if (err) { - return err.error_struct(context->context.get()); + // TODO: every Box (not just Box_other) should have a get_raw_data() method. + if (*box_other == nullptr) { + return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; } - if (propertyId < 1 || propertyId - 1 >= properties.size()) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range"}; + const auto& data = (*box_other)->get_raw_data(); + + *size_out = data.size(); + + return heif_error_success; +} + + +heif_error heif_item_get_property_raw_data(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + uint8_t* data_out) +{ + if (!context || !data_out) { + return heif_error_null_pointer_argument; + } + + auto box_other = context->context->find_property(itemId, propertyId); + if (!box_other) { + return box_other.error_struct(context->context.get()); + } + + // TODO: every Box (not just Box_other) should have a get_raw_data() method. + if (*box_other == nullptr) { + return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; + } + + auto data = (*box_other)->get_raw_data(); + + std::copy(data.begin(), data.end(), data_out); + + return heif_error_success; +} + + +heif_error heif_item_get_property_uuid_type(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + uint8_t extended_type[16]) +{ + if (!context || !extended_type) { + return heif_error_null_pointer_argument; + } + + auto box_other = context->context->find_property(itemId, propertyId); + if (!box_other) { + return box_other.error_struct(context->context.get()); + } + + if (*box_other == nullptr) { + return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; } - auto box = properties[propertyId - 1]; - *box_casted = std::dynamic_pointer_cast(box); + auto uuid = (*box_other)->get_uuid_type(); + + std::copy(uuid.begin(), uuid.end(), extended_type); + return heif_error_success; } -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -const uint64_t heif_tai_clock_info_unknown_time_uncertainty = 0xFFFFFFFFFFFFFFFF; -const uint64_t heif_unknown_tai_timestamp = 0xFFFFFFFFFFFFFFFF; -const int32_t heif_tai_clock_info_unknown_drift_rate = 0x7FFFFFFF; + +// ------------------------- intrinsic and extrinsic matrices ------------------------- -int heif_is_tai_clock_info_drift_rate_undefined(int32_t drift_rate) +int heif_image_handle_has_camera_intrinsic_matrix(const heif_image_handle* handle) { - if (drift_rate == heif_tai_clock_info_unknown_drift_rate) { - return 1; + if (!handle) { + return false; } - return 0; + + return handle->image->has_intrinsic_matrix(); } -struct heif_error heif_property_set_clock_info(struct heif_context* ctx, - heif_item_id itemId, - const heif_tai_clock_info* clock, - heif_property_id* out_propertyId) +heif_error heif_image_handle_get_camera_intrinsic_matrix(const heif_image_handle* handle, + heif_camera_intrinsic_matrix* out_matrix) { - if (!ctx || !clock) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + if (handle == nullptr || out_matrix == nullptr) { + return heif_error_null_pointer_argument; } - // Check if itemId exists - auto file = ctx->context->get_heif_file(); - if (!file->image_exists(itemId)) { - return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "itemId does not exist"}; + if (!handle->image->has_intrinsic_matrix()) { + Error err(heif_error_Usage_error, + heif_suberror_Camera_intrinsic_matrix_undefined); + return err.error_struct(handle->image.get()); } - // Create new taic if one doesn't exist for the itemId. - auto taic = ctx->context->get_heif_file()->get_property_for_item(itemId); - if (!taic) { - taic = std::make_shared(); + const auto& m = handle->image->get_intrinsic_matrix(); + out_matrix->focal_length_x = m.focal_length_x; + out_matrix->focal_length_y = m.focal_length_y; + out_matrix->principal_point_x = m.principal_point_x; + out_matrix->principal_point_y = m.principal_point_y; + out_matrix->skew = m.skew; + + return heif_error_success; +} + + +int heif_image_handle_has_camera_extrinsic_matrix(const heif_image_handle* handle) +{ + if (!handle) { + return false; } - taic->set_time_uncertainty(clock->time_uncertainty); - taic->set_clock_resolution(clock->clock_resolution); - taic->set_clock_drift_rate(clock->clock_drift_rate); - taic->set_clock_type(clock->clock_type); + return handle->image->has_extrinsic_matrix(); +} + - bool essential = false; - heif_property_id id = ctx->context->add_property(itemId, taic, essential); +struct heif_camera_extrinsic_matrix +{ + Box_cmex::ExtrinsicMatrix matrix; +}; - if (out_propertyId) { - *out_propertyId = id; + +heif_error heif_image_handle_get_camera_extrinsic_matrix(const heif_image_handle* handle, + heif_camera_extrinsic_matrix** out_matrix) +{ + if (handle == nullptr || out_matrix == nullptr) { + return heif_error_null_pointer_argument; } + if (!handle->image->has_extrinsic_matrix()) { + Error err(heif_error_Usage_error, + heif_suberror_Camera_extrinsic_matrix_undefined); + return err.error_struct(handle->image.get()); + } + + *out_matrix = new heif_camera_extrinsic_matrix; + (*out_matrix)->matrix = handle->image->get_extrinsic_matrix(); + return heif_error_success; } -struct heif_error heif_property_get_clock_info(const struct heif_context* ctx, - heif_item_id itemId, - heif_tai_clock_info* out_clock) +void heif_camera_extrinsic_matrix_release(heif_camera_extrinsic_matrix* matrix) { - if (!ctx) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL heif_context passed in"}; - } else if (!out_clock) { - return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "NULL heif_tai_clock_info passed in"}; + delete matrix; +} + + +heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const heif_camera_extrinsic_matrix* matrix, + double* out_matrix_row_major) +{ + if (matrix == nullptr || out_matrix_row_major == nullptr) { + return heif_error_null_pointer_argument; } - // Check if itemId exists - auto file = ctx->context->get_heif_file(); - if (!file->image_exists(itemId)) { - return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "itemId does not exist"}; + auto m3x3 = matrix->matrix.calculate_rotation_matrix(); + + for (int i=0;i<9;i++) { + out_matrix_row_major[i] = m3x3[i]; } - // Check if taic exists for itemId - auto taic = file->get_property_for_item(itemId); - if (!taic) { - out_clock = nullptr; - return {heif_error_Usage_error, heif_suberror_Invalid_property, "TAI Clock property not found for itemId"}; + return heif_error_success; +} + +// ====================== ISO/IEC 23001-17 properties ====================== + + +heif_error heif_image_set_bayer_pattern(heif_image* image, + uint32_t bayer_component_id, + uint16_t pattern_width, + uint16_t pattern_height, + const struct heif_bayer_pattern_pixel* patternPixels) +{ + if (image == nullptr || patternPixels == nullptr) { + return heif_error_null_pointer_argument; } - if (out_clock->version >= 1) { - out_clock->version = 1; - out_clock->time_uncertainty = taic->get_time_uncertainty(); - out_clock->clock_resolution = taic->get_clock_resolution(); - out_clock->clock_drift_rate = taic->get_clock_drift_rate(); - out_clock->clock_type = taic->get_clock_type(); + if (pattern_width == 0 || pattern_height == 0) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Bayer pattern dimensions must be non-zero."}; } + BayerPattern pattern; + pattern.pattern_width = pattern_width; + pattern.pattern_height = pattern_height; + + size_t num_pixels = size_t{pattern_width} * pattern_height; + pattern.pixels.assign(patternPixels, patternPixels + num_pixels); + + image->image->set_bayer_pattern(pattern); + return heif_error_success; } -struct heif_error heif_property_set_tai_timestamp(struct heif_context* ctx, - heif_item_id itemId, - heif_tai_timestamp_packet* timestamp, - heif_property_id* out_propertyId) +heif_error heif_image_add_bayer_component(heif_image* image, + uint16_t component_type, + uint32_t* out_component_id) +{ + if (image == nullptr || out_component_id == nullptr) { + return heif_error_null_pointer_argument; + } + + *out_component_id = image->image->add_component_without_data(component_type); + + return heif_error_success; +} + + +int heif_image_get_bayer_pattern_size(const heif_image* image, + uint32_t bayer_component_id, + uint16_t* out_pattern_width, + uint16_t* out_pattern_height) { - if (!ctx) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + if (image == nullptr || !image->image->has_bayer_pattern(bayer_component_id)) { + if (out_pattern_width) { + *out_pattern_width = 0; + } + if (out_pattern_height) { + *out_pattern_height = 0; + } + return 0; } - // Check if itemId exists - auto file = ctx->context->get_heif_file(); - if (!file->image_exists(itemId)) { - return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "itemId does not exist"}; - } - - // Create new itai if one doesn't exist for the itemId. - auto itai = file->get_property_for_item(itemId); - if (!itai) { - itai = std::make_shared(); - } - - // Set timestamp values - itai->set_tai_timestamp(timestamp->tai_timestamp); - itai->set_synchronization_state(timestamp->synchronization_state); - itai->set_timestamp_generation_failure(timestamp->timestamp_generation_failure); - itai->set_timestamp_is_modified(timestamp->timestamp_is_modified); - heif_property_id id = ctx->context->add_property(itemId, itai, false); - - // Create new taic if one doesn't exist for the itemId. - auto taic = file->get_property_for_item(itemId); - if (!taic) { - taic = std::make_shared(); - ctx->context->add_property(itemId, taic, false); - // Should we output taic_id? + const BayerPattern& pattern = image->image->get_bayer_pattern(bayer_component_id); + + if (out_pattern_width) { + *out_pattern_width = pattern.pattern_width; + } + if (out_pattern_height) { + *out_pattern_height = pattern.pattern_height; } - - if (out_propertyId) { - *out_propertyId = id; + return 1; +} + + +heif_error heif_image_get_bayer_pattern(const heif_image* image, + uint32_t bayer_component_id, + struct heif_bayer_pattern_pixel* out_patternPixels) +{ + if (image == nullptr || out_patternPixels == nullptr) { + return heif_error_null_pointer_argument; + } + + if (!image->image->has_bayer_pattern(bayer_component_id)) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Image does not have a Bayer pattern."}; } + const BayerPattern& pattern = image->image->get_bayer_pattern(bayer_component_id); + size_t num_pixels = size_t{pattern.pattern_width} * pattern.pattern_height; + std::copy(pattern.pixels.begin(), pattern.pixels.begin() + num_pixels, out_patternPixels); + return heif_error_success; } -struct heif_error heif_property_get_tai_timestamp(const struct heif_context* ctx, - heif_item_id itemId, - heif_tai_timestamp_packet* out_timestamp) + +float heif_polarization_angle_no_filter() { - if (!ctx) { - return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + uint32_t bits = 0xFFFFFFFF; + float f; + memcpy(&f, &bits, sizeof(f)); + return f; +} + + +int heif_polarization_angle_is_no_filter(float angle) +{ + uint32_t bits; + memcpy(&bits, &angle, sizeof(bits)); + return bits == 0xFFFFFFFF; +} + + +heif_error heif_image_add_polarization_pattern(heif_image* image, + uint32_t num_component_ids, + const uint32_t* component_ids, + uint16_t pattern_width, + uint16_t pattern_height, + const float* polarization_angles) +{ + if (image == nullptr || polarization_angles == nullptr) { + return heif_error_null_pointer_argument; + } + + if (num_component_ids > 0 && component_ids == nullptr) { + return heif_error_null_pointer_argument; + } + + if (pattern_width == 0 || pattern_height == 0) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Polarization pattern dimensions must be non-zero."}; + } + + PolarizationPattern pattern; + pattern.component_ids.assign(component_ids, component_ids + num_component_ids); + pattern.pattern_width = pattern_width; + pattern.pattern_height = pattern_height; + + size_t num_pixels = size_t{pattern_width} * pattern_height; + pattern.polarization_angles.assign(polarization_angles, polarization_angles + num_pixels); + + image->image->add_polarization_pattern(pattern); + + return heif_error_success; +} + + +int heif_image_get_number_of_polarization_patterns(const heif_image* image) +{ + if (image == nullptr) { + return 0; } - // Check if itemId exists - auto file = ctx->context->get_heif_file(); - if (!file->image_exists(itemId)) { - return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "itemId does not exist"}; + return static_cast(image->image->get_polarization_patterns().size()); +} + + +heif_error heif_image_get_polarization_pattern_info(const heif_image* image, + int pattern_index, + uint32_t* out_num_component_ids, + uint16_t* out_pattern_width, + uint16_t* out_pattern_height) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; } - //Check if itai exists for itemId - auto itai = file->get_property_for_item(itemId); - if (!itai) { - out_timestamp = nullptr; - return {heif_error_Usage_error, heif_suberror_Invalid_property, "Timestamp property not found for itemId"}; + const auto& patterns = image->image->get_polarization_patterns(); + if (pattern_index < 0 || static_cast(pattern_index) >= patterns.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Polarization pattern index out of range."}; } - if (out_timestamp) { - out_timestamp->version = 1; - out_timestamp->tai_timestamp = itai->get_tai_timestamp(); - out_timestamp->synchronization_state = itai->get_synchronization_state(); - out_timestamp->timestamp_generation_failure = itai->get_timestamp_generation_failure(); - out_timestamp->timestamp_is_modified = itai->get_timestamp_is_modified(); + const auto& p = patterns[pattern_index]; + if (out_num_component_ids) { + *out_num_component_ids = static_cast(p.component_ids.size()); + } + if (out_pattern_width) { + *out_pattern_width = p.pattern_width; + } + if (out_pattern_height) { + *out_pattern_height = p.pattern_height; } return heif_error_success; } -#endif -struct heif_error heif_item_get_property_raw_size(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - size_t* size_out) +heif_error heif_image_get_polarization_pattern_data(const heif_image* image, + int pattern_index, + uint32_t* out_component_ids, + float* out_polarization_angles) { - if (!context || !size_out) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument passed in"}; + if (image == nullptr || out_polarization_angles == nullptr) { + return heif_error_null_pointer_argument; } - std::shared_ptr box_other; - struct heif_error err = find_property(context, itemId, propertyId, &box_other); - if (err.code) { - return err; + + const auto& patterns = image->image->get_polarization_patterns(); + if (pattern_index < 0 || static_cast(pattern_index) >= patterns.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Polarization pattern index out of range."}; } - // TODO: every Box (not just Box_other) should have a get_raw_data() method. - if (box_other == nullptr) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; + const auto& p = patterns[pattern_index]; + + if (out_component_ids && !p.component_ids.empty()) { + std::copy(p.component_ids.begin(), p.component_ids.end(), out_component_ids); } - const auto& data = box_other->get_raw_data(); + size_t num_pixels = size_t{p.pattern_width} * p.pattern_height; + std::copy(p.polarization_angles.begin(), p.polarization_angles.begin() + num_pixels, out_polarization_angles); - *size_out = data.size(); + return heif_error_success; +} + + +int heif_image_get_polarization_pattern_index_for_component(const heif_image* image, + uint32_t component_id) +{ + if (image == nullptr) { + return -1; + } + + const auto& patterns = image->image->get_polarization_patterns(); + for (size_t i = 0; i < patterns.size(); i++) { + const auto& p = patterns[i]; + if (p.component_ids.empty()) { + // Empty component list means pattern applies to all components. + return static_cast(i); + } + for (uint32_t idx : p.component_ids) { + if (idx == component_id) { + return static_cast(i); + } + } + } + + return -1; +} + + +heif_error heif_image_add_sensor_bad_pixels_map(heif_image* image, + uint32_t num_component_ids, + const uint32_t* component_ids, + int correction_applied, + uint32_t num_bad_rows, + const uint32_t* bad_rows, + uint32_t num_bad_columns, + const uint32_t* bad_columns, + uint32_t num_bad_pixels, + const struct heif_bad_pixel* bad_pixels) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + if (num_component_ids > 0 && component_ids == nullptr) { + return heif_error_null_pointer_argument; + } + + if (num_bad_rows > 0 && bad_rows == nullptr) { + return heif_error_null_pointer_argument; + } + + if (num_bad_columns > 0 && bad_columns == nullptr) { + return heif_error_null_pointer_argument; + } + + if (num_bad_pixels > 0 && bad_pixels == nullptr) { + return heif_error_null_pointer_argument; + } + + SensorBadPixelsMap map; + map.component_ids.assign(component_ids, component_ids + num_component_ids); + map.correction_applied = (correction_applied != 0); + + map.bad_rows.assign(bad_rows, bad_rows + num_bad_rows); + map.bad_columns.assign(bad_columns, bad_columns + num_bad_columns); + + map.bad_pixels.resize(num_bad_pixels); + for (uint32_t i = 0; i < num_bad_pixels; i++) { + map.bad_pixels[i].row = bad_pixels[i].row; + map.bad_pixels[i].column = bad_pixels[i].column; + } + + image->image->add_sensor_bad_pixels_map(map); return heif_error_success; } -struct heif_error heif_item_get_property_raw_data(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - uint8_t* data_out) +int heif_image_get_number_of_sensor_bad_pixels_maps(const heif_image* image) { - if (!context || !data_out) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument passed in"}; + if (image == nullptr) { + return 0; } - std::shared_ptr box_other; - struct heif_error err = find_property(context, itemId, propertyId, &box_other); - if (err.code) { - return err; + return static_cast(image->image->get_sensor_bad_pixels_maps().size()); +} + + +heif_error heif_image_get_sensor_bad_pixels_map_info(const heif_image* image, + int map_index, + uint32_t* out_num_component_ids, + int* out_correction_applied, + uint32_t* out_num_bad_rows, + uint32_t* out_num_bad_columns, + uint32_t* out_num_bad_pixels) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; } - // TODO: every Box (not just Box_other) should have a get_raw_data() method. - if (box_other == nullptr) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; + const auto& maps = image->image->get_sensor_bad_pixels_maps(); + if (map_index < 0 || static_cast(map_index) >= maps.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Sensor bad pixels map index out of range."}; } - auto data = box_other->get_raw_data(); + const auto& m = maps[map_index]; + if (out_num_component_ids) { + *out_num_component_ids = static_cast(m.component_ids.size()); + } + if (out_correction_applied) { + *out_correction_applied = m.correction_applied ? 1 : 0; + } + if (out_num_bad_rows) { + *out_num_bad_rows = static_cast(m.bad_rows.size()); + } + if (out_num_bad_columns) { + *out_num_bad_columns = static_cast(m.bad_columns.size()); + } + if (out_num_bad_pixels) { + *out_num_bad_pixels = static_cast(m.bad_pixels.size()); + } + return heif_error_success; +} - std::copy(data.begin(), data.end(), data_out); + +heif_error heif_image_get_sensor_bad_pixels_map_data(const heif_image* image, + int map_index, + uint32_t* out_component_ids, + uint32_t* out_bad_rows, + uint32_t* out_bad_columns, + struct heif_bad_pixel* out_bad_pixels) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + const auto& maps = image->image->get_sensor_bad_pixels_maps(); + if (map_index < 0 || static_cast(map_index) >= maps.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Sensor bad pixels map index out of range."}; + } + + const auto& m = maps[map_index]; + + if (out_component_ids && !m.component_ids.empty()) { + std::copy(m.component_ids.begin(), m.component_ids.end(), out_component_ids); + } + + if (out_bad_rows && !m.bad_rows.empty()) { + std::copy(m.bad_rows.begin(), m.bad_rows.end(), out_bad_rows); + } + + if (out_bad_columns && !m.bad_columns.empty()) { + std::copy(m.bad_columns.begin(), m.bad_columns.end(), out_bad_columns); + } + + if (out_bad_pixels && !m.bad_pixels.empty()) { + for (size_t i = 0; i < m.bad_pixels.size(); i++) { + out_bad_pixels[i].row = m.bad_pixels[i].row; + out_bad_pixels[i].column = m.bad_pixels[i].column; + } + } return heif_error_success; } -struct heif_error heif_item_get_property_uuid_type(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - uint8_t extended_type[16]) + +heif_error heif_image_add_sensor_nuc(heif_image* image, + uint32_t num_component_ids, + const uint32_t* component_ids, + int nuc_is_applied, + uint32_t image_width, + uint32_t image_height, + const float* nuc_gains, + const float* nuc_offsets) { - if (!context || !extended_type) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument passed in"}; + if (image == nullptr || nuc_gains == nullptr || nuc_offsets == nullptr) { + return heif_error_null_pointer_argument; } - std::shared_ptr box_other; - struct heif_error err = find_property(context, itemId, propertyId, &box_other); - if (err.code) { - return err; + if (num_component_ids > 0 && component_ids == nullptr) { + return heif_error_null_pointer_argument; } - if (box_other == nullptr) { - return {heif_error_Usage_error, heif_suberror_Invalid_property, "this property is not read as a raw box"}; + if (image_width == 0 || image_height == 0) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "NUC image dimensions must be non-zero."}; } - auto uuid = box_other->get_uuid_type(); + SensorNonUniformityCorrection nuc; + nuc.component_ids.assign(component_ids, component_ids + num_component_ids); + nuc.nuc_is_applied = (nuc_is_applied != 0); + nuc.image_width = image_width; + nuc.image_height = image_height; - std::copy(uuid.begin(), uuid.end(), extended_type); + size_t num_pixels = size_t{image_width} * image_height; + nuc.nuc_gains.assign(nuc_gains, nuc_gains + num_pixels); + nuc.nuc_offsets.assign(nuc_offsets, nuc_offsets + num_pixels); + + image->image->add_sensor_nuc(nuc); + + return heif_error_success; +} + + +int heif_image_get_number_of_sensor_nucs(const heif_image* image) +{ + if (image == nullptr) { + return 0; + } + + return static_cast(image->image->get_sensor_nuc().size()); +} + + +heif_error heif_image_get_sensor_nuc_info(const heif_image* image, + int nuc_index, + uint32_t* out_num_component_ids, + int* out_nuc_is_applied, + uint32_t* out_image_width, + uint32_t* out_image_height) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + const auto& nucs = image->image->get_sensor_nuc(); + if (nuc_index < 0 || static_cast(nuc_index) >= nucs.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Sensor NUC index out of range."}; + } + + const auto& n = nucs[nuc_index]; + if (out_num_component_ids) { + *out_num_component_ids = static_cast(n.component_ids.size()); + } + if (out_nuc_is_applied) { + *out_nuc_is_applied = n.nuc_is_applied ? 1 : 0; + } + if (out_image_width) { + *out_image_width = n.image_width; + } + if (out_image_height) { + *out_image_height = n.image_height; + } return heif_error_success; } + + +heif_error heif_image_get_sensor_nuc_data(const heif_image* image, + int nuc_index, + uint32_t* out_component_ids, + float* out_nuc_gains, + float* out_nuc_offsets) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + const auto& nucs = image->image->get_sensor_nuc(); + if (nuc_index < 0 || static_cast(nuc_index) >= nucs.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Sensor NUC index out of range."}; + } + + const auto& n = nucs[nuc_index]; + + if (out_component_ids && !n.component_ids.empty()) { + std::copy(n.component_ids.begin(), n.component_ids.end(), out_component_ids); + } + + size_t num_pixels = size_t{n.image_width} * n.image_height; + + if (out_nuc_gains && !n.nuc_gains.empty()) { + std::copy(n.nuc_gains.begin(), n.nuc_gains.begin() + num_pixels, out_nuc_gains); + } + + if (out_nuc_offsets && !n.nuc_offsets.empty()) { + std::copy(n.nuc_offsets.begin(), n.nuc_offsets.begin() + num_pixels, out_nuc_offsets); + } + + return heif_error_success; +} + + +heif_error heif_image_set_chroma_location(heif_image* image, uint8_t chroma_location) +{ + if (image == nullptr) { + return heif_error_null_pointer_argument; + } + + if (chroma_location > 6) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Chroma location must be in the range 0-6."}; + } + + image->image->set_chroma_location(chroma_location); + + return heif_error_success; +} + + +int heif_image_has_chroma_location(const heif_image* image) +{ + if (image == nullptr) { + return 0; + } + + return image->image->has_chroma_location() ? 1 : 0; +} + + +uint8_t heif_image_get_chroma_location(const heif_image* image) +{ + if (image == nullptr) { + return 0; + } + + return image->image->get_chroma_location(); +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_properties.h libheif-1.23.4/libheif/api/libheif/heif_properties.h --- libheif-1.19.8/libheif/api/libheif/heif_properties.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_properties.h 2026-09-06 14:45:17.000000000 +0000 @@ -29,7 +29,7 @@ // ------------------------- item properties ------------------------- -enum heif_item_property_type +typedef enum heif_item_property_type { // heif_item_property_unknown = -1, heif_item_property_type_invalid = 0, @@ -40,8 +40,9 @@ heif_item_property_type_image_size = heif_fourcc('i', 's', 'p', 'e'), heif_item_property_type_uuid = heif_fourcc('u', 'u', 'i', 'd'), heif_item_property_type_tai_clock_info = heif_fourcc('t', 'a', 'i', 'c'), - heif_item_property_type_tai_timestamp = heif_fourcc('i', 't', 'a', 'i') -}; + heif_item_property_type_tai_timestamp = heif_fourcc('i', 't', 'a', 'i'), + heif_item_property_type_extended_language = heif_fourcc('e', 'l', 'n', 'g') +} heif_item_property_type; // Get the heif_property_id for a heif_item_id. // You may specify which property 'type' you want to receive. @@ -49,7 +50,7 @@ // The number of properties is returned, which are not more than 'count' if (out_list != nullptr). // By setting out_list==nullptr, you can query the number of properties, 'count' is ignored. LIBHEIF_API -int heif_item_get_properties_of_type(const struct heif_context* context, +int heif_item_get_properties_of_type(const heif_context* context, heif_item_id id, enum heif_item_property_type type, heif_property_id* out_list, @@ -60,18 +61,18 @@ // The number of properties is returned, which are not more than 'count' if (out_list != nullptr). // By setting out_list==nullptr, you can query the number of properties, 'count' is ignored. LIBHEIF_API -int heif_item_get_transformation_properties(const struct heif_context* context, +int heif_item_get_transformation_properties(const heif_context* context, heif_item_id id, heif_property_id* out_list, int count); LIBHEIF_API -enum heif_item_property_type heif_item_get_property_type(const struct heif_context* context, +enum heif_item_property_type heif_item_get_property_type(const heif_context* context, heif_item_id id, heif_property_id property_id); // The strings are managed by libheif. They will be deleted in heif_property_user_description_release(). -struct heif_property_user_description +typedef struct heif_property_user_description { int version; @@ -81,54 +82,59 @@ const char* name; const char* description; const char* tags; -}; +} heif_property_user_description; // Get the "udes" user description property content. // Undefined strings are returned as empty strings. LIBHEIF_API -struct heif_error heif_item_get_property_user_description(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - struct heif_property_user_description** out); +heif_error heif_item_get_property_user_description(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + heif_property_user_description** out); // Add a "udes" user description property to the item. // If any string pointers are NULL, an empty string will be used instead. LIBHEIF_API -struct heif_error heif_item_add_property_user_description(const struct heif_context* context, - heif_item_id itemId, - const struct heif_property_user_description* description, - heif_property_id* out_propertyId); +heif_error heif_item_add_property_user_description(const heif_context* context, + heif_item_id itemId, + const heif_property_user_description* description, + heif_property_id* out_propertyId); // Release all strings and the object itself. // Only call for objects that you received from heif_item_get_property_user_description(). LIBHEIF_API -void heif_property_user_description_release(struct heif_property_user_description*); +void heif_property_user_description_release(heif_property_user_description*); -enum heif_transform_mirror_direction +typedef enum heif_transform_mirror_direction { heif_transform_mirror_direction_invalid = -1, heif_transform_mirror_direction_vertical = 0, // flip image vertically heif_transform_mirror_direction_horizontal = 1 // flip image horizontally -}; +} heif_transform_mirror_direction; // Will return 'heif_transform_mirror_direction_invalid' in case of error. +// If 'propertyId==0', it returns the first imir property found. LIBHEIF_API -enum heif_transform_mirror_direction heif_item_get_property_transform_mirror(const struct heif_context* context, +enum heif_transform_mirror_direction heif_item_get_property_transform_mirror(const heif_context* context, heif_item_id itemId, heif_property_id propertyId); // Returns only 0, 90, 180, or 270 angle values. // Returns -1 in case of error (but it will only return an error in case of wrong usage). +// If 'propertyId==0', it returns the first irot property found. LIBHEIF_API -int heif_item_get_property_transform_rotation_ccw(const struct heif_context* context, +int heif_item_get_property_transform_rotation_ccw(const heif_context* context, heif_item_id itemId, heif_property_id propertyId); // Returns the number of pixels that should be removed from the four edges. // Because of the way this data is stored, you have to pass the image size at the moment of the crop operation // to compute the cropped border sizes. +// If 'propertyId==0', it returns the first clap property found. +// If the clap cannot be applied to the given image size (zero size or a size beyond the +// supported coordinate range), all four borders are set to 0. LIBHEIF_API -void heif_item_get_property_transform_crop_borders(const struct heif_context* context, +void heif_item_get_property_transform_crop_borders(const heif_context* context, heif_item_id itemId, heif_property_id propertyId, int image_width, int image_height, @@ -145,28 +151,28 @@ * @param out_propertyId Outputs the id of the inserted property. Can be NULL. */ LIBHEIF_API -struct heif_error heif_item_add_raw_property(const struct heif_context* context, - heif_item_id itemId, - uint32_t fourcc_type, - const uint8_t* uuid_type, - const uint8_t* data, size_t size, - int is_essential, - heif_property_id* out_propertyId); +heif_error heif_item_add_raw_property(const heif_context* context, + heif_item_id itemId, + uint32_t fourcc_type, + const uint8_t* uuid_type, + const uint8_t* data, size_t size, + int is_essential, + heif_property_id* out_propertyId); LIBHEIF_API -struct heif_error heif_item_get_property_raw_size(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - size_t* out_size); +heif_error heif_item_get_property_raw_size(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + size_t* out_size); /** * @param out_data User-supplied array to write the property data to. The required size of the output array is given by heif_item_get_property_raw_size(). */ LIBHEIF_API -struct heif_error heif_item_get_property_raw_data(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - uint8_t* out_data); +heif_error heif_item_get_property_raw_data(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + uint8_t* out_data); /** * Get the extended type for an extended "uuid" box. @@ -182,10 +188,297 @@ * @return heif_error_success or an error indicating the failure */ LIBHEIF_API -struct heif_error heif_item_get_property_uuid_type(const struct heif_context* context, - heif_item_id itemId, - heif_property_id propertyId, - uint8_t out_extended_type[16]); +heif_error heif_item_get_property_uuid_type(const heif_context* context, + heif_item_id itemId, + heif_property_id propertyId, + uint8_t out_extended_type[16]); + + +// ------------------------- ISO/IEC 23001-17 properties ------------------------- +// +// Imaging-metadata properties defined by ISO/IEC 23001-17 (uncompressed-image +// format) and currently surfaced on uncompressed images. They operate on a +// heif_image (the in-memory pixel image) rather than on (context, item_id) like +// the box-level property API above. + + +// --- Bayer / filter array pattern (cpat box) + +typedef struct heif_bayer_pattern_pixel +{ + uint32_t component_id; + float component_gain; +} heif_bayer_pattern_pixel; + + +// Set a Bayer / filter array pattern on an image. +// The pattern is a 2D array of component indices with dimensions pattern_width x pattern_height. +// The number of entries in patternPixels must be pattern_width * pattern_height. +// The component_index values are indices into the cmpd component definition table. +// On the encoder path, these indices are generated by heif_image_add_component() and the +// encoder adds reference components to cmpd for pattern entries that don't have image planes. +// On the decoder path, they come directly from the cpat box. +LIBHEIF_API +heif_error heif_image_set_bayer_pattern(heif_image*, + uint32_t bayer_component_id, + uint16_t pattern_width, + uint16_t pattern_height, + const heif_bayer_pattern_pixel* patternPixels); + +// Add a reference-only component to the image's component description table for use as +// a Bayer pattern entry. The component is registered (its component_type appears in the +// cmpd box) but carries no pixel plane of its own — the actual pixel data lives in the +// single combined Bayer component (added with heif_image_add_component()). +// +// Use this for the per-cell colors that the Bayer pattern references (e.g. red, green, +// blue) when those colors have no standalone plane. Pass the returned component_id in +// heif_bayer_pattern_pixel::component_id of the patternPixels array given to +// heif_image_set_bayer_pattern(). +// +// component_type: one of heif_cmpd_component_type_* (e.g. _red, _green, _blue). +// out_component_id: receives the minted component id. Must not be NULL. +LIBHEIF_API +heif_error heif_image_add_bayer_component(heif_image*, + uint16_t component_type, + uint32_t* out_component_id); + +// Returns whether the image has a Bayer / filter array pattern. +// If the image has a pattern, out_pattern_width and out_pattern_height are set. +// Either output pointer may be NULL if the caller does not need that value. +LIBHEIF_API +int heif_image_get_bayer_pattern_size(const heif_image*, + uint32_t bayer_component_id, + uint16_t* out_pattern_width, + uint16_t* out_pattern_height); + +// Get the Bayer / filter array pattern pixels. +// The caller must provide an array large enough for pattern_width * pattern_height entries +// (use heif_image_get_bayer_pattern_size() to query the dimensions first). +// Returns heif_error_Ok on success, or an error if no pattern is set. +LIBHEIF_API +heif_error heif_image_get_bayer_pattern(const heif_image*, + uint32_t bayer_component_id, + heif_bayer_pattern_pixel* out_patternPixels); + + +// --- Polarization pattern (ISO 23001-17, Section 6.1.5) + +// Special float value indicating "no polarization filter" at a pattern position. +// On the wire this is the IEEE 754 bit pattern 0xFFFFFFFF (a signaling NaN). +// Test with heif_polarization_angle_is_no_filter() below, or with isnan()/std::isnan(). + +// Returns a float with the 0xFFFFFFFF bit pattern (NaN) representing "no polarization filter". +LIBHEIF_API +float heif_polarization_angle_no_filter(void); + +// Returns non-zero if the given angle has the "no filter" bit pattern (0xFFFFFFFF). +LIBHEIF_API +int heif_polarization_angle_is_no_filter(float angle); + +// Add a polarization pattern to an image. +// component_indices: array of component indices this pattern applies to (may be NULL if num_component_indices == 0, +// meaning the pattern applies to all components). +// polarization_angles: array of pattern_width * pattern_height float values. +// Each is an angle in degrees [0.0, 360.0), or heif_polarization_angle_no_filter() for "no filter". +// Multiple patterns can be added (one per distinct component group). +LIBHEIF_API +heif_error heif_image_add_polarization_pattern(heif_image*, + uint32_t num_component_indices, + const uint32_t* component_indices, + uint16_t pattern_width, + uint16_t pattern_height, + const float* polarization_angles); + +// Returns the number of polarization patterns on this image (0 if none). +LIBHEIF_API +int heif_image_get_number_of_polarization_patterns(const heif_image*); + +// Get the sizes/dimensions of a polarization pattern (to allocate arrays for the data query). +LIBHEIF_API +heif_error heif_image_get_polarization_pattern_info(const heif_image*, + int pattern_index, + uint32_t* out_num_component_indices, + uint16_t* out_pattern_width, + uint16_t* out_pattern_height); + +// Get the actual data of a polarization pattern. +// Caller must provide pre-allocated arrays: +// out_component_indices: num_component_indices entries (may be NULL if num_component_indices == 0) +// out_polarization_angles: pattern_width * pattern_height entries +LIBHEIF_API +heif_error heif_image_get_polarization_pattern_data(const heif_image*, + int pattern_index, + uint32_t* out_component_indices, + float* out_polarization_angles); + +// Find the polarization pattern index that applies to a given component index. +// Returns the pattern index (>= 0), or -1 if no pattern matches. +// A pattern with an empty component list (component_count == 0) matches all components. +LIBHEIF_API +int heif_image_get_polarization_pattern_index_for_component(const heif_image*, + uint32_t component_index); + + +// --- Sensor bad pixels map (ISO 23001-17, Section 6.1.7) + +typedef struct heif_bad_pixel +{ + uint32_t row; uint32_t column; +} heif_bad_pixel; + +// Add a sensor bad pixels map to an image. +// component_indices: array of component indices this map applies to (may be NULL if num_component_indices == 0, +// meaning the map applies to all components). +// Multiple maps can be added (one per distinct component group with different defects). +LIBHEIF_API +heif_error heif_image_add_sensor_bad_pixels_map(heif_image*, + uint32_t num_component_indices, + const uint32_t* component_indices, + int correction_applied, + uint32_t num_bad_rows, + const uint32_t* bad_rows, + uint32_t num_bad_columns, + const uint32_t* bad_columns, + uint32_t num_bad_pixels, + const heif_bad_pixel* bad_pixels); + +// Returns the number of sensor bad pixels maps on this image (0 if none). +LIBHEIF_API +int heif_image_get_number_of_sensor_bad_pixels_maps(const heif_image*); + +// Get the sizes of a sensor bad pixels map (to allocate arrays for the data query). +LIBHEIF_API +heif_error heif_image_get_sensor_bad_pixels_map_info(const heif_image*, + int map_index, + uint32_t* out_num_component_indices, + int* out_correction_applied, + uint32_t* out_num_bad_rows, + uint32_t* out_num_bad_columns, + uint32_t* out_num_bad_pixels); + +// Get the actual data of a sensor bad pixels map. +// Caller must provide pre-allocated arrays: +// out_component_indices: num_component_indices entries (may be NULL if num_component_indices == 0) +// out_bad_rows: num_bad_rows entries (may be NULL if num_bad_rows == 0) +// out_bad_columns: num_bad_columns entries (may be NULL if num_bad_columns == 0) +// out_bad_pixels: num_bad_pixels entries (may be NULL if num_bad_pixels == 0) +LIBHEIF_API +heif_error heif_image_get_sensor_bad_pixels_map_data(const heif_image*, + int map_index, + uint32_t* out_component_indices, + uint32_t* out_bad_rows, + uint32_t* out_bad_columns, + struct heif_bad_pixel* out_bad_pixels); + + +// --- Sensor non-uniformity correction (ISO 23001-17, Section 6.1.6) + +// Add a sensor non-uniformity correction table to an image. +// component_indices: array of component indices this NUC applies to (may be NULL if num_component_indices == 0, +// meaning it applies to all components). +// nuc_gains and nuc_offsets: arrays of image_width * image_height float values. +// Correction equation: y = nuc_gain * x + nuc_offset. +// Multiple NUC tables can be added (one per distinct component group). +LIBHEIF_API +heif_error heif_image_add_sensor_nuc(heif_image*, + uint32_t num_component_indices, + const uint32_t* component_indices, + int nuc_is_applied, + uint32_t image_width, + uint32_t image_height, + const float* nuc_gains, + const float* nuc_offsets); + +// Returns the number of sensor NUC tables on this image (0 if none). +LIBHEIF_API +int heif_image_get_number_of_sensor_nucs(const heif_image*); + +// Get the sizes of a sensor NUC table (to allocate arrays for the data query). +LIBHEIF_API +heif_error heif_image_get_sensor_nuc_info(const heif_image*, + int nuc_index, + uint32_t* out_num_component_indices, + int* out_nuc_is_applied, + uint32_t* out_image_width, + uint32_t* out_image_height); + +// Get the actual data of a sensor NUC table. +// Caller must provide pre-allocated arrays: +// out_component_indices: num_component_indices entries (may be NULL if num_component_indices == 0) +// out_nuc_gains: image_width * image_height entries +// out_nuc_offsets: image_width * image_height entries +LIBHEIF_API +heif_error heif_image_get_sensor_nuc_data(const heif_image*, + int nuc_index, + uint32_t* out_component_indices, + float* out_nuc_gains, + float* out_nuc_offsets); + + +// --- Chroma sample location (ISO 23091-2 / ITU-T H.273 + ISO 23001-17, Section 6.1.4) [cloc box] + +typedef enum heif_chroma420_sample_location { + // values 0-5 according to ISO 23091-2 / ITU-T H.273 + heif_chroma420_sample_location_00_05 = 0, + heif_chroma420_sample_location_05_05 = 1, + heif_chroma420_sample_location_00_00 = 2, + heif_chroma420_sample_location_05_00 = 3, + heif_chroma420_sample_location_00_10 = 4, + heif_chroma420_sample_location_05_10 = 5, + + // value 6 according to ISO 23001-17 + heif_chroma420_sample_location_00_00_01_00 = 6 +} heif_chroma420_sample_location; + +// Set the chroma sample location on an image. +// chroma_location must be in the range 0-6 (see heif_chroma420_sample_location). +LIBHEIF_API +heif_error heif_image_set_chroma_location(heif_image*, uint8_t chroma_location); + +// Returns non-zero if the image has a chroma sample location set. +LIBHEIF_API +int heif_image_has_chroma_location(const heif_image*); + +// Returns the chroma sample location (0-6), or 0 if none is set. +LIBHEIF_API +uint8_t heif_image_get_chroma_location(const heif_image*); + + +// ------------------------- intrinsic and extrinsic matrices ------------------------- + +typedef struct heif_camera_intrinsic_matrix +{ + double focal_length_x; + double focal_length_y; + double principal_point_x; + double principal_point_y; + double skew; +} heif_camera_intrinsic_matrix; + + +LIBHEIF_API +int heif_image_handle_has_camera_intrinsic_matrix(const heif_image_handle* handle); + +LIBHEIF_API +heif_error heif_image_handle_get_camera_intrinsic_matrix(const heif_image_handle* handle, + heif_camera_intrinsic_matrix* out_matrix); + + +typedef struct heif_camera_extrinsic_matrix heif_camera_extrinsic_matrix; + +LIBHEIF_API +int heif_image_handle_has_camera_extrinsic_matrix(const heif_image_handle* handle); + +LIBHEIF_API +heif_error heif_image_handle_get_camera_extrinsic_matrix(const heif_image_handle* handle, + heif_camera_extrinsic_matrix** out_matrix); + +LIBHEIF_API +void heif_camera_extrinsic_matrix_release(heif_camera_extrinsic_matrix*); + +LIBHEIF_API +heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const heif_camera_extrinsic_matrix*, + double* out_matrix_row_major); #ifdef __cplusplus } diff -Nru libheif-1.19.8/libheif/api/libheif/heif_regions.cc libheif-1.23.4/libheif/api/libheif/heif_regions.cc --- libheif-1.19.8/libheif/api/libheif/heif_regions.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_regions.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,18 +26,19 @@ #include "api_structs.h" #include "context.h" #include +#include #include #include #include #include -int heif_image_handle_get_number_of_region_items(const struct heif_image_handle* handle) +int heif_image_handle_get_number_of_region_items(const heif_image_handle* handle) { return (int) handle->image->get_region_item_ids().size(); } -int heif_image_handle_get_list_of_region_item_ids(const struct heif_image_handle* handle, +int heif_image_handle_get_list_of_region_item_ids(const heif_image_handle* handle, heif_item_id* item_ids, int max_count) { @@ -50,17 +51,17 @@ } -struct heif_error heif_context_get_region_item(const struct heif_context* context, - heif_item_id region_item_id, - struct heif_region_item** out) +heif_error heif_context_get_region_item(const heif_context* context, + heif_item_id region_item_id, + heif_region_item** out) { - if (out==nullptr) { - return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL argument"}; + if (out == nullptr) { + return heif_error_null_pointer_argument; } auto r = context->context->get_region_item(region_item_id); - if (r==nullptr) { + if (r == nullptr) { return {heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced, "Region item does not exist"}; } @@ -73,23 +74,23 @@ } -heif_item_id heif_region_item_get_id(struct heif_region_item* region_item) +heif_item_id heif_region_item_get_id(heif_region_item* region_item) { if (region_item == nullptr) { - return -1; + return 0; } return region_item->region_item->item_id; } -void heif_region_item_release(struct heif_region_item* region_item) +void heif_region_item_release(heif_region_item* region_item) { delete region_item; } -void heif_region_item_get_reference_size(struct heif_region_item* region_item, uint32_t* width, uint32_t* height) +void heif_region_item_get_reference_size(heif_region_item* region_item, uint32_t* width, uint32_t* height) { auto r = region_item->context->get_region_item(region_item->region_item->item_id); @@ -98,13 +99,13 @@ } -int heif_region_item_get_number_of_regions(const struct heif_region_item* region_item) +int heif_region_item_get_number_of_regions(const heif_region_item* region_item) { return region_item->region_item->get_number_of_regions(); } -int heif_region_item_get_list_of_regions(const struct heif_region_item* region_item, - struct heif_region** out_regions, +int heif_region_item_get_list_of_regions(const heif_region_item* region_item, + heif_region** out_regions, int max_count) { auto regions = region_item->region_item->get_regions(); @@ -124,247 +125,13 @@ } -struct heif_error heif_image_handle_add_region_item(struct heif_image_handle* image_handle, - uint32_t reference_width, uint32_t reference_height, - struct heif_region_item** out_region_item) -{ - std::shared_ptr regionItem = image_handle->context->add_region_item(reference_width, reference_height); - image_handle->image->add_region_item_id(regionItem->item_id); - - if (out_region_item) { - heif_region_item* item = new heif_region_item(); - item->context = image_handle->context; - item->region_item = std::move(regionItem); - - *out_region_item = item; - } - - return heif_error_success; -} - - -static struct heif_region* create_region(const std::shared_ptr& r, - heif_region_item* item) -{ - auto region = new heif_region(); - region->region = r; - region->region_item = item->region_item; - region->context = item->context; - return region; -} - - -struct heif_error heif_region_item_add_region_point(struct heif_region_item* item, - int32_t x, int32_t y, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->x = x; - region->y = y; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_rectangle(struct heif_region_item* item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->x = x; - region->y = y; - region->width = width; - region->height = height; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_ellipse(struct heif_region_item* item, - int32_t x, int32_t y, - uint32_t radius_x, uint32_t radius_y, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->x = x; - region->y = y; - region->radius_x = radius_x; - region->radius_y = radius_y; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_polygon(struct heif_region_item* item, - const int32_t* pts, int nPoints, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->points.resize(nPoints); - - for (int i=0;ipoints[i].x = pts[2*i+0]; - region->points[i].y = pts[2*i+1]; - } - - region->closed = true; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_polyline(struct heif_region_item* item, - const int32_t* pts, int nPoints, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->points.resize(nPoints); - - for (int i=0;ipoints[i].x = pts[2*i+0]; - region->points[i].y = pts[2*i+1]; - } - - region->closed = false; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_referenced_mask(struct heif_region_item* item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - heif_item_id mask_item_id, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->x = x; - region->y = y; - region->width = width; - region->height = height; - region->referenced_item = mask_item_id; - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - /* When the geometry 'mask' of a region is represented by a mask stored in - * another image item the image item containing the mask shall be identified - * by an item reference of type 'mask' from the region item to the image item - * containing the mask. */ - std::shared_ptr ctx = item->context; - ctx->add_region_referenced_mask_ref(item->region_item->item_id, mask_item_id); - - return heif_error_success; -} - - -struct heif_error heif_region_item_add_region_inline_mask_data(struct heif_region_item* item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - const uint8_t* mask_data, - size_t mask_data_len, - struct heif_region** out_region) -{ - auto region = std::make_shared(); - region->x = x; - region->y = y; - region->width = width; - region->height = height; - region->mask_data.resize(mask_data_len); - std::memcpy(region->mask_data.data(), mask_data, region->mask_data.size()); - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - -struct heif_error heif_region_item_add_region_inline_mask(struct heif_region_item* item, - int32_t x0, int32_t y0, - uint32_t width, uint32_t height, - heif_image* mask_image, - struct heif_region** out_region) -{ - if (! heif_image_has_channel(mask_image, heif_channel_Y)) - { - return {heif_error_Usage_error, heif_suberror_Nonexisting_image_channel_referenced, "Inline mask image must have a Y channel"}; - } - auto region = std::make_shared(); - region->x = x0; - region->y = y0; - region->width = width; - region->height = height; - region->mask_data.resize((width * height + 7) / 8); - memset(region->mask_data.data(), 0, region->mask_data.size()); - - uint32_t mask_height = mask_image->image->get_height(); - uint32_t mask_width = mask_image->image->get_width(); - int stride; - uint8_t* p = heif_image_get_plane(mask_image, heif_channel_Y, &stride); - uint64_t pixel_index = 0; - - for (uint32_t y = 0; y < mask_height; y++) { - for (uint32_t x = 0; x < mask_width; x++) { - uint8_t mask_bit = p[y * stride + x] & 0x80; // use high-order bit of the 8-bit mask value as binary mask value - region->mask_data.data()[pixel_index/8] |= uint8_t(mask_bit >> (pixel_index % 8)); - - pixel_index++; - } - } - - item->region_item->add_region(region); - - if (out_region) { - *out_region = create_region(region, item); - } - - return heif_error_success; -} - - -void heif_region_release(const struct heif_region* region) +void heif_region_release(const heif_region* region) { delete region; } -void heif_region_release_many(const struct heif_region* const* regions, int num) + +void heif_region_release_many(const heif_region* const* regions, int num) { for (int i = 0; i < num; i++) { delete regions[i]; @@ -372,16 +139,16 @@ } -enum heif_region_type heif_region_get_type(const struct heif_region* region) +heif_region_type heif_region_get_type(const heif_region* region) { return region->region->getRegionType(); } -struct heif_error heif_region_get_point(const struct heif_region* region, int32_t* x, int32_t* y) +heif_error heif_region_get_point(const heif_region* region, int32_t* x, int32_t* y) { if (!x || !y) { - return heif_error_invalid_parameter_value; + return heif_error_null_pointer_argument; } const std::shared_ptr point = std::dynamic_pointer_cast(region->region); @@ -395,10 +162,10 @@ } -struct heif_error heif_region_get_point_transformed(const struct heif_region* region, heif_item_id image_id, double* x, double* y) +heif_error heif_region_get_point_transformed(const struct heif_region* region, heif_item_id image_id, double* x, double* y) { if (!x || !y) { - return heif_error_invalid_parameter_value; + return heif_error_null_pointer_argument; } const std::shared_ptr point = std::dynamic_pointer_cast(region->region); @@ -406,7 +173,10 @@ auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id, region->region_item->reference_width, region->region_item->reference_height); - RegionCoordinateTransform::Point p = t.transform_point({(double) point->x, (double) point->y}); + if (!t) { + return t.error().error_struct(region->context.get()); + } + RegionCoordinateTransform::Point p = t->transform_point({(double) point->x, (double) point->y}); *x = p.x; *y = p.y; @@ -417,9 +187,9 @@ } -struct heif_error heif_region_get_rectangle(const struct heif_region* region, - int32_t* x, int32_t* y, - uint32_t* width, uint32_t* height) +heif_error heif_region_get_rectangle(const heif_region* region, + int32_t* x, int32_t* y, + uint32_t* width, uint32_t* height) { const std::shared_ptr rect = std::dynamic_pointer_cast(region->region); if (rect) { @@ -434,19 +204,22 @@ } -struct heif_error heif_region_get_rectangle_transformed(const struct heif_region* region, - heif_item_id image_id, - double* x, double* y, - double* width, double* height) +heif_error heif_region_get_rectangle_transformed(const heif_region* region, + heif_item_id image_id, + double* x, double* y, + double* width, double* height) { const std::shared_ptr rect = std::dynamic_pointer_cast(region->region); if (rect) { auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id, region->region_item->reference_width, region->region_item->reference_height); + if (!t) { + return t.error().error_struct(region->context.get()); + } - RegionCoordinateTransform::Point p = t.transform_point({(double) rect->x, (double) rect->y}); - RegionCoordinateTransform::Extent e = t.transform_extent({(double) rect->width, (double) rect->height}); + RegionCoordinateTransform::Point p = t->transform_point({(double) rect->x, (double) rect->y}); + RegionCoordinateTransform::Extent e = t->transform_extent({(double) rect->width, (double) rect->height}); *x = p.x; *y = p.y; @@ -459,9 +232,9 @@ } -struct heif_error heif_region_get_ellipse(const struct heif_region* region, - int32_t* x, int32_t* y, - uint32_t* radius_x, uint32_t* radius_y) +heif_error heif_region_get_ellipse(const heif_region* region, + int32_t* x, int32_t* y, + uint32_t* radius_x, uint32_t* radius_y) { const std::shared_ptr ellipse = std::dynamic_pointer_cast(region->region); if (ellipse) { @@ -476,19 +249,22 @@ } -struct heif_error heif_region_get_ellipse_transformed(const struct heif_region* region, - heif_item_id image_id, - double* x, double* y, - double* radius_x, double* radius_y) +heif_error heif_region_get_ellipse_transformed(const heif_region* region, + heif_item_id image_id, + double* x, double* y, + double* radius_x, double* radius_y) { const std::shared_ptr ellipse = std::dynamic_pointer_cast(region->region); if (ellipse) { auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id, region->region_item->reference_width, region->region_item->reference_height); + if (!t) { + return t.error().error_struct(region->context.get()); + } - RegionCoordinateTransform::Point p = t.transform_point({(double) ellipse->x, (double) ellipse->y}); - RegionCoordinateTransform::Extent e = t.transform_extent({(double) ellipse->radius_x, (double) ellipse->radius_y}); + RegionCoordinateTransform::Point p = t->transform_point({(double) ellipse->x, (double) ellipse->y}); + RegionCoordinateTransform::Extent e = t->transform_extent({(double) ellipse->radius_x, (double) ellipse->radius_y}); *x = p.x; *y = p.y; @@ -501,7 +277,7 @@ } -static int heif_region_get_poly_num_points(const struct heif_region* region) +static int heif_region_get_poly_num_points(const heif_region* region) { const std::shared_ptr polygon = std::dynamic_pointer_cast(region->region); if (polygon) { @@ -511,19 +287,13 @@ } -int heif_region_get_polygon_num_points(const struct heif_region* region) +int heif_region_get_polygon_num_points(const heif_region* region) { return heif_region_get_poly_num_points(region); } -int heif_region_get_polyline_num_points(const struct heif_region* region) -{ - return heif_region_get_poly_num_points(region); -} - - -static struct heif_error heif_region_get_poly_points(const struct heif_region* region, int32_t* pts) +static heif_error heif_region_get_poly_points(const heif_region* region, int32_t* pts) { if (pts == nullptr) { return heif_error_invalid_parameter_value; @@ -541,19 +311,13 @@ } -struct heif_error heif_region_get_polygon_points(const struct heif_region* region, int32_t* pts) +heif_error heif_region_get_polygon_points(const heif_region* region, int32_t* pts) { return heif_region_get_poly_points(region, pts); } -struct heif_error heif_region_get_polyline_points(const struct heif_region* region, int32_t* pts) -{ - return heif_region_get_poly_points(region, pts); -} - - -static struct heif_error heif_region_get_poly_points_scaled(const struct heif_region* region, double* pts, heif_item_id image_id) +static heif_error heif_region_get_poly_points_scaled(const heif_region* region, double* pts, heif_item_id image_id) { if (pts == nullptr) { return heif_error_invalid_parameter_value; @@ -564,10 +328,15 @@ auto t = RegionCoordinateTransform::create(region->context->get_heif_file(), image_id, region->region_item->reference_width, region->region_item->reference_height); + if (!t) { + return t.error().error_struct(region->context.get()); + } for (int i = 0; i < (int) poly->points.size(); i++) { - RegionCoordinateTransform::Point p = t.transform_point({(double) poly->points[i].x, - (double) poly->points[i].y}); + RegionCoordinateTransform::Point p = t->transform_point({ + (double) poly->points[i].x, + (double) poly->points[i].y + }); pts[2 * i + 0] = p.x; pts[2 * i + 1] = p.y; @@ -578,29 +347,41 @@ } -struct heif_error heif_region_get_polygon_points_transformed(const struct heif_region* region, heif_item_id image_id, double* pts) +heif_error heif_region_get_polygon_points_transformed(const heif_region* region, heif_item_id image_id, double* pts) { return heif_region_get_poly_points_scaled(region, pts, image_id); } -struct heif_error heif_region_get_polyline_points_transformed(const struct heif_region* region, heif_item_id image_id, double* pts) + +int heif_region_get_polyline_num_points(const heif_region* region) +{ + return heif_region_get_poly_num_points(region); +} + + +heif_error heif_region_get_polyline_points(const heif_region* region, int32_t* pts) +{ + return heif_region_get_poly_points(region, pts); +} + + +heif_error heif_region_get_polyline_points_transformed(const heif_region* region, heif_item_id image_id, double* pts) { return heif_region_get_poly_points_scaled(region, pts, image_id); } -struct heif_error heif_region_get_referenced_mask_ID(const struct heif_region* region, - int32_t* x, int32_t* y, - uint32_t* width, uint32_t* height, - heif_item_id *mask_item_id) + +heif_error heif_region_get_referenced_mask_ID(const heif_region* region, + int32_t* x, int32_t* y, + uint32_t* width, uint32_t* height, + heif_item_id* mask_item_id) { - if ((x == nullptr) || (y == nullptr) || (width == nullptr) || (height == nullptr) || (mask_item_id == nullptr)) - { - return heif_error_invalid_parameter_value; + if ((x == nullptr) || (y == nullptr) || (width == nullptr) || (height == nullptr) || (mask_item_id == nullptr)) { + return heif_error_null_pointer_argument; } const std::shared_ptr mask = std::dynamic_pointer_cast(region->region); - if (mask) - { + if (mask) { *x = mask->x; *y = mask->y; *width = mask->width; @@ -611,7 +392,8 @@ return heif_error_invalid_parameter_value; } -size_t heif_region_get_inline_mask_data_len(const struct heif_region* region) + +size_t heif_region_get_inline_mask_data_len(const heif_region* region) { const std::shared_ptr mask = std::dynamic_pointer_cast(region->region); if (mask) { @@ -620,71 +402,114 @@ return 0; } -struct heif_error heif_region_get_inline_mask_data(const struct heif_region* region, - int32_t* x, int32_t* y, - uint32_t* width, uint32_t* height, - uint8_t* data) + +static heif_region* create_region(const std::shared_ptr& r, + heif_region_item* item) { - if ((x == nullptr) || (y == nullptr) || (width == nullptr) || (height == nullptr)) - { - return heif_error_invalid_parameter_value; + auto region = new heif_region(); + region->region = r; + region->region_item = item->region_item; + region->context = item->context; + return region; +} + + +heif_error heif_region_item_add_region_inline_mask_data(heif_region_item* item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + const uint8_t* mask_data, + size_t mask_data_len, + heif_region** out_region) +{ + if (out_region) { + *out_region = nullptr; } - const std::shared_ptr mask = std::dynamic_pointer_cast(region->region); - if (mask) - { - *x = mask->x; - *y = mask->y; - *width = mask->width; - *height = mask->height; - memcpy(data, mask->mask_data.data(), mask->mask_data.size()); - return heif_error_success; + if (mask_data == nullptr) { + return heif_error_null_pointer_argument; } - return heif_error_invalid_parameter_value; + + if (width == 0 || height == 0) { + return {heif_error_Invalid_input, heif_suberror_Invalid_region_data, + "Inline mask region has zero width or height"}; + } + + // The mask is stored with one bit per pixel, no padding between rows. Only the + // very last byte is padded. This is the same canonical size that the file parser + // (RegionGeometry_InlineMask::parse) and the reader (heif_region_get_mask_image) + // compute from the geometry. The caller-supplied buffer must hold exactly that + // many bytes: a shorter buffer would make the reader run off the end of the + // allocation, and a longer buffer indicates that the caller's geometry and mask + // data disagree, which we reject rather than silently discard. + uint64_t mask_size = (static_cast(width) * height + 7) / 8; + if (mask_size > std::numeric_limits::max()) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "Inline mask size overflow"}; + } + + if (mask_data_len != static_cast(mask_size)) { + return {heif_error_Invalid_input, heif_suberror_Invalid_region_data, + "Inline mask data length does not match the given region size"}; + } + + auto region = std::make_shared(); + region->x = x; + region->y = y; + region->width = width; + region->height = height; + region->mask_data.resize(static_cast(mask_size)); + std::memcpy(region->mask_data.data(), mask_data, region->mask_data.size()); + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; } -static struct heif_error heif_region_get_inline_mask_image(const struct heif_region* region, - int32_t* out_x0, int32_t* out_y0, - uint32_t* out_width, uint32_t* out_height, - heif_image** out_mask_image) + +static heif_error heif_region_get_inline_mask_image(const heif_region* region, + int32_t* out_x0, int32_t* out_y0, + uint32_t* out_width, uint32_t* out_height, + heif_image** out_mask_image) { - if ((out_x0 == nullptr) || (out_y0 == nullptr) || (out_width == nullptr) || (out_height == nullptr)) - { - return heif_error_invalid_parameter_value; + if ((out_x0 == nullptr) || (out_y0 == nullptr) || (out_width == nullptr) || (out_height == nullptr)) { + return heif_error_null_pointer_argument; } const std::shared_ptr mask = std::dynamic_pointer_cast(region->region); - if (mask) - { + if (mask) { *out_x0 = mask->x; *out_y0 = mask->y; uint32_t width = *out_width = mask->width; - uint32_t height= *out_height = mask->height; - uint8_t* mask_data = mask->mask_data.data(); + uint32_t height = *out_height = mask->height; + const uint8_t* mask_data = mask->mask_data.data(); + // Defence in depth: the writer API and the file parser both guarantee that + // mask_data holds ceil(width*height/8) bytes, but never read beyond the actual + // buffer even if that invariant were somehow violated. + size_t mask_data_len = mask->mask_data.size(); heif_error err = heif_image_create(width, height, heif_colorspace_monochrome, heif_chroma_monochrome, out_mask_image); - if (err.code) - { + if (err.code) { return err; } err = heif_image_add_plane(*out_mask_image, heif_channel_Y, width, height, 8); - if (err.code) - { + if (err.code) { heif_image_release(*out_mask_image); return err; } - int stride; - uint8_t* p = heif_image_get_plane(*out_mask_image, heif_channel_Y, &stride); + size_t stride; + uint8_t* p = heif_image_get_plane2(*out_mask_image, heif_channel_Y, &stride); uint64_t pixel_index = 0; - for (uint32_t y = 0; y < height; y++) - { - for (uint32_t x = 0; x < width; x++) - { + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { uint64_t mask_byte = pixel_index / 8; uint8_t pixel_bit = uint8_t(0x80U >> (pixel_index % 8)); - p[y * stride + x] = (mask_data[mask_byte] & pixel_bit) ? 255 : 0; + p[y * stride + x] = (mask_byte < mask_data_len && (mask_data[mask_byte] & pixel_bit)) ? 255 : 0; pixel_index++; } @@ -695,13 +520,13 @@ } -struct heif_error heif_region_get_mask_image(const struct heif_region* region, - int32_t* x, int32_t* y, - uint32_t* width, uint32_t* height, - struct heif_image** mask_image) +heif_error heif_region_get_mask_image(const heif_region* region, + int32_t* x, int32_t* y, + uint32_t* width, uint32_t* height, + heif_image** mask_image) { if (region->region->getRegionType() == heif_region_type_inline_mask) { - return heif_region_get_inline_mask_image(region,x,y,width,height,mask_image); + return heif_region_get_inline_mask_image(region, x, y, width, height, mask_image); } else if (region->region->getRegionType() == heif_region_type_referenced_mask) { heif_item_id referenced_item_id = 0; @@ -729,3 +554,254 @@ return heif_error_invalid_parameter_value; } + + +heif_error heif_image_handle_add_region_item(heif_image_handle* image_handle, + uint32_t reference_width, uint32_t reference_height, + heif_region_item** out_region_item) +{ + auto regionItemResult = image_handle->context->add_region_item(reference_width, reference_height); + if (!regionItemResult) { + return regionItemResult.error_struct(image_handle->context.get()); + } + std::shared_ptr regionItem = *regionItemResult; + image_handle->image->add_region_item_id(regionItem->item_id); + + if (out_region_item) { + heif_region_item* item = new heif_region_item(); + item->context = image_handle->context; + item->region_item = std::move(regionItem); + + *out_region_item = item; + } + + return heif_error_success; +} + + +heif_error heif_region_item_add_region_point(heif_region_item* item, + int32_t x, int32_t y, + heif_region** out_region) +{ + auto region = std::make_shared(); + region->x = x; + region->y = y; + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; +} + + +heif_error heif_region_item_add_region_rectangle(heif_region_item* item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + heif_region** out_region) +{ + auto region = std::make_shared(); + region->x = x; + region->y = y; + region->width = width; + region->height = height; + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; +} + + +heif_error heif_region_item_add_region_ellipse(heif_region_item* item, + int32_t x, int32_t y, + uint32_t radius_x, uint32_t radius_y, + heif_region** out_region) +{ + auto region = std::make_shared(); + region->x = x; + region->y = y; + region->radius_x = radius_x; + region->radius_y = radius_y; + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; +} + + +// Shared implementation of heif_region_item_add_region_polygon() and +// heif_region_item_add_region_polyline(). `pts` holds 2*nPoints values in the +// order X1, Y1, X2, Y2, ... +static heif_error add_region_poly(heif_region_item* item, + const int32_t* pts, int nPoints, + bool closed, + heif_region** out_region) +{ + if (out_region) { + *out_region = nullptr; + } + + // We cannot verify that the caller's array really holds 2*nPoints values, that + // is part of the API contract (GHSA-prcj-g5xh-rw95). What we can check is that + // the arguments are self-consistent: a negative count would be converted to a + // huge size_t in resize() below and throw std::length_error through the C API, + // and a NULL array with a non-zero count would be dereferenced. + if (nPoints < 0) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, + "Number of polygon points must not be negative"}; + } + + if (nPoints > 0 && pts == nullptr) { + return heif_error_null_pointer_argument; + } + + return exception_guard([&]() -> heif_error { + auto region = std::make_shared(); + region->points.resize(nPoints); + + for (int i = 0; i < nPoints; i++) { + region->points[i].x = pts[2 * i + 0]; + region->points[i].y = pts[2 * i + 1]; + } + + region->closed = closed; + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; + }); +} + + +heif_error heif_region_item_add_region_polygon(heif_region_item* item, + const int32_t* pts, int nPoints, + heif_region** out_region) +{ + return add_region_poly(item, pts, nPoints, true, out_region); +} + + +heif_error heif_region_item_add_region_polyline(heif_region_item* item, + const int32_t* pts, int nPoints, + heif_region** out_region) +{ + return add_region_poly(item, pts, nPoints, false, out_region); +} + + +heif_error heif_region_item_add_region_referenced_mask(heif_region_item* item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + heif_item_id mask_item_id, + heif_region** out_region) +{ + auto region = std::make_shared(); + region->x = x; + region->y = y; + region->width = width; + region->height = height; + region->referenced_item = mask_item_id; + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + /* When the geometry 'mask' of a region is represented by a mask stored in + * another image item the image item containing the mask shall be identified + * by an item reference of type 'mask' from the region item to the image item + * containing the mask. */ + std::shared_ptr ctx = item->context; + ctx->add_region_referenced_mask_ref(item->region_item->item_id, mask_item_id); + + return heif_error_success; +} + + +heif_error heif_region_get_inline_mask_data(const heif_region* region, + int32_t* x, int32_t* y, + uint32_t* width, uint32_t* height, + uint8_t* data) +{ + if ((x == nullptr) || (y == nullptr) || (width == nullptr) || (height == nullptr)) { + return heif_error_null_pointer_argument; + } + + const std::shared_ptr mask = std::dynamic_pointer_cast(region->region); + if (mask) { + *x = mask->x; + *y = mask->y; + *width = mask->width; + *height = mask->height; + memcpy(data, mask->mask_data.data(), mask->mask_data.size()); + return heif_error_success; + } + return heif_error_invalid_parameter_value; +} + + +heif_error heif_region_item_add_region_inline_mask(heif_region_item* item, + int32_t x0, int32_t y0, + uint32_t width, uint32_t height, + heif_image* mask_image, + heif_region** out_region) +{ + if (!heif_image_has_channel(mask_image, heif_channel_Y)) { + return {heif_error_Usage_error, heif_suberror_Nonexisting_image_channel_referenced, "Inline mask image must have a Y channel"}; + } + auto region = std::make_shared(); + region->x = x0; + region->y = y0; + region->width = width; + region->height = height; + uint64_t mask_size = (static_cast(width) * height + 7) / 8; + if (mask_size > std::numeric_limits::max()) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, "Inline mask size overflow"}; + } + region->mask_data.resize(static_cast(mask_size)); + memset(region->mask_data.data(), 0, region->mask_data.size()); + + uint32_t mask_height = mask_image->image->get_height(); + uint32_t mask_width = mask_image->image->get_width(); + size_t stride; + uint8_t* p = heif_image_get_plane2(mask_image, heif_channel_Y, &stride); + + // The destination mask buffer is sized for the declared region (width x height). + // If the source image is larger than the region, crop it; if it is smaller, the + // remaining destination bits stay zero (zero-filled by the memset above). + // The destination bit index is computed from the declared 'width' so that each + // source row maps to the correct region row. + uint32_t copy_width = std::min(width, mask_width); + uint32_t copy_height = std::min(height, mask_height); + + for (uint32_t y = 0; y < copy_height; y++) { + for (uint32_t x = 0; x < copy_width; x++) { + uint8_t mask_bit = p[y * stride + x] & 0x80; // use high-order bit of the 8-bit mask value as binary mask value + uint64_t pixel_index = static_cast(y) * width + x; + region->mask_data.data()[pixel_index / 8] |= uint8_t(mask_bit >> (pixel_index % 8)); + } + } + + item->region_item->add_region(region); + + if (out_region) { + *out_region = create_region(region, item); + } + + return heif_error_success; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_regions.h libheif-1.23.4/libheif/api/libheif/heif_regions.h --- libheif-1.19.8/libheif/api/libheif/heif_regions.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_regions.h 2026-09-06 14:45:17.000000000 +0000 @@ -22,7 +22,9 @@ #ifndef LIBHEIF_HEIF_REGIONS_H #define LIBHEIF_HEIF_REGIONS_H -#include "heif.h" +#include "heif_image_handle.h" +#include "heif_library.h" +#include "heif_error.h" #ifdef __cplusplus extern "C" { @@ -32,7 +34,7 @@ // See ISO/IEC 23008-12:2022 Section 6.10 "Region items and region annotations" -struct heif_region_item; +typedef struct heif_region_item heif_region_item; /** * Region type. @@ -40,10 +42,10 @@ * Each region item will contain zero or more regions, which may have different geometry or * mask representations. */ -enum heif_region_type +typedef enum heif_region_type { /** - * Point gemetry. + * Point geometry. * * The region is represented by a single point. */ @@ -119,9 +121,9 @@ * considered to form a closed surface. Only the edge is part of the region. */ heif_region_type_polyline = 6 -}; +} heif_region_type; -struct heif_region; +typedef struct heif_region heif_region; /** * Get the number of region items that are attached to an image. @@ -130,7 +132,7 @@ * @return the number of region items, which can be zero. */ LIBHEIF_API -int heif_image_handle_get_number_of_region_items(const struct heif_image_handle* image_handle); +int heif_image_handle_get_number_of_region_items(const heif_image_handle* image_handle); /** * Get the region item identifiers for the region items attached to an image. @@ -151,7 +153,7 @@ * @return the number of region item identifiers that were returned. */ LIBHEIF_API -int heif_image_handle_get_list_of_region_item_ids(const struct heif_image_handle* image_handle, +int heif_image_handle_get_list_of_region_item_ids(const heif_image_handle* image_handle, heif_item_id* region_item_ids_array, int max_count); @@ -166,18 +168,18 @@ * @return heif_error_ok on success, or an error value indicating the problem */ LIBHEIF_API -struct heif_error heif_context_get_region_item(const struct heif_context* context, - heif_item_id region_item_id, - struct heif_region_item** out); +heif_error heif_context_get_region_item(const heif_context* context, + heif_item_id region_item_id, + heif_region_item** out); /** * Get the item identifier for a region item. * * @param region_item the region item to query - * @return the region item identifier (or -1 if the region_item is null) + * @return the region item identifier (or 0 if the region_item is null) */ LIBHEIF_API -heif_item_id heif_region_item_get_id(struct heif_region_item* region_item); +heif_item_id heif_region_item_get_id(heif_region_item* region_item); /** * Release a region item. @@ -187,7 +189,7 @@ * @param region_item the item to release. */ LIBHEIF_API -void heif_region_item_release(struct heif_region_item* region_item); +void heif_region_item_release(heif_region_item* region_item); /** * Get the reference size for a region item. @@ -200,7 +202,7 @@ * @param out_height the return value for the reference height (before any transformation) */ LIBHEIF_API -void heif_region_item_get_reference_size(struct heif_region_item*, uint32_t* out_width, uint32_t* out_height); +void heif_region_item_get_reference_size(heif_region_item*, uint32_t* out_width, uint32_t* out_height); /** * Get the number of regions within a region item. @@ -209,7 +211,7 @@ * @return the number of regions */ LIBHEIF_API -int heif_region_item_get_number_of_regions(const struct heif_region_item* region_item); +int heif_region_item_get_number_of_regions(const heif_region_item* region_item); /** * Get the regions that are part of a region item. @@ -234,8 +236,8 @@ * @return the number of regions that were returned. */ LIBHEIF_API -int heif_region_item_get_list_of_regions(const struct heif_region_item* region_item, - struct heif_region** out_regions_array, +int heif_region_item_get_list_of_regions(const heif_region_item* region_item, + heif_region** out_regions_array, int max_count); /** @@ -248,7 +250,7 @@ * \sa heif_region_release_many() to release the whole list */ LIBHEIF_API -void heif_region_release(const struct heif_region* region); +void heif_region_release(const heif_region* region); /** * Release a list of regions. @@ -261,7 +263,7 @@ * \sa heif_region_release() to release a single region */ LIBHEIF_API -void heif_region_release_many(const struct heif_region* const* regions_array, int num_items); +void heif_region_release_many(const heif_region* const* regions_array, int num_items); /** * Get the region type for a specified region. @@ -270,7 +272,7 @@ * @return the corresponding region type as an enumeration value */ LIBHEIF_API -enum heif_region_type heif_region_get_type(const struct heif_region* region); +enum heif_region_type heif_region_get_type(const heif_region* region); // When querying the region geometry, there is a version without and a version with "_transformed" suffix. // The version without returns the coordinates in the reference coordinate space. @@ -289,7 +291,7 @@ * \sa heif_region_get_point_transformed() for a version in pixels after all transformative properties have been applied. */ LIBHEIF_API -struct heif_error heif_region_get_point(const struct heif_region* region, int32_t* out_x, int32_t* out_y); +heif_error heif_region_get_point(const heif_region* region, int32_t* out_x, int32_t* out_y); /** * Get the transformed values for a point region. @@ -305,7 +307,7 @@ * \sa heif_region_get_point() for a version that returns the values in the reference coordinate space. */ LIBHEIF_API -struct heif_error heif_region_get_point_transformed(const struct heif_region* region, heif_item_id image_id, double* out_x, double* out_y); +heif_error heif_region_get_point_transformed(const heif_region* region, heif_item_id image_id, double* out_x, double* out_y); /** * Get the values for a rectangle region. @@ -325,9 +327,9 @@ * \sa heif_region_get_rectangle_transformed() for a version in pixels after all transformative properties have been applied. */ LIBHEIF_API -struct heif_error heif_region_get_rectangle(const struct heif_region* region, - int32_t* out_x, int32_t* out_y, - uint32_t* out_width, uint32_t* out_height); +heif_error heif_region_get_rectangle(const heif_region* region, + int32_t* out_x, int32_t* out_y, + uint32_t* out_width, uint32_t* out_height); /** * Get the transformed values for a rectangle region. @@ -348,10 +350,10 @@ * \sa heif_region_get_rectangle() for a version that returns the values in the reference coordinate space. */ LIBHEIF_API -struct heif_error heif_region_get_rectangle_transformed(const struct heif_region* region, - heif_item_id image_id, - double* out_x, double* out_y, - double* out_width, double* out_height); +heif_error heif_region_get_rectangle_transformed(const heif_region* region, + heif_item_id image_id, + double* out_x, double* out_y, + double* out_width, double* out_height); /** * Get the values for an ellipse region. @@ -371,9 +373,9 @@ * \sa heif_region_get_ellipse_transformed() for a version in pixels after all transformative properties have been applied. */ LIBHEIF_API -struct heif_error heif_region_get_ellipse(const struct heif_region* region, - int32_t* out_x, int32_t* out_y, - uint32_t* out_radius_x, uint32_t* out_radius_y); +heif_error heif_region_get_ellipse(const heif_region* region, + int32_t* out_x, int32_t* out_y, + uint32_t* out_radius_x, uint32_t* out_radius_y); /** @@ -395,10 +397,10 @@ * \sa heif_region_get_ellipse() for a version that returns the values in the reference coordinate space. */ LIBHEIF_API -struct heif_error heif_region_get_ellipse_transformed(const struct heif_region* region, - heif_item_id image_id, - double* out_x, double* out_y, - double* out_radius_x, double* out_radius_y); +heif_error heif_region_get_ellipse_transformed(const heif_region* region, + heif_item_id image_id, + double* out_x, double* out_y, + double* out_radius_x, double* out_radius_y); /** * Get the number of points in a polygon. @@ -407,7 +409,7 @@ * @return the number of points, or -1 on error. */ LIBHEIF_API -int heif_region_get_polygon_num_points(const struct heif_region* region); +int heif_region_get_polygon_num_points(const heif_region* region); /** * Get the points in a polygon region. @@ -420,7 +422,7 @@ * The points are returned as pairs of X,Y coordinates, in the order X1, * Y1, X2, Y2, ..., Xn, Yn. * - * @param region the region to equery, which must be of type #heif_region_type_polygon + * @param region the region to query, which must be of type #heif_region_type_polygon * @param out_pts_array the array to return the points in, which must have twice as many entries as there are points * in the polygon. * @return heif_error_ok on success, or an error value indicating the problem on failure @@ -428,8 +430,8 @@ * \sa heif_region_get_polygon_points_transformed() for a version in pixels after all transformative properties have been applied. */ LIBHEIF_API -struct heif_error heif_region_get_polygon_points(const struct heif_region* region, - int32_t* out_pts_array); +heif_error heif_region_get_polygon_points(const heif_region* region, + int32_t* out_pts_array); /** * Get the transformed points in a polygon region. @@ -442,7 +444,7 @@ * The points are returned as pairs of X,Y coordinates, in the order X1, * Y1, X2, Y2, ..., Xn, Yn. * - * @param region the region to equery, which must be of type #heif_region_type_polygon + * @param region the region to query, which must be of type #heif_region_type_polygon * @param image_id the identifier for the image to transform / scale the region to * @param out_pts_array the array to return the points in, which must have twice as many entries as there are points * in the polygon. @@ -451,9 +453,9 @@ * \sa heif_region_get_polygon_points() for a version that returns the values in the reference coordinate space. */ LIBHEIF_API -struct heif_error heif_region_get_polygon_points_transformed(const struct heif_region* region, - heif_item_id image_id, - double* out_pts_array); +heif_error heif_region_get_polygon_points_transformed(const heif_region* region, + heif_item_id image_id, + double* out_pts_array); /** * Get the number of points in a polyline. * @@ -461,7 +463,7 @@ * @return the number of points, or -1 on error. */ LIBHEIF_API -int heif_region_get_polyline_num_points(const struct heif_region* region); +int heif_region_get_polyline_num_points(const heif_region* region); /** * Get the points in a polyline region. @@ -484,7 +486,7 @@ * } * @endcode * - * @param region the region to equery, which must be of type #heif_region_type_polyline + * @param region the region to query, which must be of type #heif_region_type_polyline * @param out_pts_array the array to return the points in, which must have twice as many entries as there are points * in the polyline. * @return heif_error_ok on success, or an error value indicating the problem on failure @@ -492,8 +494,8 @@ * \sa heif_region_get_polyline_points_transformed() for a version in pixels after all transformative properties have been applied. */ LIBHEIF_API -struct heif_error heif_region_get_polyline_points(const struct heif_region* region, - int32_t* out_pts_array); +heif_error heif_region_get_polyline_points(const heif_region* region, + int32_t* out_pts_array); /** * Get the transformed points in a polyline region. @@ -515,9 +517,9 @@ * \sa heif_region_get_polyline_points() for a version that returns the values in the reference coordinate space. */ LIBHEIF_API -struct heif_error heif_region_get_polyline_points_transformed(const struct heif_region* region, - heif_item_id image_id, - double* out_pts_array); +heif_error heif_region_get_polyline_points_transformed(const heif_region* region, + heif_item_id image_id, + double* out_pts_array); /** * Get a referenced item mask region. @@ -554,10 +556,10 @@ * @return heif_error_ok on success, or an error value indicating the problem on failure */ LIBHEIF_API -struct heif_error heif_region_get_referenced_mask_ID(const struct heif_region* region, - int32_t* out_x, int32_t* out_y, - uint32_t* out_width, uint32_t* out_height, - heif_item_id *out_mask_item_id); +heif_error heif_region_get_referenced_mask_ID(const heif_region* region, + int32_t* out_x, int32_t* out_y, + uint32_t* out_width, uint32_t* out_height, + heif_item_id* out_mask_item_id); /** * Get the length of the data in an inline mask region. @@ -566,7 +568,7 @@ * @return the number of bytes in the mask data, or 0 on error. */ LIBHEIF_API -size_t heif_region_get_inline_mask_data_len(const struct heif_region* region); +size_t heif_region_get_inline_mask_data_len(const heif_region* region); /** @@ -599,10 +601,10 @@ * @return heif_error_ok on success, or an error value indicating the problem on failure */ LIBHEIF_API -struct heif_error heif_region_get_inline_mask_data(const struct heif_region* region, - int32_t* out_x, int32_t* out_y, - uint32_t* out_width, uint32_t* out_height, - uint8_t* out_mask_data); +heif_error heif_region_get_inline_mask_data(const heif_region* region, + int32_t* out_x, int32_t* out_y, + uint32_t* out_width, uint32_t* out_height, + uint8_t* out_mask_data); /** * Get a mask region image. @@ -632,10 +634,10 @@ * \note the caller is responsible for releasing the mask image */ LIBHEIF_API -struct heif_error heif_region_get_mask_image(const struct heif_region* region, - int32_t* out_x, int32_t* out_y, - uint32_t* out_width, uint32_t* out_height, - struct heif_image** out_mask_image); +heif_error heif_region_get_mask_image(const heif_region* region, + int32_t* out_x, int32_t* out_y, + uint32_t* out_width, uint32_t* out_height, + heif_image** out_mask_image); // --- adding region items @@ -654,9 +656,9 @@ * @return heif_error_ok on success, or an error indicating the problem on failure */ LIBHEIF_API -struct heif_error heif_image_handle_add_region_item(struct heif_image_handle* image_handle, - uint32_t reference_width, uint32_t reference_height, - struct heif_region_item** out_region_item); +heif_error heif_image_handle_add_region_item(heif_image_handle* image_handle, + uint32_t reference_width, uint32_t reference_height, + heif_region_item** out_region_item); /** * Add a point region to the region item. @@ -670,9 +672,9 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_point(struct heif_region_item* region_item, - int32_t x, int32_t y, - struct heif_region** out_region); +heif_error heif_region_item_add_region_point(heif_region_item* region_item, + int32_t x, int32_t y, + heif_region** out_region); /** * Add a rectangle region to the region item. @@ -688,10 +690,10 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_rectangle(struct heif_region_item* region_item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - struct heif_region** out_region); +heif_error heif_region_item_add_region_rectangle(heif_region_item* region_item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + heif_region** out_region); /** * Add a ellipse region to the region item. @@ -707,10 +709,10 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_ellipse(struct heif_region_item* region_item, - int32_t x, int32_t y, - uint32_t radius_x, uint32_t radius_y, - struct heif_region** out_region); +heif_error heif_region_item_add_region_ellipse(heif_region_item* region_item, + int32_t x, int32_t y, + uint32_t radius_x, uint32_t radius_y, + heif_region** out_region); /** * Add a polygon region to the region item. @@ -720,6 +722,12 @@ * The points are provided as pairs of X,Y coordinates, in the order X1, * Y1, X2, Y2, ..., Xn, Yn. * + * @c pts_array must point to at least `2 * nPoints` values. The library cannot + * verify the size of the array, so passing a shorter array reads beyond its end. + * @c nPoints must not be negative and @c pts_array must not be `NULL` when + * @c nPoints is greater than zero. Otherwise the function fails without + * modifying the region item. + * * @param region_item the region item that holds this polygon region * @param pts_array the array of points in X,Y order (see above) * @param nPoints the number of points @@ -730,9 +738,9 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_polygon(struct heif_region_item* region_item, - const int32_t* pts_array, int nPoints, - struct heif_region** out_region); +heif_error heif_region_item_add_region_polygon(heif_region_item* region_item, + const int32_t* pts_array, int nPoints, + heif_region** out_region); /** * Add a polyline region to the region item. @@ -743,6 +751,12 @@ * The points are provided as pairs of X,Y coordinates, in the order X1, * Y1, X2, Y2, ..., Xn, Yn. * + * @c pts_array must point to at least `2 * nPoints` values. The library cannot + * verify the size of the array, so passing a shorter array reads beyond its end. + * @c nPoints must not be negative and @c pts_array must not be `NULL` when + * @c nPoints is greater than zero. Otherwise the function fails without + * modifying the region item. + * * @param region_item the region item that holds this polyline region * @param pts_array the array of points in X,Y order (see above) * @param nPoints the number of points @@ -753,9 +767,9 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_polyline(struct heif_region_item* region_item, - const int32_t* pts_array, int nPoints, - struct heif_region** out_region); +heif_error heif_region_item_add_region_polyline(heif_region_item* region_item, + const int32_t* pts_array, int nPoints, + heif_region** out_region); /** @@ -794,11 +808,11 @@ * @note The `out_region` parameter is optional, and can be set to `NULL` if not needed. */ LIBHEIF_API -struct heif_error heif_region_item_add_region_referenced_mask(struct heif_region_item* region_item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - heif_item_id mask_item_id, - struct heif_region** out_region); +heif_error heif_region_item_add_region_referenced_mask(heif_region_item* region_item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + heif_item_id mask_item_id, + heif_region** out_region); /** @@ -812,23 +826,27 @@ * part of the region. If the bit value is `0`, the corresponding pixel is not part of the * region. * + * @c width and @c height must both be non-zero and @c mask_data_len must be exactly + * `(width * height + 7) / 8`, the number of bytes needed to hold one bit per pixel. + * Otherwise the function fails without modifying the region item. + * * @param region_item the region item that holds this mask region * @param x the x value for the top-left corner of this mask region * @param y the y value for the top-left corner of this mask region * @param width the width of this mask region * @param height the height of this mask region - * @param mask_data the location to return the mask data + * @param mask_data the mask data to store * @param mask_data_len the length of the mask data, in bytes * @param out_region pointer to pointer to the returned region (optional, see below) * @return heif_error_ok on success, or an error value indicating the problem on failure */ - LIBHEIF_API -struct heif_error heif_region_item_add_region_inline_mask_data(struct heif_region_item* region_item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - const uint8_t* mask_data, - size_t mask_data_len, - struct heif_region** out_region); +LIBHEIF_API +heif_error heif_region_item_add_region_inline_mask_data(heif_region_item* region_item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + const uint8_t* mask_data, + size_t mask_data_len, + heif_region** out_region); /** * Add an inline mask region image to the region item. @@ -853,12 +871,12 @@ * @param out_region pointer to pointer to the returned region (optional, see below) * @return heif_error_ok on success, or an error value indicating the problem on failure */ - LIBHEIF_API -struct heif_error heif_region_item_add_region_inline_mask(struct heif_region_item* region_item, - int32_t x, int32_t y, - uint32_t width, uint32_t height, - struct heif_image* image, - struct heif_region** out_region); +LIBHEIF_API +heif_error heif_region_item_add_region_inline_mask(heif_region_item* region_item, + int32_t x, int32_t y, + uint32_t width, uint32_t height, + heif_image* image, + heif_region** out_region); #ifdef __cplusplus } #endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_security.cc libheif-1.23.4/libheif/api/libheif/heif_security.cc --- libheif-1.19.8/libheif/api/libheif/heif_security.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_security.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,67 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_security.h" +#include "api_structs.h" + +#include "context.h" + + + +const heif_security_limits* heif_get_global_security_limits() +{ + return &global_security_limits; +} + + +const heif_security_limits* heif_get_disabled_security_limits() +{ + return &disabled_security_limits; +} + + +heif_security_limits* heif_context_get_security_limits(const heif_context* ctx) +{ + if (!ctx) { + return nullptr; + } + + return ctx->context->get_security_limits(); +} + + +heif_error heif_context_set_security_limits(heif_context* ctx, const heif_security_limits* limits) +{ + if (ctx==nullptr || limits==nullptr) { + return heif_error_null_pointer_argument; + } + + ctx->context->set_security_limits(limits); + + return heif_error_ok; +} + + +// DEPRECATED + +void heif_context_set_maximum_image_size_limit(heif_context* ctx, int maximum_width) +{ + ctx->context->get_security_limits()->max_image_size_pixels = static_cast(maximum_width) * maximum_width; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_security.h libheif-1.23.4/libheif/api/libheif/heif_security.h --- libheif-1.19.8/libheif/api/libheif/heif_security.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_security.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,124 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_SECURITY_H +#define LIBHEIF_HEIF_SECURITY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include + + +// --- security limits + +// If you set a limit to 0, the limit is disabled. +typedef struct heif_security_limits +{ + uint8_t version; + + // --- version 1 (v1.19.0) + + // Limit on the maximum image size to avoid allocating too much memory. + // For example, setting this to 32768^2 pixels = 1 Gigapixels results + // in 1.5 GB memory need for YUV-4:2:0 or 4 GB for RGB32. + uint64_t max_image_size_pixels; + uint64_t max_number_of_tiles; + // Also used for polarization pattern (splz) size limit. + uint32_t max_bayer_pattern_pixels; + uint32_t max_items; + + uint32_t max_color_profile_size; + uint64_t max_memory_block_size; + + uint32_t max_components; + + uint32_t max_iloc_extents_per_item; + uint32_t max_size_entity_group; + + uint32_t max_children_per_box; // for all boxes that are not covered by other limits + + // --- version 2 (v1.20.0) + + uint64_t max_total_memory; + uint32_t max_sample_description_box_entries; + uint32_t max_sample_group_description_box_entries; + + // --- version 3 (v1.21.0) + + uint32_t max_sequence_frames; + uint32_t max_number_of_file_brands; + + // --- version 4 (v1.22.0) + + uint32_t max_bad_pixels; + + // Upper bound (in bytes) on the pixel_size field of an uncompressed (ISO 23001-17) + // uncC box. Caps the byte stride between adjacent pixels and prevents pathological + // padding values from blowing up tile-size arithmetic. + uint32_t max_iso23001_17_pixel_size_bytes; + + // Internal: when libheif derives a limits struct from another one (e.g. to + // tighten the maximum image size for a specific decode), this points back to + // the registered context whose total-memory budget the allocation should be + // accounted against. nullptr means "this is a root context" (the registered + // one). User code should leave this as nullptr; the field is set internally. + const struct heif_security_limits* parent; +} heif_security_limits; + + +// The global security limits are the default for new heif_contexts. +// These global limits cannot be changed, but you can override the limits for a specific heif_context. +LIBHEIF_API +const heif_security_limits* heif_get_global_security_limits(void); + +// Returns a set of fully disabled security limits. Use with care and only after user confirmation. +LIBHEIF_API +const heif_security_limits* heif_get_disabled_security_limits(void); + +// Returns the security limits for a heif_context. +// By default, the limits are set to the global limits, but you can change them in the returned object. +LIBHEIF_API +heif_security_limits* heif_context_get_security_limits(const heif_context*); + +// Overwrites the security limits of a heif_context. +// This is a convenience function to easily copy limits. +LIBHEIF_API +heif_error heif_context_set_security_limits(heif_context*, const heif_security_limits*); + + +// --- DEPRECATED --- + +// Set the maximum image size security limit. This function will set the maximum image area (number of pixels) +// to maximum_width ^ 2. Alternatively to using this function, you can also set the maximum image area +// in the security limits structure returned by heif_context_get_security_limits(). +LIBHEIF_API +void heif_context_set_maximum_image_size_limit(heif_context* ctx, int maximum_width); + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_sequences.cc libheif-1.23.4/libheif/api/libheif/heif_sequences.cc --- libheif-1.19.8/libheif/api/libheif/heif_sequences.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_sequences.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,917 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_sequences.h" +#include "context.h" +#include "api_structs.h" +#include "file.h" +#include "sequences/track.h" +#include "sequences/track_visual.h" +#include "sequences/track_metadata.h" + +#include +#include +#include +#include +#include +#include +#include + + +int heif_context_has_sequence(const heif_context* ctx) +{ + return ctx->context->has_sequence(); +} + + +uint32_t heif_context_get_sequence_timescale(const heif_context* ctx) +{ + return ctx->context->get_sequence_timescale(); +} + + +uint64_t heif_context_get_sequence_duration(const heif_context* ctx) +{ + return ctx->context->get_sequence_duration(); +} + + +void heif_track_release(heif_track* track) +{ + delete track; +} + + +int heif_context_number_of_sequence_tracks(const heif_context* ctx) +{ + return ctx->context->get_number_of_tracks(); +} + + +void heif_context_get_track_ids(const heif_context* ctx, uint32_t out_track_id_array[]) +{ + std::vector IDs; + IDs = ctx->context->get_track_IDs(); + + for (uint32_t id : IDs) { + *out_track_id_array++ = id; + } +} + + +uint32_t heif_track_get_id(const heif_track* track) +{ + return track->track->get_id(); +} + + +// Use id=0 for the first visual track. +heif_track* heif_context_get_track(const heif_context* ctx, uint32_t track_id) +{ + auto trackResult = ctx->context->get_track(track_id); + if (!trackResult) { + return nullptr; + } + + auto* track = new heif_track; + track->track = *trackResult; + track->context = ctx->context; + + return track; +} + + +uint32_t heif_track_get_track_handler_type(const heif_track* track) +{ + return track->track->get_handler(); +} + +heif_auxiliary_track_info_type heif_track_get_auxiliary_info_type(const heif_track* track) +{ + return track->track->get_auxiliary_info_type(); +} + +const char* heif_track_get_auxiliary_info_type_urn(const heif_track* track) +{ + std::string type = track->track->get_auxiliary_info_type_urn(); + + if (type.empty()) { + return nullptr; + } + else { + char* type_c = new char[type.length() + 1]; + strcpy(type_c, type.c_str()); + return type_c; + } +} + + +int heif_track_has_alpha_channel(const heif_track* track) +{ + return track->track->has_alpha_channel(); +} + + +uint32_t heif_track_get_timescale(const heif_track* track) +{ + return track->track->get_timescale(); +} + + +uint32_t heif_track_get_number_of_repetitions(const heif_track* track) +{ + return track->track->get_number_of_repetitions(); +} + + +heif_error heif_track_get_image_resolution(const heif_track* track_ptr, uint16_t* out_width, uint16_t* out_height) +{ + auto track = track_ptr->track; + + auto visual_track = std::dynamic_pointer_cast(track); + if (!visual_track) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot get resolution of non-visual track." + }; + } + + if (out_width) *out_width = visual_track->get_width(); + if (out_height) *out_height = visual_track->get_height(); + + return heif_error_ok; +} + + +heif_error heif_track_decode_next_image(heif_track* track_ptr, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* options) +{ + if (out_img == nullptr) { + return heif_error_null_pointer_argument; + } + + return exception_guard([&]() -> heif_error { + // --- get the visual track + + auto track = track_ptr->track; + + // --- reached end of sequence ? + + if (track->end_of_sequence_reached()) { + *out_img = nullptr; + return {heif_error_End_of_sequence, heif_suberror_Unspecified, "End of sequence"}; + } + + // --- decode next sequence image + + std::unique_ptr opts(heif_decoding_options_alloc(), heif_decoding_options_free); + heif_decoding_options_copy(opts.get(), options); + + + auto visual_track = std::dynamic_pointer_cast(track); + if (!visual_track) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot get image from non-visual track." + }; + } + + auto decodingResult = visual_track->decode_next_image_sample(*opts); + if (!decodingResult) { + return decodingResult.error_struct(track_ptr->context.get()); + } + + std::shared_ptr img = *decodingResult; + + + // --- convert to output colorspace + + auto conversion_result = track_ptr->context->convert_to_output_colorspace(img, colorspace, chroma, *opts); + if (!conversion_result) { + return conversion_result.error_struct(track_ptr->context.get()); + } + else { + img = *conversion_result; + } + + *out_img = new heif_image(); + (*out_img)->image = std::move(img); + + return {}; + }); +} + + +uint32_t heif_image_get_duration(const heif_image* img) +{ + return img->image->get_sample_duration(); +} + + +uint32_t heif_track_get_sample_entry_type_of_first_cluster(const heif_track* track) +{ + return track->track->get_first_cluster_sample_entry_type(); +} + + +heif_error heif_track_get_urim_sample_entry_uri_of_first_cluster(const heif_track* track, const char** out_uri) +{ + Result uriResult = track->track->get_first_cluster_urim_uri(); + + if (!uriResult) { + return uriResult.error_struct(track->context.get()); + } + + if (out_uri) { + const std::string uri = std::move(*uriResult); + + char* s = new char[uri.size() + 1]; + strncpy(s, uri.c_str(), uri.size()); + s[uri.size()] = '\0'; + + *out_uri = s; + } + + return heif_error_ok; +} + + +// TODO(next major API version): promote this to public API — remove 'static', add +// the LIBHEIF_API attribute, and add its declaration to +// libheif/api/libheif/heif_sequences.h. It is the options-taking counterpart of +// heif_track_get_next_raw_sequence_sample() and the fix for GHSA-xw34-mjcp-jqh8 +// variant V7 (the raw-sample path otherwise cannot honor +// heif_decoding_options::ignore_sequence_editlist). It is kept 'static' for now +// because adding a new exported symbol / public declaration is not permitted in a +// stable-API bugfix release. (The non-terminating-loop aspect of V7 is already +// fixed by the clamp in Track::init_sample_timing_table(); this only closes the +// remaining functional gap.) +// +// Proposed header declaration: +// +// /** +// * Like heif_track_get_next_raw_sequence_sample(), but takes decoding options. +// * Set heif_decoding_options::ignore_sequence_editlist to iterate the raw media +// * timeline once, ignoring edit-list repetition. `options` may be NULL, which +// * behaves like heif_track_get_next_raw_sequence_sample(). +// */ +// LIBHEIF_API +// heif_error heif_track_get_next_raw_sequence_sample2(heif_track*, +// heif_raw_sequence_sample** out_sample, +// const heif_decoding_options* options); +static heif_error heif_track_get_next_raw_sequence_sample2(heif_track* track_ptr, + heif_raw_sequence_sample** out_sample, + const heif_decoding_options* options) +{ + // The body allocates a buffer of the file-controlled 'stsz' sample size. An + // out-of-memory condition must come back as heif_error_out_of_memory instead of + // letting std::bad_alloc unwind across the extern "C" boundary and abort the host + // process (GHSA-4rv4-953r-p24q, same class as GHSA-7p2q-crf9-xm46). The guard sits + // here so that both the public wrapper below and a future public *2 variant are + // covered. + return exception_guard([&]() -> heif_error { + if (out_sample == nullptr) { + return heif_error_null_pointer_argument; + } + + auto track = track_ptr->track; + + // `options` may be null (the no-options public wrapper below passes nullptr). + // get_next_sample_raw_data() treats null as "apply the edit list" and only reads + // options->ignore_sequence_editlist when options is non-null. + // + // We intentionally do not pre-check end_of_sequence_reached() here: that helper + // compares against the edit-list-applied output count, whereas + // get_next_sample_raw_data() applies ignore_sequence_editlist itself and signals + // heif_error_End_of_sequence at the correct (option-dependent) boundary. + auto decodingResult = track->get_next_sample_raw_data(options); + if (!decodingResult) { + return decodingResult.error_struct(track_ptr->context.get()); + } + + *out_sample = *decodingResult; + + return heif_error_success; + }); +} + + +heif_error heif_track_get_next_raw_sequence_sample(heif_track* track_ptr, + heif_raw_sequence_sample** out_sample) +{ + // Thin wrapper over the (currently internal) options-taking implementation, with + // no decoding options. Once heif_track_get_next_raw_sequence_sample2() is promoted + // to public API (see the TODO above), this stays as a convenience wrapper. + return heif_track_get_next_raw_sequence_sample2(track_ptr, out_sample, nullptr); +} + + +void heif_raw_sequence_sample_release(heif_raw_sequence_sample* sample) +{ + delete sample; +} + + +const uint8_t* heif_raw_sequence_sample_get_data(const heif_raw_sequence_sample* sample, size_t* out_array_size) +{ + if (out_array_size) { *out_array_size = sample->data.size(); } + + return sample->data.data(); +} + + +size_t heif_raw_sequence_sample_get_data_size(const heif_raw_sequence_sample* sample) +{ + return sample->data.size(); +} + + +uint32_t heif_raw_sequence_sample_get_duration(const heif_raw_sequence_sample* sample) +{ + return sample->duration; +} + + +// --- writing sequences + + +void heif_context_set_sequence_timescale(heif_context* ctx, uint32_t timescale) +{ + ctx->context->set_sequence_timescale(timescale); +} + + +void heif_context_set_number_of_sequence_repetitions(heif_context* ctx, uint32_t repetitions) +{ + ctx->context->set_number_of_sequence_repetitions(repetitions); +} + + +struct heif_track_options +{ + TrackOptions options; +}; + + +heif_track_options* heif_track_options_alloc() +{ + return new heif_track_options; +} + + +void heif_track_options_release(heif_track_options* options) +{ + delete options; +} + + +void heif_track_options_set_timescale(heif_track_options* options, uint32_t timescale) +{ + options->options.track_timescale = timescale; +} + + +void heif_track_options_set_interleaved_sample_aux_infos(heif_track_options* options, int interleaved_flag) +{ + options->options.write_sample_aux_infos_interleaved = (interleaved_flag != 0); +} + + +heif_error heif_track_options_enable_sample_tai_timestamps(heif_track_options* options, + const heif_tai_clock_info* tai_info, + heif_sample_aux_info_presence presence) +{ + if (presence != heif_sample_aux_info_presence_none && + tai_info == nullptr) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "NULL tai clock info passed for track with TAI timestamps" + }; + } + + options->options.with_sample_tai_timestamps = presence; + + // delete old object in case we are calling heif_track_options_enable_sample_tai_timestamps() multiple times + delete options->options.tai_clock_info; + + if (tai_info != nullptr) { + options->options.tai_clock_info = heif_tai_clock_info_alloc(); + heif_tai_clock_info_copy(options->options.tai_clock_info, tai_info); + } + else { + options->options.tai_clock_info = nullptr; + } + + return heif_error_ok; +} + + +void heif_track_options_enable_sample_gimi_content_ids(heif_track_options* options, + heif_sample_aux_info_presence presence) +{ + options->options.with_sample_content_ids = presence; +} + + +void heif_track_options_set_gimi_track_id(heif_track_options* options, + const char* track_id) +{ + if (track_id == nullptr) { + options->options.gimi_track_content_id.clear(); + return; + } + + options->options.gimi_track_content_id = track_id; +} + + +heif_sequence_encoding_options* heif_sequence_encoding_options_alloc() +{ + heif_sequence_encoding_options* options = new heif_sequence_encoding_options(); + + options->version = 3; + options->output_nclx_profile = nullptr; + + options->color_conversion_options.version = 1; + options->color_conversion_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; + options->color_conversion_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + options->color_conversion_options.only_use_preferred_chroma_algorithm = false; + + // version 2 + + options->gop_structure = heif_sequence_gop_structure_lowdelay; + options->keyframe_distance_min = 0; + options->keyframe_distance_max = 0; + options->save_alpha_channel = 1; + + // version 3 + + options->content_kind = heif_sequence_content_kind_auto; + + return options; +} + + +// overwrite the (possibly lower version) input options over the default options +void heif_sequence_encoding_options_copy(heif_sequence_encoding_options* dst, + const heif_sequence_encoding_options* src) +{ + if (src == nullptr) { + return; + } + + int min_version = std::min(dst->version, src->version); + + switch (min_version) { + case 3: + dst->content_kind = src->content_kind; + [[fallthrough]]; + case 2: + dst->gop_structure = src->gop_structure; + dst->keyframe_distance_min = src->keyframe_distance_min; + dst->keyframe_distance_max = src->keyframe_distance_max; + dst->save_alpha_channel = src->save_alpha_channel; + [[fallthrough]]; + case 1: + dst->output_nclx_profile = src->output_nclx_profile; + dst->color_conversion_options = src->color_conversion_options; + } +} + + +void heif_sequence_encoding_options_release(heif_sequence_encoding_options* options) +{ + delete options; +} + + +heif_error heif_context_add_visual_sequence_track(heif_context* ctx, + uint16_t width, uint16_t height, + heif_track_type track_type, + const heif_track_options* track_options, + const heif_sequence_encoding_options* encoding_options, + heif_track** out_track) +{ + if (track_type != heif_track_type_video && + track_type != heif_track_type_image_sequence) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "visual track has to be of type video or image sequence" + }; + } + + TrackOptions default_track_info; + const TrackOptions* track_info = &default_track_info; + if (track_options != nullptr) { + track_info = &track_options->options; + } + + Result > addResult = ctx->context->add_visual_sequence_track(track_info, track_type, width, height); + if (!addResult) { + return addResult.error_struct(ctx->context.get()); + } + + if (out_track) { + auto* track = new heif_track; + track->track = *addResult; + track->context = ctx->context; + + *out_track = track; + } + + return heif_error_ok; +} + + +void heif_image_set_duration(heif_image* img, uint32_t duration) +{ + img->image->set_sample_duration(duration); +} + + +heif_error heif_track_encode_end_of_sequence(heif_track* track, + heif_encoder* encoder) +{ + // the input track must be a visual track + + auto visual_track = std::dynamic_pointer_cast(track->track); + if (!visual_track) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot encode image for non-visual track." + }; + } + + visual_track->encode_end_of_sequence(encoder); + + return heif_error_ok; +} + + +heif_error heif_track_encode_sequence_image(heif_track* track, + const heif_image* input_image, + heif_encoder* encoder, + const heif_sequence_encoding_options* sequence_encoding_options) +{ + // the input track must be a visual track + + auto visual_track = std::dynamic_pointer_cast(track->track); + if (!visual_track) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot encode image for non-visual track." + }; + } + +#if 0 + // convert heif_sequence_encoding_options to heif_encoding_options that is used by track->encode_image() + + heif_encoding_options* encoding_options = heif_encoding_options_alloc(); + heif_color_profile_nclx nclx; + if (sequence_encoding_options) { + // the const_cast<> is ok, because output_nclx_profile will not be changed. It should actually be const, but we cannot change that. + encoding_options->output_nclx_profile = const_cast(sequence_encoding_options->output_nclx_profile); + encoding_options->color_conversion_options = sequence_encoding_options->color_conversion_options; + + if (encoding_options->output_nclx_profile == nullptr) { + if (input_image->image->has_nclx_color_profile()) { + nclx_profile input_nclx = input_image->image->get_color_profile_nclx(); + + encoding_options->output_nclx_profile = &nclx; + nclx.version = 1; + nclx.color_primaries = (heif_color_primaries) input_nclx.get_colour_primaries(); + nclx.transfer_characteristics = (heif_transfer_characteristics) input_nclx.get_transfer_characteristics(); + nclx.matrix_coefficients = (heif_matrix_coefficients) input_nclx.get_matrix_coefficients(); + nclx.full_range_flag = input_nclx.get_full_range_flag(); + } + } + } +#endif + + // encode the image + + auto error = visual_track->encode_image(input_image->image, + encoder, + sequence_encoding_options, + heif_image_input_class_normal); +#if 0 + heif_encoding_options_free(encoding_options); +#endif + + if (error.error_code) { + return error.error_struct(track->context.get()); + } + + return heif_error_ok; +} + + +heif_error heif_context_add_uri_metadata_sequence_track(heif_context* ctx, + const char* uri, + const heif_track_options* track_options, + heif_track** out_track) +{ + TrackOptions default_track_info; + const TrackOptions* track_info = &default_track_info; + if (track_options != nullptr) { + track_info = &track_options->options; + } + + Result > addResult = ctx->context->add_uri_metadata_sequence_track(track_info, uri); + if (!addResult) { + return addResult.error_struct(ctx->context.get()); + } + + if (out_track) { + auto* track = new heif_track; + track->track = *addResult; + track->context = ctx->context; + + *out_track = track; + } + + return heif_error_ok; +} + + +heif_raw_sequence_sample* heif_raw_sequence_sample_alloc() +{ + return new heif_raw_sequence_sample(); +} + + +heif_error heif_raw_sequence_sample_set_data(heif_raw_sequence_sample* sample, const uint8_t* data, size_t size) +{ + // TODO: do we have to check the vector memory allocation? + + sample->data.clear(); + sample->data.insert(sample->data.begin(), data, data + size); + + return heif_error_ok; +} + + +void heif_raw_sequence_sample_set_duration(heif_raw_sequence_sample* sample, uint32_t duration) +{ + sample->duration = duration; +} + + +heif_error heif_track_add_raw_sequence_sample(heif_track* track, + const heif_raw_sequence_sample* sample) +{ + auto metadata_track = std::dynamic_pointer_cast(track->track); + if (!metadata_track) { + return { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot save metadata in a non-metadata track." + }; + } + + auto error = metadata_track->write_raw_metadata(sample); + if (error.error_code) { + return error.error_struct(track->context.get()); + } + + return heif_error_ok; +} + + +int heif_track_get_number_of_sample_aux_infos(const heif_track* track) +{ + std::vector aux_info_types = track->track->get_sample_aux_info_types(); + return (int) aux_info_types.size(); +} + + +void heif_track_get_sample_aux_info_types(const heif_track* track, heif_sample_aux_info_type out_types[]) +{ + std::vector aux_info_types = track->track->get_sample_aux_info_types(); + for (size_t i = 0; i < aux_info_types.size(); i++) { + out_types[i] = aux_info_types[i]; + } +} + + +const char* heif_track_get_gimi_track_content_id(const heif_track* track) +{ + std::string contentId = track->track->get_track_info().gimi_track_content_id; + if (contentId.empty()) { + return nullptr; + } + + char* id = new char[contentId.length() + 1]; + strcpy(id, contentId.c_str()); + + return id; +} + + +const char* heif_image_get_gimi_sample_content_id(const heif_image* img) +{ + if (!img->image->has_gimi_sample_content_id()) { + return nullptr; + } + + auto id_string = img->image->get_gimi_sample_content_id(); + char* id = new char[id_string.length() + 1]; + strcpy(id, id_string.c_str()); + + return id; +} + + +const char* heif_raw_sequence_sample_get_gimi_sample_content_id(const heif_raw_sequence_sample* sample) +{ + char* s = new char[sample->gimi_sample_content_id.size() + 1]; + strcpy(s, sample->gimi_sample_content_id.c_str()); + return s; +} + + +void heif_image_set_gimi_sample_content_id(heif_image* img, const char* contentID) +{ + img->image->set_gimi_sample_content_id(contentID ? contentID : std::string{}); +} + + +void heif_raw_sequence_sample_set_gimi_sample_content_id(heif_raw_sequence_sample* sample, const char* contentID) +{ + if (contentID) { + sample->gimi_sample_content_id = contentID; + } + else { + sample->gimi_sample_content_id.clear(); + } +} + + +int heif_raw_sequence_sample_has_tai_timestamp(const heif_raw_sequence_sample* sample) +{ + return sample->timestamp ? 1 : 0; +} + + +const struct heif_tai_timestamp_packet* heif_raw_sequence_sample_get_tai_timestamp(const heif_raw_sequence_sample* sample) +{ + if (!sample->timestamp) { + return nullptr; + } + + return sample->timestamp; +} + + +void heif_raw_sequence_sample_set_tai_timestamp(heif_raw_sequence_sample* sample, + const heif_tai_timestamp_packet* timestamp) +{ + // release of timestamp in case we overwrite it + heif_tai_timestamp_packet_release(sample->timestamp); + + sample->timestamp = heif_tai_timestamp_packet_alloc(); + heif_tai_timestamp_packet_copy(sample->timestamp, timestamp); +} + + +const heif_tai_clock_info* heif_track_get_tai_clock_info_of_first_cluster(heif_track* track) +{ + auto first_taic = track->track->get_first_cluster_taic(); + if (!first_taic) { + return nullptr; + } + + return first_taic->get_tai_clock_info(); +} + + +void heif_track_add_reference_to_track(heif_track* track, uint32_t reference_type, const heif_track* to_track) +{ + track->track->add_reference_to_track(reference_type, to_track->track->get_id()); +} + + +size_t heif_track_get_number_of_track_reference_types(const heif_track* track) +{ + auto tref = track->track->get_tref_box(); + if (!tref) { + return 0; + } + + return tref->get_number_of_reference_types(); +} + + +void heif_track_get_track_reference_types(const heif_track* track, uint32_t out_reference_types[]) +{ + auto tref = track->track->get_tref_box(); + if (!tref) { + return; + } + + auto refTypes = tref->get_reference_types(); + for (size_t i = 0; i < refTypes.size(); i++) { + out_reference_types[i] = refTypes[i]; + } +} + + +size_t heif_track_get_number_of_track_reference_of_type(const heif_track* track, uint32_t reference_type) +{ + auto tref = track->track->get_tref_box(); + if (!tref) { + return 0; + } + + return tref->get_number_of_references_of_type(reference_type); +} + + +size_t heif_track_get_references_from_track(const heif_track* track, uint32_t reference_type, uint32_t out_to_track_id[]) +{ + auto tref = track->track->get_tref_box(); + if (!tref) { + return 0; + } + + auto refs = tref->get_references(reference_type); + for (size_t i = 0; i < refs.size(); i++) { + out_to_track_id[i] = refs[i]; + } + + return refs.size(); +} + + +size_t heif_track_find_referring_tracks(const heif_track* track, uint32_t reference_type, uint32_t out_track_id[], size_t array_size) +{ + size_t nFound = 0; + + // iterate through all tracks + + auto trackIDs = track->context->get_track_IDs(); + for (auto id : trackIDs) { + // a track should never reference itself + if (id == track->track->get_id()) { + continue; + } + + // get the other track object + + auto other_trackResult = track->context->get_track(id); + if (!other_trackResult) { + continue; // TODO: should we return an error in this case? + } + + auto other_track = *other_trackResult; + + // get the references of the other track + + auto tref = other_track->get_tref_box(); + if (!tref) { + continue; + } + + // if the other track has a reference that points to the current track, add the other track to the list + + std::vector refs = tref->get_references(reference_type); + for (uint32_t to_track : refs) { + if (to_track == track->track->get_id() && nFound < array_size) { + out_track_id[nFound++] = other_track->get_id(); + break; + } + } + + // quick exit path + if (nFound == array_size) + break; + } + + return nFound; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_sequences.h libheif-1.23.4/libheif/api/libheif/heif_sequences.h --- libheif-1.19.8/libheif/api/libheif/heif_sequences.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_sequences.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,726 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_SEQUENCES_H +#define LIBHEIF_HEIF_SEQUENCES_H + +#include "libheif/heif.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// forward declaration of structs defined in heif_tai_timestamps.h +typedef struct heif_tai_clock_info heif_tai_clock_info; +typedef struct heif_tai_timestamp_packet heif_tai_timestamp_packet; + + +// --- reading sequence tracks + +/** + * Check whether there is an image sequence in the HEIF file. + * + * @return A boolean whether there is an image sequence in the HEIF file. + */ +LIBHEIF_API +int heif_context_has_sequence(const heif_context*); + +/** + * Get the timescale (clock ticks per second) for timing values in the sequence. + * + * @note Each track may have its independent timescale. + * + * @return Clock ticks per second. Returns 0 if there is no sequence in the file. + */ +LIBHEIF_API +uint32_t heif_context_get_sequence_timescale(const heif_context*); + +/** + * Get the total duration of the sequence in timescale clock ticks. + * Use `heif_context_get_sequence_timescale()` to get the clock ticks per second. + * + * @return Sequence duration in clock ticks. Returns 0 if there is no sequence in the file. + */ +LIBHEIF_API +uint64_t heif_context_get_sequence_duration(const heif_context*); + + +// A track, which may be an image sequence, a video track or a metadata track. +typedef struct heif_track heif_track; + +/** + * Free a `heif_track` object received from libheif. + * Passing NULL is ok. + */ +LIBHEIF_API +void heif_track_release(heif_track*); + +/** + * Get the number of tracks in the HEIF file. + * + * @return Number of tracks or 0 if there is no sequence in the HEIF file. + */ +LIBHEIF_API +int heif_context_number_of_sequence_tracks(const heif_context*); + +/** + * Returns the IDs for each of the tracks stored in the HEIF file. + * + * The caller MUST allocate `out_track_id_array` with exactly + * heif_context_number_of_sequence_tracks() entries. The function writes + * that many IDs unconditionally. Passing a smaller array results in a + * buffer overflow (undefined behavior); there is no capacity parameter + * and no truncation. + */ +LIBHEIF_API +void heif_context_get_track_ids(const heif_context* ctx, uint32_t out_track_id_array[]); + +/** + * Get the ID of the passed track. + * The track ID will never be 0. + */ +LIBHEIF_API +uint32_t heif_track_get_id(const heif_track* track); + +/** + * Get the heif_track object for the given track ID. + * If you pass `id=0`, the first visual track will be returned. + * If there is no track with the given ID or if 0 is passed and there is no visual track, NULL will be returned. + * + * @note Tracks never have a zero ID. This is why we can use this as a special value to find the first visual track. + * + * @param id Track id or 0 for the first visual track. + * @return heif_track object. You must free this after use. + */ +// Use id=0 for the first visual track. +LIBHEIF_API +heif_track* heif_context_get_track(const heif_context*, uint32_t id); + + +typedef uint32_t heif_track_type; + +typedef enum heif_track_type_4cc +{ + heif_track_type_video = heif_fourcc('v', 'i', 'd', 'e'), + heif_track_type_image_sequence = heif_fourcc('p', 'i', 'c', 't'), + heif_track_type_auxiliary = heif_fourcc('a', 'u', 'x', 'v'), + heif_track_type_metadata = heif_fourcc('m', 'e', 't', 'a') +} heif_track_type_4cc; + +/** + * Get the four-cc track handler type. + * Typical codes are "vide" for video sequences, "pict" for image sequences, "meta" for metadata tracks. + * These are defined in heif_track_type_4cc, but files may also contain other types. + * + * @return four-cc handler type + */ +LIBHEIF_API +heif_track_type heif_track_get_track_handler_type(const heif_track*); + + +typedef enum heif_auxiliary_track_info_type +{ + heif_auxiliary_track_info_type_unknown = 0, + heif_auxiliary_track_info_type_alpha = 1 +} heif_auxiliary_track_info_type; + +LIBHEIF_API +enum heif_auxiliary_track_info_type heif_track_get_auxiliary_info_type(const heif_track*); + +LIBHEIF_API +const char* heif_track_get_auxiliary_info_type_urn(const heif_track*); + +LIBHEIF_API +int heif_track_has_alpha_channel(const heif_track*); + +/** + * Get the timescale (clock ticks per second) for this track. + * Note that this can be different from the timescale used at sequence level. + * + * @return clock ticks per second + */ +LIBHEIF_API +uint32_t heif_track_get_timescale(const heif_track*); + +/** + * Special return value of `heif_track_get_number_of_repetitions()` indicating that + * the editlist requests indefinite repetition (the mvhd duration is the ISOBMFF + * "duration unknown" sentinel and the editlist is in repeat mode). + */ +#define heif_sequence_track_number_of_repetitions_infinite 0xFFFFFFFFu + +/** + * How many times the media segment should be played according to the track's edit list. + * + * Returns: + * - 0 if an edit list box is present but follows a pattern libheif does not interpret + * as a loop count. Callers should fall back to a single playback in that case. + * - 1 when no edit list is present. The media plays exactly once. + * - `heif_sequence_track_number_of_repetitions_infinite` (= UINT32_MAX) when the file + * signals indefinite playback (mvhd duration is the all-1s sentinel together with an + * editlist in repeat mode), or when the repetition count does not fit in uint32_t. + * - Otherwise the number of times the media segment is played. + * + * The reported value is informational; it does not change how + * `heif_track_decode_next_image()` walks samples. By default, that function applies + * the edit list and (for repeated playback) returns samples for every requested + * repetition; iterating until end-of-sequence on an infinite-loop file therefore + * never terminates. + * + * Clients that want to handle repetition themselves (e.g. to honor an "infinite" + * value with their own looping policy or to enforce an application-level cap) should + * set `heif_decoding_options::ignore_sequence_editlist` when calling + * `heif_track_decode_next_image()`. With that flag set, libheif plays the media + * timeline exactly once. Use the value returned by this function to decide how often + * to replay the track at the application level. + */ +LIBHEIF_API +uint32_t heif_track_get_number_of_repetitions(const heif_track*); + + +// --- reading visual tracks + +/** + * Get the image resolution of the track. + * If the passed track is no visual track, an error is returned. + */ +LIBHEIF_API +heif_error heif_track_get_image_resolution(const heif_track*, uint16_t* out_width, uint16_t* out_height); + +/** + * Decode the next image in the passed sequence track. + * If there is no more image in the sequence, `heif_error_End_of_sequence` is returned. + * The parameters `colorspace`, `chroma` and `options` are similar to heif_decode_image(). + * If you want to let libheif decide the output colorspace and chroma, set these parameters + * to heif_colorspace_undefined / heif_chroma_undefined. Usually, libheif will return the + * image in the input colorspace, but it may also modify it for example when it has to rotate the image. + * If you want to get the image in a specific colorspace/chroma format, you can specify this + * and libheif will convert the image to match this format. + */ +LIBHEIF_API +heif_error heif_track_decode_next_image(heif_track* track, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* options); + +/** + * Get the image display duration in clock ticks of this track. + * Make sure to use the timescale of the track and not the timescale of the total sequence. + */ +LIBHEIF_API +uint32_t heif_image_get_duration(const heif_image*); + + +// --- reading metadata track samples + +/** + * Get the "sample entry type" of the first sample sample cluster in the track. + * In the case of metadata tracks, this will usually be "urim" for "URI Meta Sample Entry". + * The exact URI can then be obtained with 'heif_track_get_urim_sample_entry_uri_of_first_cluster'. + */ +LIBHEIF_API +uint32_t heif_track_get_sample_entry_type_of_first_cluster(const heif_track*); + +/** + * Get the URI of the first sample cluster in an 'urim' track. + * Only call this for tracks with 'urim' sample entry types. It will return an error otherwise. + * + * @param out_uri A string with the URI will be returned. Free this string with `heif_string_release()`. + */ +LIBHEIF_API +heif_error heif_track_get_urim_sample_entry_uri_of_first_cluster(const heif_track*, + const char** out_uri); + + +/** Sequence sample object that can hold any raw byte data. + * Use this to store and read raw metadata samples. + */ +typedef struct heif_raw_sequence_sample heif_raw_sequence_sample; + +/** + * Get the next raw sample from the (metadata) sequence track. + * You have to free the returned sample with heif_raw_sequence_sample_release(). + */ +LIBHEIF_API +heif_error heif_track_get_next_raw_sequence_sample(heif_track*, + heif_raw_sequence_sample** out_sample); + +/** + * Release a heif_raw_sequence_sample object. + * You may pass NULL. + */ +LIBHEIF_API +void heif_raw_sequence_sample_release(heif_raw_sequence_sample*); + +/** + * Get a pointer to the data of the (metadata) sample. + * The data pointer stays valid until the heif_raw_sequence_sample object is released. + * + * @param out_array_size Size of the returned array (may be NULL). + */ +LIBHEIF_API +const uint8_t* heif_raw_sequence_sample_get_data(const heif_raw_sequence_sample*, size_t* out_array_size); + +/** + * Return the size of the raw data contained in the sample. + * This is the same as returned through the 'out_array_size' parameter of 'heif_raw_sequence_sample_get_data()'. + */ +LIBHEIF_API +size_t heif_raw_sequence_sample_get_data_size(const heif_raw_sequence_sample*); + +/** + * Get the sample duration in clock ticks of this track. + * Make sure to use the timescale of the track and not the timescale of the total sequence. + */ +LIBHEIF_API +uint32_t heif_raw_sequence_sample_get_duration(const heif_raw_sequence_sample*); + + +// --- writing sequences + +/** + * Set an independent global timescale for the sequence. + * If no timescale is set with this function, the timescale of the first track will be used. + */ +LIBHEIF_API +void heif_context_set_sequence_timescale(heif_context*, uint32_t timescale); + +// Number of times the sequence should be played in total (default = 1). +// Can be set to heif_sequence_maximum_number_of_repetitions. +LIBHEIF_API +void heif_context_set_number_of_sequence_repetitions(heif_context*, uint32_t number_of_repetitions); + +#define heif_sequence_maximum_number_of_repetitions 0 + +/** + * Specifies whether a 'sample auxiliary info' is stored with the samples. + * The difference between `heif_sample_aux_info_presence_optional` and `heif_sample_aux_info_presence_mandatory` + * is that `heif_sample_aux_info_presence_mandatory` will throw an error if the data is missing when writing a sample. + */ +typedef enum heif_sample_aux_info_presence +{ + heif_sample_aux_info_presence_none = 0, + heif_sample_aux_info_presence_optional = 1, + heif_sample_aux_info_presence_mandatory = 2 +} heif_sample_aux_info_presence; + + +typedef struct heif_track_options heif_track_options; + +/** + * Allocate track options object that is required to set options for a new track. + * When you create a new track, you can also pass a NULL heif_track_options pointer, in which case the default options are used. + */ +LIBHEIF_API +heif_track_options* heif_track_options_alloc(void); + +LIBHEIF_API +void heif_track_options_release(heif_track_options*); + +/** + * Set the track specific timescale. This is the number of clock ticks per second. + * The default is 90,000 Hz. + * @param timescale + */ +LIBHEIF_API +void heif_track_options_set_timescale(heif_track_options*, uint32_t timescale); + +/** + * Set whether the aux-info data should be stored interleaved with the sequence samples. + * Default is: false. + * + * If 'true', the aux_info data blocks will be interleaved with the compressed image. + * This has the advantage that the aux_info is localized near the image data. + * + * If 'false', all aux_info will be written as one block after the compressed image data. + * This has the advantage that no aux_info offsets have to be written. + * + * Note: currently ignored. Interleaved writing is disabled. + */ +LIBHEIF_API +void heif_track_options_set_interleaved_sample_aux_infos(heif_track_options*, int interleaved_flag); + +LIBHEIF_API +heif_error heif_track_options_enable_sample_tai_timestamps(heif_track_options*, + const heif_tai_clock_info*, + enum heif_sample_aux_info_presence); + +LIBHEIF_API +void heif_track_options_enable_sample_gimi_content_ids(heif_track_options*, + enum heif_sample_aux_info_presence); + +/** + * Set the GIMI format track ID string. If NULL is passed, no track ID is saved. + * @param track_id + */ +LIBHEIF_API +void heif_track_options_set_gimi_track_id(heif_track_options*, + const char* track_id); + + +// --- writing visual tracks + +typedef enum heif_sequence_gop_structure +{ + // Only independently decodable keyframes. + heif_sequence_gop_structure_intra_only, + + // No frame reordering, usually an IPPPP structure. + heif_sequence_gop_structure_lowdelay, + + // All frame types are allowed, including frame reordering, to achieve + // the best compression ratio. + heif_sequence_gop_structure_unrestricted +} heif_sequence_gop_structure; + + +// Describes the intent of the encoded sequence content. Encoder plugins may +// use this to choose different default tunings (e.g. perceptual quality vs. +// rate-distortion) for slide-show-style image sequences vs. video. +// +// Pass `_auto` to let libheif pick a value. Today libheif derives the kind +// from the track's handler type (`pict` -> image_sequence, `vide` -> video); +// in the future it may use additional input signals (e.g. frame rate or +// frame-to-frame similarity). Plugins never see `_auto`: libheif resolves it +// to a concrete kind before passing the options to the encoder plugin. +typedef enum heif_sequence_content_kind +{ + heif_sequence_content_kind_auto = 0, + heif_sequence_content_kind_image_sequence = 1, + heif_sequence_content_kind_video = 2 +} heif_sequence_content_kind; + + +typedef struct heif_sequence_encoding_options +{ + uint8_t version; + + // version 1 options + + // Set this to the NCLX parameters to be used in the output images or set to NULL + // when the same parameters as in the input images should be used. + const heif_color_profile_nclx* output_nclx_profile; + + heif_color_conversion_options color_conversion_options; + + // version 2 options + + enum heif_sequence_gop_structure gop_structure; + int keyframe_distance_min; // 0 - undefined + int keyframe_distance_max; // 0 - undefined + + int save_alpha_channel; + + // version 3 options + + // Intent of the encoded content. Encoder plugins may use this to choose + // different tunings for slide-show-style image sequences vs. video. + // Set to `_auto` (the default) to let libheif pick. libheif resolves the + // value to a concrete kind before passing the options to the encoder + // plugin, so plugins never see `_auto`. + enum heif_sequence_content_kind content_kind; +} heif_sequence_encoding_options; + + +LIBHEIF_API +heif_sequence_encoding_options* heif_sequence_encoding_options_alloc(void); + +/** + * Copy fields from `src` into `dst`, respecting both structs' version numbers. + * Only fields present in `min(dst->version, src->version)` are copied, so this + * is safe when libheif and the caller were built against different header + * versions of `heif_sequence_encoding_options`. Pass NULL `src` to leave `dst` + * unchanged. + */ +LIBHEIF_API +void heif_sequence_encoding_options_copy(heif_sequence_encoding_options* dst, + const heif_sequence_encoding_options* src); + +LIBHEIF_API +void heif_sequence_encoding_options_release(heif_sequence_encoding_options*); + +/** + * Add a visual track to the sequence. + * The track ID is assigned automatically. + * + * @param width Image resolution width + * @param height Image resolution height + * @param track_type Has to be heif_track_type_video or heif_track_type_image_sequence + * @param track_options Optional track creation options. If NULL, default options will be used. + * @param encoding_options Options for sequence encoding. If NULL, default options will be used. + * @param out_track Output parameter to receive the track object for the just created track. + * @return + */ +LIBHEIF_API +heif_error heif_context_add_visual_sequence_track(heif_context*, + uint16_t width, uint16_t height, + heif_track_type track_type, + const heif_track_options* track_options, + const heif_sequence_encoding_options* encoding_options, + heif_track** out_track); + +/** + * Set the image display duration in the track's timescale units. + */ +LIBHEIF_API +void heif_image_set_duration(heif_image*, uint32_t duration); + +/** + * Encode the image into a visual track. + * If the passed track is no visual track, an error will be returned. + * + * @param image The input image to append to the sequence. + * @param encoder The encoder used for encoding the image. + * @param sequence_encoding_options Options for sequence encoding. If NULL, default options will be used. + */ +LIBHEIF_API +heif_error heif_track_encode_sequence_image(heif_track*, + const heif_image* image, + heif_encoder* encoder, + const heif_sequence_encoding_options* sequence_encoding_options); + +/** + * When all sequence frames have been sent, you can to call this function to let the + * library know that no more frames will follow. This is strongly recommended, but optional + * for backwards compatibility. + * If you do not end the sequence explicitly with this function, it will be closed + * automatically when the HEIF file is written. Using this function has the advantage + * that you can free the {@link heif_encoder} afterwards (with {@link heif_encoder_release}). + * If you do not use call function, you have to keep the {@link heif_encoder} alive + * until the HEIF file is written. + */ +LIBHEIF_API +heif_error heif_track_encode_end_of_sequence(heif_track*, + heif_encoder* encoder); + +// --- metadata tracks + +/** + * Add a metadata track. + * The track is created as a 'urim' "URI Meta Sample Entry". + * The track content type is specified by the 'uri' parameter. + * + * @param uri Track content type. + * @param options Optional track creation options. If NULL, default options will be used. + * @param out_track Returns the created track object. If this is not NULL, you have to + * free the returned track with {@link heif_track_release}. + */ +LIBHEIF_API +heif_error heif_context_add_uri_metadata_sequence_track(heif_context*, + const char* uri, + const heif_track_options* options, + heif_track** out_track); + +/** + * Allocate a new heif_raw_sequence_sample object. + * Free with heif_raw_sequence_sample_release(). + */ +LIBHEIF_API +heif_raw_sequence_sample* heif_raw_sequence_sample_alloc(void); + +/** + * Set the raw sequence sample data. + */ +LIBHEIF_API +heif_error heif_raw_sequence_sample_set_data(heif_raw_sequence_sample*, const uint8_t* data, size_t size); + +/** + * Set the sample duration in track timescale units. + */ +LIBHEIF_API +void heif_raw_sequence_sample_set_duration(heif_raw_sequence_sample*, uint32_t duration); + +/** + * Add a raw sequence sample (usually a metadata sample) to the (metadata) track. + */ +LIBHEIF_API +heif_error heif_track_add_raw_sequence_sample(heif_track*, + const heif_raw_sequence_sample*); + + +// --- sample auxiliary data + +/** + * Contains the type of sample auxiliary data assigned to the track samples. + */ +typedef struct heif_sample_aux_info_type +{ + uint32_t type; + uint32_t parameter; +} heif_sample_aux_info_type; + +/** + * Returns how many different types of sample auxiliary data units are assigned to this track's samples. + */ +LIBHEIF_API +int heif_track_get_number_of_sample_aux_infos(const heif_track*); + +/** + * Get get the list of sample auxiliary data types used in the track. + * The passed array has to have heif_track_get_number_of_sample_aux_infos() entries. + */ +LIBHEIF_API +void heif_track_get_sample_aux_info_types(const heif_track*, heif_sample_aux_info_type out_types[]); + + +// --- GIMI content IDs + +/** + * Get the GIMI content ID for the track (as a whole). + * If there is no content ID, nullptr is returned. + * + * @return The returned string has to be released with `heif_string_release()`. + */ +LIBHEIF_API +const char* heif_track_get_gimi_track_content_id(const heif_track*); + +/** + * Get the GIMI content ID stored in the image sample. + * If there is no content ID, NULL is returned. + * @return + */ +LIBHEIF_API +const char* heif_image_get_gimi_sample_content_id(const heif_image*); + +/** + * Get the GIMI content ID stored in the metadata sample. + * If there is no content ID, NULL is returned. + * @return + */ +LIBHEIF_API +const char* heif_raw_sequence_sample_get_gimi_sample_content_id(const heif_raw_sequence_sample*); + +/** + * Set the GIMI content ID for an image sample. It will be stored as SAI. + * When passing NULL, a previously set ID will be removed. + */ +LIBHEIF_API +void heif_image_set_gimi_sample_content_id(heif_image*, const char* contentID); + +/** + * Set the GIMI content ID for a (metadata) sample. It will be stored as SAI. + * When passing NULL, a previously set ID will be removed. + */ +LIBHEIF_API +void heif_raw_sequence_sample_set_gimi_sample_content_id(heif_raw_sequence_sample*, const char* contentID); + + +// --- TAI timestamps + +// Note: functions for setting timestamps on images are in heif_tai_timestamps.h + +/** + * Returns whether the raw (metadata) sample has a TAI timestamp attached to it (stored as SAI). + * + * @return boolean flag whether a TAI exists for this sample. + */ +LIBHEIF_API +int heif_raw_sequence_sample_has_tai_timestamp(const heif_raw_sequence_sample*); + +/** + * Get the TAI timestamp of the (metadata) sample. + * If there is no timestamp assigned to it, NULL will be returned. + * + * @note You should NOT free the returned timestamp with 'heif_tai_timestamp_packet_release()'. + * The returned struct stays valid until the heif_raw_sequence_sample is released. + */ +LIBHEIF_API +const heif_tai_timestamp_packet* heif_raw_sequence_sample_get_tai_timestamp(const heif_raw_sequence_sample*); + +/** + * Set the TAI timestamp for a raw sequence sample. + * The timestamp will be copied, you can release it after calling this function. + */ +LIBHEIF_API +void heif_raw_sequence_sample_set_tai_timestamp(heif_raw_sequence_sample* sample, + const heif_tai_timestamp_packet* timestamp); + +/** + * Returns the TAI clock info of the track. + * If there is no TAI clock info, NULL is returned. + * You should NOT free the returned heif_tai_clock_info. + * The structure stays valid until the heif_track object is released. + */ +LIBHEIF_API +const heif_tai_clock_info* heif_track_get_tai_clock_info_of_first_cluster(heif_track*); + + +// --- track references + +typedef enum heif_track_reference_type +{ + heif_track_reference_type_description = heif_fourcc('c', 'd', 's', 'c'), // track_description + heif_track_reference_type_thumbnails = heif_fourcc('t', 'h', 'm', 'b'), // thumbnails + heif_track_reference_type_auxiliary = heif_fourcc('a', 'u', 'x', 'l') // auxiliary data (e.g. depth maps or alpha channel) +} heif_track_reference_type; + +/** + * Add a reference between tracks. + * 'reference_type' can be one of the four-cc codes listed in heif_track_reference_type or any other type. + */ +LIBHEIF_API +void heif_track_add_reference_to_track(heif_track*, uint32_t reference_type, const heif_track* to_track); + +/** + * Return the number of different reference types used in this track's tref box. + */ +LIBHEIF_API +size_t heif_track_get_number_of_track_reference_types(const heif_track*); + +/** + * List the reference types used in this track. + * + * The caller MUST allocate `out_reference_types` with exactly + * heif_track_get_number_of_track_reference_types() entries. The function + * writes that many values unconditionally. Passing a smaller array + * results in a buffer overflow (undefined behavior); there is no + * capacity parameter and no truncation. + */ +LIBHEIF_API +void heif_track_get_track_reference_types(const heif_track*, uint32_t out_reference_types[]); + +/** + * Get the number of references of the passed type. + */ +LIBHEIF_API +size_t heif_track_get_number_of_track_reference_of_type(const heif_track*, uint32_t reference_type); + +/** + * List the track ids this track points to with the passed reference type. + * The passed array must have heif_track_get_number_of_track_reference_of_type() entries. + */ +LIBHEIF_API +size_t heif_track_get_references_from_track(const heif_track*, uint32_t reference_type, uint32_t out_to_track_id[]); + +/** + * Find tracks that are referring to the current track through the passed reference_type. + * The found track IDs will be filled into the passed array, but no more than `array_size` entries will be filled. + * + * @return number of tracks found. If this is equal to 'array_size', you should ask again with a larger array size to be sure you got all tracks. + */ +LIBHEIF_API +size_t heif_track_find_referring_tracks(const heif_track*, uint32_t reference_type, uint32_t out_track_id[], size_t array_size); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_tai_timestamps.cc libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.cc --- libheif-1.19.8/libheif/api/libheif/heif_tai_timestamps.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,279 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include "api_structs.h" +#include "box.h" +#include "file.h" +#include + + +void initialize_heif_tai_clock_info(heif_tai_clock_info* taic) +{ + taic->version = 1; + taic->time_uncertainty = heif_tai_clock_info_time_uncertainty_unknown; + taic->clock_resolution = 0; + taic->clock_drift_rate = heif_tai_clock_info_clock_drift_rate_unknown; + taic->clock_type = heif_tai_clock_info_clock_type_unknown; +} + +heif_tai_clock_info* heif_tai_clock_info_alloc() +{ + auto* taic = new heif_tai_clock_info; + initialize_heif_tai_clock_info(taic); + + return taic; +} + + +void initialize_heif_tai_timestamp_packet(heif_tai_timestamp_packet* itai) +{ + itai->version = 1; + itai->tai_timestamp = 0; + itai->synchronization_state = false; + itai->timestamp_generation_failure = false; + itai->timestamp_is_modified = false; +} + +heif_tai_timestamp_packet* heif_tai_timestamp_packet_alloc() +{ + auto* itai = new heif_tai_timestamp_packet; + initialize_heif_tai_timestamp_packet(itai); + + return itai; +} + + +void heif_tai_timestamp_packet_copy(heif_tai_timestamp_packet* dst, const heif_tai_timestamp_packet* src) +{ + if (dst->version >= 1 && src->version >= 1) { + dst->tai_timestamp = src->tai_timestamp; + dst->synchronization_state = src->synchronization_state; + dst->timestamp_is_modified = src->timestamp_is_modified; + dst->timestamp_generation_failure = src->timestamp_generation_failure; + } + + // in the future when copying with "src->version > dst->version", + // the remaining dst fields have to be filled with defaults +} + +void heif_tai_clock_info_copy(heif_tai_clock_info* dst, const heif_tai_clock_info* src) +{ + if (dst->version >= 1 && src->version >= 1) { + dst->time_uncertainty = src->time_uncertainty; + dst->clock_resolution = src->clock_resolution; + dst->clock_drift_rate = src->clock_drift_rate; + dst->clock_type = src->clock_type; + } + + // in the future when copying with "src->version > dst->version", + // the remaining dst fields have to be filled with defaults +} + + +heif_error heif_item_set_property_tai_clock_info(heif_context* ctx, + heif_item_id itemId, + const heif_tai_clock_info* clock, + heif_property_id* out_propertyId) +{ + if (!ctx || !clock) { + return heif_error_null_pointer_argument; + } + + // Check if itemId exists + auto file = ctx->context->get_heif_file(); + if (!file->item_exists(itemId)) { + return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "itemId does not exist"}; + } + + // make sure that we do not add two taic boxes to one image + + if (auto img = ctx->context->get_image(itemId, false)) { + auto existing_taic = img->get_property(); + if (existing_taic) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "item already has an taic property"}; + } + } + + // Create new taic (it will be deduplicated automatically in add_property()) + + auto taic = std::make_shared(); + taic->set_from_tai_clock_info(clock); + + auto id = ctx->context->add_property(itemId, taic, false); + if (!id) { + return id.error_struct(ctx->context.get()); + } + + if (out_propertyId) { + *out_propertyId = *id; + } + + return heif_error_success; +} + + +heif_error heif_item_get_property_tai_clock_info(const heif_context* ctx, + heif_item_id itemId, + heif_tai_clock_info** out_clock) +{ + if (!ctx || !out_clock) { + return heif_error_null_pointer_argument; + } + + *out_clock = nullptr; + + // Check if itemId exists + auto file = ctx->context->get_heif_file(); + if (!file->item_exists(itemId)) { + return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "item ID does not exist"}; + } + + // Check if taic exists for itemId + auto taic = file->get_property_for_item(itemId); + if (!taic) { + // return NULL heif_tai_clock_info + return heif_error_success; + } + + *out_clock = new heif_tai_clock_info; + **out_clock = *taic->get_tai_clock_info(); + + return heif_error_success; +} + + +void heif_tai_clock_info_release(heif_tai_clock_info* clock_info) +{ + delete clock_info; +} + + +void heif_tai_timestamp_packet_release(heif_tai_timestamp_packet* tai) +{ + delete tai; +} + + +heif_error heif_item_set_property_tai_timestamp(heif_context* ctx, + heif_item_id itemId, + const heif_tai_timestamp_packet* timestamp, + heif_property_id* out_propertyId) +{ + if (!ctx) { + return heif_error_null_pointer_argument; + } + + // Check if itemId exists + auto file = ctx->context->get_heif_file(); + if (!file->item_exists(itemId)) { + return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "item does not exist"}; + } + + // make sure that we do not add two TAI timestamps to one image + + if (auto img = ctx->context->get_image(itemId, false)) { + auto existing_itai = img->get_property(); + if (existing_itai) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "item already has an itai property"}; + } + } + + // Create new itai (it will be deduplicated automatically in add_property()) + + auto itai = std::make_shared(); + itai->set_from_tai_timestamp_packet(timestamp); + + auto id = ctx->context->add_property(itemId, itai, false); + if (!id) { + return id.error_struct(ctx->context.get()); + } + + if (out_propertyId) { + *out_propertyId = *id; + } + + return heif_error_success; +} + + +heif_error heif_item_get_property_tai_timestamp(const heif_context* ctx, + heif_item_id itemId, + heif_tai_timestamp_packet** out_timestamp) +{ + if (!ctx || !out_timestamp) { + return heif_error_null_pointer_argument; + } + + *out_timestamp = nullptr; + + // Check if itemId exists + auto file = ctx->context->get_heif_file(); + if (!file->item_exists(itemId)) { + return {heif_error_Input_does_not_exist, heif_suberror_Invalid_parameter_value, "item does not exist"}; + } + + // Check if itai exists for itemId + auto itai = file->get_property_for_item(itemId); + if (!itai) { + // return NULL heif_tai_timestamp_packet; + return heif_error_success; + } + + *out_timestamp = new heif_tai_timestamp_packet; + **out_timestamp = *itai->get_tai_timestamp_packet(); + + return heif_error_success; +} + + +heif_error heif_image_set_tai_timestamp(heif_image* img, + const heif_tai_timestamp_packet* timestamp) +{ + Error err = img->image->set_tai_timestamp(timestamp); + if (err) { + return err.error_struct(img->image.get()); + } + else { + return heif_error_success; + } +} + + +heif_error heif_image_get_tai_timestamp(const heif_image* img, + heif_tai_timestamp_packet** out_timestamp) +{ + if (!out_timestamp) { + return heif_error_null_pointer_argument; + } + + *out_timestamp = nullptr; + + auto* tai = img->image->get_tai_timestamp(); + if (!tai) { + *out_timestamp = nullptr; + return heif_error_success; + } + + *out_timestamp = new heif_tai_timestamp_packet; + **out_timestamp = *tai; + + return heif_error_success; +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_tai_timestamps.h libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.h --- libheif-1.19.8/libheif/api/libheif/heif_tai_timestamps.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_tai_timestamps.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,202 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_TAI_TIMESTAMPS_H +#define LIBHEIF_HEIF_TAI_TIMESTAMPS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct heif_tai_clock_info +{ + uint8_t version; + + // --- version 1 + + // Standard deviation for timestamp generation process. + // May be `heif_tai_clock_info_time_uncertainty_unknown` if unknown. + uint64_t time_uncertainty; + + // Receptor clock resolution in nanoseconds. + uint32_t clock_resolution; + + // Clock drift rate in picoseconds/second when synchronization is stopped. + // Maybe `heif_tai_clock_info_clock_drift_rate_unknown` if unknown. + int32_t clock_drift_rate; + + // Whether clock is synchronized to an atomic source, + // see the clock_type defines below. + uint8_t clock_type; +} heif_tai_clock_info; + +#define heif_tai_clock_info_time_uncertainty_unknown UINT64_C(0xFFFFFFFFFFFFFFFF) +#define heif_tai_clock_info_clock_drift_rate_unknown INT32_C(0x7FFFFFFF) +#define heif_tai_clock_info_clock_type_unknown 0 +#define heif_tai_clock_info_clock_type_not_synchronized_to_atomic_source 1 +#define heif_tai_clock_info_clock_type_synchronized_to_atomic_source 2 + + +/** + * Allocate a new heif_tai_clock_info object and initialize with default values. + */ +LIBHEIF_API +heif_tai_clock_info* heif_tai_clock_info_alloc(void); + +/** + * Copies the source object into the destination object. + * Only the fields that are present in both objects are copied. + * The version property has to be set in both structs. + */ +LIBHEIF_API +void heif_tai_clock_info_copy(heif_tai_clock_info* dst, const heif_tai_clock_info* src); + +LIBHEIF_API +void heif_tai_clock_info_release(heif_tai_clock_info* clock_info); + + +typedef struct heif_tai_timestamp_packet +{ + uint8_t version; + + // --- version 1 + + // number of nanoseconds since TAI epoch (1958-01-01T00:00:00.0) + uint64_t tai_timestamp; + + // whether the remote and receiptor clocks are in sync + uint8_t synchronization_state; // bool + + // whether the receptor clock failed to generate a timestamp + uint8_t timestamp_generation_failure; // bool + + // whether the original clock value has been modified + uint8_t timestamp_is_modified; // bool +} heif_tai_timestamp_packet; + +/** + * Allocate a new heif_tai_timestamp_packet object and initialize with default values. + */ +LIBHEIF_API +heif_tai_timestamp_packet* heif_tai_timestamp_packet_alloc(void); + +/** + * Copies the source object into the destination object. + * Only the fields that are present in both objects are copied. + * The version property has to be set in both structs. + */ +LIBHEIF_API +void heif_tai_timestamp_packet_copy(heif_tai_timestamp_packet* dst, const heif_tai_timestamp_packet* src); + +LIBHEIF_API +void heif_tai_timestamp_packet_release(heif_tai_timestamp_packet*); + + + +/** + * Creates a new clock info property if it doesn't exist yet. + * You can only add one tai_clock_info to an image. + * + * @param clock_info The TAI clock info to set for the item. This object will be copied. + * @param out_optional_propertyId Output parameter for the property ID of the tai_clock_info. + * This parameter may be NULL if the info is not required. + */ +LIBHEIF_API +heif_error heif_item_set_property_tai_clock_info(heif_context* ctx, + heif_item_id itemId, + const heif_tai_clock_info* clock_info, + heif_property_id* out_optional_propertyId); + +/** + * Get the heif_tai_clock_info attached to the item. + * This function allocates a new heif_tai_clock_info and returns it through out_clock. + * + * @param out_clock This parameter must not be nullptr. The object returned through this parameter must + * be released with heif_tai_clock_info_release(). + * If no tai_clock_info property exists for the item, out_clock is set to NULL and + * no error is returned. + */ +LIBHEIF_API +heif_error heif_item_get_property_tai_clock_info(const heif_context* ctx, + heif_item_id itemId, + heif_tai_clock_info** out_clock); + + +/** + * Creates a new TAI timestamp property if it doesn't exist yet. + * You can only add one tai_timestamp to an image. + * + * @param timestamp The TAI timestamp to set for the item. This object will be copied. + * @param out_optional_propertyId Output parameter for the property ID of the TAI timestamp. + * This parameter may be NULL if the info is not required. + */ +LIBHEIF_API +heif_error heif_item_set_property_tai_timestamp(heif_context* ctx, + heif_item_id itemId, + const heif_tai_timestamp_packet* timestamp, + heif_property_id* out_optional_propertyId); + +/** + * Get the heif_tai_timestamp_packet attached to the item. + * This function allocates a new heif_tai_timestamp_packet and returns it through out_timestamp. + * + * @param out_timestamp This parameter must not be NULL. The object returned through this parameter must + * be released with heif_tai_timestamp_packet_release(). + * If no tai_timestamp_packet property exists for the item, *out_timestamp is set to NULL and + * no error is returned. + */ +LIBHEIF_API +heif_error heif_item_get_property_tai_timestamp(const heif_context* ctx, + heif_item_id itemId, + heif_tai_timestamp_packet** out_timestamp); + +/** + * Attach a TAI timestamp to the image. + * The main use of this function is for image sequences, but it can also be used for still images. + * If used for still images, note that you also have to set the heif_tai_clock_info to the image item + * through heif_item_set_property_tai_clock_info(). + * + * @param timestamp The TAI timestamp to set to the image. This object will be copied. + */ +LIBHEIF_API +heif_error heif_image_set_tai_timestamp(heif_image* img, + const heif_tai_timestamp_packet* timestamp); + +/** + * Get the heif_tai_timestamp_packet attached to the image. + * The main use of this function is for image sequences, but it can also be used for still images. + * This function allocates a new heif_tai_timestamp_packet and returns it through out_timestamp. + * + * @param out_timestamp This parameter must not be NULL. The object returned through this parameter must + * be released with heif_tai_timestamp_packet_release(). + * If no tai_timestamp_packet property exists for the image, *out_timestamp is set to NULL and + * no error is returned. + */ +LIBHEIF_API +heif_error heif_image_get_tai_timestamp(const heif_image* img, + heif_tai_timestamp_packet** out_timestamp); + +#ifdef __cplusplus +} +#endif + +#endif //LIBHEIF_HEIF_TAI_TIMESTAMPS_H diff -Nru libheif-1.19.8/libheif/api/libheif/heif_text.cc libheif-1.23.4/libheif/api/libheif/heif_text.cc --- libheif-1.19.8/libheif/api/libheif/heif_text.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_text.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,162 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * Copyright (c) 2025 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_text.h" +#include "api_structs.h" +#include "file.h" +#include "text.h" +#include +#include +#include +#include +#include +#include + +heif_error heif_image_handle_add_text_item(heif_image_handle *image_handle, + const char *content_type, + const char *text, + heif_text_item** out_text_item) +{ + + auto textItemResult = image_handle->context->add_text_item(content_type, text); + if (!textItemResult) { + return textItemResult.error_struct(image_handle->context.get()); + } + std::shared_ptr textItem = *textItemResult; + image_handle->image->add_text_item_id(textItem->get_item_id()); + if (out_text_item) { + heif_text_item* item = new heif_text_item(); + item->context = image_handle->context; + item->text_item = std::move(textItem); + *out_text_item = item; + } + return heif_error_success; +} + +void heif_text_item_release(heif_text_item* text_item) +{ + delete text_item; +} + +int heif_image_handle_get_number_of_text_items(const heif_image_handle* handle) +{ + return (int) handle->image->get_text_item_ids().size(); +} + +int heif_image_handle_get_list_of_text_item_ids(const heif_image_handle* handle, + heif_item_id* item_ids, + int max_count) +{ + auto text_item_ids = handle->image->get_text_item_ids(); + int num = std::min((int) text_item_ids.size(), max_count); + + memcpy(item_ids, text_item_ids.data(), num * sizeof(heif_item_id)); + + return num; +} + + +heif_error heif_context_get_text_item(const heif_context* context, + heif_item_id text_item_id, + heif_text_item** out) +{ + if (out==nullptr) { + return heif_error_null_pointer_argument; + } + + auto r = context->context->get_text_item(text_item_id); + + if (r==nullptr) { + return {heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced, "Text item does not exist"}; + } + + heif_text_item* item = new heif_text_item(); + item->context = context->context; + item->text_item = std::move(r); + *out = item; + + return heif_error_success; +} + +heif_item_id heif_text_item_get_id(heif_text_item* text_item) +{ + if (text_item == nullptr) { + return 0; + } + + return text_item->text_item->get_item_id(); +} + +const char* heif_text_item_get_content(heif_text_item* text_item) +{ + if (text_item == nullptr) { + return nullptr; + } + + // return text as c-string + + std::string txt = text_item->text_item->get_item_text(); + + char* text_c = new char[txt.length() + 1]; + strcpy(text_c, txt.c_str()); + + return text_c; +} + + +heif_error heif_text_item_get_property_extended_language(const heif_text_item* text_item, char** out_language) +{ + if (!out_language || !text_item) { + return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "NULL passed"}; + } + + auto elng = text_item->context->find_property(text_item->text_item->get_item_id()); + if (!elng) { + return elng.error_struct(text_item->context.get()); + } + + std::string lang = (*elng)->get_extended_language(); + *out_language = new char[lang.length() + 1]; + strcpy(*out_language, lang.c_str()); + + return heif_error_success; +} + + +heif_error heif_text_item_set_extended_language(heif_text_item* text_item, const char *language, heif_property_id* out_optional_propertyId) +{ + if (!text_item || !language) { + return {heif_error_Usage_error, heif_suberror_Null_pointer_argument, "NULL passed"}; + } + + Result property_id_result = text_item->context->add_text_property(text_item->text_item->get_item_id(), + language); + + if (auto err = property_id_result.error()) { + return err.error_struct(text_item->context.get()); + } + + if (out_optional_propertyId) { + *out_optional_propertyId = *property_id_result; + } + + return heif_error_success; +} \ No newline at end of file diff -Nru libheif-1.19.8/libheif/api/libheif/heif_text.h libheif-1.23.4/libheif/api/libheif/heif_text.h --- libheif-1.19.8/libheif/api/libheif/heif_text.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_text.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,161 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * Copyright (c) 2025 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_TEXT_H +#define LIBHEIF_HEIF_TEXT_H + +#include "heif_image_handle.h" +#include "heif_library.h" +#include "heif_error.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// --- text items and fonts + +// See ISO/IEC 23008-12:2025 Section 6.10 "Text and font items" + +typedef struct heif_text_item heif_text_item; + +/** + * Get the number of text items that are attached to an image. + * + * @param image_handle the image handle for the image to query. + * @return the number of text items, which can be zero. + */ +LIBHEIF_API +int heif_image_handle_get_number_of_text_items(const heif_image_handle* image_handle); + +/** + * Get the text item identifiers for the text items attached to an image. + * + * Possible usage (in C++): + * @code + * int numTextItems = heif_image_handle_get_number_of_text_items(handle); + * if (numTextItems > 0) { + * std::vector text_item_ids(numTextItems); + * heif_image_handle_get_list_of_text_item_ids(handle, text_item_ids.data(), numTextItems); + * // use text item ids + * } + * @endcode + * + * @param image_handle the image handle for the parent image to query + * @param text_item_ids_array array to put the item identifiers into + * @param max_count the maximum number of text identifiers + * @return the number of text item identifiers that were returned. + */ +LIBHEIF_API +int heif_image_handle_get_list_of_text_item_ids(const heif_image_handle* image_handle, + heif_item_id* text_item_ids_array, + int max_count); + + +/** + * Get the text item. + * + * Caller is responsible for release of the output heif_text_item with heif_text_item_release(). + * + * @param context the context to get the text item from, usually from a file operation + * @param text_item_id the identifier for the text item + * @param out pointer to pointer to the resulting text item + * @return heif_error_ok on success, or an error value indicating the problem + */ +LIBHEIF_API +heif_error heif_context_get_text_item(const heif_context* context, + heif_item_id text_item_id, + heif_text_item** out); + +/** + * Get the item identifier for a text item. + * + * @param text_item the text item to query + * @return the text item identifier (or 0 if the text_item is null) + */ +LIBHEIF_API +heif_item_id heif_text_item_get_id(heif_text_item* text_item); + +/** + * Get the item content for a text item. + * + * This is the payload text, in the format given by the associated content_type. + * + * @param text_item the text item to query + * @return the text item content (or null if the text_item is null). The returned string shall be released + * with heif_string_release(). + */ +LIBHEIF_API +const char* heif_text_item_get_content(heif_text_item* text_item); + +/** + * This function is similar to heif_item_get_property_extended_language(), but + * takes a `heif_text_item` as parameter. + * + * @param text_item The text item for which we are requesting the language. + * @param out_language Output parameter for the text language. Free with heif_string_release(). + * @return + */ +LIBHEIF_API +heif_error heif_text_item_get_property_extended_language(const heif_text_item* text_item, + char** out_language); + +// --- adding text items + +/** + * Add a text item to an image. +*/ +LIBHEIF_API +heif_error heif_image_handle_add_text_item(heif_image_handle *image_handle, + const char *content_type, + const char *text, + heif_text_item** out_text_item); + +/** + * Release a text item. + * + * This should be called on items from heif_context_add_text_item(). + * + * @param text_item the item to release. + */ +LIBHEIF_API +void heif_text_item_release(heif_text_item* text_item); + +/** + * Set the extended language property to the text item. + * + * This adds an RFC 5346 (IETF BCP 47) extended language tag, such as "en-AU". + * + * @param text_item the text item to query + * @param language the language to set + * @param out_optional_propertyId Output parameter for the property ID of the language property. + * This parameter may be NULL if the info is not required. + * @return heif_error_ok on success, or an error value indicating the problem + */ +LIBHEIF_API +heif_error heif_text_item_set_extended_language(heif_text_item* text_item, + const char *language, + heif_property_id* out_optional_propertyId); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_tiling.cc libheif-1.23.4/libheif/api/libheif/heif_tiling.cc --- libheif-1.19.8/libheif/api/libheif/heif_tiling.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_tiling.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,321 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_tiling.h" +#include "api_structs.h" +#include "image-items/grid.h" +#include "image-items/tiled.h" +#include "security_limits.h" + +#if WITH_UNCOMPRESSED_CODEC +#include "image-items/unc_image.h" +#endif + +#include +#include +#include +#include + + +heif_error heif_image_handle_get_image_tiling(const heif_image_handle* handle, int process_image_transformations, struct heif_image_tiling* tiling) +{ + if (!handle || !tiling) { + return heif_error_null_pointer_argument; + } + + *tiling = handle->image->get_heif_image_tiling(); + + // Every tile has to be decodable on its own, so apply the same size limit that the + // decoding path applies to the 'ispe' size. For plain (non-tiled) items, the single + // tile is the whole image. The whole-image size is deliberately not checked here, + // because tiled images larger than the limit can still be decoded tile by tile. + // A tile size of zero means that the size is unknown (e.g. a grid whose first tile + // is missing); there is nothing to check in that case. + // (GHSA-gh5q-69gg-c964: the tiling API returned dimensions that no other part of + // the library accepts.) + if (tiling->tile_width != 0 && tiling->tile_height != 0) { + if (Error err = check_for_valid_image_size(handle->context->get_security_limits(), + tiling->tile_width, tiling->tile_height)) { + return err.error_struct(handle->context.get()); + } + } + + if (process_image_transformations) { + Error error = handle->image->process_image_transformations_on_tiling(*tiling); + if (error) { + return error.error_struct(handle->context.get()); + } + } + + return heif_error_ok; +} + + +heif_error heif_image_handle_get_grid_image_tile_id(const heif_image_handle* handle, + int process_image_transformations, + uint32_t tile_x, uint32_t tile_y, + heif_item_id* tile_item_id) +{ + if (!handle || !tile_item_id) { + return heif_error_null_pointer_argument; + } + + std::shared_ptr gridItem = std::dynamic_pointer_cast(handle->image); + if (!gridItem) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Image is no grid image" + }; + } + + const ImageGrid& gridspec = gridItem->get_grid_spec(); + + if (process_image_transformations) { + // transform_requested_tile_position_to_original_tile_position() validates + // (tile_x, tile_y) against the *displayed* tile-grid dimensions (which may + // differ from the file's dimensions when a 90°/270° rotation is present) + // before mapping the coordinates back to the in-file grid. + Error err = gridItem->transform_requested_tile_position_to_original_tile_position(tile_x, tile_y); + if (err) { + return err.error_struct(handle->context.get()); + } + } + else if (tile_x >= gridspec.get_columns() || tile_y >= gridspec.get_rows()) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Grid tile index out of range" + }; + } + + *tile_item_id = gridItem->get_grid_tiles()[tile_y * gridspec.get_columns() + tile_x]; + + return heif_error_ok; +} + + +heif_error heif_image_handle_decode_image_tile(const heif_image_handle* in_handle, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* input_options, + uint32_t x0, uint32_t y0) +{ + if (!in_handle) { + return heif_error_null_pointer_argument; + } + + return exception_guard([&]() -> heif_error { + heif_item_id id = in_handle->image->get_id(); + + // RAII so the options are freed even if decode_image() throws. + std::unique_ptr + dec_options(heif_decoding_options_alloc(), heif_decoding_options_free); + heif_decoding_options_copy(dec_options.get(), input_options); + + Result > decodingResult = in_handle->context->decode_image(id, + colorspace, + chroma, + *dec_options, + true, x0, y0, + {}); + + if (!decodingResult) { + return decodingResult.error_struct(in_handle->image.get()); + } + + std::shared_ptr img = *decodingResult; + + *out_img = new heif_image(); + (*out_img)->image = std::move(img); + + return Error::Ok.error_struct(in_handle->image.get()); + }); +} + + +// --- encoding --- + +heif_error heif_context_encode_grid(heif_context* ctx, + heif_image** tiles, + uint16_t columns, + uint16_t rows, + heif_encoder* encoder, + const heif_encoding_options* input_options, + heif_image_handle** out_image_handle) +{ + if (!encoder || !tiles) { + return heif_error_null_pointer_argument; + } + else if (rows == 0 || columns == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); + } + + // TODO: Don't repeat this code from heif_context_encode_image() + heif_encoding_options* options = heif_encoding_options_alloc(); + + heif_color_profile_nclx nclx; + if (input_options) { + heif_encoding_options_copy(options, input_options); + + if (options->output_nclx_profile == nullptr) { + if (tiles[0]->image->has_nclx_color_profile()) { + nclx_profile input_nclx = tiles[0]->image->get_color_profile_nclx(); + + options->output_nclx_profile = &nclx; + nclx.version = 1; + nclx.color_primaries = input_nclx.get_colour_primaries(); + nclx.transfer_characteristics = input_nclx.get_transfer_characteristics(); + nclx.matrix_coefficients = input_nclx.get_matrix_coefficients(); + nclx.full_range_flag = input_nclx.get_full_range_flag(); + } + } + } + + // Convert heif_images to a vector of HeifPixelImages + std::vector > pixel_tiles; + pixel_tiles.reserve(rows * columns); + for (int i = 0; i < rows * columns; i++) { + pixel_tiles.push_back(tiles[i]->image); + } + + // Encode Grid + std::shared_ptr out_grid; + auto addGridResult = ImageItem_Grid::add_and_encode_full_grid(ctx->context.get(), + pixel_tiles, + rows, columns, + encoder, + *options); + heif_encoding_options_free(options); + + if (!addGridResult) { + return addGridResult.error_struct(ctx->context.get()); + } + + out_grid = *addGridResult; + + // Mark as primary image + if (ctx->context->is_primary_image_set() == false) { + ctx->context->set_primary_image(out_grid); + } + + if (out_image_handle) { + *out_image_handle = new heif_image_handle; + (*out_image_handle)->image = std::move(out_grid); + (*out_image_handle)->context = ctx->context; + } + + return heif_error_success; +} + + +heif_error heif_context_add_grid_image(heif_context* ctx, + uint32_t image_width, + uint32_t image_height, + uint32_t tile_columns, + uint32_t tile_rows, + const heif_encoding_options* encoding_options, + heif_image_handle** out_grid_image_handle) +{ + if (!ctx) { + return heif_error_null_pointer_argument; + } + + if (tile_rows == 0 || tile_columns == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value).error_struct(ctx->context.get()); + } + else if (tile_rows > 0xFFFF || tile_columns > 0xFFFF) { + return heif_error{ + heif_error_Usage_error, + heif_suberror_Invalid_image_size, + "Number of tile rows/columns may not exceed 65535" + }; + } + + // Fall back to default options when the caller passes nullptr. + // add_new_grid_item() copies these into ImageItem_Grid::m_tile_encoding_options, + // so the fallback struct only needs to live for the duration of the call. + heif_encoding_options* default_options = nullptr; + if (!encoding_options) { + default_options = heif_encoding_options_alloc(); + encoding_options = default_options; + } + + auto generateGridItemResult = ImageItem_Grid::add_new_grid_item(ctx->context.get(), + image_width, + image_height, + static_cast(tile_rows), + static_cast(tile_columns), + encoding_options); + heif_encoding_options_free(default_options); + + if (!generateGridItemResult) { + return generateGridItemResult.error_struct(ctx->context.get()); + } + + if (ctx->context->is_primary_image_set() == false) { + ctx->context->set_primary_image(*generateGridItemResult); + } + + if (out_grid_image_handle) { + *out_grid_image_handle = new heif_image_handle; + (*out_grid_image_handle)->image = *generateGridItemResult; + (*out_grid_image_handle)->context = ctx->context; + } + + return heif_error_success; +} + + +heif_error heif_context_add_image_tile(heif_context* ctx, + heif_image_handle* tiled_image, + uint32_t tile_x, uint32_t tile_y, + const heif_image* image, + heif_encoder* encoder) +{ + if (!ctx || !tiled_image || !image || !encoder) { + return heif_error_null_pointer_argument; + } + + if (auto tili_image = std::dynamic_pointer_cast(tiled_image->image)) { + Error err = tili_image->add_image_tile(tile_x, tile_y, image->image, encoder); + return err.error_struct(ctx->context.get()); + } +#if WITH_UNCOMPRESSED_CODEC + else if (auto unci = std::dynamic_pointer_cast(tiled_image->image)) { + Error err = unci->add_image_tile(tile_x, tile_y, image->image, true); // TODO: how do we handle 'save_alpha=false' ? + return err.error_struct(ctx->context.get()); + } +#endif + else if (auto grid_item = std::dynamic_pointer_cast(tiled_image->image)) { + Error err = grid_item->add_image_tile(tile_x, tile_y, image->image, encoder); + return err.error_struct(ctx->context.get()); + } + else { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Cannot add tile to a non-tiled image" + }; + } +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_tiling.h libheif-1.23.4/libheif/api/libheif/heif_tiling.h --- libheif-1.19.8/libheif/api/libheif/heif_tiling.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_tiling.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,137 @@ +/* + * HEIF codec. + * Copyright (c) 2017-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_TILING_H +#define LIBHEIF_HEIF_TILING_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +// forward declaration from other headers +typedef struct heif_encoder heif_encoder; +typedef struct heif_encoding_options heif_encoding_options; + + +typedef struct heif_image_tiling +{ + int version; + + // --- version 1 + + uint32_t num_columns; + uint32_t num_rows; + uint32_t tile_width; + uint32_t tile_height; + + uint32_t image_width; + uint32_t image_height; + + // Position of the top left tile. + // Usually, this is (0;0), but if a tiled image is rotated or cropped, it may be that the top left tile should be placed at a negative position. + // The offsets define this negative shift. + uint32_t top_offset; + uint32_t left_offset; + + uint8_t number_of_extra_dimensions; // 0 for normal images, 1 for volumetric (3D), ... + uint32_t extra_dimension_size[8]; // size of extra dimensions (first 8 dimensions) +} heif_image_tiling; + + +// --- decoding --- + +// If 'process_image_transformations' is true, this returns modified sizes. +// If it is false, the top_offset and left_offset will always be (0;0). +LIBHEIF_API +heif_error heif_image_handle_get_image_tiling(const heif_image_handle* handle, int process_image_transformations, struct heif_image_tiling* out_tiling); + + +// For grid images, return the image item ID of a specific grid tile. +// If 'process_image_transformations' is true, the tile positions are given in the transformed image coordinate system and +// are internally mapped to the original image tile positions. +LIBHEIF_API +heif_error heif_image_handle_get_grid_image_tile_id(const heif_image_handle* handle, + int process_image_transformations, + uint32_t tile_x, uint32_t tile_y, + heif_item_id* out_tile_item_id); + + +typedef struct heif_decoding_options heif_decoding_options; + +// The tile position is given in tile indices, not in pixel coordinates. +// If the image transformations are processed (option->ignore_image_transformations==false), the tile position +// is given in the transformed coordinates. +LIBHEIF_API +heif_error heif_image_handle_decode_image_tile(const heif_image_handle* in_handle, + heif_image** out_img, + heif_colorspace colorspace, + heif_chroma chroma, + const heif_decoding_options* options, + uint32_t tile_x, uint32_t tile_y); + + +// --- encoding --- + +/** + * @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 +heif_error heif_context_encode_grid(heif_context* ctx, + heif_image** tiles, + uint16_t rows, + uint16_t columns, + heif_encoder* encoder, + const heif_encoding_options* input_options, + heif_image_handle** out_image_handle); + +LIBHEIF_API +heif_error heif_context_add_grid_image(heif_context* ctx, + uint32_t image_width, + uint32_t image_height, + uint32_t tile_columns, + uint32_t tile_rows, + const heif_encoding_options* encoding_options, + heif_image_handle** out_grid_image_handle); + +LIBHEIF_API +heif_error heif_context_add_image_tile(heif_context* ctx, + heif_image_handle* tiled_image, + uint32_t tile_x, uint32_t tile_y, + const heif_image* image, + heif_encoder* encoder); + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api/libheif/heif_uncompressed.cc libheif-1.23.4/libheif/api/libheif/heif_uncompressed.cc --- libheif-1.19.8/libheif/api/libheif/heif_uncompressed.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_uncompressed.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,134 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "heif_uncompressed.h" +#include "context.h" +#include "api_structs.h" +#include "image/pixelimage.h" +#include "image-items/unc_image.h" + +#include +#include +#include +#include + + +// ISO/IEC 23001-17 property functions (Bayer / polarization / sensor bad +// pixels / NUC / chroma sample location) live in heif_properties.cc. + + +heif_unci_image_parameters* heif_unci_image_parameters_alloc() +{ + auto* params = new heif_unci_image_parameters(); + + params->version = 1; + + // --- version 1 + + params->image_width = 0; + params->image_height = 0; + + // TODO: should we define that tile size = 0 means no tiling? + params->tile_width = 0; + params->tile_height = 0; + + params->compression = heif_unci_compression_off; + + return params; +} + + +void heif_unci_image_parameters_copy(heif_unci_image_parameters* dst, + const heif_unci_image_parameters* src) +{ + if (src == nullptr || dst == nullptr) { + return; + } + + int min_version = std::min(src->version, dst->version); + + switch (min_version) { + case 1: + dst->image_width = src->image_width; + dst->image_height = src->image_height; + dst->tile_width = src->tile_width; + dst->tile_height = src->tile_height; + dst->compression = src->compression; + break; + } +} + + +void heif_unci_image_parameters_release(heif_unci_image_parameters* params) +{ + delete params; +} + + +// Multi-component access functions (heif_image_get/add_component_*, +// heif_image_set_gimi_component_content_id) live in heif_components.cc. + + +heif_error heif_context_add_empty_unci_image(heif_context* ctx, + const heif_unci_image_parameters* parameters, + const heif_encoding_options* encoding_options, + const heif_image* prototype, + heif_image_handle** out_unci_image_handle) +{ +#if WITH_UNCOMPRESSED_CODEC + if (prototype == nullptr || out_unci_image_handle == nullptr) { + return heif_error_null_pointer_argument; + } + + heif_encoding_options* default_options = nullptr; + if (encoding_options == nullptr) { + default_options = heif_encoding_options_alloc(); + encoding_options = default_options; + } + + Result> unciImageResult; + unciImageResult = ImageItem_uncompressed::add_unci_item(ctx->context.get(), parameters, encoding_options, prototype->image); + + if (encoding_options) { + heif_encoding_options_free(default_options); + } + + if (!unciImageResult) { + return unciImageResult.error_struct(ctx->context.get()); + } + + // mark the new image as primary image + + if (ctx->context->is_primary_image_set() == false) { + ctx->context->set_primary_image(*unciImageResult); + } + + assert(out_unci_image_handle); + *out_unci_image_handle = new heif_image_handle; + (*out_unci_image_handle)->image = *unciImageResult; + (*out_unci_image_handle)->context = ctx->context; + + return heif_error_success; +#else + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "support for uncompressed images (ISO23001-17) has been disabled."}; +#endif +} diff -Nru libheif-1.19.8/libheif/api/libheif/heif_uncompressed.h libheif-1.23.4/libheif/api/libheif/heif_uncompressed.h --- libheif-1.19.8/libheif/api/libheif/heif_uncompressed.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api/libheif/heif_uncompressed.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,133 @@ +/* + * HEIF codec. + * Copyright (c) 2024-2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_HEIF_UNCOMPRESSED_H +#define LIBHEIF_HEIF_UNCOMPRESSED_H + +#include "libheif/heif_components.h" +#include "libheif/heif_properties.h" +#include "libheif/heif.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* @file heif_uncompressed.h + * @brief Functions for adding ISO 23001-17 (uncompressed) images to a HEIF file. + * Despite its name, this is not limited to uncompressed images. + * It is also possible to add images with lossless compression methods. + * See heif_metadata_compression for more information. + * + * ISO/IEC 23001-17 metadata properties (Bayer / polarization / sensor bad + * pixels / NUC / chroma sample location) live in heif_properties.h, which is + * included above for back-compat. + */ + + +// --- 'unci' images + +// Compression methods for 'unci' (ISO 23001-17) images. +// This is similar to heif_metadata_compression. We should try to keep the integers compatible, but each enum will just +// contain the allowed values. +typedef enum heif_unci_compression +{ + heif_unci_compression_off = 0, + //heif_unci_compression_auto = 1, + //heif_unci_compression_unknown = 2, // only used when reading unknown method from input file + heif_unci_compression_deflate = 3, + heif_unci_compression_zlib = 4, + heif_unci_compression_brotli = 5 +} heif_unci_compression; + + +// --- 'unci' image parameters + +typedef struct heif_unci_image_parameters +{ + int version; + + // --- version 1 + + uint32_t image_width; + uint32_t image_height; + + uint32_t tile_width; + uint32_t tile_height; + + heif_unci_compression compression; + + // TODO: interleave type, padding +} heif_unci_image_parameters; + + +LIBHEIF_API +heif_unci_image_parameters* heif_unci_image_parameters_alloc(void); + +LIBHEIF_API +void heif_unci_image_parameters_copy(heif_unci_image_parameters* dst, + const heif_unci_image_parameters* src); + +LIBHEIF_API +void heif_unci_image_parameters_release(heif_unci_image_parameters*); + + +/* + * This adds an empty iso23001-17 (uncompressed) image to the HEIF file. + * The actual image data is added later using heif_context_add_image_tile(). + * If you do not need tiling, you can use heif_context_encode_image() instead. + * However, this will by default disable any compression and any control about + * the data layout. + * + * The function also accepts a direct heif_unci_image_parameters argument and + * indirectly through encoding_options. At least one of the two must be non-null. + * If both are non-null and differ, the direct argument takes precedence. + * + * @param ctx The file context + * @param parameters The parameters for the image. May be NULL if + * heif_encoding_options::unci_parameters is set instead. If both this + * argument and encoding_options->unci_parameters are non-null and + * differ, this argument takes precedence. + * @param encoding_options Optional, may be NULL. If non-null and unci_parameters is set, + * it may carry the unci parameters in place of the direct argument. + * @param prototype An image with the same channel configuration as the image data + * that will be later inserted. The image size need not match this. + * Must not be NULL. + * @param out_unci_image_handle Returns a handle to the image. The caller is responsible for freeing it. + * Must not be NULL because this is required to fill in image data. + * @return Returns an error if the passed parameters are incorrect. + * If ISO23001-17 images are not supported, returns heif_error_Unsupported_feature. + */ +LIBHEIF_API +heif_error heif_context_add_empty_unci_image(heif_context* ctx, + const heif_unci_image_parameters* parameters, + const heif_encoding_options* encoding_options, + const heif_image* prototype, + heif_image_handle** out_unci_image_handle); + +// Multi-component access functions (heif_image_get/add_component_*, +// heif_image_handle_get_component_*, heif_image_set_gimi_component_content_id) +// live in heif_components.h, which is included above. + + +#ifdef __cplusplus +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/api_structs.h libheif-1.23.4/libheif/api_structs.h --- libheif-1.19.8/libheif/api_structs.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/api_structs.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,147 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_API_STRUCTS_H +#define HEIF_API_STRUCTS_H + +#include "image/pixelimage.h" +#include "context.h" +#include "error.h" + +#include +#include +#include +#include +#include +#include +#include "image-items/image_item.h" + + +// Run a C API entry point body and translate any escaping C++ exception into a +// heif_error. No exception may cross the extern "C" boundary (that is undefined +// behaviour for C callers), and an out-of-memory condition must not abort the +// process (GHSA-7p2q-crf9-xm46). The returned errors have string-literal messages, +// so reporting them never touches the allocator that is already failing. +template +static inline heif_error exception_guard(F&& body) noexcept +{ + try { + return body(); + } + catch (const std::bad_alloc&) { + return heif_error_out_of_memory; + } + catch (const std::length_error&) { + // e.g. a std::vector/std::string asked to exceed max_size() + return heif_error_out_of_memory; + } + catch (const std::exception&) { + return heif_error_internal_exception; + } + catch (...) { + return heif_error_internal_exception; + } +} + +struct heif_image_handle +{ + std::shared_ptr image; + + // store reference to keep the context alive while we are using the handle (issue #147) + std::shared_ptr context; +}; + + +struct heif_track +{ + std::shared_ptr track; + + // store reference to keep the context alive while we are using the handle (issue #147) + std::shared_ptr context; +}; + +struct heif_raw_sequence_sample +{ + ~heif_raw_sequence_sample() + { + heif_tai_timestamp_packet_release(timestamp); + } + + std::vector data; + uint32_t duration = 0; + + heif_tai_timestamp_packet* timestamp = nullptr; + std::string gimi_sample_content_id; +}; + + +struct heif_image +{ + std::shared_ptr image; +}; + + +struct heif_context +{ + std::shared_ptr context; +}; + + +struct heif_encoder +{ + explicit heif_encoder(const heif_encoder_plugin* plugin); + + ~heif_encoder(); + + heif_error alloc(); + + void release(); + + void copy_parameters_from(const heif_encoder& src); + + + const struct heif_encoder_plugin* plugin; + void* encoder = nullptr; +}; + + +struct heif_region_item +{ + std::shared_ptr context; + std::shared_ptr region_item; +}; + + +struct heif_region +{ + std::shared_ptr context; // we need this to perform coordinate transformation + //heif_item_id parent_region_item_id; // we need this to perform coordinate transformation + std::shared_ptr region_item; + std::shared_ptr region; +}; + +struct heif_text_item +{ + std::shared_ptr context; + std::shared_ptr text_item; +}; + + +#endif diff -Nru libheif-1.19.8/libheif/bitstream.cc libheif-1.23.4/libheif/bitstream.cc --- libheif-1.19.8/libheif/bitstream.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/bitstream.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,7 +24,9 @@ #include #include -#if ((defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(__PGI)) && __GNUC__ < 9) || (defined(__clang__) && __clang_major__ < 10) +#include "common_utils.h" + +#if !defined(HAVE_BIT) #include #else #include @@ -73,8 +75,7 @@ StreamReader_memory::StreamReader_memory(const uint8_t* data, size_t size, bool copy) - : m_length(size), - m_position(0) + : m_length(size) { if (copy) { m_owned_data = new uint8_t[m_length]; @@ -163,8 +164,10 @@ BitstreamRange::BitstreamRange(std::shared_ptr istr, size_t start, size_t end) // one past end - : m_istr(std::move(istr)), m_remaining(end) + : m_istr(std::move(istr)), m_remaining(end - start) { + assert(end >= start); + bool success = m_istr->seek(start); assert(success); (void)success; // TODO @@ -268,10 +271,7 @@ return 0; } - return (uint32_t) ((buf[0] << 24) | - (buf[1] << 16) | - (buf[2] << 8) | - (buf[3])); + return four_bytes_to_uint32(buf[0], buf[1], buf[2], buf[3]); } @@ -360,7 +360,7 @@ // TODO: I am not sure this works everywhere as there seem to be systems where // the float byte order is different from the integer endianness // https://en.wikipedia.org/wiki/Endianness#Floating_point - int i = read32(); + uint32_t i = read32(); float f; memcpy(&f, &i, sizeof(float)); return f; @@ -395,12 +395,13 @@ return std::string(); } + auto istr = get_istream(); + for (;;) { if (!prepare_read(1)) { return std::string(); } - auto istr = get_istream(); char c; bool success = istr->read(&c, 1); @@ -409,7 +410,7 @@ return std::string(); } - if (c == 0) { + if (c == 0 || m_remaining==0) { break; } else { @@ -421,6 +422,55 @@ } +std::string BitstreamRange::read_fixed_string(int len) +{ + std::string str; + + if (!prepare_read(len)) { + return std::string(); + } + + auto istr = get_istream(); + + uint8_t n; + bool success = istr->read(&n, 1); + if (!success || n > len - 1) { + return {}; + } + + for (int i = 0; i < n; i++) { + char c; + success = istr->read(&c, 1); + + if (!success) { + set_eof_while_reading(); + return std::string(); + } + + str += (char) c; + } + + istr->seek_cur(len-n-1); + + return str; +} + + +std::string BitstreamRange::read_string_until_eof() +{ + size_t n = get_remaining_bytes(); + + [[maybe_unused]] bool success = prepare_read(n); + assert(success); // we are reading exactly the rest of the box + + std::string str; + str.resize(n); + get_istream()->read(str.data(), n); + + return str; +} + + bool BitstreamRange::read(uint8_t* data, size_t n) { if (!prepare_read(n)) { @@ -486,10 +536,11 @@ } -BitReader::BitReader(const uint8_t* buffer, int len) +BitReader::BitReader(const uint8_t* buffer, size_t len) + : data_start(buffer), + data_length(len) { data = buffer; - data_length = len; bytes_remaining = len; nextbits = 0; @@ -499,6 +550,18 @@ } +void BitReader::reset() +{ + data = data_start; + bytes_remaining = data_length; + + nextbits = 0; + nextbits_cnt = 0; + + refill(); +} + + uint32_t BitReader::get_bits(int n) { assert(n <= 32); @@ -587,11 +650,67 @@ return (int) val; } -void BitReader::skip_bytes(int nBytes) +void BitReader::skip_bytes(uint32_t nBytes) { - // TODO: this is slow - while (nBytes--) { - skip_bits(8); + // This has to run in constant time. The number of bytes to skip is taken directly + // from 32-bit file fields (e.g. the 'uncC' row/tile alignment), so a byte-at-a-time + // loop spins for billions of iterations when a malformed file asks to skip far + // beyond the end of the data. MinimizedImageBox::parse() guards against the same + // failure mode by validating its declared chunk sizes up front. + + uint64_t nBits = uint64_t{nBytes} * 8; + + // --- consume the bits that are already buffered in 'nextbits' + + if (nextbits_cnt > 0) { + uint64_t from_buffer = std::min(nBits, static_cast(nextbits_cnt)); + + if (from_buffer >= 64) { + nextbits = 0; + } + else { +#if AVOID_FUZZER_FALSE_POSITIVE + nextbits &= (0xffffffffffffffffULL >> from_buffer); +#endif + nextbits <<= from_buffer; + } + + nextbits_cnt -= static_cast(from_buffer); + nBits -= from_buffer; + } + + if (nBits == 0) { + return; + } + + // --- skip whole bytes directly in the input buffer, without pushing them + // through the bit buffer + + uint64_t whole_bytes = nBits / 8; + int residual_bits = static_cast(nBits % 8); + + if (whole_bytes >= bytes_remaining) { + // Skipping past the end of the data. Record the overshoot in 'nextbits_cnt' (which + // thereby goes negative) so that get_current_byte_index() keeps advancing exactly + // as it did with the previous bit-by-bit implementation. + uint64_t overshoot_bits = (whole_bytes - bytes_remaining) * 8 + static_cast(residual_bits); + + data += bytes_remaining; + bytes_remaining = 0; + nextbits = 0; + nextbits_cnt -= static_cast(overshoot_bits); + return; + } + + data += static_cast(whole_bytes); + bytes_remaining -= static_cast(whole_bytes); + nextbits = 0; + nextbits_cnt = 0; + + refill(); + + if (residual_bits > 0) { + skip_bits(residual_bits); } } @@ -621,7 +740,7 @@ void BitReader::skip_to_byte_boundary() { - int nskip = (nextbits_cnt & 7); + int nskip = static_cast(nextbits_cnt & 7); #if AVOID_FUZZER_FALSE_POSITIVE nextbits &= (0xffffffffffffffffULL >> nskip); @@ -631,7 +750,7 @@ nextbits_cnt -= nskip; } -bool BitReader::get_uvlc(int* value) +bool BitReader::get_uvlc(uint32_t* value) { int num_zeros = 0; @@ -641,10 +760,9 @@ if (num_zeros > MAX_UVLC_LEADING_ZEROS) { return false; } } - int offset = 0; if (num_zeros != 0) { - offset = (int) get_bits(num_zeros); - *value = offset + (1 << num_zeros) - 1; + uint32_t offset = get_bits(num_zeros); + *value = offset + (1u << num_zeros) - 1u; assert(*value > 0); return true; } @@ -654,19 +772,19 @@ } } -bool BitReader::get_svlc(int* value) +bool BitReader::get_svlc(int32_t* value) { - int v; + uint32_t v; if (!get_uvlc(&v)) { return false; } else if (v == 0) { - *value = v; + *value = 0; return true; } - bool negative = ((v & 1) == 0); - *value = negative ? -v / 2 : (v + 1) / 2; + bool negative = ((v & 1u) == 0); + *value = negative ? -static_cast(v / 2) : static_cast((v + 1) / 2); return true; } @@ -683,7 +801,13 @@ nextbits |= newval; } #else - int shift = 64 - nextbits_cnt; + if (bytes_remaining == 0) { + // Nothing to refill. Returning early also keeps the shift below out of range + // when nextbits_cnt is far negative after skipping past the end of the data. + return; + } + + int64_t shift = 64 - nextbits_cnt; while (shift >= 8 && bytes_remaining) { uint64_t newval = *data++; @@ -699,6 +823,57 @@ } +// --- BitWriter --- + +void BitWriter::write_bits(uint32_t value, int n) +{ + assert(n >= 0 && n <= 32); + + for (int i = n - 1; i >= 0; i--) { + uint8_t bit = (value >> i) & 1; + m_current_byte |= (bit << (7 - m_bits_in_current_byte)); + m_bits_in_current_byte++; + + if (m_bits_in_current_byte == 8) { + m_data.push_back(m_current_byte); + m_current_byte = 0; + m_bits_in_current_byte = 0; + } + } +} + +void BitWriter::write_bytes(const std::vector& data) +{ + write_bytes(data.data(), data.size()); +} + +void BitWriter::write_bytes(const uint8_t* data, size_t len) +{ + assert(m_bits_in_current_byte == 0); + m_data.insert(m_data.end(), data, data + len); +} + +void BitWriter::skip_to_byte_boundary() +{ + if (m_bits_in_current_byte > 0) { + m_data.push_back(m_current_byte); + m_current_byte = 0; + m_bits_in_current_byte = 0; + } +} + +std::vector BitWriter::get_data() const +{ + std::vector result = m_data; + if (m_bits_in_current_byte > 0) { + result.push_back(m_current_byte); + } + return result; +} + + +// --- StreamWriter --- + void StreamWriter::write8(uint8_t v) { if (m_position == m_data.size()) { @@ -830,9 +1005,9 @@ } -void StreamWriter::write(const std::string& str) +void StreamWriter::write(const std::string& str, bool end_with_null) { - size_t required_size = m_position + str.size() + 1; + size_t required_size = m_position + str.size() + (end_with_null ? 1 : 0); if (required_size > m_data.size()) { m_data.resize(required_size); @@ -842,7 +1017,31 @@ m_data[m_position++] = str[i]; } - m_data[m_position++] = 0; + if (end_with_null) { + m_data[m_position++] = 0; + } +} + + +void StreamWriter::write_fixed_string(std::string s, size_t len) +{ + size_t required_size = m_position + len; + + if (required_size > m_data.size()) { + m_data.resize(required_size); + } + + size_t n_chars = std::min(s.length(), len - 1); + assert(n_chars <= 255); + m_data[m_position++] = static_cast(n_chars); + + for (size_t i = 0; i < s.size() && i < len - 1; i++) { + m_data[m_position++] = s[i]; + } + + for (size_t i = s.size(); i < len - 1; i++) { + m_data[m_position++] = 0; + } } diff -Nru libheif-1.19.8/libheif/bitstream.h libheif-1.23.4/libheif/bitstream.h --- libheif-1.19.8/libheif/bitstream.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/bitstream.h 2026-09-06 14:45:17.000000000 +0000 @@ -143,7 +143,7 @@ private: const uint8_t* m_data; uint64_t m_length; - uint64_t m_position; + uint64_t m_position = 0; // if we made a copy of the data, we store a pointer to the owned memory area here uint8_t* m_owned_data = nullptr; @@ -206,7 +206,41 @@ } } else { - return std::numeric_limits::max(); + // wait_for_file_size() takes a signed int64_t, so we cannot probe a position + // beyond INT64_MAX: it would wrap to a negative value and the reader would + // misreport the size. No file accessed through this API can be that large, + // so clamp the search range to INT64_MAX. (Callers may pass UINT64_MAX to + // ask for the total file size.) + uint64_t hi = std::min(end_pos, std::numeric_limits::max()); + + auto result = m_func_table->wait_for_file_size(static_cast(hi), m_userdata); + if (result == heif_reader_grow_status_size_reached) { + return hi; + } + else { + uint64_t pos = m_func_table->get_position(m_userdata); + return bisect_filesize(pos, hi); + } + } + } + + uint64_t bisect_filesize(uint64_t mini, uint64_t maxi) { + // mini - <= filesize + // maxi - > filesize + + if (maxi == mini + 1) { + return mini; + } + + // Overflow-safe midpoint. 'maxi' is bounded by INT64_MAX (see request_range), + // but computing (mini + maxi) directly could still overflow uint64_t. + uint64_t pos = mini + (maxi - mini) / 2; + auto result = m_func_table->wait_for_file_size(static_cast(pos), m_userdata); + if (result == heif_reader_grow_status_size_reached) { + return bisect_filesize(pos, maxi); + } + else { + return bisect_filesize(mini, pos); } } @@ -278,6 +312,12 @@ std::string read_string(); + // A string stored with a fixed number of bytes. The first byte contains the string length and the extra bytes + // are filled with a padding 0. + std::string read_fixed_string(int len); + + std::string read_string_until_eof(); + bool read(uint8_t* data, size_t n); bool prepare_read(size_t nBytes); @@ -377,7 +417,9 @@ class BitReader { public: - BitReader(const uint8_t* buffer, int len); + BitReader(const uint8_t* buffer, size_t len); + + void reset(); uint32_t get_bits(int n); @@ -402,7 +444,7 @@ int peek_bits(int n); - void skip_bytes(int nBytes); + void skip_bytes(uint32_t nBytes); void skip_bits(int n); @@ -410,13 +452,19 @@ void skip_to_byte_boundary(); - bool get_uvlc(int* value); + bool get_uvlc(uint32_t* value); - bool get_svlc(int* value); + bool get_svlc(int32_t* value); - int get_current_byte_index() const + size_t get_current_byte_index() const { - return data_length - bytes_remaining - nextbits_cnt / 8; + // Computed in signed arithmetic: when we skipped past the end of the data, + // nextbits_cnt is negative and the index keeps growing beyond data_length (the + // 'uncC' alignment handling relies on that). Doing the subtraction in size_t + // would convert the negative operand first, which trips the 'integer' sanitizer. + // The result itself is always >= 0. + int64_t bytes_read = static_cast(data_length - bytes_remaining); + return static_cast(bytes_read - nextbits_cnt / 8); } int64_t get_bits_remaining() const @@ -425,17 +473,61 @@ } private: + const uint8_t* const data_start; const uint8_t* data; - int data_length; - int bytes_remaining; + const size_t data_length; + size_t bytes_remaining; uint64_t nextbits; // left-aligned bits - int nextbits_cnt; + + // Number of valid bits in 'nextbits'. Goes negative when we read or skip past the + // end of the data, in which case it holds the (negated) overshoot so that + // get_current_byte_index() keeps advancing. Has to be 64-bit because skip_bytes() + // may be asked to skip up to 2^32 bytes (= 2^35 bits) past the end. + int64_t nextbits_cnt; void refill(); // refill to at least 56+1 bits }; +class BitWriter +{ +public: + // Write n bits from the LSBs of value (n must be in [0,32]) + void write_bits(uint32_t value, int n); + + void write_bits8(uint8_t value, int n) { assert(n >= 0 && n <= 8); write_bits(value, n); } + + void write_bits16(uint16_t value, int n) { assert(n >= 0 && n <= 16); write_bits(value, n); } + + void write_bits32(uint32_t value, int n) { assert(n >= 0 && n <= 32); write_bits(value, n); } + + void write_bits32s(int32_t value) { write_bits(static_cast(value), 32); } + + void write_flag(bool flag) { write_bits(flag ? 1 : 0, 1); } + + // Write raw bytes. Must be at a byte boundary. + void write_bytes(const std::vector& data); + + void write_bytes(const uint8_t* data, size_t len); + + // Pad with zero bits to the next byte boundary. No-op if already aligned. + void skip_to_byte_boundary(); + + // Return all written data. Flushes any partial byte (zero-padded). + std::vector get_data() const; + + int get_current_byte_index() const { return static_cast(m_data.size()); } + + int64_t get_bits_written() const { return static_cast(m_data.size()) * 8 + m_bits_in_current_byte; } + +private: + std::vector m_data; + uint8_t m_current_byte = 0; + int m_bits_in_current_byte = 0; // bits already written into m_current_byte (0-7), packed from MSB +}; + + class StreamWriter { public: @@ -459,7 +551,9 @@ void write(int size, uint64_t value); - void write(const std::string&); + void write(const std::string&, bool end_with_null = true); + + void write_fixed_string(std::string s, size_t len); void write(const std::vector&); diff -Nru libheif-1.19.8/libheif/box.cc libheif-1.23.4/libheif/box.cc --- libheif-1.19.8/libheif/box.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/box.cc 2026-09-06 14:45:17.000000000 +0000 @@ -31,6 +31,7 @@ #include "codecs/avc_boxes.h" #include "codecs/avif_boxes.h" #include "image-items/tiled.h" +#include "sequences/seq_boxes.h" #include #include @@ -79,12 +80,28 @@ } } -Fraction::Fraction(uint32_t num, uint32_t den) +Result Fraction::from_signed(int64_t num, int64_t den) { - assert(num <= (uint32_t) std::numeric_limits::max()); - assert(den <= (uint32_t) std::numeric_limits::max()); + if (num < std::numeric_limits::min() || num > std::numeric_limits::max() || + den < std::numeric_limits::min() || den > std::numeric_limits::max()) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_fractional_number, + "Fraction value exceeds the supported range"); + } + + Fraction f(static_cast(num), static_cast(den)); + if (!f.is_valid()) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_fractional_number, + "Fraction with zero denominator"); + } + + return f; +} - *this = Fraction(int32_t(num), int32_t(den)); +Result Fraction::from_unsigned(uint32_t num, uint32_t den) +{ + return from_signed(num, den); } Fraction::Fraction(int64_t num, int64_t den) @@ -390,7 +407,15 @@ std::string BoxHeader::dump(Indent& indent) const { std::ostringstream sstr; - sstr << indent << "Box: " << get_type_string() << " -----\n"; + sstr << indent << "Box: " << get_type_string(); + const char* debug_name = debug_box_name(); + if (debug_name) { + sstr << " ----- (" << debug_name << ")\n"; + } + else { + sstr << " -----\n"; + } + sstr << indent << "size: " << get_box_size() << " (header size: " << get_header_size() << ")\n"; return sstr.str(); @@ -521,22 +546,42 @@ box = std::make_shared(); break; + case fourcc("iscl"): + box = std::make_shared(); + break; + case fourcc("iref"): box = std::make_shared(); break; + case fourcc("rref"): + box = std::make_shared(); + break; + case fourcc("hvcC"): box = std::make_shared(); break; + case fourcc("hvc1"): + box = std::make_shared(); + break; + case fourcc("av1C"): box = std::make_shared(); break; + case fourcc("av01"): + box = std::make_shared(); + break; + case fourcc("vvcC"): box = std::make_shared(); break; + case fourcc("vvc1"): + box = std::make_shared(); + break; + case fourcc("idat"): box = std::make_shared(); break; @@ -605,6 +650,10 @@ box = std::make_shared(); break; + case fourcc("ndwt"): + box = std::make_shared(); + break; + case fourcc("cmin"): box = std::make_shared(); break; @@ -621,6 +670,15 @@ box = std::make_shared(); break; + case fourcc("mjpg"): + box = std::make_shared(); + break; + + case fourcc("elng"): + box = std::make_shared(); + break; + + #if WITH_UNCOMPRESSED_CODEC case fourcc("cmpd"): box = std::make_shared(); @@ -641,10 +699,30 @@ case fourcc("cpat"): box = std::make_shared(); break; + + case fourcc("splz"): + box = std::make_shared(); + break; + + case fourcc("sbpm"): + box = std::make_shared(); + break; + + case fourcc("snuc"): + box = std::make_shared(); + break; + + case fourcc("cloc"): + box = std::make_shared(); + break; + + case fourcc("uncv"): + box = std::make_shared(); + break; #endif // --- JPEG 2000 - + case fourcc("j2kH"): box = std::make_shared(); break; @@ -665,15 +743,18 @@ box = std::make_shared(); break; + case fourcc("j2ki"): + box = std::make_shared(); + break; + // --- mski - + case fourcc("mskC"): box = std::make_shared(); break; -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES - // --- TAI timestamps + // --- TAI timestamps case fourcc("itai"): box = std::make_shared(); @@ -682,7 +763,6 @@ case fourcc("taic"): box = std::make_shared(); break; -#endif // --- AVC (H.264) @@ -690,17 +770,19 @@ box = std::make_shared(); break; + case fourcc("avc1"): + box = std::make_shared(); + break; + #if HEIF_ENABLE_EXPERIMENTAL_FEATURES case fourcc("tilC"): box = std::make_shared(); break; #endif -#if ENABLE_EXPERIMENTAL_MINI_FORMAT case fourcc("mini"): box = std::make_shared(); break; -#endif case fourcc("mdat"): // avoid generating a 'Box_other' @@ -714,11 +796,146 @@ else if (hdr.get_uuid_type() == std::vector{0x43, 0x63, 0xe9, 0x14, 0x5b, 0x7d, 0x4a, 0xab, 0x97, 0xae, 0xbe, 0xa6, 0x98, 0x03, 0xb4, 0x34}) { box = std::make_shared(); } + else if (hdr.get_uuid_type() == std::vector{0x26, 0x1e, 0xf3, 0x74, 0x1d, 0x97, 0x5b, 0xba, 0xac, 0xbd, 0x9d, 0x2c, 0x8e, 0xa7, 0x35, 0x22}) { + box = std::make_shared(); + } +#if WITH_UNCOMPRESSED_CODEC + else if (hdr.get_uuid_type() == std::vector{0x9d, 0xb9, 0xdd, 0x6e, 0x37, 0x3c, 0x5a, 0x4e, 0x81, 0x10, 0x21, 0xfc, 0x83, 0xa9, 0x11, 0xfd}) { + box = std::make_shared(); + } +#endif else { box = std::make_shared(hdr.get_short_type()); } break; + // --- sequences + + case fourcc("moov"): + box = std::make_shared(); + break; + + case fourcc("mvhd"): + box = std::make_shared(); + break; + + case fourcc("trak"): + box = std::make_shared(); + break; + + case fourcc("tkhd"): + box = std::make_shared(); + break; + + case fourcc("mdia"): + box = std::make_shared(); + break; + + case fourcc("mdhd"): + box = std::make_shared(); + break; + + case fourcc("minf"): + box = std::make_shared(); + break; + + case fourcc("vmhd"): + box = std::make_shared(); + break; + + case fourcc("stbl"): + box = std::make_shared(); + break; + + case fourcc("stsd"): + box = std::make_shared(); + break; + + case fourcc("stts"): + box = std::make_shared(); + break; + + case fourcc("ctts"): + box = std::make_shared(); + break; + + case fourcc("stsc"): + box = std::make_shared(); + break; + + case fourcc("stco"): + box = std::make_shared(); + break; + + case fourcc("stsz"): + box = std::make_shared(); + break; + + case fourcc("stss"): + box = std::make_shared(); + break; + + case fourcc("ccst"): + box = std::make_shared(); + break; + + case fourcc("auxi"): + box = std::make_shared(); + break; + + case fourcc("edts"): + box = std::make_shared(); + break; + + case fourcc("elst"): + box = std::make_shared(); + break; + + case fourcc("sbgp"): + box = std::make_shared(); + break; + + case fourcc("sgpd"): + box = std::make_shared(); + break; + + case fourcc("btrt"): + box = std::make_shared(); + break; + + case fourcc("saiz"): + box = std::make_shared(); + break; + + case fourcc("saio"): + box = std::make_shared(); + break; + + case fourcc("urim"): + box = std::make_shared(); + break; + + case fourcc("uri "): + box = std::make_shared(); + break; + + case fourcc("nmhd"): + box = std::make_shared(); + break; + + case fourcc("tref"): + box = std::make_shared(); + break; + + case fourcc("sdtp"): + box = std::make_shared(); + break; + + // OMAF + case fourcc("prfr"): + box = std::make_shared(); + break; + default: box = std::make_shared(hdr.get_short_type()); break; @@ -890,7 +1107,52 @@ Error Box::read_children(BitstreamRange& range, uint32_t max_number, const heif_security_limits* limits) { - uint32_t count = 0; + // A freshly parsed box has no children yet, so m_children.size() below tracks + // the number of children read so far. + assert(m_children.empty()); + + // Determine the applicable limit on the number of child boxes. + uint32_t max_children; + if (get_short_type() == fourcc("iinf")) { + max_children = limits->max_items; + } + else { + max_children = limits->max_children_per_box; + } + + // If the number of children is known in advance, reject an excessive count + // directly instead of reading children until the accumulated count reaches + // the limit. + if (max_number != READ_CHILDREN_ALL) { + if (max_children && max_number > max_children) { + std::stringstream sstr; + sstr << "Number of child boxes (" << max_number << ") in '" << get_type_string() + << "' box exceeds the security limit of " << max_children << "."; + + return Error(heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()); + } + + // The declared number of children cannot exceed what the remaining input + // could possibly hold (each box occupies at least an 8-byte header). A + // larger count means the box is truncated or malformed, so reject it + // before reading anything. This also bounds the reservation below, which + // matters when security limits are disabled and 'max_number' is otherwise + // unbounded. + size_t max_possible_children = range.get_remaining_bytes() / 8; + if (max_number > max_possible_children) { + std::stringstream sstr; + sstr << "'" << get_type_string() << "' box declares " << max_number + << " child boxes, but the remaining data can hold at most " + << max_possible_children << "."; + return Error(heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str()); + } + + m_children.reserve(max_number); + } while (!range.eof() && !range.error()) { std::shared_ptr box; @@ -900,14 +1162,6 @@ } if (max_number == READ_CHILDREN_ALL) { - uint32_t max_children; - if (get_short_type() == fourcc("iinf")) { - max_children = limits->max_items; - } - else { - max_children = limits->max_children_per_box; - } - if (max_children && m_children.size() > max_children) { std::stringstream sstr; sstr << "Maximum number of child boxes (" << max_children << ") in '" << get_type_string() << "' box exceeded."; @@ -921,17 +1175,26 @@ m_children.push_back(std::move(box)); - - // count the new child and end reading new children when we reached the expected number - - count++; - + // Stop once we have read the expected number of children. if (max_number != READ_CHILDREN_ALL && - count == max_number) { + m_children.size() == max_number) { break; } } + // If a specific number of children was expected, we must have read exactly + // that many. A short read means the box is truncated or malformed. Prefer + // this specific error over the lower-level range error below, which would + // otherwise mask it. + if (max_number != READ_CHILDREN_ALL && m_children.size() != max_number) { + std::stringstream sstr; + sstr << "'" << get_type_string() << "' box declares " << max_number + << " child boxes, but only " << m_children.size() << " could be read."; + return Error(heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str()); + } + return range.get_error(); } @@ -988,6 +1251,16 @@ } +void Box::patch_file_pointers_recursively(StreamWriter& writer, size_t offset) +{ + patch_file_pointers(writer, offset); + + for (auto& child : m_children) { + child->patch_file_pointers_recursively(writer, offset); + } +} + + Error Box_other::parse(BitstreamRange& range, const heif_security_limits* limits) { if (has_fixed_box_size()) { @@ -1053,8 +1326,8 @@ } sstr << write_raw_data_as_hex(m_data.data(), len, - "data: ", - " "); + indent.get_string() + "data: ", + indent.get_string() + " "); return sstr.str(); } @@ -1095,6 +1368,14 @@ uint64_t n_minor_brands = (get_box_size() - get_header_size() - 8) / 4; + if (limits->max_number_of_file_brands && n_minor_brands > limits->max_number_of_file_brands) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Number of minor brands in file exceeds security limit" + }; + } + for (uint64_t i = 0; i < n_minor_brands && !range.error(); i++) { m_compatible_brands.push_back(range.read32()); } @@ -1598,8 +1879,46 @@ // this function clears the array in some cases. This should be corrected. for (const auto& extent : item->extents) { + + // --- data that was appended in memory (append_data()) and not written to a file yet + + // This is the case for items added to a context that is being written. There may be no + // input stream at all (writer-only context), and even if there is one (context read from + // a file, then modified), the stream does not contain this data. + + if (!extent.data.empty() || extent.length == 0) { + uint64_t skip_len = std::min(offset, extent.length); + offset -= skip_len; + + uint64_t read_len = std::min(extent.length - skip_len, size); + + if (offset > 0 || read_len == 0) { + continue; + } + + auto max_memory_block_size = limits->max_memory_block_size; + if (max_memory_block_size && max_memory_block_size - dest->size() < read_len) { + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "iloc item data exceeds the maximum memory block size"}; + } + + dest->insert(dest->end(), + extent.data.begin() + static_cast(skip_len), + extent.data.begin() + static_cast(skip_len + read_len)); + + size -= read_len; + continue; + } + if (item->construction_method == 0) { + if (!istr) { + return {heif_error_Invalid_input, + heif_suberror_No_item_data, + "Item data is not available: the context has no input file"}; + } + // --- make sure that all data is available if (extent.offset > MAX_FILE_POS || @@ -1701,12 +2020,35 @@ "idat box referenced in iref box is not present in file"}; } - idat->read_data(istr, - extent.offset + item->base_offset, - extent.length, - *dest, limits); + // Honor the requested (offset, size) window, exactly like the in-memory + // and construction_method==0 branches above. Reading the whole extent and + // doing `size -= extent.length` unconditionally ignored the caller's + // sub-range: any partial read (size < extent.length) underflowed `size` + // and fell into the "Not enough data" check below, which broke per-tile + // and per-icef-unit reads of uncompressed items stored in 'idat' + // (unc_decoder::get_compressed_image_data_uncompressed()). + uint64_t skip_len = std::min(offset, extent.length); + offset -= skip_len; + + uint64_t read_len = std::min(extent.length - skip_len, size); + + if (offset > 0) { + continue; + } + + if (read_len == 0) { + continue; + } + + Error err = idat->read_data(istr, + extent.offset + item->base_offset + skip_len, + read_len, + *dest, limits); + if (err) { + return err; + } - size -= extent.length; + size -= read_len; } else { std::stringstream sstr; @@ -1931,9 +2273,24 @@ m_offset_size = 4; m_length_size = 4; - //m_base_offset_size = 4; // set above m_index_size = 0; + // extent.length is already known; extent.offset within an item equals + // the cumulative length of all preceding extents (they are written sequentially). + // base_offset absorbs the absolute file position, so extent.offset is relative. + for (const auto& item : m_items) { + uint64_t cumulative_offset = 0; + for (const auto& extent : item.extents) { + if (cumulative_offset > 0xFFFFFFFF) { + m_offset_size = 8; + } + if (extent.length > 0xFFFFFFFF) { + m_length_size = 8; + } + cumulative_offset += extent.length; + } + } + set_version((uint8_t) min_version); } @@ -1996,6 +2353,8 @@ writer.skip(nSkip); prepend_header(writer, box_start); + patch_iloc_header(writer); // Write iloc box. If there is an mdat, it will later be overwritten. + return Error::Ok; } @@ -2317,6 +2676,17 @@ return Error::Ok; } + // Sanity check. + if (limits->max_items && item_count > limits->max_items) { + std::stringstream sstr; + sstr << "iinf box contains " << item_count << " items, which exceeds the security limit of " + << limits->max_items << " items."; + + return Error(heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()); + } + return read_children(range, item_count, limits); } @@ -2580,7 +2950,7 @@ Box_mdcv::Box_mdcv() { set_short_type(fourcc("mdcv")); - + memset(&mdcv, 0, sizeof(heif_mastering_display_colour_volume)); } @@ -2688,6 +3058,43 @@ } +Error Box_ndwt::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("ndwt"); + } + + m_diffuse_white_luminance = range.read32(); + + return range.get_error(); +} + + +std::string Box_ndwt::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + + sstr << indent << "diffuse_white_luminance: " << m_diffuse_white_luminance << "\n"; + + return sstr.str(); +} + + +Error Box_ndwt::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(m_diffuse_white_luminance); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + Box_cclv::Box_cclv() { set_short_type(fourcc("cclv")); @@ -3025,8 +3432,8 @@ } -void Box_ipma::add_property_for_item_ID(heif_item_id itemID, - PropertyAssociation assoc) +heif_property_id Box_ipma::add_property_for_item_ID(heif_item_id itemID, + PropertyAssociation assoc) { size_t idx; for (idx = 0; idx < m_entries.size(); idx++) { @@ -3045,7 +3452,7 @@ // If the property is already associated with the item, skip. for (auto const& a : m_entries[idx].associations) { if (a.property_index == assoc.property_index) { - return; + return get_property_id_for_item_ID(itemID, assoc.property_index); } // TODO: should we check that the essential flag matches and return an internal error if not? @@ -3053,6 +3460,43 @@ // add the property association m_entries[idx].associations.push_back(assoc); + + return get_property_id_for_item_ID(itemID, assoc.property_index); +} + + +heif_property_id Box_ipma::get_property_id_for_item_ID(heif_item_id itemID, uint16_t property_index) const +{ + // Index 0 means "no property". It is skipped by Box_ipco::get_properties_for_item_ID() and + // hence has no position in the item's property list. + if (property_index == 0) { + return 0; + } + + for (const auto& entry : m_entries) { + if (entry.item_ID != itemID) { + continue; + } + + // Count the associations the way Box_ipco::get_properties_for_item_ID() does, i.e. skipping + // the associations with index 0, so that the returned id indexes the vector it produces. + heif_property_id id = 0; + for (const auto& assoc : entry.associations) { + if (assoc.property_index == 0) { + continue; + } + + id++; + + if (assoc.property_index == property_index) { + return id; + } + } + + break; + } + + return 0; } @@ -3151,19 +3595,17 @@ { auto properties = ipco->get_all_child_boxes(); - for (auto& item : m_entries) { - size_t nAssoc = item.associations.size(); - - // simple Bubble sort as a stable sorting algorithm + // Stable-partition: all descriptive properties before all transformative properties. + // The order within each group is preserved (spec requires it for transformative properties). - for (size_t n = 0; n < nAssoc - 1; n++) - for (size_t i = 0; i < nAssoc - 1 - n; i++) { - // If transformative property preceds descriptive property, swap them. - if (properties[item.associations[i].property_index - 1]->is_transformative_property() && - !properties[item.associations[i + 1].property_index - 1]->is_transformative_property()) { - std::swap(item.associations[i], item.associations[i+1]); - } - } + for (auto& item : m_entries) { + std::stable_partition(item.associations.begin(), item.associations.end(), + [&properties](const PropertyAssociation& assoc) { + if (assoc.property_index == 0 || assoc.property_index > properties.size()) { + return true; // keep invalid/unset entries in the descriptive group + } + return !properties[assoc.property_index - 1]->is_transformative_property(); + }); } } @@ -3305,6 +3747,57 @@ } +Error Box_iscl::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("iscl"); + } + + m_target_width_numerator = range.read16(); + m_target_width_denominator = range.read16(); + m_target_height_numerator = range.read16(); + m_target_height_denominator = range.read16(); + + if (m_target_width_numerator == 0 || m_target_width_denominator == 0 || + m_target_height_numerator == 0 || m_target_height_denominator == 0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_fractional_number, + "iscl property has zero numerator or denominator"}; + } + + return range.get_error(); +} + + +Error Box_iscl::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write16(m_target_width_numerator); + writer.write16(m_target_width_denominator); + writer.write16(m_target_height_numerator); + writer.write16(m_target_height_denominator); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_iscl::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "horizontal scaling factor: " << m_target_width_numerator << " / " << m_target_width_denominator << "\n"; + sstr << indent << "vertical scaling factor: " << m_target_height_numerator << " / " << m_target_height_denominator << "\n"; + + return sstr.str(); +} + + Error Box_clap::parse(BitstreamRange& range, const heif_security_limits* limits) { //parse_full_box_header(range); @@ -3321,28 +3814,24 @@ int32_t vertical_offset_num = (int32_t) range.read32(); uint32_t vertical_offset_den = (uint32_t) range.read32(); - if (clean_aperture_width_num > (uint32_t) std::numeric_limits::max() || - clean_aperture_width_den > (uint32_t) std::numeric_limits::max() || - clean_aperture_height_num > (uint32_t) std::numeric_limits::max() || - clean_aperture_height_den > (uint32_t) std::numeric_limits::max() || - horizontal_offset_den > (uint32_t) std::numeric_limits::max() || - vertical_offset_den > (uint32_t) std::numeric_limits::max()) { - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_fractional_number, - "Exceeded supported value range."); + // The checked Fraction construction rejects values outside the int32_t range and + // zero denominators. + auto clean_aperture_width = Fraction::from_unsigned(clean_aperture_width_num, clean_aperture_width_den); + auto clean_aperture_height = Fraction::from_unsigned(clean_aperture_height_num, clean_aperture_height_den); + auto horizontal_offset = Fraction::from_signed(horizontal_offset_num, horizontal_offset_den); + auto vertical_offset = Fraction::from_signed(vertical_offset_num, vertical_offset_den); + + for (const Result* f : {&clean_aperture_width, &clean_aperture_height, + &horizontal_offset, &vertical_offset}) { + if (!*f) { + return f->error(); + } } - m_clean_aperture_width = Fraction(clean_aperture_width_num, - clean_aperture_width_den); - m_clean_aperture_height = Fraction(clean_aperture_height_num, - clean_aperture_height_den); - m_horizontal_offset = Fraction(horizontal_offset_num, (int32_t) horizontal_offset_den); - m_vertical_offset = Fraction(vertical_offset_num, (int32_t) vertical_offset_den); - if (!m_clean_aperture_width.is_valid() || !m_clean_aperture_height.is_valid() || - !m_horizontal_offset.is_valid() || !m_vertical_offset.is_valid()) { - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_fractional_number); - } + m_clean_aperture_width = *clean_aperture_width; + m_clean_aperture_height = *clean_aperture_height; + m_horizontal_offset = *horizontal_offset; + m_vertical_offset = *vertical_offset; return range.get_error(); } @@ -3356,9 +3845,9 @@ writer.write32(m_clean_aperture_width.denominator); writer.write32(m_clean_aperture_height.numerator); writer.write32(m_clean_aperture_height.denominator); - writer.write32(m_horizontal_offset.numerator); + writer.write32s(m_horizontal_offset.numerator); writer.write32(m_horizontal_offset.denominator); - writer.write32(m_vertical_offset.numerator); + writer.write32s(m_vertical_offset.numerator); writer.write32(m_vertical_offset.denominator); prepend_header(writer, box_start); @@ -3400,39 +3889,45 @@ } -int Box_clap::left_rounded(uint32_t image_width) const +Result Box_clap::get_crop(uint32_t image_width, uint32_t image_height) const { - // pcX = horizOff + (width - 1)/2 - // pcX ± (cleanApertureWidth - 1)/2 - - // left = horizOff + (width-1)/2 - (clapWidth-1)/2 - - Fraction pcX = m_horizontal_offset + Fraction(image_width - 1U, 2U); - Fraction left = pcX - (m_clean_aperture_width - 1) / 2; - - return left.round_down(); -} + // The clean aperture is specified relative to the image center: + // pcX = horizOff + (width - 1)/2 + // left = pcX - (clapWidth - 1)/2 + // right = left + clapWidth - 1 + // (and likewise vertically). + // + // Computing this needs the image size inside the int32_t-based Fraction. A zero size + // would underflow `size - 1` (GHSA-jc8f-p23p-5hjg) and a size above INT32_MAX + 1 + // does not fit (GHSA-gh5q-69gg-c964). Such an image cannot be cropped with a 'clap', + // which is reported as an error instead of computing a bogus crop. + if (image_width == 0 || image_height == 0) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_clean_aperture, + "Clean aperture cannot be applied to an image with zero size"); + } -int Box_clap::right_rounded(uint32_t image_width) const -{ - Fraction right = m_clean_aperture_width - 1 + left_rounded(image_width); + auto halfWidth = Fraction::from_unsigned(image_width - 1, 2); + auto halfHeight = Fraction::from_unsigned(image_height - 1, 2); + if (!halfWidth || !halfHeight) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_clean_aperture, + "Clean aperture cannot be applied to an image larger than 2^31 pixels in any direction"); + } - return right.round(); -} + Fraction pcX = m_horizontal_offset + *halfWidth; + Fraction pcY = m_vertical_offset + *halfHeight; -int Box_clap::top_rounded(uint32_t image_height) const -{ - Fraction pcY = m_vertical_offset + Fraction(image_height - 1U, 2U); + Fraction left = pcX - (m_clean_aperture_width - 1) / 2; Fraction top = pcY - (m_clean_aperture_height - 1) / 2; - return top.round(); -} - -int Box_clap::bottom_rounded(uint32_t image_height) const -{ - Fraction bottom = m_clean_aperture_height - 1 + top_rounded(image_height); + Crop crop; + crop.left = left.round_down(); + crop.top = top.round(); + crop.right = (m_clean_aperture_width - 1 + crop.left).round(); + crop.bottom = (m_clean_aperture_height - 1 + crop.top).round(); - return bottom.round(); + return crop; } int Box_clap::get_width_rounded() const @@ -3445,17 +3940,32 @@ return m_clean_aperture_height.round(); } -void Box_clap::set(uint32_t clap_width, uint32_t clap_height, - uint32_t image_width, uint32_t image_height) +Error Box_clap::set(uint32_t clap_width, uint32_t clap_height, + uint32_t image_width, uint32_t image_height) { - assert(image_width >= clap_width); - assert(image_height >= clap_height); - - m_clean_aperture_width = Fraction(clap_width, 1U); - m_clean_aperture_height = Fraction(clap_height, 1U); + if (clap_width > image_width || clap_height > image_height) { + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Clean aperture is larger than the image"); + } + + auto width = Fraction::from_unsigned(clap_width, 1); + auto height = Fraction::from_unsigned(clap_height, 1); + auto horizontal_offset = Fraction::from_signed(-int64_t{image_width - clap_width}, 2); + auto vertical_offset = Fraction::from_signed(-int64_t{image_height - clap_height}, 2); + + if (!width || !height || !horizontal_offset || !vertical_offset) { + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Clean aperture values exceed the supported range"); + } + + m_clean_aperture_width = *width; + m_clean_aperture_height = *height; + m_horizontal_offset = *horizontal_offset; + m_vertical_offset = *vertical_offset; - m_horizontal_offset = Fraction(-(int32_t) (image_width - clap_width), 2); - m_vertical_offset = Fraction(-(int32_t) (image_height - clap_height), 2); + return Error::Ok; } @@ -3468,6 +3978,29 @@ } while (!range.eof()) { + // Cap the total number of reference entries, matching the item-count caps in + // Box_iloc/Box_iinf/Box_ipma (and the per-entry nRefs cap below). Without it + // the entry count is bounded only by the file size. That is a linear, + // file-bounded allocation rather than an amplification, so this is a + // consistency limit and not a security fix, but it rejects a pathological + // 'iref' early with a clear error instead of parsing it in full. + // + // max_items is a heuristic ceiling here, not a semantically exact bound: the + // number of entries is not strictly limited by the number of items, because + // one item may be the source (from_item_ID) of several entries, one per + // reference type (e.g. 'dimg', 'thmb', 'cdsc'). A well-formed file stays far + // below max_items (default 1000) regardless, so the generous ceiling is fine + // as a sanity limit; raise max_items if a legitimate file ever exceeds it. + if (limits->max_items && m_references.size() >= limits->max_items) { + std::stringstream sstr; + sstr << "'iref' box contains more than " << limits->max_items + << " reference entries, which exceeds the security limit."; + + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + Reference ref; Error err = ref.header.parse_header(range); @@ -3520,7 +4053,7 @@ #if 0 - // Note: This input sanity check does not work as expected. + // Note: This input sanity check first did not work as expected. // Its idea was to prevent infinite recursions while decoding when the input file // contains cyclic references. However, apparently there are cases where cyclic // references are actually allowed, like with images that have premultiplied alpha channels: @@ -3529,17 +4062,35 @@ // | reference with type 'auxl' from ID: 2 to IDs: 1 // | reference with type 'prem' from ID: 1 to IDs: 2 // - // TODO: implement the infinite recursion detection in a different way. E.g. by passing down - // the already processed item-ids while decoding an image and checking whether the current - // item has already been decoded before. + // We now test for cyclic references during the image decoding. + // We pass down the item IDs that have already been seen during the decoding process. + // If we try to decode an image IDs that has already been seen previously, we throw an error. + // The advantage is that the error only occurs when we are trying to decode the faulty image. // --- check for cyclic references for (const auto& ref : m_references) { + if (ref.header.get_short_type() != fourcc("dimg") && + ref.header.get_short_type() != fourcc("auxl")) { + continue; + } + std::set reached_ids; // IDs that we have already reached in the DAG std::set todo; // IDs that still need to be followed - todo.insert(ref.from_item_ID); // start at base item + bool reverse = (ref.header.get_short_type() != fourcc("auxl")); + + if (!reverse) { + todo.insert(ref.from_item_ID); // start at base item + } + else { + if (ref.to_item_ID.empty()) { + continue; + } + + // TODO: what if aux image is assigned to multiple images? + todo.insert(ref.to_item_ID[0]); // start at base item + } while (!todo.empty()) { // transfer ID into set of reached IDs @@ -3550,12 +4101,33 @@ // if this ID refers to another 'iref', follow it for (const auto& succ_ref : m_references) { - if (succ_ref.from_item_ID == id) { + if (succ_ref.header.get_short_type() != fourcc("dimg") && + succ_ref.header.get_short_type() != fourcc("auxl")) { + continue; + } + + heif_item_id from; + std::vector to; + + if (succ_ref.header.get_short_type() == fourcc("auxl")) { + if (succ_ref.to_item_ID.empty()) { + continue; + } + + from = succ_ref.to_item_ID[0]; + to = {succ_ref.from_item_ID}; + } + else { + from = succ_ref.from_item_ID; + to = succ_ref.to_item_ID; + } + + if (from == id) { // Check whether any successor IDs has been visited yet, which would be an error. // Otherwise, put that ID into the 'todo' set. - for (const auto& succ_ref_id : succ_ref.to_item_ID) { + for (const auto& succ_ref_id : to) { if (reached_ids.find(succ_ref_id) != reached_ids.end()) { return Error(heif_error_Invalid_input, heif_suberror_Unspecified, @@ -3570,6 +4142,8 @@ } #endif + build_index(); + return range.get_error(); } @@ -3666,15 +4240,25 @@ } -bool Box_iref::has_references(uint32_t itemID) const +void Box_iref::build_index() { - for (const Reference& ref : m_references) { - if (ref.from_item_ID == itemID) { - return true; - } + m_from_id_index.clear(); + m_from_id_index.reserve(m_references.size()); + for (size_t i = 0; i < m_references.size(); i++) { + add_to_index(i); } +} - return false; + +void Box_iref::add_to_index(size_t reference_index) +{ + m_from_id_index[m_references[reference_index].from_item_ID].push_back(reference_index); +} + + +bool Box_iref::has_references(uint32_t itemID) const +{ + return m_from_id_index.find(itemID) != m_from_id_index.end(); } @@ -3682,9 +4266,11 @@ { std::vector references; - for (const Reference& ref : m_references) { - if (ref.from_item_ID == itemID) { - references.push_back(ref); + auto iter = m_from_id_index.find(itemID); + if (iter != m_from_id_index.end()) { + references.reserve(iter->second.size()); + for (size_t ref_idx : iter->second) { + references.push_back(m_references[ref_idx]); } } @@ -3694,10 +4280,13 @@ std::vector Box_iref::get_references(uint32_t itemID, uint32_t ref_type) const { - for (const Reference& ref : m_references) { - if (ref.from_item_ID == itemID && - ref.header.get_short_type() == ref_type) { - return ref.to_item_ID; + auto iter = m_from_id_index.find(itemID); + if (iter != m_from_id_index.end()) { + for (size_t ref_idx : iter->second) { + const Reference& ref = m_references[ref_idx]; + if (ref.header.get_short_type() == ref_type) { + return ref.to_item_ID; + } } } @@ -3715,6 +4304,7 @@ assert(to_ids.size() <= 0xFFFF); m_references.push_back(ref); + add_to_index(m_references.size() - 1); } @@ -3733,6 +4323,149 @@ } +Error Box_rref::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 1) { + return unsupported_version_error("rref"); + } + + size_t remainingBytes = range.get_remaining_bytes(); + + // The number of reference types should be stored in uint(8), but the + // common test images C043, C044 store them as uint(32). + // Let us detect this case by the number of data bytes in this box. + bool cnt_as_uint32 = (remainingBytes > 0 && remainingBytes % 4 == 0); + + uint8_t nRefTypes; + if (cnt_as_uint32) { + // fix broken C043, C044 files + // TODO: remove me when the files have been corrected as the above check can misfire when there is extra padding + nRefTypes = (uint8_t)range.read32(); + } + else { + nRefTypes = range.read8(); + } + + for (uint8_t i = 0; i < nRefTypes; i++) { + uint32_t refType = range.read32(); + + if (range.error()) { + std::stringstream sstr; + sstr << "rref box should contain " << ((int)nRefTypes) << " reference types, but we can only read " << m_reference_types.size() << " reference types."; + + return { + heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str() + }; + } + + m_reference_types.push_back(refType); + } + + return range.get_error(); +} + + +static constexpr std::array supported_reference_types{ + fourcc("thmb"), + fourcc("base"), + fourcc("cdsc"), + fourcc("dimg"), + fourcc("auxl"), + fourcc("prem") +}; + + +bool Box_rref::all_reference_types_supported() const +{ + // TODO: replace with std::ranges variant once it is widely supported (e.g. on older clang/libc++): + // return std::ranges::all_of(m_reference_types, [](uint32_t t) { + // return std::ranges::find(supported_reference_types, t) != supported_reference_types.end(); + // }); + return std::all_of(m_reference_types.begin(), m_reference_types.end(), [](uint32_t t) { + return std::find(supported_reference_types.begin(), supported_reference_types.end(), t) + != supported_reference_types.end(); + }); +} + + +Error Box_rref::reference_types_supported_error() const +{ + std::vector unsupported_types; + + for (uint32_t refType : m_reference_types) { + // TODO: replace with std::ranges variant once it is widely supported (e.g. on older clang/libc++): + // if (std::ranges::find(supported_reference_types, refType) == supported_reference_types.end()) { + if (std::find(supported_reference_types.begin(), supported_reference_types.end(), refType) == supported_reference_types.end()) { + unsupported_types.push_back(refType); + } + } + + if (unsupported_types.empty()) { + return Error::Ok; + } + + std::stringstream sstr; + + sstr << "Unsupported reference types: "; + for (size_t i = 0; i < unsupported_types.size(); i++) { + if (i > 0) { + sstr << ", "; + } + sstr << fourcc_to_string(unsupported_types[i]); + } + + return { + heif_error_Unsupported_feature, + heif_suberror_Unspecified, + sstr.str() + }; +} + + +Error Box_rref::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (m_reference_types.size() > std::numeric_limits::max()) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Too many rref reference types." + }; + } + + writer.write8(static_cast(m_reference_types.size())); + + for (uint32_t refType : m_reference_types) { + writer.write32(refType); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_rref::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + + sstr << indent << "reference types: "; + for (size_t i = 0; i < m_reference_types.size(); i++) { + if (i > 0) sstr << ", "; + sstr << fourcc_to_string(m_reference_types[i]); + } + sstr << "\n"; + + return sstr.str(); +} + + Error Box_idat::parse(BitstreamRange& range, const heif_security_limits* limits) { //parse_full_box_header(range); @@ -4074,16 +4807,28 @@ "Too many entities in dref box."); } - Error err = read_children(range, (int)nEntities, limits); - if (err) { - return err; - } + // read_children verifies that exactly nEntities children are present. + return read_children(range, (int)nEntities, limits); +} + + +Error Box_dref::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); - if (m_children.size() != nEntities) { - // TODO return Error( + if (m_children.size() > 0xFFFF) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Too many dref children boxes." + }; } - return err; + writer.write32(static_cast(m_children.size())); + write_children(writer); + + prepend_header(writer, box_start); + return Error::Ok; } @@ -4117,6 +4862,23 @@ } +Error Box_url::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (m_location.empty()) { + assert(get_flags() == 1); + } + else { + assert(get_flags() == 0); + writer.write(m_location); + } + + prepend_header(writer, box_start); + return Error::Ok; +} + + std::string Box_url::dump(Indent& indent) const { std::ostringstream sstr; @@ -4151,7 +4913,7 @@ sstr << indent << "lang: " << m_lang << "\n"; sstr << indent << "name: " << m_name << "\n"; sstr << indent << "description: " << m_description << "\n"; - sstr << indent << "tags: " << m_lang << "\n"; + sstr << indent << "tags: " << m_tags << "\n"; return sstr.str(); } @@ -4642,30 +5404,50 @@ } -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES -std::string Box_taic::dump(Indent& indent) const { +std::string Box_taic::dump(const heif_tai_clock_info& info, Indent& indent) +{ std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << indent << "time_uncertainty: " << m_time_uncertainty << "\n"; - sstr << indent << "clock_resolution: " << m_clock_resolution << "\n"; + sstr << indent << "time_uncertainty: "; + if (info.time_uncertainty == heif_tai_clock_info_time_uncertainty_unknown) { + sstr << "unknown\n"; + } + else { + sstr << info.time_uncertainty << "\n"; + } + sstr << indent << "clock_resolution: " << info.clock_resolution << "\n"; sstr << indent << "clock_drift_rate: "; - if (heif_is_tai_clock_info_drift_rate_undefined(m_clock_drift_rate)) { + if (info.clock_drift_rate == heif_tai_clock_info_clock_drift_rate_unknown) { sstr << "undefined\n"; } else { - sstr << m_clock_drift_rate << "\n"; + sstr << info.clock_drift_rate << "\n"; } - sstr << indent << "clock_type: " << static_cast(m_clock_type) << "\n"; + sstr << indent << "clock_type: " << int(info.clock_type) << " "; + switch (info.clock_type) { + case heif_tai_clock_info_clock_type_unknown: sstr << "(unknown)\n"; break; + case heif_tai_clock_info_clock_type_synchronized_to_atomic_source: sstr << "(synchronized to atomic source)\n"; break; + case heif_tai_clock_info_clock_type_not_synchronized_to_atomic_source: sstr << "(not synchronized to atomic source)\n"; break; + default: sstr << "(illegal value)\n"; break;; + } + return sstr.str(); +} + + +std::string Box_taic::dump(Indent& indent) const { + std::ostringstream sstr; + sstr << Box::dump(indent); + sstr << dump(m_info, indent); + return sstr.str(); } Error Box_taic::write(StreamWriter& writer) const { size_t box_start = reserve_box_header_space(writer); - writer.write64(m_time_uncertainty); - writer.write32(m_clock_resolution); - writer.write32(m_clock_drift_rate); - writer.write8(m_clock_type); + writer.write64(m_info.time_uncertainty); + writer.write32(m_info.clock_resolution); + writer.write32(m_info.clock_drift_rate); + writer.write8(static_cast(m_info.clock_type << 6)); prepend_header(writer, box_start); @@ -4675,34 +5457,127 @@ Error Box_taic::parse(BitstreamRange& range, const heif_security_limits*) { parse_full_box_header(range); - m_time_uncertainty = range.read64(); - m_clock_resolution = range.read32(); + m_info.time_uncertainty = range.read64(); + m_info.clock_resolution = range.read32(); - m_clock_drift_rate = range.read32s(); - m_clock_type = range.read8(); + m_info.clock_drift_rate = range.read32s(); + m_info.clock_type = range.read8() >> 6; return range.get_error(); } + +bool operator==(const heif_tai_clock_info& a, + const heif_tai_clock_info& b) +{ + return a.version == b.version && + a.time_uncertainty == b.time_uncertainty && + a.clock_resolution == b.clock_resolution && + a.clock_drift_rate == b.clock_drift_rate && + a.clock_type == b.clock_type; +} + +bool Box_taic::operator==(const Box& other) const +{ + const auto* other_taic = dynamic_cast(&other); + if (other_taic == nullptr) { + return false; + } + + return m_info == other_taic->m_info; +} + + std::string Box_itai::dump(Indent& indent) const { std::ostringstream sstr; sstr << Box::dump(indent); - sstr << indent << "tai_timestamp: " << m_tai_timestamp << "\n"; - sstr << indent << "synchronization_state: " << m_synchronization_state << "\n"; - sstr << indent << "timestamp_generation_failure: " << m_timestamp_generation_failure << "\n"; - sstr << indent << "timestamp_is_modified: " << m_timestamp_is_modified << "\n"; + sstr << indent << "tai_timestamp: " << m_timestamp.tai_timestamp << "\n"; + sstr << indent << "synchronization_state: " << int(m_timestamp.synchronization_state) << "\n"; + sstr << indent << "timestamp_generation_failure: " << int(m_timestamp.timestamp_generation_failure) << "\n"; + sstr << indent << "timestamp_is_modified: " << int(m_timestamp.timestamp_is_modified) << "\n"; return sstr.str(); } -Error Box_itai::write(StreamWriter& writer) const { - size_t box_start = reserve_box_header_space(writer); - writer.write64(m_tai_timestamp); + +std::vector Box_itai::encode_tai_to_bitstream(const heif_tai_timestamp_packet* tai) +{ + StreamWriter writer; + writer.write64(tai->tai_timestamp); uint8_t status_bits = 0; - status_bits |= m_synchronization_state ? (1 << 7) : 0; - status_bits |= m_timestamp_generation_failure ? (1 << 6) : 0; - status_bits |= m_timestamp_is_modified ? (1 << 5) : 0; + status_bits |= tai->synchronization_state ? (1 << 7) : 0; + status_bits |= tai->timestamp_generation_failure ? (1 << 6) : 0; + status_bits |= tai->timestamp_is_modified ? (1 << 5) : 0; writer.write8(status_bits); + + return writer.get_data(); +} + +bool operator==(const heif_tai_timestamp_packet& a, + const heif_tai_timestamp_packet& b) +{ + return a.version == b.version && + a.tai_timestamp == b.tai_timestamp && + a.synchronization_state == b.synchronization_state && + a.timestamp_generation_failure == b.timestamp_generation_failure && + a.timestamp_is_modified == b.timestamp_is_modified; +} + +bool Box_itai::operator==(const Box& other) const +{ + const auto* other_itai = dynamic_cast(&other); + if (other_itai == nullptr) { + return false; + } + + return m_timestamp == other_itai->m_timestamp; +} + + + +uint64_t uint8_vector_to_uint64_BE(const uint8_t* data) +{ + uint64_t value = ((static_cast(data[0]) << 56) | + (static_cast(data[1]) << 48) | + (static_cast(data[2]) << 40) | + (static_cast(data[3]) << 32) | + (static_cast(data[4]) << 24) | + (static_cast(data[5]) << 16) | + (static_cast(data[6]) << 8) | + (static_cast(data[7]) << 0)); + + return value; +} + + +Result Box_itai::decode_tai_from_vector(const std::vector& data) +{ + if (data.size() != 9) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Wrong size of TAI timestamp data"}; + } + + uint8_t status_bits = data[8]; + + heif_tai_timestamp_packet tai; + tai.version = 1; + tai.tai_timestamp = uint8_vector_to_uint64_BE(data.data()); + tai.synchronization_state = !!(status_bits & 0x80); + tai.timestamp_generation_failure = !!(status_bits & 0x40); + tai.timestamp_is_modified = !!(status_bits & 0x20); + + return tai; +} + + +Error Box_itai::write(StreamWriter& writer) const { + size_t box_start = reserve_box_header_space(writer); + + std::vector tai_data = encode_tai_to_bitstream(&m_timestamp); + + writer.write(tai_data); + prepend_header(writer, box_start); return Error::Ok; } @@ -4710,14 +5585,73 @@ Error Box_itai::parse(BitstreamRange& range, const heif_security_limits*) { parse_full_box_header(range); - m_tai_timestamp = range.read64(); + m_timestamp.version = 1; + m_timestamp.tai_timestamp = range.read64(); uint8_t status_bits = range.read8(); - m_synchronization_state = !!(status_bits & 0x80); - m_timestamp_generation_failure = !!(status_bits & 0x40); - m_timestamp_is_modified = !!(status_bits & 0x20); + m_timestamp.synchronization_state = !!(status_bits & 0x80); + m_timestamp.timestamp_generation_failure = !!(status_bits & 0x40); + m_timestamp.timestamp_is_modified = !!(status_bits & 0x20); return range.get_error(); } -#endif + +Error Box_elng::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("elng"); + } + + m_lang = range.read_string(); + return range.get_error(); +} + +std::string Box_elng::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + sstr << indent << "extended_language: " << m_lang << "\n"; + return sstr.str(); +} + +Error Box_elng::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + writer.write(m_lang); + prepend_header(writer, box_start); + return Error::Ok; +} + + +Error Box_gimi_content_id::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + m_content_id = range.read_string_until_eof(); + + return range.get_error(); +} + + +Error Box_gimi_content_id::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write(m_content_id, false); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_gimi_content_id::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + + sstr << indent << "content ID: " << m_content_id << "\n"; + + return sstr.str(); +} diff -Nru libheif-1.19.8/libheif/box.h libheif-1.23.4/libheif/box.h --- libheif-1.19.8/libheif/box.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/box.h 2026-09-06 14:45:17.000000000 +0000 @@ -26,11 +26,13 @@ #include "libheif/heif.h" #include "libheif/heif_experimental.h" #include "libheif/heif_properties.h" +#include "libheif/heif_tai_timestamps.h" #include #include #include #include +#include #include #include #include @@ -64,10 +66,15 @@ public: Fraction() = default; - Fraction(int32_t num, int32_t den); + // Checked construction from externally supplied values (e.g. box fields or image + // sizes). Fails if a value does not fit into int32_t or if the denominator is zero. + static Result from_signed(int64_t num, int64_t den); + + static Result from_unsigned(uint32_t num, uint32_t den); - // may only use values up to int32_t maximum - Fraction(uint32_t num, uint32_t den); + // The constructors are meant for arithmetic on values that are already known to be + // in range. They never fail: values are reduced in precision until they fit. + Fraction(int32_t num, int32_t den); // Values will be reduced until they fit into int32_t. Fraction(int64_t num, int64_t den); @@ -127,6 +134,8 @@ std::string get_type_string() const; + virtual const char* debug_box_name() const { return nullptr; } + void set_short_type(uint32_t type) { m_type = type; } @@ -193,6 +202,10 @@ void derive_box_version_recursive(); + virtual void patch_file_pointers(StreamWriter&, size_t offset) {} + + void patch_file_pointers_recursively(StreamWriter&, size_t offset); + std::string dump(Indent&) const override; template [[nodiscard]] std::shared_ptr get_child_box() const @@ -256,6 +269,8 @@ virtual parse_error_fatality get_parse_error_fatality() const { return parse_error_fatality::fatal; } + // Note: this function may never be called for `ispe` items since it depends + // on the image item type whether the `ispe` is essential. virtual bool is_essential() const { return m_is_essential; } // only used for properties void set_is_essential(bool flag) { m_is_essential = flag; } @@ -399,6 +414,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "File Type"; } + bool has_compatible_brand(uint32_t brand) const; std::vector list_brands() const { return m_compatible_brands; } @@ -437,6 +454,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Free Space"; } + Error write(StreamWriter& writer) const override; protected: @@ -454,6 +473,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Metadata"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -469,6 +490,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Handler Reference"; } + uint32_t get_handler_type() const { return m_handler_type; } void set_handler_type(uint32_t handler) { m_handler_type = handler; } @@ -498,6 +521,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Primary Item"; } + heif_item_id get_item_ID() const { return m_item_ID; } void set_item_ID(heif_item_id id) { m_item_ID = id; } @@ -525,6 +550,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Location"; } + struct Extent { uint64_t index = 0; @@ -624,6 +651,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Info Entry"; } + bool is_hidden_item() const { return m_hidden_item; } void set_hidden_item(bool hidden); @@ -684,6 +713,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Information"; } + void derive_box_version() override; Error write(StreamWriter& writer) const override; @@ -706,6 +737,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Properties"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -735,6 +768,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Property Container"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -760,11 +795,14 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Image Spatial Extents"; } + Error write(StreamWriter& writer) const override; bool operator==(const Box& other) const override; - bool is_essential() const override { return false; } + // Note: this depends on the image item type. Never call this for an `ispe` property. + bool is_essential() const override { assert(false); return false; } protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; @@ -785,6 +823,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Property Association"; } + struct PropertyAssociation { bool essential; @@ -795,8 +835,18 @@ bool is_property_essential_for_item(heif_item_id itemId, int propertyIndex) const; - void add_property_for_item_ID(heif_item_id itemID, - PropertyAssociation assoc); + // Associates the property with the item and returns the 1-based position of the property + // within this item's property list, i.e. the 'heif_property_id' that the public API uses. + // Note that this is not the index of the property in the 'ipco' box, as one 'ipco' is shared + // by all items of the file. If the property is already associated with the item, the position + // of the existing association is returned. + heif_property_id add_property_for_item_ID(heif_item_id itemID, + PropertyAssociation assoc); + + // Returns the 1-based position of the property with the given 'ipco' index within the item's + // property list, or 0 if the property is not associated with the item. This matches the + // indices into the vector filled by Box_ipco::get_properties_for_item_ID(). + heif_property_id get_property_id_for_item_ID(heif_item_id itemID, uint16_t property_index) const; void derive_box_version() override; @@ -838,6 +888,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Image Properties for Auxiliary Images"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; @@ -863,6 +915,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Image Rotation"; } + int get_rotation_ccw() const { return m_rotation; } // Only these multiples of 90 are allowed: 0, 90, 180, 270. @@ -898,6 +952,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Image Mirroring"; } + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; } protected: @@ -910,6 +966,50 @@ }; +class Box_iscl : public FullBox +{ +public: + Box_iscl() + { + set_short_type(fourcc("iscl")); + } + + bool is_essential() const override { return true; } + + bool is_transformative_property() const override { return true; } + + uint16_t get_target_width_numerator() const { return m_target_width_numerator; } + uint16_t get_target_width_denominator() const { return m_target_width_denominator; } + uint16_t get_target_height_numerator() const { return m_target_height_numerator; } + uint16_t get_target_height_denominator() const { return m_target_height_denominator; } + + void set_scale(uint16_t w_num, uint16_t w_den, uint16_t h_num, uint16_t h_den) + { + m_target_width_numerator = w_num; + m_target_width_denominator = w_den; + m_target_height_numerator = h_num; + m_target_height_denominator = h_den; + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Image Scaling"; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + Error write(StreamWriter& writer) const override; + +private: + uint16_t m_target_width_numerator = 1; + uint16_t m_target_width_denominator = 1; + uint16_t m_target_height_numerator = 1; + uint16_t m_target_height_denominator = 1; +}; + + class Box_clap : public Box { public: @@ -924,10 +1024,21 @@ std::string dump(Indent&) const override; - int left_rounded(uint32_t image_width) const; // first column - int right_rounded(uint32_t image_width) const; // last column that is part of the cropped image - int top_rounded(uint32_t image_height) const; // first row - int bottom_rounded(uint32_t image_height) const; // last row included in the cropped image + const char* debug_box_name() const override { return "Clean Aperture"; } + + // The clean aperture in pixel coordinates of an image with the given size. + // 'right' and 'bottom' are the last column/row that are part of the cropped image. + struct Crop + { + int left = 0; + int top = 0; + int right = 0; + int bottom = 0; + }; + + // Fails if the clean aperture cannot be applied to an image of that size + // (zero size, or a size that exceeds the int32_t coordinate range). + Result get_crop(uint32_t image_width, uint32_t image_height) const; double left(int image_width) const; double top(int image_height) const; @@ -936,8 +1047,10 @@ int get_height_rounded() const; - void set(uint32_t clap_width, uint32_t clap_height, - uint32_t image_width, uint32_t image_height); + // Set a clean aperture of size clap_width x clap_height at the top-left corner of an + // image of size image_width x image_height. + Error set(uint32_t clap_width, uint32_t clap_height, + uint32_t image_width, uint32_t image_height); [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; } @@ -973,6 +1086,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Reference"; } + bool has_references(heif_item_id itemID) const; std::vector get_references(heif_item_id itemID, uint32_t ref_type) const; @@ -994,6 +1109,49 @@ private: std::vector m_references; + + // Index from 'from_item_ID' to the positions in m_references, so the + // reference queries run in O(matches) instead of O(m_references). It is kept + // in sync incrementally: parse() builds it, add_references() appends to it, + // and overwrite_reference() leaves it untouched (it only edits to_item_ID, + // not from_item_ID). + std::unordered_map> m_from_id_index; + + void build_index(); + + void add_to_index(size_t reference_index); +}; + + +class Box_rref : public FullBox +{ +public: + Box_rref() + { + set_short_type(fourcc("rref")); + } + + bool is_essential() const override { return true; } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Required Reference Types"; } + + const std::vector& get_reference_types() const { return m_reference_types; } + + bool all_reference_types_supported() const; + + Error reference_types_supported_error() const; + + void add_reference_type(uint32_t type) { m_reference_types.push_back(type); } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + Error write(StreamWriter& writer) const override; + +private: + std::vector m_reference_types; }; @@ -1002,6 +1160,8 @@ public: std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item Data"; } + Error read_data(const std::shared_ptr& istr, uint64_t start, uint64_t length, std::vector& out_data, @@ -1039,6 +1199,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Groups List"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -1079,6 +1241,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Stereo pair"; } + heif_item_id get_left_image() const { return entity_ids[0]; } heif_item_id get_right_image() const { return entity_ids[1]; } @@ -1098,6 +1262,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Image pyramid group"; } + Error write(StreamWriter& writer) const override; struct LayerInfo { @@ -1136,8 +1302,15 @@ class Box_dinf : public Box { public: + Box_dinf() + { + set_short_type(fourcc("dinf")); + } + std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Data Information"; } + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -1146,8 +1319,17 @@ class Box_dref : public FullBox { public: + Box_dref() + { + set_short_type(fourcc("dref")); + } + std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Data Reference"; } + + Error write(StreamWriter& writer) const override; + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; }; @@ -1156,10 +1338,24 @@ class Box_url : public FullBox { public: + Box_url() + { + set_short_type(fourcc("url ")); + set_flags(1); + } + std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Data Entry URL"; } + bool is_same_file() const { return m_location.empty(); } + void set_location(const std::string& loc) { m_location = loc; set_flags(m_location.empty() ? 1 : 0); } + + void set_location_same_file() { m_location.clear(); set_flags(1); } + + Error write(StreamWriter& writer) const override; + protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; @@ -1178,13 +1374,21 @@ int get_bits_per_channel(int channel) const { return m_bits_per_channel[channel]; } - void add_channel_bits(uint8_t c) + bool add_channel_bits(uint16_t c) { - m_bits_per_channel.push_back(c); + if (c <= 255) { + m_bits_per_channel.push_back(static_cast(c)); + return true; + } + else { + return false; + } } std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Pixel Information"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1210,6 +1414,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Pixel Aspect Ratio"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1231,6 +1437,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Layer Selection"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1255,6 +1463,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Content Light Level Information"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1273,6 +1483,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Master Display Colour Volume"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1291,6 +1503,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Ambient Viewing Environment"; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1300,6 +1514,36 @@ }; +class Box_ndwt : public FullBox +{ +public: + Box_ndwt() + { + set_short_type(fourcc("ndwt")); + } + + // Nominal diffuse white luminance in units of 0.0001 cd/m^2. + // A value of 0 means the default definition of ISO/TS 22028-5 should be used. + uint32_t get_diffuse_white_luminance() const { return m_diffuse_white_luminance; } + + void set_diffuse_white_luminance(uint32_t luminance) { m_diffuse_white_luminance = luminance; } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Nominal Diffuse White"; } + + Error write(StreamWriter& writer) const override; + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_diffuse_white_luminance = 0; +}; + + class Box_cclv : public Box { public: @@ -1328,6 +1572,8 @@ std::string dump(Indent&) const override; + // TODO const char* debug_box_name() const override { return ""; } + Error write(StreamWriter& writer) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1388,7 +1634,7 @@ principal_point_y -= clap->top(image_height); } - void apply_imir(const Box_imir* imir, int image_width, int image_height) { + void apply_imir(const Box_imir* imir, uint32_t image_width, uint32_t image_height) { switch (imir->get_mirror_direction()) { case heif_transform_mirror_direction_horizontal: focal_length_x *= -1; @@ -1407,6 +1653,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Camera Intrinsic Matrix"; } + RelativeIntrinsicMatrix get_intrinsic_matrix() const { return m_matrix; } void set_intrinsic_matrix(RelativeIntrinsicMatrix matrix); @@ -1462,6 +1710,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Camera Extrinsic Matrix"; } + ExtrinsicMatrix get_extrinsic_matrix() const { return m_matrix; } Error set_extrinsic_matrix(ExtrinsicMatrix matrix); @@ -1514,6 +1764,8 @@ std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "User Description"; } + Error write(StreamWriter& writer) const override; /** @@ -1531,7 +1783,7 @@ * An RFC 5646 compliant language identifier for the language of the text contained in the other properties. * Examples: "en-AU", "de-DE", or "zh-CN“. */ - void set_lang(const std::string lang) { m_lang = lang; } + void set_lang(const std::string& lang) { m_lang = lang; } /** * Name. @@ -1546,7 +1798,7 @@ * * Human readable name for the item or group being described. */ - void set_name(const std::string name) { m_name = name; } + void set_name(const std::string& name) { m_name = name; } /** * Description. @@ -1561,7 +1813,7 @@ * * Human readable description for the item or group. */ - void set_description(const std::string description) { m_description = description; } + void set_description(const std::string& description) { m_description = description; } /** * Tags. @@ -1576,7 +1828,7 @@ * * Comma separated user defined tags applicable to the item or group. */ - void set_tags(const std::string tags) { m_tags = tags; } + void set_tags(const std::string& tags) { m_tags = tags; } [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } @@ -1591,17 +1843,25 @@ }; -#if HEIF_ENABLE_EXPERIMENTAL_FEATURES +void initialize_heif_tai_clock_info(heif_tai_clock_info* taic); +void initialize_heif_tai_timestamp_packet(heif_tai_timestamp_packet* itai); + + class Box_taic : public FullBox { public: Box_taic() { set_short_type(fourcc("taic")); + initialize_heif_tai_clock_info(&m_info); } + static std::string dump(const heif_tai_clock_info& info, Indent&); + std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "TAI Clock Information"; } + Error write(StreamWriter& writer) const override; /** @@ -1610,7 +1870,7 @@ * The standard deviation measurement uncertainty in nanoseconds * for the timestamp generation process. */ - void set_time_uncertainty(uint64_t time_uncertainty) { m_time_uncertainty = time_uncertainty;} + void set_time_uncertainty(uint64_t time_uncertainty) { m_info.time_uncertainty = time_uncertainty;} /** * clock_resolution. @@ -1618,7 +1878,7 @@ * Specifies the resolution of the receptor clock in nanoseconds. * For example, a microsecond clock has a clock_resolution of 1000. */ - void set_clock_resolution(uint32_t clock_resolution) { m_clock_resolution = clock_resolution; } + void set_clock_resolution(uint32_t clock_resolution) { m_info.clock_resolution = clock_resolution; } /** * clock_drift_rate. @@ -1626,85 +1886,192 @@ * The difference between the synchronized and unsynchronized * time, over a period of one second. */ - void set_clock_drift_rate(int32_t clock_drift_rate) { m_clock_drift_rate = clock_drift_rate; } + void set_clock_drift_rate(int32_t clock_drift_rate) { m_info.clock_drift_rate = clock_drift_rate; } /** * clock_type. * - * 0 = Clock type is unkown + * 0 = Clock type is unknown * 1 = The clock does not synchronize to an atomic source of absolute TAI time * 2 = The clock can synchronize to an atomic source of absolute TAI time */ - void set_clock_type(uint8_t clock_type) { m_clock_type = clock_type; } + void set_clock_type(uint8_t clock_type) { m_info.clock_type = clock_type; } - uint64_t get_time_uncertainty() const { return m_time_uncertainty; } + uint64_t get_time_uncertainty() const { return m_info.time_uncertainty; } - uint32_t get_clock_resolution() const { return m_clock_resolution; } + uint32_t get_clock_resolution() const { return m_info.clock_resolution; } - int32_t get_clock_drift_rate() const { return m_clock_drift_rate; } + int32_t get_clock_drift_rate() const { return m_info.clock_drift_rate; } - uint8_t get_clock_type() const { return m_clock_type; } + uint8_t get_clock_type() const { return m_info.clock_type; } + + void set_from_tai_clock_info(const heif_tai_clock_info* info) { + heif_tai_clock_info_copy(&m_info, info); + } + + const heif_tai_clock_info* get_tai_clock_info() const + { + return &m_info; + } + + bool operator==(const Box& other) const override; protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; private: - uint64_t m_time_uncertainty = heif_tai_clock_info_unknown_time_uncertainty; - uint32_t m_clock_resolution = 0; - int32_t m_clock_drift_rate = heif_tai_clock_info_unknown_drift_rate; - uint8_t m_clock_type = 0; + heif_tai_clock_info m_info; }; +bool operator==(const heif_tai_clock_info& a, + const heif_tai_clock_info& b); -class Box_itai : public FullBox + +class Box_itai : public FullBox { public: Box_itai() { set_short_type(fourcc("itai")); + initialize_heif_tai_timestamp_packet(&m_timestamp); } std::string dump(Indent&) const override; + const char* debug_box_name() const override { return "Item TAI Timestamp"; } + Error write(StreamWriter& writer) const override; + static std::vector encode_tai_to_bitstream(const heif_tai_timestamp_packet*); + + static Result decode_tai_from_vector(const std::vector&); + /** * The number of nanoseconds since the TAI epoch of 1958-01-01T00:00:00.0Z. */ - void set_tai_timestamp(uint64_t timestamp) { m_tai_timestamp = timestamp; } + void set_tai_timestamp(uint64_t timestamp) { m_timestamp.tai_timestamp = timestamp; } /** * synchronization_state (0=unsynchronized, 1=synchronized) */ - void set_synchronization_state(bool state) { m_synchronization_state = state; } + void set_synchronization_state(bool state) { m_timestamp.synchronization_state = state; } /** * timestamp_generation_failure (0=generated, 1=failed) */ - void set_timestamp_generation_failure(bool failure) { m_timestamp_generation_failure = failure; } + void set_timestamp_generation_failure(bool failure) { m_timestamp.timestamp_generation_failure = failure; } /** * timestamp_is_modified (0=original 1=modified) */ - void set_timestamp_is_modified(bool is_modified) { m_timestamp_is_modified = is_modified; } + void set_timestamp_is_modified(bool is_modified) { m_timestamp.timestamp_is_modified = is_modified; } - uint64_t get_tai_timestamp() const { return m_tai_timestamp; } + uint64_t get_tai_timestamp() const { return m_timestamp.tai_timestamp; } - bool get_synchronization_state() const { return m_synchronization_state; } + bool get_synchronization_state() const { return m_timestamp.synchronization_state; } - bool get_timestamp_generation_failure() const { return m_timestamp_generation_failure; } + bool get_timestamp_generation_failure() const { return m_timestamp.timestamp_generation_failure; } - bool get_timestamp_is_modified() const { return m_timestamp_is_modified; } + bool get_timestamp_is_modified() const { return m_timestamp.timestamp_is_modified; } + + void set_from_tai_timestamp_packet(const heif_tai_timestamp_packet* tai) { + heif_tai_timestamp_packet_copy(&m_timestamp, tai); + } + + const heif_tai_timestamp_packet* get_tai_timestamp_packet() const + { + return &m_timestamp; + } + + bool operator==(const Box& other) const override; protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; private: - uint64_t m_tai_timestamp = 0; - bool m_synchronization_state = false; - bool m_timestamp_generation_failure = false; - bool m_timestamp_is_modified = false; + heif_tai_timestamp_packet m_timestamp; }; -#endif -#endif \ No newline at end of file +class Box_gimi_content_id : public Box +{ +public: + Box_gimi_content_id() + { + set_uuid_type(std::vector{0x26, 0x1e, 0xf3, 0x74, 0x1d, 0x97, 0x5b, 0xba, 0xac, 0xbd, 0x9d, 0x2c, 0x8e, 0xa7, 0x35, 0x22}); + } + + bool is_essential() const override { return false; } + + bool is_transformative_property() const override { return false; } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "GIMI Content ID"; } + + std::string get_content_id() const { return m_content_id; } + + void set_content_id(const std::string& id) { m_content_id = id; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + Error write(StreamWriter& writer) const override; + +private: + std::string m_content_id; +}; + + +bool operator==(const heif_tai_timestamp_packet& a, + const heif_tai_timestamp_packet& b); + + +/** + * Extended language property. + * + * Permits the association of language information with an item. + * + * See ISO/IEC 23008-12:2025(E) Section 6.10.2.2 and ISO/IEC 14496-12:2022(E) Section 8.4.6. + */ +class Box_elng : public FullBox +{ +public: + Box_elng() + { + set_short_type(fourcc("elng")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Extended language"; } + + Error write(StreamWriter& writer) const override; + + /** + * Language. + * + * An RFC 5646 (IETF BCP 47) compliant language identifier for the language of the text. + * Examples: "en-AU", "de-DE", or "zh-CN“. + */ + std::string get_extended_language() const { return m_lang; } + + /** + * Set the language. + * + * An RFC 5646 (IETF BCP 47) compliant language identifier for the language of the text. + * Examples: "en-AU", "de-DE", or "zh-CN“. + */ + void set_lang(const std::string& lang) { m_lang = lang; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::string m_lang; +}; + +#endif diff -Nru libheif-1.19.8/libheif/brands.cc libheif-1.23.4/libheif/brands.cc --- libheif-1.19.8/libheif/brands.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/brands.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,183 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "brands.h" +#include "file.h" +#include "sequences/track_visual.h" +#include + + +static bool check_mif1(const HeifContext* ctx) +{ + auto file = ctx->get_heif_file(); + + auto meta = file->get_meta_box(); + if (!meta || meta->get_version() != 0) { + return false; + } + + auto hdlr = meta->get_child_box(); + if (!hdlr || hdlr->get_version() != 0) { + return false; + } + + auto iloc = meta->get_child_box(); + if (!iloc || iloc->get_version() > 2) { + return false; + } + + auto iinf = meta->get_child_box(); + if (!iinf || iinf->get_version() > 1) { + return false; + } + + auto infe = iinf->get_child_box(); + if (!infe || infe->get_version() < 2 || infe->get_version() > 3) { + return false; + } + + auto pitm = meta->get_child_box(); + if (!pitm || pitm->get_version() > 1) { + return false; + } + + auto iprp = meta->get_child_box(); + if (!iprp) { + return false; + } + + return true; +} + + +std::vector> get_primary_and_alternative_images(const HeifContext* ctx) +{ + auto img = ctx->get_primary_image(false); + if (img) { + return {std::move(img)}; + } + else { + return {}; + } +} + + +std::vector compute_compatible_brands(const HeifContext* ctx, heif_brand2* out_main_brand) +{ + std::vector compatible_brands; + + heif_brand2 dummy; + if (out_main_brand == nullptr) { + out_main_brand = &dummy; // so that we do not have to check for NULL out_main_brand in the following + } + + *out_main_brand = 0; + + // --- "mif" brands + + bool is_mif1 = check_mif1(ctx); + + if (is_mif1) { + compatible_brands.push_back(heif_brand2_mif1); + *out_main_brand = heif_brand2_mif1; + } + + bool is_structural_image = is_mif1; + + + // --- image brand + + std::vector> images = get_primary_and_alternative_images(ctx); + + bool miaf_compatible = true; + + for (auto& img : images) { + heif_brand2 brand = img->get_compatible_brand(); + if (brand != 0 && is_structural_image) { + compatible_brands.push_back(brand); + } + + if (!img->is_miaf_compatible()) { + miaf_compatible = false; + } + } + + // --- "miaf" + + if (miaf_compatible && is_structural_image && !images.empty()) { + compatible_brands.push_back(heif_brand2_miaf); + } + + + // --- main brand is first image brand + + if (!images.empty()) { + heif_brand2 brand = images[0]->get_compatible_brand(); + if (brand != 0) { + *out_main_brand = brand; + } + } + + // --- --- sequences + + if (ctx->has_sequence()) { + compatible_brands.push_back(heif_brand2_msf1); + compatible_brands.push_back(heif_brand2_isom); + + auto track_result = ctx->get_track(0); + assert(track_result); + + std::shared_ptr track = *track_result; + std::shared_ptr visual_track = std::dynamic_pointer_cast(track); + + if (visual_track) { + heif_brand2 track_brand = visual_track->get_compatible_brand(); + if (track_brand != 0) { + compatible_brands.push_back(track_brand); + + // overwrite any image brand + *out_main_brand = track_brand; + } + } + + // if we don't have a track brand, use at least the sequence structural brand + if (*out_main_brand == 0) { + *out_main_brand = heif_brand2_msf1; + } + } + + // --- "unif" brand + + if (ctx->get_unif()) { + compatible_brands.push_back(heif_brand2_unif); + } + + // --- remove duplicate brands + + std::vector unique_brands; + for (auto brand : compatible_brands) { + if (std::find(unique_brands.begin(), unique_brands.end(), brand) == unique_brands.end()) { + unique_brands.push_back(brand); + } + } + + return unique_brands; +} + diff -Nru libheif-1.19.8/libheif/brands.h libheif-1.23.4/libheif/brands.h --- libheif-1.19.8/libheif/brands.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/brands.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,32 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_BRANDS_H +#define LIBHEIF_BRANDS_H + +#include "context.h" +#include "error.h" + +#include +#include + +std::vector compute_compatible_brands(const HeifContext* ctx, heif_brand2* out_main_brand); + +#endif diff -Nru libheif-1.19.8/libheif/codecs/avc_boxes.cc libheif-1.23.4/libheif/codecs/avc_boxes.cc --- libheif-1.19.8/libheif/codecs/avc_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,6 +26,9 @@ #include "file.h" #include "context.h" #include "avc_dec.h" +#include "hevc_boxes.h" +#include +#include Error Box_avcC::parse(BitstreamRange& range, const heif_security_limits* limits) @@ -55,18 +58,20 @@ } // See ISO/IEC 14496-15 2017 Section 5.3.3.1.2 - if ((m_configuration.AVCProfileIndication != 66) && - (m_configuration.AVCProfileIndication != 77) && - (m_configuration.AVCProfileIndication != 88)) { - m_configuration.chroma_format = (heif_chroma) (range.read8() & 0b00000011); - m_configuration.bit_depth_luma = 8 + (range.read8() & 0b00000111); - m_configuration.bit_depth_chroma = 8 + (range.read8() & 0b00000111); - uint8_t numOfSequenceParameterSetExt = range.read8(); - for (int i = 0; i < numOfSequenceParameterSetExt; i++) { - uint16_t sequenceParameterSetExtLength = range.read16(); - std::vector sps_ext(sequenceParameterSetExtLength); - range.read(sps_ext.data(), sps_ext.size()); - m_sps_ext.push_back(sps_ext); + if (range.get_remaining_bytes() > 0) { + if ((m_configuration.AVCProfileIndication != 66) && + (m_configuration.AVCProfileIndication != 77) && + (m_configuration.AVCProfileIndication != 88)) { + m_configuration.chroma_format = (heif_chroma) (range.read8() & 0b00000011); + m_configuration.bit_depth_luma = 8 + (range.read8() & 0b00000111); + m_configuration.bit_depth_chroma = 8 + (range.read8() & 0b00000111); + uint8_t numOfSequenceParameterSetExt = range.read8(); + for (int i = 0; i < numOfSequenceParameterSetExt; i++) { + uint16_t sequenceParameterSetExtLength = range.read16(); + std::vector sps_ext(sequenceParameterSetExtLength); + range.read(sps_ext.data(), sps_ext.size()); + m_sps_ext.push_back(sps_ext); + } } } @@ -85,35 +90,43 @@ writer.write8(lengthSizeMinusOneWithReserved); if (m_sps.size() > 0b00011111) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write more than 31 PPS into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write more than 31 PPS into avcC box." + }; } uint8_t numSpsWithReserved = 0b11100000 | (m_sps.size() & 0b00011111); writer.write8(numSpsWithReserved); for (const auto& sps : m_sps) { if (sps.size() > 0xFFFF) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write SPS larger than 65535 bytes into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write SPS larger than 65535 bytes into avcC box." + }; } writer.write16((uint16_t) sps.size()); writer.write(sps); } if (m_pps.size() > 0xFF) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write more than 255 PPS into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write more than 255 PPS into avcC box." + }; } writer.write8(m_pps.size() & 0xFF); for (const auto& pps : m_pps) { if (pps.size() > 0xFFFF) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write PPS larger than 65535 bytes into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write PPS larger than 65535 bytes into avcC box." + }; } writer.write16((uint16_t) pps.size()); writer.write(pps); @@ -127,17 +140,21 @@ writer.write8(m_configuration.bit_depth_chroma - 8); if (m_sps_ext.size() > 0xFF) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write more than 255 SPS-Ext into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write more than 255 SPS-Ext into avcC box." + }; } writer.write8(m_sps_ext.size() & 0xFF); for (const auto& spsext : m_sps_ext) { if (spsext.size() > 0xFFFF) { - return {heif_error_Encoding_error, - heif_suberror_Unspecified, - "Cannot write SPS-Ext larger than 65535 bytes into avcC box."}; + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot write SPS-Ext larger than 65535 bytes into avcC box." + }; } writer.write16((uint16_t) spsext.size()); writer.write(spsext); @@ -154,10 +171,10 @@ std::ostringstream sstr; sstr << Box::dump(indent); sstr << indent << "configuration_version: " << ((int) m_configuration.configuration_version) << "\n" - << indent << "AVCProfileIndication: " << ((int) m_configuration.AVCProfileIndication) << " (" << profileIndicationAsText() << ")\n" - << indent << "profile_compatibility: " << ((int) m_configuration.profile_compatibility) << "\n" - << indent << "AVCLevelIndication: " << ((int) m_configuration.AVCLevelIndication) << "\n" - << indent << "Chroma format: "; + << indent << "AVCProfileIndication: " << ((int) m_configuration.AVCProfileIndication) << " (" << profileIndicationAsText() << ")\n" + << indent << "profile_compatibility: " << ((int) m_configuration.profile_compatibility) << "\n" + << indent << "AVCLevelIndication: " << ((int) m_configuration.AVCLevelIndication) << "\n" + << indent << "Chroma format: "; switch (m_configuration.chroma_format) { case heif_chroma_monochrome: @@ -178,7 +195,7 @@ } sstr << indent << "Bit depth luma: " << ((int) m_configuration.bit_depth_luma) << "\n" - << indent << "Bit depth chroma: " << ((int) m_configuration.bit_depth_chroma) << "\n"; + << indent << "Bit depth chroma: " << ((int) m_configuration.bit_depth_chroma) << "\n"; for (const auto& sps : m_sps) { sstr << indent << "SPS: "; @@ -265,3 +282,229 @@ data.insert(data.end(), pps.begin(), pps.end()); } } + + +void Box_avcC::append_sps_nal(const uint8_t* data, size_t size) +{ + std::vector vec(data, data + size); + m_sps.emplace_back(std::move(vec)); +} + +void Box_avcC::append_sps_ext_nal(const uint8_t* data, size_t size) +{ + std::vector vec(data, data + size); + m_sps_ext.emplace_back(std::move(vec)); +} + +void Box_avcC::append_pps_nal(const uint8_t* data, size_t size) +{ + std::vector vec(data, data + size); + m_pps.emplace_back(std::move(vec)); +} + +static bool skip_scaling_list(BitReader& reader, int sizeOfScalingList) +{ + int lastScale = 8; + int nextScale = 8; + + // TODO: it seems that this can be simplified by exiting the loop + // as soon as nextScale==0. + +#if 0 + // original version + for (int j = 0; j < sizeOfScalingList; j++) { + if (nextScale != 0) { + int32_t delta_scale; + if (!reader.get_svlc(&delta_scale)) { + return false; + } + nextScale = (lastScale + delta_scale + 256) % 256; + } + + lastScale = (nextScale == 0) ? lastScale : nextScale; + } +#else + // fast version + for (int j = 0; j < sizeOfScalingList; j++) { + int32_t delta_scale; + if (!reader.get_svlc(&delta_scale)) { + return false; + } + nextScale = (lastScale + delta_scale + 256) % 256; + + if (nextScale == 0) { + break; + } + + lastScale = nextScale; + } +#endif + + return true; +} + + +Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size, + Box_avcC::configuration* config, + uint32_t* width, uint32_t* height, + ImageSize* coded_size) +{ + // remove start-code emulation bytes from SPS header stream + + std::vector sps_no_emul = remove_start_code_emulation(sps, size); + + sps = sps_no_emul.data(); + size = sps_no_emul.size(); + + + BitReader reader(sps, size); + + // skip NAL header + reader.skip_bits(8); + + config->configuration_version = 1; + config->AVCProfileIndication = reader.get_bits8(8); + config->profile_compatibility = reader.get_bits8(8); + config->AVCLevelIndication = reader.get_bits8(8); + config->lengthSize = 4; + + Error invalidUVLC{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Invalid variable length code in AVC SPS header" + }; + + uint32_t value; + if (!reader.get_uvlc(&value)) { return invalidUVLC; } // SPS ID + + if (std::set{100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135}.contains(config->AVCProfileIndication)) { + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + + if (value > heif_chroma_444) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Invalid chroma format in AVC SPS header" + }; + } + + config->chroma_format = static_cast(value); + if (config->chroma_format == heif_chroma_444) { + reader.skip_bits(1); + } + + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + config->bit_depth_luma = static_cast(8 + value); + + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + config->bit_depth_chroma = static_cast(8 + value); + + reader.skip_bits(1); + int seq_scaling_matrix_present_flag = reader.get_bits(1); + if (seq_scaling_matrix_present_flag) { + for (int i = 0; i < ((config->chroma_format != heif_chroma_444) ? 8 : 12); i++) { + int scaling_list_present_flag = reader.get_bits(1); + if (scaling_list_present_flag) { + if (!skip_scaling_list(reader, i < 6 ? 16 : 64)) { + return invalidUVLC; + } + } + } + } + } + else { + config->chroma_format = heif_chroma_420; + config->bit_depth_luma = 8; + config->bit_depth_chroma = 8; + } + + if (!reader.get_uvlc(&value)) { return invalidUVLC; } // log2_max_frame_num_minus4 + uint32_t pic_order_cnt_type; + if (!reader.get_uvlc(&pic_order_cnt_type)) { return invalidUVLC; } + if (pic_order_cnt_type == 0) { + if (!reader.get_uvlc(&value)) { return invalidUVLC; } + } + else if (pic_order_cnt_type == 1) { + reader.get_bits(1); + int32_t svalue; + if (!reader.get_svlc(&svalue) || + !reader.get_svlc(&svalue)) { return invalidUVLC; } + uint32_t num_ref_franes_in_pic_order_cnt_cycle; + if (!reader.get_uvlc(&num_ref_franes_in_pic_order_cnt_cycle)) { return invalidUVLC; } + for (uint32_t i = 0; i < num_ref_franes_in_pic_order_cnt_cycle; i++) { + if (!reader.get_uvlc(&value)) { return invalidUVLC; } + } + } + + if (!reader.get_uvlc(&value)) { return invalidUVLC; } // num_ref_frames + reader.skip_bits(1); + + uint32_t pic_width_in_mbs_minus1; + uint32_t pic_height_in_map_units_minus1; + if (!reader.get_uvlc(&pic_width_in_mbs_minus1) || + !reader.get_uvlc(&pic_height_in_map_units_minus1)) { + return invalidUVLC; + } + + uint32_t frame_mbs_only_flag = reader.get_bits(1); + if (!frame_mbs_only_flag) { + reader.skip_bits(1); // mb_adaptive_frame_field_flag + } + reader.skip_bits(1); // direct_8x8_inference_flag + + // for interlaced content, a map unit covers two macroblock rows + + uint64_t width64 = (static_cast(pic_width_in_mbs_minus1) + 1) * 16; + uint64_t height64 = (static_cast(pic_height_in_map_units_minus1) + 1) * 16 * (2 - frame_mbs_only_flag); + + if (width64 > UINT32_MAX || height64 > UINT32_MAX) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_image_size, + "AVC SPS image size too large"}; + } + + *width = static_cast(width64); + *height = static_cast(height64); + + if (coded_size) { + coded_size->width = *width; + coded_size->height = *height; + } + + uint32_t frame_cropping_flag = reader.get_bits(1); + if (frame_cropping_flag) { + uint32_t left, right, top, bottom; + if (!reader.get_uvlc(&left) || + !reader.get_uvlc(&right) || + !reader.get_uvlc(&top) || + !reader.get_uvlc(&bottom)) { + return invalidUVLC; + } + + // The crop offsets are not in luma samples, but in crop units, which depend on the + // chroma format and the frame_mbs_only_flag (CropUnitX/CropUnitY, H.264 7.4.2.1.1). + + uint32_t crop_unit_x = (config->chroma_format == heif_chroma_420 || + config->chroma_format == heif_chroma_422) ? 2 : 1; + uint32_t crop_unit_y = ((config->chroma_format == heif_chroma_420) ? 2 : 1) * (2 - frame_mbs_only_flag); + + uint64_t crop_horizontal = (static_cast(left) + right) * crop_unit_x; + uint64_t crop_vertical = (static_cast(top) + bottom) * crop_unit_y; + if (crop_horizontal >= *width || crop_vertical >= *height) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_image_size, + "AVC SPS cropping exceeds image size"}; + } + + *width -= static_cast(crop_horizontal); + *height -= static_cast(crop_vertical); + } + + return {}; +} diff -Nru libheif-1.19.8/libheif/codecs/avc_boxes.h libheif-1.23.4/libheif/codecs/avc_boxes.h --- libheif-1.19.8/libheif/codecs/avc_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -28,6 +28,7 @@ #include #include #include "image-items/image_item.h" +#include "sequences/seq_boxes.h" class Box_avcC : public Box { @@ -41,7 +42,7 @@ uint8_t AVCProfileIndication = 0; // profile_idc uint8_t profile_compatibility = 0; // constraint set flags uint8_t AVCLevelIndication = 0; // level_idc - uint8_t lengthSize = 0; + uint8_t lengthSize = 0; // length of NAL size field. Must be one of: 1,2,4 heif_chroma chroma_format = heif_chroma_420; // Note: avcC integer value can be cast to heif_chroma enum uint8_t bit_depth_luma = 8; uint8_t bit_depth_chroma = 8; @@ -57,6 +58,11 @@ return m_configuration; } + configuration& get_configuration() + { + return m_configuration; + } + const std::vector< std::vector > getSequenceParameterSets() const { return m_sps; @@ -74,6 +80,12 @@ void get_header_nals(std::vector& data) const; + void append_sps_nal(const uint8_t* data, size_t size); + + void append_sps_ext_nal(const uint8_t* data, size_t size); + + void append_pps_nal(const uint8_t* data, size_t size); + std::string dump(Indent &) const override; Error write(StreamWriter &writer) const override; @@ -90,4 +102,25 @@ std::vector< std::vector > m_sps_ext; }; + + +class Box_avc1 : public Box_VisualSampleEntry +{ +public: + Box_avc1() + { + set_short_type(fourcc("avc1")); + } +}; + +struct ImageSize; + +// Parses an AVC/H.264 SPS NAL unit. *width / *height return the post-frame- +// cropping (display) dimensions. If non-null, *coded_size receives the +// pre-cropping dimensions actually allocated by the decoder. +Error parse_sps_for_avcC_configuration(const uint8_t* sps, size_t size, + Box_avcC::configuration* inout_config, + uint32_t* width, uint32_t* height, + ImageSize* coded_size = nullptr); + #endif diff -Nru libheif-1.19.8/libheif/codecs/avc_dec.cc libheif-1.23.4/libheif/codecs/avc_dec.cc --- libheif-1.19.8/libheif/codecs/avc_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -47,6 +47,29 @@ } +Result> Decoder_AVC::get_coded_image_size_from_config() const +{ + const auto& sps_set = m_avcC->getSequenceParameterSets(); + + for (const auto& sps : sps_set) { + if (sps.empty()) continue; + Box_avcC::configuration scratch = m_avcC->get_configuration(); + uint32_t cropped_w = 0, cropped_h = 0; + ImageSize coded{}; + + Error e = parse_sps_for_avcC_configuration(sps.data(), sps.size(), &scratch, + &cropped_w, &cropped_h, &coded); + if (e) { + return e; + } + + return std::optional{coded}; + } + + return std::optional{}; +} + + Error Decoder_AVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { *out_chroma = m_avcC->get_configuration().chroma_format; diff -Nru libheif-1.19.8/libheif/codecs/avc_dec.h libheif-1.23.4/libheif/codecs/avc_dec.h --- libheif-1.19.8/libheif/codecs/avc_dec.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_dec.h 2026-09-06 14:45:17.000000000 +0000 @@ -46,6 +46,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_coded_image_size_from_config() const override; + private: const std::shared_ptr m_avcC; }; diff -Nru libheif-1.19.8/libheif/codecs/avc_enc.cc libheif-1.23.4/libheif/codecs/avc_enc.cc --- libheif-1.19.8/libheif/codecs/avc_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,303 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "avc_enc.h" +#include "avc_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" + +#include + +#include "plugins/nalu_utils.h" + + +// TODO: can we use the new sequences interface for this to avoid duplicate code. +Result Encoder_AVC::encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) +{ + CodedImageData codedImage; + + auto avcC = std::make_shared(); + + heif_image c_api_image; + c_api_image.image = image; + + heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + uint32_t encoded_width = 0; + uint32_t encoded_height = 0; + + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + uint8_t nal_type = data[0] & 0x1f; + + if (nal_type == AVC_NAL_UNIT_SPS_NUT) { + parse_sps_for_avcC_configuration(data, size, &avcC->get_configuration(), &encoded_width, &encoded_height); + + codedImage.encoded_image_width = encoded_width; + codedImage.encoded_image_height = encoded_height; + } + + switch (nal_type) { + case AVC_NAL_UNIT_SPS_NUT: + avcC->append_sps_nal(data, size); + break; + case AVC_NAL_UNIT_SPS_EXT_NUT: + avcC->append_sps_ext_nal(data, size); + break; + case AVC_NAL_UNIT_PPS_NUT: + avcC->append_pps_nal(data, size); + break; + + default: + codedImage.append_with_4bytes_size(data, size); + } + } + + if (!encoded_width || !encoded_height) { + return Error(heif_error_Encoder_plugin_error, + heif_suberror_Invalid_image_size); + } + + codedImage.properties.push_back(avcC); + + + // Make sure that the encoder plugin works correctly and the encoded image has the correct size. + + if (encoder->plugin->plugin_api_version >= 3 && + encoder->plugin->query_encoded_size != nullptr) { + uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); + + encoder->plugin->query_encoded_size(encoder->encoder, + image->get_width(), image->get_height(), + &check_encoded_width, + &check_encoded_height); + + assert(check_encoded_width == encoded_width); + assert(check_encoded_height == encoded_height); + } + + codedImage.codingConstraints.intra_pred_used = true; + codedImage.codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return codedImage; +} + + +Error Encoder_AVC::encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) +{ + heif_image c_api_image; + c_api_image.image = image; + + if (!m_encoder_active) { + heif_error err = encoder->plugin->start_sequence_encoding(encoder->encoder, &c_api_image, + input_class, + framerate_num, framerate_denom, + &options); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + m_avcC = std::make_shared(); + m_encoder_active = true; + } + + Error dataErr = get_data(encoder); + if (dataErr) { + return dataErr; + } + + heif_error err = encoder->plugin->encode_sequence_frame(encoder->encoder, &c_api_image, frame_number); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + return get_data(encoder); +} + + +Error Encoder_AVC::encode_sequence_flush(heif_encoder* encoder) +{ + encoder->plugin->end_sequence_encoding(encoder->encoder); + m_encoder_active = false; + m_end_of_sequence_reached = true; + + return get_data(encoder); +} + + +std::optional Encoder_AVC::encode_sequence_get_data() +{ + if (m_output_image_complete) { + m_output_image_complete = false; + return std::move(m_current_output_data); + } + else { + return std::nullopt; + } +} + +Error Encoder_AVC::get_data(heif_encoder* encoder) +{ + //CodedImageData codedImage; + + for (;;) { + uint8_t* data; + int size; + + uintptr_t frameNr=0; + int more_frame_packets = 1; + struct heif_error err = encoder->plugin->get_compressed_data2(encoder->encoder, &data, &size, &frameNr, nullptr, &more_frame_packets); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + const uint8_t nal_type = (data[0] & 0x1f); + const bool is_sync = (nal_type == 5); + const bool is_image_data = (nal_type > 0 && nal_type <= AVC_NAL_UNIT_MAX_VCL); + + m_output_image_complete |= is_image_data; + + // std::cout << "received frameNr=" << frameNr << " nal_type:" << ((int)nal_type) << " size: " << size << "\n"; + + if (nal_type == AVC_NAL_UNIT_SPS_NUT && m_avcC) { + parse_sps_for_avcC_configuration(data, size, + &m_avcC->get_configuration(), + &m_encoded_image_width, &m_encoded_image_height); + } + + if (is_image_data) { + // more_frame_packets = 0; + } + + switch (nal_type) { + case AVC_NAL_UNIT_SPS_NUT: + if (m_avcC && !m_avcC_has_SPS) m_avcC->append_sps_nal(data, size); + m_avcC_has_SPS = true; + break; + + case AVC_NAL_UNIT_SPS_EXT_NUT: + if (m_avcC /*&& !m_avcC_has_SPS*/) m_avcC->append_sps_ext_nal(data, size); + //m_avcC_has_SPS = true; + break; + + case AVC_NAL_UNIT_PPS_NUT: + if (m_avcC && !m_avcC_has_PPS) m_avcC->append_pps_nal(data, size); + m_avcC_has_PPS = true; + break; + + default: + if (!m_current_output_data) { + m_current_output_data = CodedImageData{}; + } + m_current_output_data->append_with_4bytes_size(data, size); + + if (is_image_data) { + m_current_output_data->is_sync_frame = is_sync; + m_current_output_data->frame_nr = frameNr; + } + } + + if (!more_frame_packets) { + break; + } + } + + if (!m_output_image_complete) { + return {}; + } + + if (!m_encoded_image_width || !m_encoded_image_height) { + return Error(heif_error_Encoder_plugin_error, + heif_suberror_Invalid_image_size); + } + + + // --- return avcC when all headers are included and it was not returned yet + // TODO: it's maybe better to return this at the end so that we are sure to have all headers + // and also complete codingConstraints. + + if (m_end_of_sequence_reached && m_avcC && !m_avcC_sent) { + m_current_output_data->properties.push_back(m_avcC); + m_avcC = nullptr; + m_avcC_sent = true; + } + + m_current_output_data->encoded_image_width = m_encoded_image_width; + m_current_output_data->encoded_image_height = m_encoded_image_height; + + m_current_output_data->codingConstraints.intra_pred_used = true; + m_current_output_data->codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return {}; +} + + +std::shared_ptr Encoder_AVC::get_sample_description_box(const CodedImageData& data) const +{ + auto avc1 = std::make_shared(); + avc1->get_VisualSampleEntry().compressorname = "AVC"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("avcC")) { + avc1->append_child_box(prop); + return avc1; + } + } + + // box not yet available + return nullptr; +} diff -Nru libheif-1.19.8/libheif/codecs/avc_enc.h libheif-1.23.4/libheif/codecs/avc_enc.h --- libheif-1.19.8/libheif/codecs/avc_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avc_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,78 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_AVC_H +#define HEIF_ENCODER_AVC_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_AVC : public Encoder { +public: + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + bool encode_sequence_started() const override { return m_encoder_active; } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override; + + Error encode_sequence_flush(heif_encoder* encoder) override; + + std::optional encode_sequence_get_data() override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + +private: + bool m_encoder_active = false; + bool m_end_of_sequence_reached = false; + + // Whether the hvcC is complete and was returned in an encode_sequence_get_data() call. + bool m_avcC_has_SPS = false; + bool m_avcC_has_PPS = false; + std::shared_ptr m_avcC; + bool m_avcC_sent = false; + + uint32_t m_encoded_image_width = 0; + uint32_t m_encoded_image_height = 0; + + std::optional m_current_output_data; + bool m_output_image_complete = false; + + Error get_data(heif_encoder*); +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/avif_boxes.cc libheif-1.23.4/libheif/codecs/avif_boxes.cc --- libheif-1.19.8/libheif/codecs/avif_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avif_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -18,12 +18,12 @@ * along with libheif. If not, see . */ -#include "pixelimage.h" +#include "image/pixelimage.h" #include "avif_boxes.h" #include "avif_dec.h" #include "bitstream.h" #include "common_utils.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "file.h" #include #include @@ -361,7 +361,7 @@ // --- read sequence header - int dummy; // throw away value + uint32_t dummy; // throw away value bool decoder_model_info_present = false; int buffer_delay_length_minus1 = 0; diff -Nru libheif-1.19.8/libheif/codecs/avif_boxes.h libheif-1.23.4/libheif/codecs/avif_boxes.h --- libheif-1.19.8/libheif/codecs/avif_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avif_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -31,6 +31,7 @@ #include "box.h" #include "error.h" #include "image-items/image_item.h" +#include "sequences/seq_boxes.h" class Box_av1C : public Box @@ -114,6 +115,15 @@ }; +class Box_av01 : public Box_VisualSampleEntry +{ +public: + Box_av01() + { + set_short_type(fourcc("av01")); + } +}; + class Box_a1op : public Box { public: diff -Nru libheif-1.19.8/libheif/codecs/avif_dec.cc libheif-1.23.4/libheif/codecs/avif_dec.cc --- libheif-1.19.8/libheif/codecs/avif_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avif_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -61,7 +61,7 @@ Error Decoder_AVIF::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { - *out_chroma = (heif_chroma) (m_av1C->get_configuration().get_heif_chroma()); + *out_chroma = m_av1C->get_configuration().get_heif_chroma(); if (*out_chroma == heif_chroma_monochrome) { *out_colorspace = heif_colorspace_monochrome; diff -Nru libheif-1.19.8/libheif/codecs/avif_enc.cc libheif-1.23.4/libheif/codecs/avif_enc.cc --- libheif-1.19.8/libheif/codecs/avif_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avif_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,222 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "avif_enc.h" +#include "avif_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" + +#include + +enum heif_av1_obu_type : uint8_t +{ + heif_av1_obu_type_frame = 2 +}; + + +Result Encoder_AVIF::encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) +{ + CodedImageData codedImage; + + Box_av1C::configuration config; + + // Fill preliminary av1C in case we cannot parse the sequence_header() correctly in the code below. + // TODO: maybe we can remove this later. + fill_av1C_configuration(&config, image); + + heif_image c_api_image; + c_api_image.image = image; + + heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + bool found_config = fill_av1C_configuration_from_stream(&config, data, size); + (void) found_config; + + if (data == nullptr) { + break; + } + + codedImage.append(data, size); + } + + auto av1C = std::make_shared(); + av1C->set_configuration(config); + codedImage.properties.push_back(av1C); + + codedImage.codingConstraints.intra_pred_used = true; + codedImage.codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return codedImage; +} + + +Error Encoder_AVIF::encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) +{ + // Box_av1C::configuration config; + + // Fill preliminary av1C in case we cannot parse the sequence_header() correctly in the code below. + // TODO: maybe we can remove this later. + fill_av1C_configuration(&m_config, image); + + heif_image c_api_image; + c_api_image.image = image; + + if (!m_encoder_active) { + heif_error err = encoder->plugin->start_sequence_encoding(encoder->encoder, + &c_api_image, + input_class, + framerate_num, framerate_denom, + &options); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + //m_hvcC = std::make_shared(); + m_encoder_active = true; + } + + + heif_error err = encoder->plugin->encode_sequence_frame(encoder->encoder, &c_api_image, frame_number); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + m_all_refs_intra = (options.gop_structure == heif_sequence_gop_structure_intra_only); + + return get_data(encoder); +} + + +Error Encoder_AVIF::get_data(heif_encoder* encoder) +{ + CodedImageData codedImage; + + for (;;) { + uint8_t* data; + int size; + + uintptr_t out_frame_number; + int is_keyframe = 0; + struct heif_error err = encoder->plugin->get_compressed_data2(encoder->encoder, &data, &size, &out_frame_number, &is_keyframe, nullptr); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + uint8_t obu_type = (data[0]>>3) & 0x0F; + + // printf("got OBU type=%d size=%d framenr=%d\n", obu_type, size, out_frame_number); + + bool found_config = fill_av1C_configuration_from_stream(&m_config, data, size); + (void) found_config; + + codedImage.append(data, size); + codedImage.frame_nr = out_frame_number; + + if (encoder->plugin->plugin_api_version >= 4 && + encoder->plugin->does_indicate_keyframes) { + codedImage.is_sync_frame = is_keyframe; + } + + // If we got an image, stop collecting data packets. + if (obu_type == heif_av1_obu_type_frame) { + break; + } + } + + if (m_end_of_sequence_reached && !m_av1C_sent) { + auto av1C = std::make_shared(); + av1C->set_configuration(m_config); + codedImage.properties.push_back(av1C); + m_av1C_sent = true; + } + + codedImage.codingConstraints.intra_pred_used = true; + codedImage.codingConstraints.all_ref_pics_intra = m_all_refs_intra; + + m_current_output_data = std::move(codedImage); + + return {}; +} + + +Error Encoder_AVIF::encode_sequence_flush(heif_encoder* encoder) +{ + encoder->plugin->end_sequence_encoding(encoder->encoder); + m_encoder_active = false; + m_end_of_sequence_reached = true; + + return get_data(encoder); +} + + +std::optional Encoder_AVIF::encode_sequence_get_data() +{ + return std::move(m_current_output_data); +} + + +std::shared_ptr Encoder_AVIF::get_sample_description_box(const CodedImageData& data) const +{ + auto av01 = std::make_shared(); + av01->get_VisualSampleEntry().compressorname = "AVIF"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("av1C")) { + av01->append_child_box(prop); + return av01; + } + } + + // box not available yet + return nullptr; +} diff -Nru libheif-1.19.8/libheif/codecs/avif_enc.h libheif-1.23.4/libheif/codecs/avif_enc.h --- libheif-1.19.8/libheif/codecs/avif_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/avif_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,75 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_AVIF_H +#define HEIF_ENCODER_AVIF_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include + +#include "avif_boxes.h" +#include "codecs/encoder.h" + + +class Encoder_AVIF : public Encoder { +public: + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + bool encode_sequence_started() const override { return m_encoder_active; } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override; + + Error encode_sequence_flush(heif_encoder* encoder) override; + + std::optional encode_sequence_get_data() override; + + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + +private: + bool m_encoder_active = false; + bool m_end_of_sequence_reached = false; + + Box_av1C::configuration m_config; + bool m_av1C_sent = false; + + std::optional m_current_output_data; + + bool m_all_refs_intra = false; + Error get_data(heif_encoder* encoder); +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/decoder.cc libheif-1.23.4/libheif/codecs/decoder.cc --- libheif-1.19.8/libheif/codecs/decoder.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/decoder.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,7 +24,8 @@ #include "error.h" #include "context.h" #include "plugin_registry.h" -#include "libheif/api_structs.h" +#include "api_structs.h" +#include "security_limits.h" #include "codecs/hevc_dec.h" #include "codecs/avif_dec.h" @@ -38,8 +39,11 @@ #include "vvc_boxes.h" #include "jpeg_boxes.h" #include "jpeg2000_boxes.h" -#include "codecs/uncompressed/unc_dec.h" +#if WITH_UNCOMPRESSED_CODEC +#include "codecs/uncompressed/unc_dec.h" +#include "codecs/uncompressed/unc_boxes.h" +#endif void DataExtent::set_from_image_item(std::shared_ptr file, heif_item_id item) { @@ -49,6 +53,15 @@ } +void DataExtent::set_file_range(std::shared_ptr file, uint64_t offset, uint32_t size) +{ + m_file = std::move(file); + m_source = Source::FileRange; + m_offset = offset; + m_size = size; +} + + Result*> DataExtent::read_data() const { if (!m_raw.empty()) { @@ -62,10 +75,33 @@ if (err) { return err; } + + // Account the (now-known) buffer size against the file's total-memory budget. + // append_data_from_iloc has already enforced max_memory_block_size per extent. + if (auto memErr = m_raw_memory_handle.alloc(m_raw.size(), m_file->get_security_limits(), + "decoder input buffer (iloc)")) { + m_raw.clear(); + m_raw.shrink_to_fit(); + return memErr; + } } else { - // sequence - assert(false); // TODO + assert(m_file); + + // Reserve the buffer in the total-memory tracker before allocating it. + // This also enforces max_memory_block_size and rejects sizes that would + // exceed max_total_memory across all concurrently-live DataExtents. + if (auto memErr = m_raw_memory_handle.alloc(m_size, m_file->get_security_limits(), + "decoder input buffer (sample)")) { + return memErr; + } + + // file range + Error err = m_file->append_data_from_file_range(m_raw, m_offset, m_size); + if (err) { + m_raw_memory_handle.free(); + return err; + } } return &m_raw; @@ -77,6 +113,17 @@ std::vector data; if (!m_raw.empty()) { + // No caller currently reaches this cached path with an out-of-range request, so + // hitting it indicates an internal logic error rather than malformed input. Guard + // it defensively anyway. The subtraction form avoids a uint64_t wrap in + // 'offset + size' that would otherwise allow an out-of-bounds read below. + // TODO: this would be better reported as an internal error; change it once we have + // a dedicated error code for that. + if (offset > m_raw.size() || size > m_raw.size() - offset) { + return Error{heif_error_Invalid_input, + heif_suberror_End_of_data, + "Requested data range exceeds the cached extent buffer"}; + } data.insert(data.begin(), m_raw.begin() + offset, m_raw.begin() + offset + size); return data; } @@ -84,16 +131,20 @@ // TODO: cache data // image - Error err = m_file->append_data_from_iloc(m_item_id, m_raw, 0, size); + Error err = m_file->append_data_from_iloc(m_item_id, data, offset, size); if (err) { return err; } return data; } else { - // sequence - assert(false); // TODO - return Error::Ok; + // file range + Error err = m_file->append_data_from_file_range(data, m_offset, m_size); + if (err) { + return err; + } + + return data; } } @@ -105,14 +156,17 @@ switch (format_4cc) { case fourcc("hvc1"): { auto hvcC = item->get_property(); + if (!hvcC) return nullptr; return std::make_shared(hvcC); } case fourcc("av01"): { auto av1C = item->get_property(); + if (!av1C) return nullptr; return std::make_shared(av1C); } case fourcc("avc1"): { auto avcC = item->get_property(); + if (!avcC) return nullptr; return std::make_shared(avcC); } case fourcc("j2k1"): { @@ -121,6 +175,7 @@ } case fourcc("vvc1"): { auto vvcC = item->get_property(); + if (!vvcC) return nullptr; return std::make_shared(vvcC); } case fourcc("jpeg"): { @@ -131,7 +186,16 @@ case fourcc("unci"): { auto uncC = item->get_property(); auto cmpd = item->get_property(); - return std::make_shared(uncC,cmpd); + auto ispe = item->get_property(); + auto decoder = std::make_shared(uncC, cmpd, ispe); + decoder->set_cpat(item->get_property()); + decoder->set_cmpC(item->get_property()); + decoder->set_icef(item->get_property()); + decoder->set_cloc(item->get_property()); + decoder->set_splz(item->get_all_properties()); + decoder->set_sbpm(item->get_all_properties()); + decoder->set_snuc(item->get_all_properties()); + return decoder; } #endif case fourcc("mski"): { @@ -143,83 +207,374 @@ } -Result> Decoder::get_compressed_data() const +std::shared_ptr Decoder::alloc_for_sequence_sample_description_box(const std::shared_ptr& sample_description_box) +{ + std::string compressor = sample_description_box->get_VisualSampleEntry_const().compressorname; + uint32_t sampleType = sample_description_box->get_short_type(); + + switch (sampleType) { + case fourcc("hvc1"): { + auto hvcC = sample_description_box->get_child_box(); + if (!hvcC) return nullptr; + return std::make_shared(hvcC); + } + + case fourcc("av01"): { + auto av1C = sample_description_box->get_child_box(); + if (!av1C) return nullptr; + return std::make_shared(av1C); + } + + case fourcc("vvc1"): { + auto vvcC = sample_description_box->get_child_box(); + if (!vvcC) return nullptr; + return std::make_shared(vvcC); + } + + case fourcc("avc1"): { + auto avcC = sample_description_box->get_child_box(); + if (!avcC) return nullptr; + return std::make_shared(avcC); + } + +#if WITH_UNCOMPRESSED_CODEC + case fourcc("uncv"): { + auto uncC = sample_description_box->get_child_box(); + auto cmpd = sample_description_box->get_child_box(); + auto ispe = std::make_shared(); + ispe->set_size(sample_description_box->get_VisualSampleEntry_const().width, + sample_description_box->get_VisualSampleEntry_const().height); + auto decoder = std::make_shared(uncC, cmpd, ispe); + decoder->set_cpat(sample_description_box->get_child_box()); + decoder->set_cmpC(sample_description_box->get_child_box()); + decoder->set_icef(sample_description_box->get_child_box()); + decoder->set_cloc(sample_description_box->get_child_box()); + decoder->set_splz(sample_description_box->get_child_boxes()); + decoder->set_sbpm(sample_description_box->get_child_boxes()); + decoder->set_snuc(sample_description_box->get_child_boxes()); + return decoder; + } +#endif + + case fourcc("j2ki"): { + auto j2kH = sample_description_box->get_child_box(); + return std::make_shared(j2kH); + } + + case fourcc("mjpg"): { + auto jpgC = sample_description_box->get_child_box(); + return std::make_shared(jpgC); + } + + default: + return nullptr; + } +} + + +Result> Decoder::get_compressed_data(bool with_configuration_NALs) const { // --- get the compressed image data - // data from configuration blocks + if (with_configuration_NALs) { + // data from configuration blocks + + Result> confData = read_bitstream_configuration_data(); + if (!confData) { + return confData.error(); + } + + std::vector data = *confData; + + // append image data + + auto dataResult = m_data_extent.read_data(); + if (!dataResult) { + return dataResult.error(); + } + + data.insert(data.end(), (*dataResult)->begin(), (*dataResult)->end()); - Result> confData = read_bitstream_configuration_data(); - if (confData.error) { - return confData.error; + return data; } + else { + auto dataResult = m_data_extent.read_data(); + if (!dataResult) { + return dataResult.error(); + } + + return {*(*dataResult)}; + } +} + - std::vector data = confData.value; +Decoder::~Decoder() +{ + release_decoder(); +} - // append image data - Result dataResult = m_data_extent.read_data(); - if (dataResult.error) { - return dataResult.error; +void Decoder::release_decoder() +{ + if (m_decoder) { + assert(m_decoder_plugin); + m_decoder_plugin->free_decoder(m_decoder); + m_decoder = nullptr; } +} - data.insert(data.end(), dataResult.value->begin(), dataResult.value->end()); - return data; +// Names the compression format and, where one exists, an example decoder +// implementation, so that users can tell which decoder plugin package or +// build option is missing (issue #1876). +static std::string missing_decoder_error_message(heif_compression_format format) +{ + const char* format_name = nullptr; + const char* example_decoder = nullptr; + + switch (format) { + case heif_compression_HEVC: + format_name = "HEVC"; + example_decoder = "libde265"; + break; + case heif_compression_AVC: + format_name = "AVC"; + example_decoder = "openh264"; + break; + case heif_compression_JPEG: + format_name = "JPEG"; + example_decoder = "libjpeg"; + break; + case heif_compression_AV1: + format_name = "AV1"; + example_decoder = "dav1d"; + break; + case heif_compression_VVC: + format_name = "VVC"; + example_decoder = "vvdec"; + break; + case heif_compression_EVC: + format_name = "EVC"; + break; + case heif_compression_JPEG2000: + format_name = "JPEG 2000"; + example_decoder = "openjpeg"; + break; + case heif_compression_HTJ2K: + format_name = "HT-J2K"; + example_decoder = "openjpeg"; + break; + case heif_compression_uncompressed: + format_name = "ISO/IEC 23001-17 uncompressed"; + break; + case heif_compression_mask: + format_name = "mask image"; + break; + case heif_compression_undefined: + break; + } + + if (format_name == nullptr) { + return {}; + } + + std::string msg = format_name; + if (example_decoder) { + msg += " (a suitable decoder plugin is "; + msg += example_decoder; + msg += ")"; + } + + return msg; } -Result> -Decoder::decode_single_frame_from_compressed_data(const struct heif_decoding_options& options) +Error Decoder::require_decoder_plugin(const heif_decoding_options& options) { - const struct heif_decoder_plugin* decoder_plugin = get_decoder(get_compression_format(), options.decoder_id); - if (!decoder_plugin) { - return Error(heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed); + if (!m_decoder_plugin) { + if (options.decoder_id && !has_decoder(get_compression_format(), options.decoder_id)) { + return { + heif_error_Plugin_loading_error, + heif_suberror_Unspecified, + "No decoder with that ID found." + }; + } + + m_decoder_plugin = get_decoder(get_compression_format(), options.decoder_id); + if (!m_decoder_plugin) { + return {heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed, + missing_decoder_error_message(get_compression_format())}; + } + + if (m_decoder_plugin->plugin_api_version < 5) { + return Error{ + heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed, + "Decoder plugin needs to be at least version 5." + }; + } } + return {}; +} + + +Error Decoder::decode_sequence_frame_from_compressed_data(bool upload_configuration_NALs, + const heif_decoding_options& options, + uintptr_t user_data, + const heif_security_limits* limits) +{ + auto pluginErr = require_decoder_plugin(options); + if (pluginErr) { + return pluginErr; + } + + // Reject memory-bomb inputs whose codec configuration record (SPS) declares + // a coded picture size beyond libheif's security limits, before handing any + // bytes to the decoder plugin. Codecs whose configuration record does not + // carry dimensions (e.g. AV1's av1C) return nullopt and skip the check. + // + // TODO: check this also in the decoder plugin since SPS packets may be + // found within the actual image bitstream. + auto codedSize = get_coded_image_size_from_config(); + if (codedSize.is_error()) { + return codedSize.error(); + } + + if (codedSize->has_value()) { + Error sizeErr = check_for_valid_image_size(limits, (*codedSize)->width, (*codedSize)->height); + if (sizeErr) { + return sizeErr; + } + } // --- decode image with the plugin - if (decoder_plugin->new_decoder == nullptr) { - return Error(heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed, - "Cannot decode with a dummy decoder plugins."); + heif_error err; + + if (!m_decoder) { + if (m_decoder_plugin->new_decoder == nullptr) { + return Error(heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed, + "Cannot decode with a dummy decoder plugin."); + } + + if (m_decoder_plugin->plugin_api_version >= 5) { + heif_decoder_plugin_options plugin_options; + plugin_options.format = get_compression_format(); + plugin_options.num_threads = options.num_codec_threads; + plugin_options.strict_decoding = options.strict_decoding; + plugin_options.limits = limits; + + err = m_decoder_plugin->new_decoder2(&m_decoder, &plugin_options); + if (err.code != heif_error_Ok) { + return Error(err.code, err.subcode, err.message); + } + } + else { + err = m_decoder_plugin->new_decoder(&m_decoder); + if (err.code != heif_error_Ok) { + return Error(err.code, err.subcode, err.message); + } + + // automatically delete decoder plugin when we leave the scope + //std::unique_ptr decoderSmartPtr(m_decoder, m_decoder_plugin->free_decoder); + + if (m_decoder_plugin->plugin_api_version >= 2) { + if (m_decoder_plugin->set_strict_decoding) { + m_decoder_plugin->set_strict_decoding(m_decoder, options.strict_decoding); + } + } + } + } + + auto dataResult = get_compressed_data(upload_configuration_NALs); + if (!dataResult) { + return dataResult.error(); } - void* decoder; - struct heif_error err = decoder_plugin->new_decoder(&decoder); + // Check that we are pushing at least some data into the decoder. + // Some decoders (e.g. aom) do not complain when the input data is empty and we might + // get stuck in an endless decoding loop, waiting for the decompressed image. + + if (dataResult->size() == 0) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Input with empty data extent." + }; + } + + //std::cout << "Decoder::decode_sequence_frame_from_compressed_data push " << dataResult->size() << "\n"; + if (m_decoder_plugin->plugin_api_version >= 5 && m_decoder_plugin->push_data2) { + err = m_decoder_plugin->push_data2(m_decoder, dataResult->data(), dataResult->size(), user_data); + } + else { + err = m_decoder_plugin->push_data(m_decoder, dataResult->data(), dataResult->size()); + } if (err.code != heif_error_Ok) { return Error(err.code, err.subcode, err.message); } - // automatically delete decoder plugin when we leave the scope - std::unique_ptr decoderSmartPtr(decoder, decoder_plugin->free_decoder); + return {}; +} - if (decoder_plugin->plugin_api_version >= 2) { - if (decoder_plugin->set_strict_decoding) { - decoder_plugin->set_strict_decoding(decoder, options.strict_decoding); - } +Error Decoder::flush_decoder() +{ + assert(m_decoder_plugin); + + if (m_decoder_plugin->plugin_api_version >= 5) { + heif_error err = m_decoder_plugin->flush_data(m_decoder); + return Error::from_heif_error(err); } - auto dataResult = get_compressed_data(); - if (dataResult.error) { - return dataResult.error; + return {}; +} + +Result > Decoder::get_decoded_frame(const heif_decoding_options& options, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + auto pluginErr = require_decoder_plugin(options); + if (pluginErr) { + return pluginErr; } - err = decoder_plugin->push_data(decoder, dataResult.value.data(), dataResult.value.size()); - if (err.code != heif_error_Ok) { - return Error(err.code, err.subcode, err.message); + // The plugin's per-decoder context is created lazily on the first push of + // compressed data. If a caller polls for a frame before any data was pushed + // (e.g. when a sequence advances into a new chunk that uses a freshly- + // allocated decoder), there is nothing buffered yet — return nullptr. + if (!m_decoder) { + return {nullptr}; } heif_image* decoded_img = nullptr; - err = decoder_plugin->decode_image(decoder, &decoded_img); - if (err.code != heif_error_Ok) { - return Error(err.code, err.subcode, err.message); + heif_error err; + + if (m_decoder_plugin->plugin_api_version >= 5 && + m_decoder_plugin->decode_next_image2 != nullptr) { + + err = m_decoder_plugin->decode_next_image2(m_decoder, &decoded_img, out_user_data, limits); + if (err.code != heif_error_Ok) { + return Error::from_heif_error(err); + } + } + else if (m_decoder_plugin->plugin_api_version >= 4 && + m_decoder_plugin->decode_next_image != nullptr) { + + err = m_decoder_plugin->decode_next_image(m_decoder, &decoded_img, limits); + if (err.code != heif_error_Ok) { + return Error::from_heif_error(err); + } + } + else { + err = m_decoder_plugin->decode_image(m_decoder, &decoded_img); + if (err.code != heif_error_Ok) { + return Error::from_heif_error(err); + } } if (!decoded_img) { - // TODO(farindk): The plugin should return an error in this case. - return Error(heif_error_Decoder_plugin_error, heif_suberror_Unspecified); + return {nullptr}; } // -- cleanup @@ -229,3 +584,46 @@ return img; } + + +Result> +Decoder::decode_single_frame_from_compressed_data(const heif_decoding_options& options, + const heif_security_limits* limits) +{ + Error decodeError = decode_sequence_frame_from_compressed_data(true, options, 0, limits); + if (decodeError) { + release_decoder(); + return decodeError; + } + + flush_decoder(); + + // We might have to try several times to get an image out of the decoder. + // However, we stop after a maximum number of tries because the decoder might not + // give any image when the input data is incomplete. + const int max_decoding_tries = 50; // hardcoded value, should be large enough + + for (int i = 0; i < max_decoding_tries; i++) { + Result> imgResult; + imgResult = get_decoded_frame(options, nullptr, limits); + if (imgResult.error()) { + release_decoder(); + return imgResult.error(); + } + + if (*imgResult != nullptr) { + release_decoder(); + return imgResult; + } + } + + // We did not receive an image from the decoder. We give up. + + release_decoder(); + + return Error{ + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding the input data did not give a decompressed image." + }; +} diff -Nru libheif-1.19.8/libheif/codecs/decoder.h libheif-1.23.4/libheif/codecs/decoder.h --- libheif-1.19.8/libheif/codecs/decoder.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/decoder.h 2026-09-06 14:45:17.000000000 +0000 @@ -25,33 +25,52 @@ #include "box.h" #include "error.h" #include "file.h" +#include "security_limits.h" #include +#include #include #include #include #include "image-items/hevc.h" +// Image dimensions in luma samples. Reused wherever libheif needs to pass +// a (width, height) pair around — initially for SPS-derived coded sizes, +// but designed to fit other uses (ispe, image-item dimensions) over time. +struct ImageSize +{ + uint32_t width; + uint32_t height; +}; + + // Specifies the input data for decoding. // For images, this points to the iloc extents. // For sequences, this points to the track data. struct DataExtent { std::shared_ptr m_file; - enum class Source : uint8_t { Raw, Image, Sequence } m_source = Source::Raw; + enum class Source : uint8_t { Raw, Image, FileRange } m_source = Source::Raw; // --- raw data mutable std::vector m_raw; // also for cached data + // Holds m_raw's allocation against the file's max_total_memory budget. + // Released when DataExtent is destroyed (or moved-from). + mutable MemoryHandle m_raw_memory_handle; + // --- image heif_item_id m_item_id = 0; - // --- sequence - // TODO + // --- file range + uint64_t m_offset = 0; + uint32_t m_size = 0; void set_from_image_item(std::shared_ptr file, heif_item_id item); + void set_file_range(std::shared_ptr file, uint64_t offset, uint32_t size); + Result*> read_data() const; Result> read_data(uint64_t offset, uint64_t size) const; @@ -63,13 +82,17 @@ public: static std::shared_ptr alloc_for_infe_type(const ImageItem* item); + static std::shared_ptr alloc_for_sequence_sample_description_box(const std::shared_ptr& sample_description_box); - virtual ~Decoder() = default; + + virtual ~Decoder(); virtual heif_compression_format get_compression_format() const = 0; void set_data_extent(DataExtent extent) { m_data_extent = std::move(extent); } + const DataExtent& get_data_extent() const { return m_data_extent; } + // --- information about the image format [[nodiscard]] virtual int get_luma_bits_per_pixel() const = 0; @@ -80,17 +103,63 @@ // --- raw data access + // Returns a stream of packets. Each packet is starts with a 4-byte size (MSB first). [[nodiscard]] virtual Result> read_bitstream_configuration_data() const = 0; - Result> get_compressed_data() const; + // Returns the *coded* picture size from the codec configuration record (the + // SPS for HEVC/AVC/VVC) — i.e. the buffer dimensions the decoder will + // actually allocate, BEFORE conformance-window cropping. The cropped output + // size is unsuitable for security checks: a malicious file can declare a + // huge SPS picture size with a near-equal-sized conformance window, so the + // displayed image looks small while the decoder still allocates the full + // uncropped buffer. + // + // Returns nullopt when the codec does not store dimensions in its + // configuration record (e.g. AV1's av1C) or when no SPS NAL is present. + // Returns Error only on a structurally invalid configuration record. + [[nodiscard]] virtual Result> + get_coded_image_size_from_config() const + { + return std::optional{}; + } + + Result> get_compressed_data(bool with_configuration_NALs) const; // --- decoding + // Decode a stream image that contains exactly one image. Decoder input is flushed and + // it always should return an image. virtual Result> - decode_single_frame_from_compressed_data(const struct heif_decoding_options& options); + decode_single_frame_from_compressed_data(const heif_decoding_options& options, + const heif_security_limits* limits); + + // Push data for one frame into decoder. + virtual Error + decode_sequence_frame_from_compressed_data(bool upload_configuration_NALs, + const heif_decoding_options& options, + uintptr_t user_data, + const heif_security_limits* limits); + + virtual Error flush_decoder(); + + // Get a decoded frame from the decoder. + // It may return NULL when there is buffering in the codec. + virtual Result > get_decoded_frame(const heif_decoding_options& options, + uintptr_t* out_user_data, + const heif_security_limits* limits); + + // Release the codec plugin decoder context (frees worker threads). + // Safe to call multiple times. The decoder will be re-created on next use. + void release_decoder(); private: DataExtent m_data_extent; + + const heif_decoder_plugin* m_decoder_plugin = nullptr; + void* m_decoder = nullptr; + + // get the decoder plugin if it is not set already + Error require_decoder_plugin(const heif_decoding_options& options); }; #endif diff -Nru libheif-1.19.8/libheif/codecs/encoder.cc libheif-1.23.4/libheif/codecs/encoder.cc --- libheif-1.19.8/libheif/codecs/encoder.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/encoder.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,168 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "codecs/encoder.h" + +#include "error.h" +#include "context.h" +#include "plugin_registry.h" +#include "api_structs.h" +#include "color-conversion/colorconversion.h" + + +void Encoder::CodedImageData::append(const uint8_t* data, size_t size) +{ + bitstream.insert(bitstream.end(), data, data + size); +} + + +void Encoder::CodedImageData::append_with_4bytes_size(const uint8_t* data, size_t size) +{ + assert(size <= 0xFFFFFFFF); + + uint8_t size_field[4]; + size_field[0] = (uint8_t) ((size >> 24) & 0xFF); + size_field[1] = (uint8_t) ((size >> 16) & 0xFF); + size_field[2] = (uint8_t) ((size >> 8) & 0xFF); + size_field[3] = (uint8_t) ((size >> 0) & 0xFF); + + bitstream.insert(bitstream.end(), size_field, size_field + 4); + bitstream.insert(bitstream.end(), data, data + size); +} + + + +static nclx_profile compute_target_nclx_profile(const std::shared_ptr& image, const heif_color_profile_nclx* output_nclx_profile) +{ + nclx_profile target_nclx_profile; + + // If there is an output NCLX specified, use that. + if (output_nclx_profile) { + target_nclx_profile.set_from_heif_color_profile_nclx(output_nclx_profile); + } + // Otherwise, if there is an input NCLX, keep that. + else if (image->has_nclx_color_profile()) { + target_nclx_profile = image->get_color_profile_nclx(); + } + // Otherwise, just use the defaults (set below) + else { + target_nclx_profile.set_undefined(); + } + + target_nclx_profile.replace_undefined_values_with_sRGB_defaults(); + + return target_nclx_profile; +} + + +static bool nclx_profile_matches_spec(heif_colorspace colorspace, + std::optional image_nclx, + const heif_color_profile_nclx* spec_nclx) +{ + if (colorspace != heif_colorspace_YCbCr) { + return true; + } + + // No target specification -> always matches + if (!spec_nclx) { + return true; + } + + if (!image_nclx) { + static nclx_profile default_nclx; + default_nclx.set_sRGB_defaults(); + + // if no input nclx is specified, compare against default one + image_nclx = default_nclx; + } + + if (image_nclx->get_full_range_flag() != (spec_nclx->full_range_flag == 0 ? false : true)) { + return false; + } + + if (image_nclx->get_matrix_coefficients() != spec_nclx->matrix_coefficients) { + return false; + } + + // TODO: are the colour primaries relevant for matrix-coefficients != 12,13 ? + // If not, we should skip this test for anything else than matrix-coefficients != 12,13. + if (image_nclx->get_colour_primaries() != spec_nclx->color_primaries) { + return false; + } + + return true; +} + + +extern void fill_default_color_conversion_options_ext(heif_color_conversion_options_ext& options); + +Result> Encoder::convert_colorspace_for_encoding(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_color_profile_nclx* user_requested_output_nclx, + const heif_color_conversion_options* color_conversion_options, + const heif_security_limits* security_limits) +{ + const heif_color_profile_nclx* output_nclx_profile; + + if (const auto* nclx = get_forced_output_nclx()) { + output_nclx_profile = nclx; + } else { + output_nclx_profile = user_requested_output_nclx; + } + + + heif_colorspace colorspace = image->get_colorspace(); + heif_chroma chroma = image->get_chroma_format(); + + if (encoder->plugin->plugin_api_version >= 2) { + encoder->plugin->query_input_colorspace2(encoder->encoder, &colorspace, &chroma); + } + else { + encoder->plugin->query_input_colorspace(&colorspace, &chroma); + } + + + // If output format forces an NCLX, use that. Otherwise use user selected NCLX. + + nclx_profile target_nclx_profile = compute_target_nclx_profile(image, output_nclx_profile); + + // --- convert colorspace + + std::shared_ptr output_image; + + const std::optional image_nclx = image->get_color_profile_nclx(); + + if (colorspace == image->get_colorspace() && + chroma == image->get_chroma_format() && + nclx_profile_matches_spec(colorspace, image_nclx, output_nclx_profile)) { + return image; + } + + + // @TODO: use color profile when converting + int output_bpp = 0; // same as input + + //auto target_nclx = std::make_shared(); + //target_nclx->set_from_heif_color_profile_nclx(target_heif_nclx); + + return convert_colorspace(image, colorspace, chroma, target_nclx_profile, + output_bpp, *color_conversion_options, nullptr, + security_limits); +} diff -Nru libheif-1.19.8/libheif/codecs/encoder.h libheif-1.23.4/libheif/codecs/encoder.h --- libheif-1.19.8/libheif/codecs/encoder.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/encoder.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,93 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_H +#define HEIF_ENCODER_H + +#include "libheif/heif.h" +#include "error.h" +#include "libheif/heif_plugin.h" + +#include +#include +#include +#include +#include "sequences/seq_boxes.h" + +class HeifPixelImage; + +class Box; + + +class Encoder { +public: + virtual ~Encoder() = default; + + struct CodedImageData { + std::vector> properties; + std::vector bitstream; + CodingConstraints codingConstraints; + + // If 0, the encoded size is unknown. + uint32_t encoded_image_width = 0; + uint32_t encoded_image_height = 0; + + bool is_sync_frame = true; // TODO: set in encoder + uintptr_t frame_nr = 0; + + void append(const uint8_t* data, size_t size); + + void append_with_4bytes_size(const uint8_t* data, size_t size); + }; + + // If the output format requires a specific nclx (like JPEG), return this. Otherwise, return NULL. + virtual const heif_color_profile_nclx* get_forced_output_nclx() const { return nullptr; } + + Result> convert_colorspace_for_encoding(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_color_profile_nclx* user_requested_output_nclx, + const heif_color_conversion_options* color_conversion_options, + const heif_security_limits* security_limits); + + virtual Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { return {}; } + + // --- encode sequence + + virtual bool encode_sequence_started() const { return false; } + + virtual Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) { return {}; } + + virtual Error encode_sequence_flush(heif_encoder* encoder) { return {}; } + + virtual std::optional encode_sequence_get_data() { return std::nullopt; } + + virtual std::shared_ptr get_sample_description_box(const CodedImageData&) const { return {}; } +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/hevc_boxes.cc libheif-1.23.4/libheif/codecs/hevc_boxes.cc --- libheif-1.19.8/libheif/codecs/hevc_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -30,45 +30,42 @@ #include #include #include -#include +#include +#include "api_structs.h" -Error Box_hvcC::parse(BitstreamRange& range, const heif_security_limits* limits) +Error HEVCDecoderConfigurationRecord::parse(BitstreamRange& range, const heif_security_limits* limits) { - //parse_full_box_header(range); - uint8_t byte; - auto& c = m_configuration; // abbreviation - - c.configuration_version = range.read8(); + configuration_version = range.read8(); byte = range.read8(); - c.general_profile_space = (byte >> 6) & 3; - c.general_tier_flag = (byte >> 5) & 1; - c.general_profile_idc = (byte & 0x1F); + general_profile_space = (byte >> 6) & 3; + general_tier_flag = (byte >> 5) & 1; + general_profile_idc = (byte & 0x1F); - c.general_profile_compatibility_flags = range.read32(); + general_profile_compatibility_flags = range.read32(); for (int i = 0; i < 6; i++) { byte = range.read8(); for (int b = 0; b < 8; b++) { - c.general_constraint_indicator_flags[i * 8 + b] = (byte >> (7 - b)) & 1; + general_constraint_indicator_flags[i * 8 + b] = (byte >> (7 - b)) & 1; } } - c.general_level_idc = range.read8(); - c.min_spatial_segmentation_idc = range.read16() & 0x0FFF; - c.parallelism_type = range.read8() & 0x03; - c.chroma_format = range.read8() & 0x03; - c.bit_depth_luma = static_cast((range.read8() & 0x07) + 8); - c.bit_depth_chroma = static_cast((range.read8() & 0x07) + 8); - c.avg_frame_rate = range.read16(); + general_level_idc = range.read8(); + min_spatial_segmentation_idc = range.read16() & 0x0FFF; + parallelism_type = range.read8() & 0x03; + chroma_format = range.read8() & 0x03; + bit_depth_luma = static_cast((range.read8() & 0x07) + 8); + bit_depth_chroma = static_cast((range.read8() & 0x07) + 8); + avg_frame_rate = range.read16(); byte = range.read8(); - c.constant_frame_rate = (byte >> 6) & 0x03; - c.num_temporal_layers = (byte >> 3) & 0x07; - c.temporal_id_nested = (byte >> 2) & 1; + constant_frame_rate = (byte >> 6) & 0x03; + num_temporal_layers = (byte >> 3) & 0x07; + temporal_id_nested = (byte >> 2) & 1; m_length_size = static_cast((byte & 0x03) + 1); @@ -112,6 +109,94 @@ } +Error HEVCDecoderConfigurationRecord::write(StreamWriter& writer) const +{ + writer.write8(configuration_version); + + writer.write8((uint8_t) (((general_profile_space & 3) << 6) | + ((general_tier_flag & 1) << 5) | + (general_profile_idc & 0x1F))); + + writer.write32(general_profile_compatibility_flags); + + for (int i = 0; i < 6; i++) { + uint8_t byte = 0; + + for (int b = 0; b < 8; b++) { + if (general_constraint_indicator_flags[i * 8 + b]) { + byte |= 1; + } + + byte = (uint8_t) (byte << 1); + } + + writer.write8(byte); + } + + writer.write8(general_level_idc); + writer.write16((min_spatial_segmentation_idc & 0x0FFF) | 0xF000); + writer.write8(parallelism_type | 0xFC); + writer.write8(chroma_format | 0xFC); + writer.write8((uint8_t) ((bit_depth_luma - 8) | 0xF8)); + writer.write8((uint8_t) ((bit_depth_chroma - 8) | 0xF8)); + writer.write16(avg_frame_rate); + + writer.write8((uint8_t) (((constant_frame_rate & 0x03) << 6) | + ((num_temporal_layers & 0x07) << 3) | + ((temporal_id_nested & 1) << 2) | + ((m_length_size - 1) & 0x03))); + + size_t nArrays = m_nal_array.size(); + // There cannot be an overflow because the nal-type is less than 8-bit. + assert(nArrays <= 0xFF); + + writer.write8(static_cast(nArrays)); + + for (const HEVCDecoderConfigurationRecord::NalArray& array : m_nal_array) { + + writer.write8((uint8_t) (((array.m_array_completeness & 1) << 6) | + (array.m_NAL_unit_type & 0x3F))); + + size_t nUnits = array.m_nal_units.size(); + if (nUnits > 0xFFFF) { + return Error{heif_error_Invalid_input, heif_suberror_Unspecified, "Too many NAL units in hvcC"}; + } + + writer.write16(static_cast(nUnits)); + + for (const std::vector& nal_unit : array.m_nal_units) { + if (nal_unit.size() > 0xFFFF) { + return Error{heif_error_Invalid_input, heif_suberror_Unspecified, "hvcC NAL unit exceeds maximum size (64kB)"}; + } + + writer.write16(static_cast(nal_unit.size())); + writer.write(nal_unit); + } + } + + return Error::Ok; +} + + +bool HEVCDecoderConfigurationRecord::get_general_profile_compatibility_flag(int idx) const +{ + return general_profile_compatibility_flags & (UINT32_C(0x80000000) >> idx); +} + + +bool HEVCDecoderConfigurationRecord::is_profile_compatible(Profile profile) const +{ + return (general_profile_idc == profile || + get_general_profile_compatibility_flag(profile)); +} + + +Error Box_hvcC::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + return m_configuration.parse(range, limits); +} + + std::string Box_hvcC::dump(Indent& indent) const { std::ostringstream sstr; @@ -134,7 +219,7 @@ sstr << indent << "general_constraint_indicator_flags: "; int cnt = 0; - for (int i = 0; i < configuration::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) { + for (int i = 0; i < HEVCDecoderConfigurationRecord::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) { bool b = c.general_constraint_indicator_flags[i]; sstr << (b ? 1 : 0); @@ -150,6 +235,9 @@ << indent << "chroma_format: "; switch (c.chroma_format) { + case 0: + sstr << "monochrome"; + break; case 1: sstr << "4:2:0"; break; @@ -171,9 +259,9 @@ << indent << "constant_frame_rate: " << ((int) c.constant_frame_rate) << "\n" << indent << "num_temporal_layers: " << ((int) c.num_temporal_layers) << "\n" << indent << "temporal_id_nested: " << ((int) c.temporal_id_nested) << "\n" - << indent << "length_size: " << ((int) m_length_size) << "\n"; + << indent << "length_size: " << ((int) c.m_length_size) << "\n"; - for (const auto& array : m_nal_array) { + for (const auto& array : c.m_nal_array) { sstr << indent << "\n"; indent++; @@ -197,21 +285,21 @@ } -bool Box_hvcC::get_headers(std::vector* dest) const +bool Box_hvcC::get_header_nals(std::vector* dest) const { - for (const auto& array : m_nal_array) { + // Concatenate all header NALs, each prefixed by a 4-byte size. + + for (const auto& array : m_configuration.m_nal_array) { for (const auto& unit : array.m_nal_units) { + // Write 4-byte NALs size + dest->push_back((unit.size() >> 24) & 0xFF); dest->push_back((unit.size() >> 16) & 0xFF); dest->push_back((unit.size() >> 8) & 0xFF); dest->push_back((unit.size() >> 0) & 0xFF); - /* - dest->push_back(0); - dest->push_back(0); - dest->push_back(1); - */ + // Copy NAL data dest->insert(dest->end(), unit.begin(), unit.end()); } @@ -220,96 +308,86 @@ return true; } - -void Box_hvcC::append_nal_data(const std::vector& nal) -{ - NalArray array; - array.m_array_completeness = 0; - array.m_NAL_unit_type = uint8_t(nal[0] >> 1); - array.m_nal_units.push_back(nal); - - m_nal_array.push_back(array); -} - void Box_hvcC::append_nal_data(const uint8_t* data, size_t size) { std::vector nal; nal.resize(size); memcpy(nal.data(), data, size); - NalArray array; - array.m_array_completeness = 0; - array.m_NAL_unit_type = uint8_t(nal[0] >> 1); - array.m_nal_units.push_back(std::move(nal)); - - m_nal_array.push_back(array); + append_nal_data(nal); } - -Error Box_hvcC::write(StreamWriter& writer) const +void Box_hvcC::append_nal_data(const std::vector& nal) { - size_t box_start = reserve_box_header_space(writer); + for (auto& nal_array : m_configuration.m_nal_array) { + if (nal_array.m_NAL_unit_type == uint8_t(nal[0] >> 1)) { - const auto& c = m_configuration; // abbreviation + // kvazaar may send the same headers multiple times. Filter out the identical copies. - writer.write8(c.configuration_version); + for (auto& nal_unit : nal_array.m_nal_units) { - writer.write8((uint8_t) (((c.general_profile_space & 3) << 6) | - ((c.general_tier_flag & 1) << 5) | - (c.general_profile_idc & 0x1F))); + // Note: sometimes kvazaar even sends the same packet twice, but with an extra zero byte. + // We detect this by comparing only the common length. This is correct since each NAL + // packet must be complete and thus, if a packet is longer than another complete packet, + // its extra data must be superfluous. + // + // Example: + //| | | + //| | | | array_completeness: 1 + //| | | | NAL_unit_type: 34 + //| | | | 44 01 c1 71 82 99 20 00 + //| | | | 44 01 c1 71 82 99 20 + + // Check whether packets have similar content. + + const size_t common_length = std::min(nal_unit.size(), nal.size()); + bool similar = true; + for (size_t i = 0; i < common_length; i++) { + if (nal_unit[i] != nal[i]) { + similar = false; + break; + } + } - writer.write32(c.general_profile_compatibility_flags); + if (similar) { + // If they are similar, keep the smaller one. - for (int i = 0; i < 6; i++) { - uint8_t byte = 0; + if (nal_unit.size() > nal.size()) { + nal_unit = nal; + } - for (int b = 0; b < 8; b++) { - if (c.general_constraint_indicator_flags[i * 8 + b]) { - byte |= 1; + // Exit. Do not add a copy of the packet. + + return; + } } - byte = (uint8_t) (byte << 1); - } + nal_array.m_nal_units.push_back(nal); - writer.write8(byte); + return; + } } - writer.write8(c.general_level_idc); - writer.write16((c.min_spatial_segmentation_idc & 0x0FFF) | 0xF000); - writer.write8(c.parallelism_type | 0xFC); - writer.write8(c.chroma_format | 0xFC); - writer.write8((uint8_t) ((c.bit_depth_luma - 8) | 0xF8)); - writer.write8((uint8_t) ((c.bit_depth_chroma - 8) | 0xF8)); - writer.write16(c.avg_frame_rate); - - writer.write8((uint8_t) (((c.constant_frame_rate & 0x03) << 6) | - ((c.num_temporal_layers & 0x07) << 3) | - ((c.temporal_id_nested & 1) << 2) | - ((m_length_size - 1) & 0x03))); + // This is a new NAL type. Add a new NAL array. - size_t nArrays = m_nal_array.size(); - if (nArrays > 0xFF) { - // TODO: error: too many NAL units - } + HEVCDecoderConfigurationRecord::NalArray array; + array.m_array_completeness = 1; + array.m_NAL_unit_type = uint8_t(nal[0] >> 1); + array.m_nal_units.push_back(nal); - writer.write8((uint8_t) nArrays); + m_configuration.m_nal_array.push_back(array); +} - for (const NalArray& array : m_nal_array) { - writer.write8((uint8_t) (((array.m_array_completeness & 1) << 6) | - (array.m_NAL_unit_type & 0x3F))); - - size_t nUnits = array.m_nal_units.size(); - if (nUnits > 0xFFFF) { - // TODO: error: too many NAL units - } +Error Box_hvcC::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); - writer.write16((uint16_t) nUnits); + const auto& c = m_configuration; // abbreviation - for (const std::vector& nal_unit : array.m_nal_units) { - writer.write16((uint16_t) nal_unit.size()); - writer.write(nal_unit); - } + Error err = c.write(writer); + if (err) { + return err; } prepend_header(writer, box_start); @@ -354,8 +432,6 @@ static Result> read_depth_representation_info(BitReader& reader) { - Result> result; - auto msg = std::make_shared(); @@ -375,15 +451,13 @@ msg->has_d_min = (uint8_t) reader.get_bits(1); msg->has_d_max = (uint8_t) reader.get_bits(1); - int rep_type; + uint32_t rep_type; if (!reader.get_uvlc(&rep_type)) { - result.error = {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid depth representation type in input"}; - return result; + return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid depth representation type in input"}; } - if (rep_type < 0 || rep_type > 3) { - result.error = {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "input depth representation type out of range"}; - return result; + if (rep_type > 3) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "input depth representation type out of range"}; } msg->depth_representation_type = (enum heif_depth_representation_type) rep_type; @@ -392,10 +466,9 @@ //printf("type: %d\n",rep_type); if (msg->has_d_min || msg->has_d_max) { - int ref_view; + uint32_t ref_view; if (!reader.get_uvlc(&ref_view)) { - result.error = {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid disparity_reference_view in input"}; - return result; + return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "invalid disparity_reference_view in input"}; } msg->disparity_reference_view = ref_view; @@ -418,8 +491,7 @@ // TODO: load non-uniform response curve } - result.value = msg; - return result; + return {msg}; } @@ -431,7 +503,7 @@ // TODO: we probably do not need a full BitReader just for the array size. // Read this and the NAL size directly on the array data. - BitReader reader(data.data(), (int) data.size()); + BitReader reader(data.data(), data.size()); if (reader.get_bits_remaining() < 32) { return {heif_error_Invalid_input, heif_suberror_End_of_data, @@ -444,10 +516,10 @@ // ERROR: read past end of data } - while (reader.get_current_byte_index() < (int) len) { - int currPos = reader.get_current_byte_index(); + while (reader.get_current_byte_index() < len) { + size_t currPos = reader.get_current_byte_index(); - BitReader sei_reader(data.data() + currPos, (int) data.size() - currPos); + BitReader sei_reader(data.data() + currPos, data.size() - currPos); if (sei_reader.get_bits_remaining() < 32+8) { return {heif_error_Invalid_input, @@ -480,11 +552,11 @@ if (payload_id == 177) { // depth_representation_info Result> seiResult = read_depth_representation_info(sei_reader); - if (seiResult.error) { - return seiResult.error; + if (!seiResult) { + return seiResult.error(); } - msgs.push_back(seiResult.value); + msgs.push_back(*seiResult); } } @@ -496,7 +568,8 @@ } -static std::vector remove_start_code_emulation(const uint8_t* sps, size_t size) +// Used for AVC, HEVC, and VVC. +std::vector remove_start_code_emulation(const uint8_t* sps, size_t size) { std::vector out_data; @@ -519,8 +592,9 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size, - Box_hvcC::configuration* config, - int* width, int* height) + HEVCDecoderConfigurationRecord* config, + uint32_t* width, uint32_t* height, + ImageSize* coded_size) { // remove start-code emulation bytes from SPS header stream @@ -530,7 +604,7 @@ size = sps_no_emul.size(); - BitReader reader(sps, (int) size); + BitReader reader(sps, size); // skip NAL header reader.skip_bits(2 * 8); @@ -584,44 +658,88 @@ // --- SPS continued --- - int dummy, value; - reader.get_uvlc(&dummy); // skip seq_parameter_seq_id - - reader.get_uvlc(&value); + Error invalidUVLC{ + heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "Invalid variable length code in HEVC SPS header" + }; + + uint32_t dummy, value; + if (!reader.get_uvlc(&dummy) || // skip seq_parameter_seq_id + !reader.get_uvlc(&value)) { + return invalidUVLC; + } + if (value > 3) { + // chroma_format_idc is in the range 0..3 (H.265 section 7.4.3.2.1). The + // value is later cast to heif_chroma, so it must not be left unchecked. + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS chroma_format_idc out of range"}; + } config->chroma_format = (uint8_t) value; if (config->chroma_format == 3) { reader.skip_bits(1); } - reader.get_uvlc(width); - reader.get_uvlc(height); + if (!reader.get_uvlc(width) || + !reader.get_uvlc(height)) { + return invalidUVLC; + } + + if (coded_size) { + coded_size->width = *width; + coded_size->height = *height; + } bool conformance_window = reader.get_bits(1); if (conformance_window) { - int left, right, top, bottom; - reader.get_uvlc(&left); - reader.get_uvlc(&right); - reader.get_uvlc(&top); - reader.get_uvlc(&bottom); + uint32_t left, right, top, bottom; + if (!reader.get_uvlc(&left) || + !reader.get_uvlc(&right) || + !reader.get_uvlc(&top) || + !reader.get_uvlc(&bottom)) { + return invalidUVLC; + } - //printf("conformance borders: %d %d %d %d\n",left,right,top,bottom); + //printf("conformance borders: %u %u %u %u\n",left,right,top,bottom); - int subH = 1, subV = 1; + uint32_t subH = 1, subV = 1; if (config->chroma_format == 1) { subV = 2; subH = 2; } if (config->chroma_format == 2) { subH = 2; } - *width -= subH * (left + right); - *height -= subV * (top + bottom); + const uint64_t crop_w = (uint64_t)subH * ((uint64_t)left + (uint64_t)right); + const uint64_t crop_h = (uint64_t)subV * ((uint64_t)top + (uint64_t)bottom); + if (crop_w > *width || crop_h > *height) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS conformance window exceeds image dimensions"}; + } + *width -= (uint32_t)crop_w; + *height -= (uint32_t)crop_h; + } + + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + if (value > 8) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS bit_depth_luma_minus8 out of range"}; } - - reader.get_uvlc(&value); config->bit_depth_luma = (uint8_t) (value + 8); - reader.get_uvlc(&value); + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + if (value > 8) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS bit_depth_chroma_minus8 out of range"}; + } config->bit_depth_chroma = (uint8_t) (value + 8); @@ -631,8 +749,8 @@ config->configuration_version = 1; config->min_spatial_segmentation_idc = 0; // TODO: get this value from the VUI, 0 should be safe config->parallelism_type = 0; // TODO, 0 should be safe - config->avg_frame_rate = 0; // makes no sense for HEIF - config->constant_frame_rate = 0; // makes no sense for HEIF + config->avg_frame_rate = 0; // makes no sense for HEIF (TODO) + config->constant_frame_rate = 0; // makes no sense for HEIF (TODO) config->num_temporal_layers = 1; // makes no sense for HEIF return Error::Ok; diff -Nru libheif-1.19.8/libheif/codecs/hevc_boxes.h libheif-1.23.4/libheif/codecs/hevc_boxes.h --- libheif-1.19.8/libheif/codecs/hevc_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -29,11 +29,66 @@ #include #include #include "image-items/image_item.h" +#include "sequences/seq_boxes.h" -class Box_hvcC : public Box +struct HEVCDecoderConfigurationRecord { + uint8_t configuration_version; + uint8_t general_profile_space; + bool general_tier_flag; + uint8_t general_profile_idc; + uint32_t general_profile_compatibility_flags; + + static const int NUM_CONSTRAINT_INDICATOR_FLAGS = 48; + std::bitset general_constraint_indicator_flags; + + uint8_t general_level_idc; + + uint16_t min_spatial_segmentation_idc; + uint8_t parallelism_type; + uint8_t chroma_format; + uint8_t bit_depth_luma; + uint8_t bit_depth_chroma; + uint16_t avg_frame_rate; + + uint8_t constant_frame_rate; + uint8_t num_temporal_layers; + uint8_t temporal_id_nested; + uint8_t m_length_size = 4; // default: 4 bytes for NAL unit lengths + + struct NalArray + { + uint8_t m_array_completeness; + uint8_t m_NAL_unit_type; + + std::vector > m_nal_units; + }; + + enum Profile { + Profile_Main = 1, + Profile_Main10 = 2, + Profile_MainStillPicture = 3, + Profile_RExt = 4, + Profile_HighThroughput = 5, + Profile_ScreenCoding = 9, + Profile_HighTHroughputScreenCoding = 11 + }; + + std::vector m_nal_array; + + Error parse(BitstreamRange& range, const heif_security_limits* limits); + + Error write(StreamWriter& writer) const; + + bool get_general_profile_compatibility_flag(int idx) const; + bool is_profile_compatible(Profile) const; +}; + + +class Box_hvcC : public Box +{ // allow access to protected parse() method friend class Box_mini; @@ -45,40 +100,23 @@ bool is_essential() const override { return true; } - struct configuration - { - uint8_t configuration_version; - uint8_t general_profile_space; - bool general_tier_flag; - uint8_t general_profile_idc; - uint32_t general_profile_compatibility_flags; - - static const int NUM_CONSTRAINT_INDICATOR_FLAGS = 48; - std::bitset general_constraint_indicator_flags; - - uint8_t general_level_idc; - - uint16_t min_spatial_segmentation_idc; - uint8_t parallelism_type; - uint8_t chroma_format; - uint8_t bit_depth_luma; - uint8_t bit_depth_chroma; - uint16_t avg_frame_rate; - - uint8_t constant_frame_rate; - uint8_t num_temporal_layers; - uint8_t temporal_id_nested; - }; - std::string dump(Indent&) const override; - bool get_headers(std::vector* dest) const; + const char* debug_box_name() const override { return "HEVC Configuration Item"; } - void set_configuration(const configuration& config) { m_configuration = config; } + bool get_header_nals(std::vector* dest) const; - const configuration& get_configuration() const { return m_configuration; } + void set_configuration(const HEVCDecoderConfigurationRecord& config) { m_configuration = config; } + const HEVCDecoderConfigurationRecord& get_configuration() const { return m_configuration; } + + HEVCDecoderConfigurationRecord& get_configuration() { return m_configuration; } + + // Add a header NAL to the hvcC. The data is the raw NAL data without any start-code or NAL size field. + // + // Note: we expect that all header NALs are added to the hvcC and + // there are no additional header NALs in the bitstream. void append_nal_data(const std::vector& nal); void append_nal_data(const uint8_t* data, size_t size); @@ -89,20 +127,21 @@ Error parse(BitstreamRange& range, const heif_security_limits* limits) override; private: - struct NalArray - { - uint8_t m_array_completeness; - uint8_t m_NAL_unit_type; - - std::vector > m_nal_units; - }; + HEVCDecoderConfigurationRecord m_configuration; +}; - configuration m_configuration; - uint8_t m_length_size = 4; // default: 4 bytes for NAL unit lengths - std::vector m_nal_array; +class Box_hvc1 : public Box_VisualSampleEntry +{ +public: + Box_hvc1() + { + set_short_type(fourcc("hvc1")); + } }; + + class SEIMessage { public: @@ -120,9 +159,17 @@ Error decode_hevc_aux_sei_messages(const std::vector& data, std::vector>& msgs); +// Used for AVC, HEVC, and VVC. +std::vector remove_start_code_emulation(const uint8_t* sps, size_t size); + +struct ImageSize; +// Parses an HEVC SPS NAL unit. *width / *height return the post-conformance- +// window cropping (display) dimensions. If non-null, *coded_size receives the +// pre-cropping dimensions actually allocated by the decoder. Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size, - Box_hvcC::configuration* inout_config, - int* width, int* height); + HEVCDecoderConfigurationRecord* inout_config, + uint32_t* width, uint32_t* height, + ImageSize* coded_size = nullptr); #endif diff -Nru libheif-1.19.8/libheif/codecs/hevc_dec.cc libheif-1.23.4/libheif/codecs/hevc_dec.cc --- libheif-1.19.8/libheif/codecs/hevc_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,6 +22,7 @@ #include "hevc_boxes.h" #include "error.h" #include "context.h" +#include "plugins/nalu_utils.h" #include @@ -29,7 +30,7 @@ Result> Decoder_HEVC::read_bitstream_configuration_data() const { std::vector data; - if (!m_hvcC->get_headers(&data)) { + if (!m_hvcC->get_header_nals(&data)) { return Error{heif_error_Invalid_input, heif_suberror_No_item_data}; } @@ -50,6 +51,32 @@ } +Result> Decoder_HEVC::get_coded_image_size_from_config() const +{ + const auto& nal_arrays = m_hvcC->get_configuration().m_nal_array; + + for (const auto& arr : nal_arrays) { + if (arr.m_NAL_unit_type != HEVC_NAL_UNIT_SPS_NUT || arr.m_nal_units.empty()) { + continue; + } + + const std::vector& sps = arr.m_nal_units[0]; + HEVCDecoderConfigurationRecord scratch = m_hvcC->get_configuration(); + uint32_t cropped_w = 0, cropped_h = 0; + ImageSize coded{}; + Error e = parse_sps_for_hvcC_configuration(sps.data(), sps.size(), &scratch, + &cropped_w, &cropped_h, &coded); + if (e) { + return e; + } + + return std::optional{coded}; + } + + return std::optional{}; +} + + Error Decoder_HEVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { *out_chroma = (heif_chroma) (m_hvcC->get_configuration().chroma_format); diff -Nru libheif-1.19.8/libheif/codecs/hevc_dec.h libheif-1.23.4/libheif/codecs/hevc_dec.h --- libheif-1.19.8/libheif/codecs/hevc_dec.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_dec.h 2026-09-06 14:45:17.000000000 +0000 @@ -47,6 +47,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_coded_image_size_from_config() const override; + private: const std::shared_ptr m_hvcC; }; diff -Nru libheif-1.19.8/libheif/codecs/hevc_enc.cc libheif-1.23.4/libheif/codecs/hevc_enc.cc --- libheif-1.19.8/libheif/codecs/hevc_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,309 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "hevc_enc.h" +#include "hevc_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" + +#include + +#include "plugins/nalu_utils.h" + + +// TODO: can we use the new sequences interface for this to avoid duplicate code. +Result Encoder_HEVC::encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) +{ + CodedImageData codedImage; + + auto hvcC = std::make_shared(); + + heif_image c_api_image; + c_api_image.image = image; + + heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + uint32_t encoded_width = 0; + uint32_t encoded_height = 0; + + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + + if ((data[0] >> 1) == HEVC_NAL_UNIT_SPS_NUT) { + parse_sps_for_hvcC_configuration(data, size, &hvcC->get_configuration(), &encoded_width, &encoded_height); + + codedImage.encoded_image_width = encoded_width; + codedImage.encoded_image_height = encoded_height; + } + + switch (data[0] >> 1) { + case HEVC_NAL_UNIT_VPS_NUT: + case HEVC_NAL_UNIT_SPS_NUT: + case HEVC_NAL_UNIT_PPS_NUT: + hvcC->append_nal_data(data, size); + break; + + default: + codedImage.append_with_4bytes_size(data, size); + } + } + + if (!encoded_width || !encoded_height) { + return Error(heif_error_Encoder_plugin_error, + heif_suberror_Invalid_image_size); + } + + codedImage.properties.push_back(hvcC); + + + // Make sure that the encoder plugin works correctly and the encoded image has the correct size. + + if (encoder->plugin->plugin_api_version >= 3 && + encoder->plugin->query_encoded_size != nullptr) { + uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); + + encoder->plugin->query_encoded_size(encoder->encoder, + image->get_width(), image->get_height(), + &check_encoded_width, + &check_encoded_height); + + assert(check_encoded_width == encoded_width); + assert(check_encoded_height == encoded_height); + } + + codedImage.codingConstraints.intra_pred_used = true; + codedImage.codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return codedImage; +} + + +Error Encoder_HEVC::encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) +{ + heif_image c_api_image; + c_api_image.image = image; + + if (!m_encoder_active) { + heif_error err = encoder->plugin->start_sequence_encoding(encoder->encoder, &c_api_image, + input_class, + framerate_num, framerate_denom, + &options); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + m_hvcC = std::make_shared(); + m_encoder_active = true; + } + + Error dataErr = get_data(encoder); + if (dataErr) { + return dataErr; + } + + heif_error err = encoder->plugin->encode_sequence_frame(encoder->encoder, &c_api_image, frame_number); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + return get_data(encoder); +} + + +Error Encoder_HEVC::encode_sequence_flush(heif_encoder* encoder) +{ + encoder->plugin->end_sequence_encoding(encoder->encoder); + m_encoder_active = false; + m_end_of_sequence_reached = true; + + return get_data(encoder); +} + + +std::optional Encoder_HEVC::encode_sequence_get_data() +{ + return std::move(m_current_output_data); +} + +Error Encoder_HEVC::get_data(heif_encoder* encoder) +{ + //CodedImageData codedImage; + + bool got_some_data = false; + + for (;;) { + uint8_t* data; + int size; + + uintptr_t frameNr=0; + int more_frame_packets = 1; + struct heif_error err = encoder->plugin->get_compressed_data2(encoder->encoder, &data, &size, &frameNr, nullptr, &more_frame_packets); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + got_some_data = true; + + const uint8_t nal_type = (data[0] >> 1); + const bool is_sync = (nal_type == 19 || nal_type == 20 || nal_type == 21); + const bool is_image_data = (nal_type >= 0 && nal_type <= HEVC_NAL_UNIT_MAX_VCL); + + // std::cout << "received frameNr=" << frameNr << " nal_type:" << ((int)nal_type) << " size: " << size << "\n"; + + if (nal_type == HEVC_NAL_UNIT_SPS_NUT && m_hvcC) { + parse_sps_for_hvcC_configuration(data, size, + &m_hvcC->get_configuration(), + &m_encoded_image_width, &m_encoded_image_height); + } + + switch (nal_type) { + case HEVC_NAL_UNIT_VPS_NUT: + if (m_hvcC && !m_hvcC_has_VPS) m_hvcC->append_nal_data(data, size); + m_hvcC_has_VPS = true; + break; + + case HEVC_NAL_UNIT_SPS_NUT: + if (m_hvcC && !m_hvcC_has_SPS) m_hvcC->append_nal_data(data, size); + m_hvcC_has_SPS = true; + break; + + case HEVC_NAL_UNIT_PPS_NUT: + if (m_hvcC && !m_hvcC_has_PPS) m_hvcC->append_nal_data(data, size); + m_hvcC_has_PPS = true; + break; + + default: + if (!m_current_output_data) { + m_current_output_data = CodedImageData{}; + } + m_current_output_data->append_with_4bytes_size(data, size); + + if (is_image_data) { + m_current_output_data->is_sync_frame = is_sync; + m_current_output_data->frame_nr = frameNr; + } + } + + if (!more_frame_packets) { + break; + } + } + + if (!got_some_data) { + return {}; + } + + if (!m_encoded_image_width || !m_encoded_image_height) { + return Error(heif_error_Encoder_plugin_error, + heif_suberror_Invalid_image_size); + } + + + // --- return hvcC when all headers are included and it was not returned yet + // TODO: it's maybe better to return this at the end so that we are sure to have all headers + // and also complete codingConstraints. + + if (!m_current_output_data->bitstream.empty() && + m_hvcC_has_VPS && m_hvcC_has_SPS && m_hvcC_has_PPS && !m_hvcC_sent) { + //if (/*m_end_of_sequence_reached &&*/ m_hvcC && !m_hvcC_sent) { + m_current_output_data->properties.push_back(m_hvcC); + m_hvcC = nullptr; + m_hvcC_sent = true; + } + + m_current_output_data->encoded_image_width = m_encoded_image_width; + m_current_output_data->encoded_image_height = m_encoded_image_height; + + + // Make sure that the encoder plugin works correctly and the encoded image has the correct size. +#if 0 + if (encoder->plugin->plugin_api_version >= 3 && + encoder->plugin->query_encoded_size != nullptr) { + uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); + + encoder->plugin->query_encoded_size(encoder->encoder, + image->get_width(), image->get_height(), + &check_encoded_width, + &check_encoded_height); + + assert((int)check_encoded_width == encoded_width); + assert((int)check_encoded_height == encoded_height); + } +#endif + + m_current_output_data->codingConstraints.intra_pred_used = true; + m_current_output_data->codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return {}; +} + + +std::shared_ptr Encoder_HEVC::get_sample_description_box(const CodedImageData& data) const +{ + auto hvc1 = std::make_shared(); + hvc1->get_VisualSampleEntry().compressorname = "HEVC"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("hvcC")) { + hvc1->append_child_box(prop); + return hvc1; + } + } + + // box not yet available + return nullptr; +} diff -Nru libheif-1.19.8/libheif/codecs/hevc_enc.h libheif-1.23.4/libheif/codecs/hevc_enc.h --- libheif-1.19.8/libheif/codecs/hevc_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/hevc_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,78 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_HEVC_H +#define HEIF_ENCODER_HEVC_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_HEVC : public Encoder { +public: + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + bool encode_sequence_started() const override { return m_encoder_active; } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override; + + Error encode_sequence_flush(heif_encoder* encoder) override; + + std::optional encode_sequence_get_data() override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + +private: + bool m_encoder_active = false; + bool m_end_of_sequence_reached = false; + + // Whether the hvcC is complete and was returned in an encode_sequence_get_data() call. + bool m_hvcC_has_VPS = false; + bool m_hvcC_has_SPS = false; + bool m_hvcC_has_PPS = false; + std::shared_ptr m_hvcC; + bool m_hvcC_sent = false; + + uint32_t m_encoded_image_width = 0; + uint32_t m_encoded_image_height = 0; + + std::optional m_current_output_data; + + Error get_data(heif_encoder*); +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/jpeg2000_boxes.cc libheif-1.23.4/libheif/codecs/jpeg2000_boxes.cc --- libheif-1.19.8/libheif/codecs/jpeg2000_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg2000_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,7 +19,7 @@ */ #include "jpeg2000_boxes.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include @@ -195,9 +195,24 @@ } m_bitDepths.push_back(bit_depth); } + // Number of bytes each palette entry occupies in the box. Used to bound + // num_entries by the data actually present, so a small header cannot force + // a large allocation (analogous to the 'cdef'/'j2kL' checks). + size_t bytes_per_entry = 0; + for (uint8_t bd : m_bitDepths) { + bytes_per_entry += (bd <= 8) ? 1 : 2; + } + + if (bytes_per_entry != 0 && + num_entries > range.get_remaining_bytes() / bytes_per_entry) { + return Error(heif_error_Invalid_input, + heif_suberror_End_of_data, + "pclr box declares more entries than the box contains"); + } + for (uint16_t j = 0; j < num_entries; j++) { PaletteEntry entry; - for (unsigned long int i = 0; i < entry.columns.size(); i++) { + for (size_t i = 0; i < m_bitDepths.size(); i++) { if (m_bitDepths[i] <= 8) { entry.columns.push_back(range.read8()); } @@ -205,7 +220,7 @@ entry.columns.push_back(range.read16()); } } - m_entries.push_back(entry); + m_entries.push_back(std::move(entry)); } return range.get_error(); @@ -430,7 +445,7 @@ heif_suberror_Invalid_J2K_codestream, std::string("Out of range Csiz value")); } - if (cursor > headerData.size() - (3 * csiz)) { + if (3 * static_cast(csiz) > headerData.size() - cursor) { return Error(heif_error_Invalid_input, heif_suberror_Invalid_J2K_codestream); } @@ -449,7 +464,8 @@ Error JPEG2000MainHeader::parse_CAP_segment_body() { - if (cursor > headerData.size() - 8) { + // Need at least Lcap (2) + Pcap (4) = 6 bytes. + if (headerData.size() - cursor < 6) { return Error(heif_error_Invalid_input, heif_suberror_Invalid_J2K_codestream); } @@ -459,15 +475,25 @@ heif_suberror_Invalid_J2K_codestream, std::string("Out of range Lcap value")); } + // Lcap counts the Lcap field itself, so Lcap-2 bytes must follow it. + if (headerData.size() - cursor < static_cast(lcap) - 2) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_J2K_codestream); + } uint32_t pcap = read32(); + size_t segment_end = cursor + (static_cast(lcap) - 6); for (uint8_t i = 2; i <= 32; i++) { - if (pcap & (1 << (32 - i))) { + if (pcap & (1u << (32 - i))) { + if (segment_end - cursor < 2) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_J2K_codestream, + std::string("CAP segment Pcap inconsistent with Lcap")); + } switch (i) { case JPEG2000_Extension_Capability_HT::IDENT: parse_Ccap15(); break; default: - std::cout << "unhandled extended capabilities value: " << (int)i << std::endl; read16(); } } diff -Nru libheif-1.19.8/libheif/codecs/jpeg2000_boxes.h libheif-1.23.4/libheif/codecs/jpeg2000_boxes.h --- libheif-1.19.8/libheif/codecs/jpeg2000_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg2000_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -228,7 +228,7 @@ return (uint8_t)(m_bitDepths.size()); } - void add_entry(const PaletteEntry entry) + void add_entry(const PaletteEntry& entry) { m_entries.push_back(entry); } @@ -507,7 +507,10 @@ uint32_t read32() { - uint32_t res = (headerData[cursor] << 24) | (headerData[cursor + 1] << 16) | (headerData[cursor + 2] << 8) | headerData[cursor + 3]; + uint32_t res = (uint32_t{headerData[cursor]} << 24) | + (uint32_t{headerData[cursor + 1]} << 16) | + (uint32_t{headerData[cursor + 2]} << 8) | + uint32_t{headerData[cursor + 3]}; cursor += 4; return res; } @@ -516,4 +519,14 @@ size_t cursor = 0; }; + +class Box_j2ki : public Box_VisualSampleEntry +{ +public: + Box_j2ki() + { + set_short_type(fourcc("j2ki")); + } +}; + #endif // LIBHEIF_JPEG2000_BOXES_H diff -Nru libheif-1.19.8/libheif/codecs/jpeg2000_dec.cc libheif-1.23.4/libheif/codecs/jpeg2000_dec.cc --- libheif-1.19.8/libheif/codecs/jpeg2000_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg2000_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -34,8 +34,8 @@ int Decoder_JPEG2000::get_luma_bits_per_pixel() const { - Result> imageDataResult = get_compressed_data(); - if (imageDataResult.error) { + Result> imageDataResult = get_compressed_data(true); + if (!imageDataResult) { return -1; } @@ -50,8 +50,8 @@ int Decoder_JPEG2000::get_chroma_bits_per_pixel() const { - Result> imageDataResult = get_compressed_data(); - if (imageDataResult.error) { + Result> imageDataResult = get_compressed_data(true); + if (!imageDataResult) { return -1; } diff -Nru libheif-1.19.8/libheif/codecs/jpeg2000_enc.cc libheif-1.23.4/libheif/codecs/jpeg2000_enc.cc --- libheif-1.19.8/libheif/codecs/jpeg2000_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg2000_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,112 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "jpeg2000_enc.h" +#include "jpeg2000_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" + +#include + + +Result Encoder_JPEG2000::encode(const std::shared_ptr& image, + struct heif_encoder* encoder, + const struct heif_encoding_options& options, + enum heif_image_input_class input_class) +{ + Encoder::CodedImageData codedImageData; + heif_error err; + + heif_image c_api_image; + c_api_image.image = image; + + err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class);\ + if (err.code != heif_error_Ok) { + return Error::from_heif_error(err); + } + + // get compressed data + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); + if (err.code != heif_error_Ok) { + return Error::from_heif_error(err); + } + + if (data == nullptr) { + break; + } + + codedImageData.append(data, size); + } + + // add 'j2kH' property + auto j2kH = std::make_shared(); + + // add 'cdef' to 'j2kH' + auto cdef = std::make_shared(); + cdef->set_channels(image->get_colorspace()); + j2kH->append_child_box(cdef); + + codedImageData.properties.push_back(j2kH); + + codedImageData.codingConstraints.intra_pred_used = false; + codedImageData.codingConstraints.all_ref_pics_intra = true; + codedImageData.codingConstraints.max_ref_per_pic = 0; + + return codedImageData; +} + + +std::shared_ptr Encoder_JPEG2000::get_sample_description_box(const CodedImageData& data) const +{ + auto j2ki = std::make_shared(); + j2ki->get_VisualSampleEntry().compressorname = "JPEG2000"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("j2kH")) { + j2ki->append_child_box(prop); + return j2ki; + } + } + + assert(false); // no hvcC generated + return nullptr; +} + + +std::shared_ptr Encoder_HTJ2K::get_sample_description_box(const CodedImageData& data) const +{ + auto j2ki = std::make_shared(); + j2ki->get_VisualSampleEntry().compressorname = "HTJ2K"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("j2kH")) { + j2ki->append_child_box(prop); + return j2ki; + } + } + + assert(false); // no hvcC generated + return nullptr; +} diff -Nru libheif-1.19.8/libheif/codecs/jpeg2000_enc.h libheif-1.23.4/libheif/codecs/jpeg2000_enc.h --- libheif-1.19.8/libheif/codecs/jpeg2000_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg2000_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,90 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_JPEG2000_H +#define HEIF_ENCODER_JPEG2000_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_JPEG2000 : public Encoder { +public: + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + + bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override + { + heif_encoding_options dummy_options{}; + + auto encodeResult = encode(image, encoder, dummy_options, input_class); + if (encodeResult.error()) { + return encodeResult.error(); + } + + m_codedImageData = std::move(*encodeResult); + + m_codedImageData->frame_nr = frame_number; + m_codedImageData->is_sync_frame = true; + + return {}; + } + + Error encode_sequence_flush(heif_encoder* encoder) override + { + return {}; + } + + std::optional encode_sequence_get_data() override + { + return std::move(m_codedImageData); + } + +private: + std::optional m_codedImageData; +}; + + +class Encoder_HTJ2K : public Encoder_JPEG2000 { +public: + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/jpeg_boxes.cc libheif-1.23.4/libheif/codecs/jpeg_boxes.cc --- libheif-1.19.8/libheif/codecs/jpeg_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -76,4 +76,3 @@ range.read(m_data.data(), nBytes); return range.get_error(); } - diff -Nru libheif-1.19.8/libheif/codecs/jpeg_boxes.h libheif-1.23.4/libheif/codecs/jpeg_boxes.h --- libheif-1.19.8/libheif/codecs/jpeg_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -26,6 +26,7 @@ #include #include "image-items/image_item.h" #include +#include "sequences/seq_boxes.h" class Box_jpgC : public Box @@ -51,4 +52,15 @@ std::vector m_data; }; + +class Box_mjpg : public Box_VisualSampleEntry +{ +public: + Box_mjpg() + { + set_short_type(fourcc("mjpg")); + } +}; + + #endif // LIBHEIF_JPEG_BOXES_H diff -Nru libheif-1.19.8/libheif/codecs/jpeg_dec.cc libheif-1.23.4/libheif/codecs/jpeg_dec.cc --- libheif-1.19.8/libheif/codecs/jpeg_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -51,14 +51,14 @@ // image data, usually from 'mdat' - auto dataResult = get_compressed_data(); - if (dataResult.error) { - return dataResult.error; + auto dataResult = get_compressed_data(true); + if (!dataResult) { + return dataResult.error(); } - const std::vector& data = dataResult.value; + const std::vector& data = *dataResult; - const Error error_invalidSOF{heif_error_Invalid_input, + Error error_invalidSOF{heif_error_Invalid_input, heif_suberror_Unspecified, "Invalid JPEG SOF header"}; diff -Nru libheif-1.19.8/libheif/codecs/jpeg_enc.cc libheif-1.23.4/libheif/codecs/jpeg_enc.cc --- libheif-1.19.8/libheif/codecs/jpeg_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,134 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "jpeg_enc.h" +#include "jpeg_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" +#include + +#include + + +static uint8_t JPEG_SOS = 0xDA; + + +const heif_color_profile_nclx* Encoder_JPEG::get_forced_output_nclx() const +{ + // JPEG always uses CCIR-601 + + static heif_color_profile_nclx target_heif_nclx; + target_heif_nclx.version = 1; + target_heif_nclx.matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; + target_heif_nclx.color_primaries = heif_color_primaries_ITU_R_BT_601_6; + target_heif_nclx.transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_601_6; + target_heif_nclx.full_range_flag = true; + + return &target_heif_nclx; +} + + +Result Encoder_JPEG::encode(const std::shared_ptr& image, + struct heif_encoder* encoder, + const struct heif_encoding_options& options, + enum heif_image_input_class input_class) +{ + Encoder::CodedImageData codedImage; + + + heif_image c_api_image; + c_api_image.image = image; + + struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + std::vector vec; + + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + size_t oldsize = vec.size(); + vec.resize(oldsize + size); + memcpy(vec.data() + oldsize, data, size); + } + +#if 0 + // Optional: split the JPEG data into a jpgC box and the actual image data. + // Currently disabled because not supported yet in other decoders. + if (false) { + size_t pos = find_jpeg_marker_start(vec, JPEG_SOS); + if (pos > 0) { + std::vector jpgC_data(vec.begin(), vec.begin() + pos); + auto jpgC = std::make_shared(); + jpgC->set_data(jpgC_data); + + auto ipma_box = m_heif_file->get_ipma_box(); + int index = m_heif_file->get_ipco_box()->find_or_append_child_box(jpgC); + ipma_box->add_property_for_item_ID(image_id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)}); + + std::vector image_data(vec.begin() + pos, vec.end()); + vec = std::mo ve(image_data); + } + } +#endif + (void) JPEG_SOS; + + codedImage.bitstream = std::move(vec); + +#if 0 + // TODO: extract 'jpgC' header data +#endif + + codedImage.codingConstraints.intra_pred_used = false; + codedImage.codingConstraints.all_ref_pics_intra = true; + codedImage.codingConstraints.max_ref_per_pic = 0; + + return {codedImage}; +} + + +std::shared_ptr Encoder_JPEG::get_sample_description_box(const CodedImageData& data) const +{ + auto mjpg = std::make_shared(); + mjpg->get_VisualSampleEntry().compressorname = "JPEG"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("jpgC")) { + mjpg->append_child_box(prop); + } + } + + return mjpg; +} diff -Nru libheif-1.19.8/libheif/codecs/jpeg_enc.h libheif-1.23.4/libheif/codecs/jpeg_enc.h --- libheif-1.19.8/libheif/codecs/jpeg_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/jpeg_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,87 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_JPEG_H +#define HEIF_ENCODER_JPEG_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_JPEG : public Encoder { +public: + const heif_color_profile_nclx* get_forced_output_nclx() const override; + + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + enum heif_image_input_class input_class) override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + + + bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override + { + heif_encoding_options dummy_options{}; + + auto encodeResult = encode(image, encoder, dummy_options, input_class); + if (encodeResult.error()) { + return encodeResult.error(); + } + + m_codedImageData = std::move(*encodeResult); + + m_codedImageData->frame_nr = frame_number; + m_codedImageData->is_sync_frame = true; + + return {}; + } + + Error encode_sequence_flush(heif_encoder* encoder) override + { + return {}; + } + + std::optional encode_sequence_get_data() override + { + return std::move(m_codedImageData); + } + +private: + std::optional m_codedImageData; +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_abstract.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_abstract.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,303 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#include -#include -#include -#include -#include - -#if ((defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(__PGI)) && __GNUC__ < 9) || (defined(__clang__) && __clang_major__ < 10) -#include -#else -#include -#endif - -#include "common_utils.h" -#include "context.h" -#include "compression.h" -#include "error.h" -#include "libheif/heif.h" -#include "unc_types.h" -#include "unc_boxes.h" -#include "unc_codec.h" -#include "decoder_abstract.h" - - -AbstractDecoder::AbstractDecoder(uint32_t width, uint32_t height, const std::shared_ptr cmpd, const std::shared_ptr uncC) : - m_width(width), - m_height(height), - m_cmpd(std::move(cmpd)), - m_uncC(std::move(uncC)) -{ - m_tile_height = m_height / m_uncC->get_number_of_tile_rows(); - m_tile_width = m_width / m_uncC->get_number_of_tile_columns(); - - assert(m_tile_width > 0); - assert(m_tile_height > 0); -} - -void AbstractDecoder::buildChannelList(std::shared_ptr& img) -{ - for (Box_uncC::Component component : m_uncC->get_components()) { - ChannelListEntry entry = buildChannelListEntry(component, img); - channelList.push_back(entry); - } -} - -void AbstractDecoder::memcpy_to_native_endian(uint8_t* dst, uint32_t value, uint32_t bytes_per_sample) -{ - // TODO: this assumes that the file endianness is always big-endian. The endianness flags in the uncC header are not taken into account yet. - - if (bytes_per_sample==1) { - *dst = static_cast(value); - return; - } - else if (std::endian::native == std::endian::big) { - for (uint32_t i = 0; i < bytes_per_sample; i++) { - dst[bytes_per_sample - 1 - i] = static_cast((value >> (i * 8)) & 0xFF); - } - } - else { - for (uint32_t i = 0; i < bytes_per_sample; i++) { - dst[i] = static_cast((value >> (i * 8)) & 0xFF); - } - } -} - -void AbstractDecoder::processComponentSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_row_offset, uint32_t tile_column, uint32_t tile_x) -{ - uint64_t dst_col_number = static_cast(tile_column) * entry.tile_width + tile_x; - uint64_t dst_column_offset = dst_col_number * entry.bytes_per_component_sample; - int val = srcBits.get_bits(entry.bits_per_component_sample); // get_bits() reads input in big-endian order - memcpy_to_native_endian(entry.dst_plane + dst_row_offset + dst_column_offset, val, entry.bytes_per_component_sample); -} - -// Handles the case where a row consists of a single component type -// Not valid for Pixel interleave -// Not valid for the Cb/Cr channels in Mixed Interleave -// Not valid for multi-Y pixel interleave -void AbstractDecoder::processComponentRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_row_offset, uint32_t tile_column) -{ - for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { - if (entry.component_alignment != 0) { - srcBits.skip_to_byte_boundary(); - int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; - srcBits.skip_bits(numPadBits); - } - processComponentSample(srcBits, entry, dst_row_offset, tile_column, tile_x); - } - srcBits.skip_to_byte_boundary(); -} - -void AbstractDecoder::processComponentTileSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_offset, uint32_t tile_x) -{ - uint64_t dst_sample_offset = uint64_t{tile_x} * entry.bytes_per_component_sample; - int val = srcBits.get_bits(entry.bits_per_component_sample); - memcpy_to_native_endian(entry.dst_plane + dst_offset + dst_sample_offset, val, entry.bytes_per_component_sample); -} - -// Handles the case where a row consists of a single component type -// Not valid for Pixel interleave -// Not valid for the Cb/Cr channels in Mixed Interleave -// Not valid for multi-Y pixel interleave -void AbstractDecoder::processComponentTileRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_offset) -{ - for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { - if (entry.component_alignment != 0) { - srcBits.skip_to_byte_boundary(); - int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; - srcBits.skip_bits(numPadBits); - } - processComponentTileSample(srcBits, entry, dst_offset, tile_x); - } - srcBits.skip_to_byte_boundary(); -} - - -AbstractDecoder::ChannelListEntry AbstractDecoder::buildChannelListEntry(Box_uncC::Component component, - std::shared_ptr& img) -{ - ChannelListEntry entry; - entry.use_channel = map_uncompressed_component_to_channel(m_cmpd, m_uncC, component, &(entry.channel)); - entry.dst_plane = img->get_plane(entry.channel, &(entry.dst_plane_stride)); - entry.tile_width = m_tile_width; - entry.tile_height = m_tile_height; - entry.other_chroma_dst_plane_stride = 0; // will be overwritten below if used - if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) { - if (m_uncC->get_sampling_type() == sampling_mode_422) { - entry.tile_width /= 2; - } - else if (m_uncC->get_sampling_type() == sampling_mode_420) { - entry.tile_width /= 2; - entry.tile_height /= 2; - } - if (entry.channel == heif_channel_Cb) { - entry.other_chroma_dst_plane = img->get_plane(heif_channel_Cr, &(entry.other_chroma_dst_plane_stride)); - } - else if (entry.channel == heif_channel_Cr) { - entry.other_chroma_dst_plane = img->get_plane(heif_channel_Cb, &(entry.other_chroma_dst_plane_stride)); - } - } - entry.bits_per_component_sample = component.component_bit_depth; - entry.component_alignment = component.component_align_size; - entry.bytes_per_component_sample = (component.component_bit_depth + 7) / 8; - entry.bytes_per_tile_row_src = entry.tile_width * entry.bytes_per_component_sample; - return entry; -} - - -// generic compression and uncompressed, per 23001-17 -const Error AbstractDecoder::get_compressed_image_data_uncompressed(const HeifContext* context, heif_item_id ID, - std::vector* data, - uint64_t range_start_offset, uint64_t range_size, - uint32_t tile_idx, - const Box_iloc::Item* item) const -{ - auto image = context->get_image(ID, false); - if (!image) { - return {heif_error_Invalid_input, - heif_suberror_Nonexisting_item_referenced}; - } - - // --- get codec configuration - - std::shared_ptr cmpC_box = image->get_property(); - std::shared_ptr icef_box = image->get_property(); - - if (!cmpC_box) { - // assume no generic compression - return context->get_heif_file()->append_data_from_iloc(ID, *data, range_start_offset, range_size); - } - - if (icef_box && cmpC_box->get_compressed_unit_type() == heif_cmpC_compressed_unit_type_image_tile) { - const auto& units = icef_box->get_units(); - if (tile_idx >= units.size()) { - return {heif_error_Invalid_input, - heif_suberror_Unspecified, - "no icef-box entry for tile index"}; - } - - const auto unit = units[tile_idx]; - - // get all data and decode all - std::vector compressed_bytes; - Error err = context->get_heif_file()->append_data_from_iloc(ID, compressed_bytes, unit.unit_offset, unit.unit_size); - if (err) { - return err; - } - - // decompress only the unit - err = do_decompress_data(cmpC_box, compressed_bytes, data); - if (err) { - return err; - } - } - else if (icef_box) { - // get all data and decode all - std::vector compressed_bytes; - Error err = context->get_heif_file()->append_data_from_iloc(ID, compressed_bytes); // image_id, src_data, tile_start_offset, total_tile_size); - if (err) { - return err; - } - - for (Box_icef::CompressedUnitInfo unit_info : icef_box->get_units()) { - auto unit_start = compressed_bytes.begin() + unit_info.unit_offset; - auto unit_end = unit_start + unit_info.unit_size; - std::vector compressed_unit_data = std::vector(unit_start, unit_end); - std::vector uncompressed_unit_data; - err = do_decompress_data(cmpC_box, std::move(compressed_unit_data), &uncompressed_unit_data); - if (err) { - return err; - } - data->insert(data->end(), uncompressed_unit_data.data(), uncompressed_unit_data.data() + uncompressed_unit_data.size()); - } - - // cut out the range that we actually need - memcpy(data->data(), data->data() + range_start_offset, range_size); - data->resize(range_size); - } - else { - // get all data and decode all - std::vector compressed_bytes; - Error err = context->get_heif_file()->append_data_from_iloc(ID, compressed_bytes); // image_id, src_data, tile_start_offset, total_tile_size); - if (err) { - return err; - } - - // Decode as a single blob - err = do_decompress_data(cmpC_box, compressed_bytes, data); - if (err) { - return err; - } - - // cut out the range that we actually need - memcpy(data->data(), data->data() + range_start_offset, range_size); - data->resize(range_size); - } - - return Error::Ok; -} - -const Error AbstractDecoder::do_decompress_data(std::shared_ptr& cmpC_box, - std::vector compressed_data, - std::vector* data) const -{ - if (cmpC_box->get_compression_type() == fourcc("brot")) { -#if HAVE_BROTLI - return decompress_brotli(compressed_data, data); -#else - std::stringstream sstr; - sstr << "cannot decode unci item with brotli compression - not enabled" << std::endl; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_generic_compression_method, - sstr.str()); -#endif - } - else if (cmpC_box->get_compression_type() == fourcc("zlib")) { -#if HAVE_ZLIB - return decompress_zlib(compressed_data, data); -#else - std::stringstream sstr; - sstr << "cannot decode unci item with zlib compression - not enabled" << std::endl; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_generic_compression_method, - sstr.str()); -#endif - } - else if (cmpC_box->get_compression_type() == fourcc("defl")) { -#if HAVE_ZLIB - return decompress_deflate(compressed_data, data); -#else - std::stringstream sstr; - sstr << "cannot decode unci item with deflate compression - not enabled" << std::endl; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_generic_compression_method, - sstr.str()); -#endif - } - else { - std::stringstream sstr; - sstr << "cannot decode unci item with unsupported compression type: " << cmpC_box->get_compression_type() << std::endl; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_generic_compression_method, - sstr.str()); - } -} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_abstract.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_abstract.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_abstract.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,211 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_ABSTRACT_H -#define UNCI_DECODER_ABSTRACT_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common_utils.h" -#include "context.h" -#include "compression.h" -#include "error.h" -#include "libheif/heif.h" -#include "unc_types.h" -#include "unc_boxes.h" -#include "unc_codec.h" - - -class UncompressedBitReader : public BitReader -{ -public: - UncompressedBitReader(const std::vector& data) : BitReader(data.data(), (int) data.size()) {} - - void markPixelStart() - { - m_pixelStartOffset = get_current_byte_index(); - } - - void markRowStart() - { - m_rowStartOffset = get_current_byte_index(); - } - - void markTileStart() - { - m_tileStartOffset = get_current_byte_index(); - } - - inline void handlePixelAlignment(uint32_t pixel_size) - { - if (pixel_size != 0) { - uint32_t bytes_in_pixel = get_current_byte_index() - m_pixelStartOffset; - uint32_t padding = pixel_size - bytes_in_pixel; - skip_bytes(padding); - } - } - - void handleRowAlignment(uint32_t alignment) - { - skip_to_byte_boundary(); - if (alignment != 0) { - uint32_t bytes_in_row = get_current_byte_index() - m_rowStartOffset; - uint32_t residual = bytes_in_row % alignment; - if (residual != 0) { - uint32_t padding = alignment - residual; - skip_bytes(padding); - } - } - } - - void handleTileAlignment(uint32_t alignment) - { - if (alignment != 0) { - uint32_t bytes_in_tile = get_current_byte_index() - m_tileStartOffset; - uint32_t residual = bytes_in_tile % alignment; - if (residual != 0) { - uint32_t tile_padding = alignment - residual; - skip_bytes(tile_padding); - } - } - } - -private: - int m_pixelStartOffset = 0; - int m_rowStartOffset = 0; - int m_tileStartOffset = 0; -}; - - -template void skip_to_alignment(T& position, uint32_t alignment) -{ - if (alignment == 0) { - return; - } - - T residual = position % alignment; - if (residual == 0) { - return; - } - - position += alignment - residual; -} - - -class AbstractDecoder -{ -public: - virtual ~AbstractDecoder() = default; - - virtual Error decode_tile(const HeifContext* context, - heif_item_id item_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) = 0; - - void buildChannelList(std::shared_ptr& img); - -protected: - AbstractDecoder(uint32_t width, uint32_t height, - const std::shared_ptr cmpd, - const std::shared_ptr uncC); - - const uint32_t m_width; - const uint32_t m_height; - const std::shared_ptr m_cmpd; - const std::shared_ptr m_uncC; - // TODO: see if we can make this const - uint32_t m_tile_height; - uint32_t m_tile_width; - - class ChannelListEntry - { - public: - uint32_t get_bytes_per_tile() const - { - return bytes_per_tile_row_src * tile_height; - } - - inline uint64_t getDestinationRowOffset(uint32_t tile_row, uint32_t tile_y) const - { - uint64_t dst_row_number = uint64_t{tile_row} * tile_height + tile_y; - return dst_row_number * dst_plane_stride; - } - - heif_channel channel = heif_channel_Y; - uint8_t* dst_plane = nullptr; - uint8_t* other_chroma_dst_plane = nullptr; - size_t dst_plane_stride; - size_t other_chroma_dst_plane_stride; - uint32_t tile_width; - uint32_t tile_height; - uint32_t bytes_per_component_sample; - uint16_t bits_per_component_sample; - uint8_t component_alignment; - uint32_t bytes_per_tile_row_src; - bool use_channel; - }; - - std::vector channelList; - - void processComponentSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_row_offset, uint32_t tile_column, uint32_t tile_x); - - // Handles the case where a row consists of a single component type - // Not valid for Pixel interleave - // Not valid for the Cb/Cr channels in Mixed Interleave - // Not valid for multi-Y pixel interleave - void processComponentRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_row_offset, uint32_t tile_column); - - void processComponentTileSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_offset, uint32_t tile_x); - - // Handles the case where a row consists of a single component type - // Not valid for Pixel interleave - // Not valid for the Cb/Cr channels in Mixed Interleave - // Not valid for multi-Y pixel interleave - void processComponentTileRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_offset); - - // generic compression and uncompressed, per 23001-17 - const Error get_compressed_image_data_uncompressed(const HeifContext* context, heif_item_id ID, - std::vector* data, - uint64_t range_start_offset, uint64_t range_size, - uint32_t tile_idx, - const Box_iloc::Item* item) const; - - const Error do_decompress_data(std::shared_ptr& cmpC_box, - std::vector compressed_data, - std::vector* data) const; - -protected: - void memcpy_to_native_endian(uint8_t* dst, uint32_t value, uint32_t bytes_per_sample); - -private: - ChannelListEntry buildChannelListEntry(Box_uncC::Component component, std::shared_ptr& img); -}; - -#endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_component_interleave.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,96 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#include "decoder_component_interleave.h" -#include "context.h" -#include "error.h" - -#include -#include - - -Error ComponentInterleaveDecoder::decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) -{ - if (m_tile_width == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: ComponentInterleaveDecoder tile_width=0"}; - } - - // --- compute which file range we need to read for the tile - - uint64_t total_tile_size = 0; - - for (ChannelListEntry& entry : channelList) { - uint32_t bits_per_component = entry.bits_per_component_sample; - if (entry.component_alignment > 0) { - uint32_t bytes_per_component = (bits_per_component + 7) / 8; - skip_to_alignment(bytes_per_component, entry.component_alignment); - bits_per_component = bytes_per_component * 8; - } - - uint32_t bytes_per_tile_row = (bits_per_component * entry.tile_width + 7) / 8; - skip_to_alignment(bytes_per_tile_row, m_uncC->get_row_align_size()); - uint64_t bytes_per_tile = uint64_t{bytes_per_tile_row} * entry.tile_height; - total_tile_size += bytes_per_tile; - } - - if (m_uncC->get_tile_align_size() != 0) { - skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); - } - - assert(m_tile_width > 0); - uint32_t tileIdx = tile_x + tile_y * (image_width / m_tile_width); - uint64_t tile_start_offset = total_tile_size * tileIdx; - - - // --- read required file range - - std::vector src_data; - //Error err = context->get_heif_file()->append_data_from_iloc(image_id, src_data, tile_start_offset, total_tile_size); - Error err = get_compressed_image_data_uncompressed(context, image_id, &src_data, tile_start_offset, total_tile_size, tileIdx, nullptr); - if (err) { - return err; - } - - UncompressedBitReader srcBits(src_data); - - - // --- decode tile - - for (ChannelListEntry& entry : channelList) { - for (uint32_t y = 0; y < entry.tile_height; y++) { - srcBits.markRowStart(); - if (entry.use_channel) { - uint64_t dst_row_offset = uint64_t{(out_y0 + y)} * entry.dst_plane_stride; - processComponentTileRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample); - } - else { - srcBits.skip_bytes(entry.bytes_per_tile_row_src); - } - srcBits.handleRowAlignment(m_uncC->get_row_align_size()); - } - } - - return Error::Ok; -} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_component_interleave.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_COMPONENT_INTERLEAVE_H -#define UNCI_DECODER_COMPONENT_INTERLEAVE_H - -#include "decoder_abstract.h" -#include -#include - - -class ComponentInterleaveDecoder : public AbstractDecoder -{ -public: - ComponentInterleaveDecoder(uint32_t width, uint32_t height, - std::shared_ptr cmpd, - std::shared_ptr uncC) : - AbstractDecoder(width, height, std::move(cmpd), std::move(uncC)) {} - - Error decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) override; -}; - -#endif // UNCI_DECODER_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_mixed_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_mixed_interleave.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,130 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#include "decoder_mixed_interleave.h" -#include "context.h" -#include "error.h" - -#include -#include -#include - - -Error MixedInterleaveDecoder::decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) -{ - if (m_tile_width == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: MixedInterleaveDecoder tile_width=0"}; - } - - // --- compute which file range we need to read for the tile - - uint64_t tile_size = 0; - - for (ChannelListEntry& entry : channelList) { - if (entry.channel == heif_channel_Cb || entry.channel == heif_channel_Cr) { - uint32_t bits_per_row = entry.bits_per_component_sample * entry.tile_width; - bits_per_row = (bits_per_row + 7) & ~7U; // align to byte boundary - - tile_size += uint64_t{bits_per_row} / 8 * entry.tile_height; - } - else { - uint32_t bits_per_component = entry.bits_per_component_sample; - if (entry.component_alignment > 0) { - uint32_t bytes_per_component = (bits_per_component + 7) / 8; - skip_to_alignment(bytes_per_component, entry.component_alignment); - bits_per_component = bytes_per_component * 8; - } - - uint32_t bits_per_row = bits_per_component * entry.tile_width; - bits_per_row = (bits_per_row + 7) & ~7U; // align to byte boundary - - tile_size += uint64_t{bits_per_row} / 8 * entry.tile_height; - } - } - - - if (m_uncC->get_tile_align_size() != 0) { - skip_to_alignment(tile_size, m_uncC->get_tile_align_size()); - } - - assert(m_tile_width > 0); - uint32_t tileIdx = tile_x + tile_y * (image_width / m_tile_width); - uint64_t tile_start_offset = tile_size * tileIdx; - - - // --- read required file range - - std::vector src_data; - Error err = get_compressed_image_data_uncompressed(context, image_id, &src_data, tile_start_offset, tile_size, tileIdx, nullptr); - //Error err = context->get_heif_file()->append_data_from_iloc(image_id, src_data, tile_start_offset, tile_size); - if (err) { - return err; - } - - UncompressedBitReader srcBits(src_data); - - processTile(srcBits, tile_y, tile_x, out_x0, out_y0); - - return Error::Ok; -} - - -void MixedInterleaveDecoder::processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, uint32_t out_x0, uint32_t out_y0) -{ - bool haveProcessedChromaForThisTile = false; - for (ChannelListEntry& entry : channelList) { - if (entry.use_channel) { - if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) { - if (!haveProcessedChromaForThisTile) { - for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { - // TODO: row padding - uint64_t dst_row_number = tile_y + out_y0; - uint64_t dst_row_offset = dst_row_number * entry.dst_plane_stride; - for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { - uint64_t dst_column_number = out_x0 + tile_x; - uint64_t dst_column_offset = dst_column_number * entry.bytes_per_component_sample; - int val = srcBits.get_bits(entry.bytes_per_component_sample * 8); - memcpy_to_native_endian(entry.dst_plane + dst_row_offset + dst_column_offset, val, entry.bytes_per_component_sample); - val = srcBits.get_bits(entry.bytes_per_component_sample * 8); - memcpy_to_native_endian(entry.other_chroma_dst_plane + dst_row_offset + dst_column_offset, val, entry.bytes_per_component_sample); - } - haveProcessedChromaForThisTile = true; - } - } - } - else { - for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { - uint64_t dst_row_offset = entry.getDestinationRowOffset(tile_row, tile_y); - processComponentRow(entry, srcBits, dst_row_offset, tile_column); - } - } - } - else { - // skip over the data we are not using - srcBits.skip_bytes(entry.get_bytes_per_tile()); - continue; - } - } -} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_mixed_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_mixed_interleave.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_mixed_interleave.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_MIXED_INTERLEAVE_H -#define UNCI_DECODER_MIXED_INTERLEAVE_H - -#include "decoder_abstract.h" -#include -#include - - -class MixedInterleaveDecoder : public AbstractDecoder -{ -public: - MixedInterleaveDecoder(uint32_t width, uint32_t height, std::shared_ptr cmpd, std::shared_ptr uncC) : - AbstractDecoder(width, height, std::move(cmpd), std::move(uncC)) {} - - Error decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) override; - - void processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, - uint32_t out_x0, uint32_t out_y0); -}; - -#endif // UNCI_DECODER_MIXED_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_pixel_interleave.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,122 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#include "decoder_pixel_interleave.h" -#include "context.h" -#include "error.h" - -#include -#include - - -Error PixelInterleaveDecoder::decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) -{ - if (m_tile_width == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: PixelInterleaveDecoder tile_width=0"}; - } - - // --- compute which file range we need to read for the tile - - uint32_t bits_per_row = 0; - for (uint32_t x = 0; x < m_tile_width; x++) { - uint32_t bits_per_pixel = 0; - - for (ChannelListEntry& entry : channelList) { - uint32_t bits_per_component = entry.bits_per_component_sample; - if (entry.component_alignment > 0) { - // start at byte boundary - bits_per_row = (bits_per_row + 7) & ~7U; - - uint32_t bytes_per_component = (bits_per_component + 7) / 8; - skip_to_alignment(bytes_per_component, entry.component_alignment); - bits_per_component = bytes_per_component * 8; - } - - bits_per_pixel += bits_per_component; - } - - if (m_uncC->get_pixel_size() != 0) { - uint32_t bytes_per_pixel = (bits_per_pixel + 7) / 8; - skip_to_alignment(bytes_per_pixel, m_uncC->get_pixel_size()); - bits_per_pixel = bytes_per_pixel * 8; - } - - bits_per_row += bits_per_pixel; - } - - uint32_t bytes_per_row = (bits_per_row + 7) / 8; - skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); - - uint64_t total_tile_size = bytes_per_row * static_cast(m_tile_height); - if (m_uncC->get_tile_align_size() != 0) { - skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); - } - - assert(m_tile_width > 0); - uint32_t tileIdx = tile_x + tile_y * (image_width / m_tile_width); - uint64_t tile_start_offset = total_tile_size * tileIdx; - - - // --- read required file range - - std::vector src_data; - Error err = get_compressed_image_data_uncompressed(context, image_id, &src_data, tile_start_offset, total_tile_size, tileIdx, nullptr); - //Error err = context->get_heif_file()->append_data_from_iloc(image_id, src_data, tile_start_offset, total_tile_size); - if (err) { - return err; - } - - UncompressedBitReader srcBits(src_data); - - processTile(srcBits, tile_y, tile_x, out_x0, out_y0); - - return Error::Ok; -} - -void PixelInterleaveDecoder::processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, uint32_t out_x0, uint32_t out_y0) -{ - for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { - srcBits.markRowStart(); - for (uint32_t tile_x = 0; tile_x < m_tile_width; tile_x++) { - srcBits.markPixelStart(); - for (ChannelListEntry& entry : channelList) { - if (entry.use_channel) { - uint64_t dst_row_offset = entry.getDestinationRowOffset(0, tile_y + out_y0); - if (entry.component_alignment != 0) { - srcBits.skip_to_byte_boundary(); - int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; - srcBits.skip_bits(numPadBits); - } - processComponentSample(srcBits, entry, dst_row_offset, 0, out_x0 + tile_x); - } - else { - srcBits.skip_bytes(entry.bytes_per_component_sample); - } - } - srcBits.handlePixelAlignment(m_uncC->get_pixel_size()); - } - srcBits.handleRowAlignment(m_uncC->get_row_align_size()); - } -} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_pixel_interleave.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_PIXEL_INTERLEAVE_H -#define UNCI_DECODER_PIXEL_INTERLEAVE_H - -#include -#include -#include -#include -#include -#include -#include - -#include "common_utils.h" -#include "context.h" -#include "compression.h" -#include "error.h" -#include "libheif/heif.h" -#include "unc_types.h" -#include "unc_boxes.h" -#include "unc_codec.h" -#include "unc_dec.h" - -#include "decoder_abstract.h" -#include "decoder_component_interleave.h" -#include -#include - - -class PixelInterleaveDecoder : public AbstractDecoder -{ -public: - PixelInterleaveDecoder(uint32_t width, uint32_t height, std::shared_ptr cmpd, std::shared_ptr uncC) : - AbstractDecoder(width, height, std::move(cmpd), std::move(uncC)) {} - - Error decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) override; - - void processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, - uint32_t out_x0, uint32_t out_y0); -}; - -#endif // UNCI_DECODER_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_row_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_row_interleave.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#include "decoder_row_interleave.h" -#include "context.h" -#include "error.h" - -#include -#include - - -Error RowInterleaveDecoder::decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) -{ - if (m_tile_width == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: RowInterleaveDecoder tile_width=0"}; - } - - // --- compute which file range we need to read for the tile - - uint32_t bits_per_row = 0; - for (ChannelListEntry& entry : channelList) { - uint32_t bits_per_component = entry.bits_per_component_sample; - if (entry.component_alignment > 0) { - // start at byte boundary - bits_per_row = (bits_per_row + 7) & ~7U; - - uint32_t bytes_per_component = (bits_per_component + 7) / 8; - skip_to_alignment(bytes_per_component, entry.component_alignment); - bits_per_component = bytes_per_component * 8; - } - - if (m_uncC->get_row_align_size() != 0) { - uint32_t bytes_this_row = (bits_per_component * m_tile_width + 7) / 8; - skip_to_alignment(bytes_this_row, m_uncC->get_row_align_size()); - bits_per_row += bytes_this_row * 8; - } - else { - bits_per_row += bits_per_component * m_tile_width; - } - - bits_per_row = (bits_per_row + 7) & ~7U; - } - - uint32_t bytes_per_row = (bits_per_row + 7) / 8; - if (m_uncC->get_row_align_size()) { - skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); - } - - uint64_t total_tile_size = 0; - total_tile_size += bytes_per_row * static_cast(m_tile_height); - - if (m_uncC->get_tile_align_size() != 0) { - skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); - } - - assert(m_tile_width > 0); - uint32_t tileIdx = tile_x + tile_y * (image_width / m_tile_width); - uint64_t tile_start_offset = total_tile_size * tileIdx; - - - // --- read required file range - - std::vector src_data; - Error err = get_compressed_image_data_uncompressed(context, image_id, &src_data, tile_start_offset, total_tile_size, tileIdx, nullptr); - //Error err = context->get_heif_file()->append_data_from_iloc(image_id, src_data, tile_start_offset, total_tile_size); - if (err) { - return err; - } - - UncompressedBitReader srcBits(src_data); - - processTile(srcBits, tile_y, tile_x, out_x0, out_y0); - - return Error::Ok; -} - -void RowInterleaveDecoder::processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, uint32_t out_x0, uint32_t out_y0) -{ - for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { - for (ChannelListEntry& entry : channelList) { - srcBits.markRowStart(); - if (entry.use_channel) { - uint64_t dst_row_offset = entry.getDestinationRowOffset(0, tile_y + out_y0); - processComponentRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample, 0); - } - else { - srcBits.skip_bytes(entry.bytes_per_tile_row_src); - } - srcBits.handleRowAlignment(m_uncC->get_row_align_size()); - } - } -} - diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_row_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_row_interleave.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_row_interleave.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_ROW_INTERLEAVE_H -#define UNCI_DECODER_ROW_INTERLEAVE_H - -#include "decoder_abstract.h" -#include -#include - - -class RowInterleaveDecoder : public AbstractDecoder -{ -public: - RowInterleaveDecoder(uint32_t width, uint32_t height, - std::shared_ptr cmpd, std::shared_ptr uncC) : - AbstractDecoder(width, height, std::move(cmpd), std::move(uncC)) {} - - Error decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_x, uint32_t tile_y) override; - -private: - void processTile(UncompressedBitReader& srcBits, uint32_t tile_row, uint32_t tile_column, - uint32_t out_x0, uint32_t out_y0); -}; - -#endif // UNCI_DECODER_ROW_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_tile_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_tile_component_interleave.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - - -#include "decoder_tile_component_interleave.h" -#include "context.h" -#include "error.h" - -#include -#include -#include - - -Error TileComponentInterleaveDecoder::decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_column, uint32_t tile_row) -{ - if (m_tile_width == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: TileComponentInterleaveDecoder tile_width=0"}; - } - if (m_tile_height == 0) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: TileComponentInterleaveDecoder tile_height=0"}; - } - - // --- compute which file range we need to read for the tile - - std::map channel_tile_size; - - //uint64_t total_tile_size = 0; - - for (ChannelListEntry& entry : channelList) { - uint32_t bits_per_pixel = entry.bits_per_component_sample; - if (entry.component_alignment > 0) { - // start at byte boundary - //bits_per_row = (bits_per_row + 7) & ~7U; - - uint32_t bytes_per_component = (bits_per_pixel + 7) / 8; - skip_to_alignment(bytes_per_component, entry.component_alignment); - bits_per_pixel = bytes_per_component * 8; - } - - uint32_t bytes_per_row; - if (m_uncC->get_pixel_size() != 0) { // TODO: does pixel_size apply here? - uint32_t bytes_per_pixel = (bits_per_pixel + 7) / 8; - skip_to_alignment(bytes_per_pixel, m_uncC->get_pixel_size()); - bytes_per_row = bytes_per_pixel * m_tile_width; - } - else { - bytes_per_row = (bits_per_pixel * m_tile_width + 7) / 8; - } - - skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); - - uint64_t component_tile_size = bytes_per_row * static_cast(m_tile_height); - - if (m_uncC->get_tile_align_size() != 0) { - skip_to_alignment(component_tile_size, m_uncC->get_tile_align_size()); - } - - channel_tile_size[entry.channel] = component_tile_size; - - //total_tile_size += component_tile_size; - } - - uint64_t component_start_offset = 0; - - assert(m_tile_width > 0); - assert(m_tile_height > 0); - - for (ChannelListEntry& entry : channelList) { - //processTile(srcBits, tile_y, tile_x, out_x0, out_y0); - - if (!entry.use_channel) { - //uint64_t bytes_per_component = entry.get_bytes_per_tile() * m_uncC->get_number_of_tile_columns() * m_uncC->get_number_of_tile_rows(); - //srcBits.skip_bytes((int)bytes_per_component); - - component_start_offset += channel_tile_size[entry.channel] * (m_width / m_tile_width) * (m_height / m_tile_height); - continue; - } - - // --- read required file range - - uint32_t tileIdx = tile_column + tile_row * (image_width / m_tile_width); - uint64_t tile_start_offset = component_start_offset + channel_tile_size[entry.channel] * tileIdx; - - std::vector src_data; - Error err = get_compressed_image_data_uncompressed(context, image_id, &src_data, tile_start_offset, channel_tile_size[entry.channel], tileIdx, nullptr); - //Error err = context->get_heif_file()->append_data_from_iloc(image_id, src_data, tile_start_offset, channel_tile_size[entry.channel]); - if (err) { - return err; - } - - UncompressedBitReader srcBits(src_data); - - srcBits.markTileStart(); - for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { - srcBits.markRowStart(); - uint64_t dst_row_offset = entry.getDestinationRowOffset(0, tile_y + out_y0); - processComponentRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample, 0); - srcBits.handleRowAlignment(m_uncC->get_row_align_size()); - } - srcBits.handleTileAlignment(m_uncC->get_tile_align_size()); - - - component_start_offset += channel_tile_size[entry.channel] * (m_width / m_tile_width) * (m_height / m_tile_height); - } - - return Error::Ok; -} - - diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/decoder_tile_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/decoder_tile_component_interleave.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/decoder_tile_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,44 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2023 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . - */ - -#ifndef UNCI_DECODER_TILE_COMPONENT_INTERLEAVE_H -#define UNCI_DECODER_TILE_COMPONENT_INTERLEAVE_H - -#include "decoder_abstract.h" -#include -#include - - -class TileComponentInterleaveDecoder : public AbstractDecoder -{ -public: - TileComponentInterleaveDecoder(uint32_t width, uint32_t height, - std::shared_ptr cmpd, std::shared_ptr uncC) : - AbstractDecoder(width, height, std::move(cmpd), std::move(uncC)) {} - - Error decode_tile(const HeifContext* context, - heif_item_id image_id, - std::shared_ptr& img, - uint32_t out_x0, uint32_t out_y0, - uint32_t image_width, uint32_t image_height, - uint32_t tile_column, uint32_t tile_row) override; -}; - -#endif // UNCI_DECODER_TILE_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_boxes.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -46,6 +46,7 @@ static std::map sNames_uncompressed_component_format{ {component_format_unsigned, "unsigned"}, + {component_format_signed, "signed"}, {component_format_float, "float"}, {component_format_complex, "complex"} }; @@ -95,27 +96,27 @@ { // check whether the component type can be mapped to heif_uncompressed_component_type and we have a name defined for // it in sNames_uncompressed_component_type. - return type <= component_type_max_valid; + return type <= heif_cmpd_component_type_max_valid; } -static std::map sNames_uncompressed_component_type{ - {component_type_monochrome, "monochrome"}, - {component_type_Y, "Y"}, - {component_type_Cb, "Cb"}, - {component_type_Cr, "Cr"}, - {component_type_red, "red"}, - {component_type_green, "green"}, - {component_type_blue, "blue"}, - {component_type_alpha, "alpha"}, - {component_type_depth, "depth"}, - {component_type_disparity, "disparity"}, - {component_type_palette, "palette"}, - {component_type_filter_array, "filter-array"}, - {component_type_padded, "padded"}, - {component_type_cyan, "cyan"}, - {component_type_magenta, "magenta"}, - {component_type_yellow, "yellow"}, - {component_type_key_black, "key (black)"} +static std::map sNames_uncompressed_component_type{ + {heif_cmpd_component_type_monochrome, "monochrome"}, + {heif_cmpd_component_type_Y, "Y"}, + {heif_cmpd_component_type_Cb, "Cb"}, + {heif_cmpd_component_type_Cr, "Cr"}, + {heif_cmpd_component_type_red, "red"}, + {heif_cmpd_component_type_green, "green"}, + {heif_cmpd_component_type_blue, "blue"}, + {heif_cmpd_component_type_alpha, "alpha"}, + {heif_cmpd_component_type_depth, "depth"}, + {heif_cmpd_component_type_disparity, "disparity"}, + {heif_cmpd_component_type_palette, "palette"}, + {heif_cmpd_component_type_filter_array, "filter-array"}, + {heif_cmpd_component_type_padded, "padded"}, + {heif_cmpd_component_type_cyan, "cyan"}, + {heif_cmpd_component_type_magenta, "magenta"}, + {heif_cmpd_component_type_yellow, "yellow"}, + {heif_cmpd_component_type_key_black, "key (black)"} }; template const char* get_name(T val, const std::map& table) @@ -129,13 +130,23 @@ } } +void Box_cmpd::set_components(const std::vector& components) +{ + m_components.clear(); + + for (const auto& component : components) { + m_components.push_back({component, {}}); + } +} + + Error Box_cmpd::parse(BitstreamRange& range, const heif_security_limits* limits) { uint32_t component_count = range.read32(); if (limits->max_components && component_count > limits->max_components) { std::stringstream sstr; - sstr << "cmpd box should countain " << component_count << " components, but security limit is set to " + sstr << "cmpd box should contain " << component_count << " components, but security limit is set to " << limits->max_components << " components"; return {heif_error_Invalid_input, @@ -147,7 +158,7 @@ for (unsigned int i = 0; i < component_count ; i++) { if (range.eof()) { std::stringstream sstr; - sstr << "cmpd box should countain " << component_count << " components, but box only contained " + sstr << "cmpd box should contain " << component_count << " components, but box only contained " << i << " components"; return {heif_error_Invalid_input, @@ -175,7 +186,7 @@ std::stringstream sstr; if (is_predefined_component_type(component_type)) { - sstr << get_name(heif_uncompressed_component_type(component_type), sNames_uncompressed_component_type) << "\n"; + sstr << get_name(heif_cmpd_component_type(component_type), sNames_uncompressed_component_type) << "\n"; } else { sstr << "0x" << std::hex << component_type << std::dec << "\n"; @@ -185,6 +196,13 @@ } +bool Box_cmpd::has_component(heif_cmpd_component_type type) const +{ + return std::any_of(m_components.begin(), m_components.end(), + [type](const auto& cmp) { return cmp.component_type == type; }); +} + + std::string Box_cmpd::dump(Indent& indent) const { std::ostringstream sstr; @@ -225,26 +243,29 @@ m_profile = range.read32(); if (get_version() == 1) { - if (m_profile == fourcc("rgb3")) { - Box_uncC::Component component0 = {0, 8, component_format_unsigned, 0}; - add_component(component0); - Box_uncC::Component component1 = {1, 8, component_format_unsigned, 0}; - add_component(component1); - Box_uncC::Component component2 = {2, 8, component_format_unsigned, 0}; - add_component(component2); - } - else if ((m_profile == fourcc("rgba")) || (m_profile == fourcc("abgr"))) { - Box_uncC::Component component0 = {0, 8, component_format_unsigned, 0}; - add_component(component0); - Box_uncC::Component component1 = {1, 8, component_format_unsigned, 0}; - add_component(component1); - Box_uncC::Component component2 = {2, 8, component_format_unsigned, 0}; - add_component(component2); - Box_uncC::Component component3 = {3, 8, component_format_unsigned, 0}; - add_component(component3); - } - else { - return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "Invalid component format"}; + switch (m_profile) { + case fourcc("rgb3"): + case fourcc("rgba"): + case fourcc("abgr"): + case fourcc("2vuy"): + case fourcc("yuv2"): + case fourcc("yvyu"): + case fourcc("vyuy"): + case fourcc("yuv1"): + case fourcc("v308"): + case fourcc("v408"): + case fourcc("y210"): + case fourcc("v410"): + case fourcc("v210"): + case fourcc("i420"): + case fourcc("nv12"): + case fourcc("nv21"): + case fourcc("yu22"): + case fourcc("yv22"): + case fourcc("yv20"): + break; + default: + return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "Unknown uncC v1 profile"}; } } else if (get_version() == 0) { @@ -271,6 +292,17 @@ if (!is_valid_component_format(component.component_format)) { return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "Invalid component format"}; } + + // When component_align_size != 0, the component is padded up to that many bytes. + // It therefore must be large enough to hold the component's bit depth; otherwise + // the decoder computes a negative pad-bits count and shifts by a negative amount. + if (component.component_align_size != 0 && + uint32_t(component.component_align_size) * 8 < component.component_bit_depth) { + std::stringstream sstr; + sstr << "Component alignment (" << int(component.component_align_size) + << " bytes) is too small for component bit depth (" << component.component_bit_depth << " bits)"; + return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, sstr.str()}; + } } m_sampling_type = range.read8(); @@ -294,6 +326,17 @@ m_pixel_size = range.read32(); + if (limits->version >= 4 && + limits->max_iso23001_17_pixel_size_bytes != 0 && + m_pixel_size > limits->max_iso23001_17_pixel_size_bytes) { + std::stringstream sstr; + sstr << "uncC pixel_size (" << m_pixel_size << " bytes) exceeds security limit of " + << limits->max_iso23001_17_pixel_size_bytes << " bytes"; + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + m_row_align_size = range.read32(); m_tile_align_size = range.read32(); @@ -301,6 +344,16 @@ uint32_t num_tile_cols_minus_one = range.read32(); uint32_t num_tile_rows_minus_one = range.read32(); + // The field is stored as `count - 1`, so 0xFFFFFFFF would mean 2^32 tiles, + // which we cannot represent in our uint32 m_num_tile_cols/rows. Reject this + // before the security-limit check so that disabling the security limit does + // not silently turn this into a divide-by-zero in get_heif_image_tiling(). + if (num_tile_cols_minus_one == 0xFFFFFFFF || num_tile_rows_minus_one == 0xFFFFFFFF) { + return {heif_error_Unsupported_feature, + heif_suberror_Invalid_parameter_value, + "uncC num_tile_cols/rows_minus_one of 0xFFFFFFFF (2^32 tiles) exceeds the supported range"}; + } + if (limits->max_number_of_tiles && static_cast(num_tile_cols_minus_one) + 1 > limits->max_number_of_tiles / (static_cast(num_tile_rows_minus_one) + 1)) { std::stringstream sstr; @@ -380,7 +433,10 @@ return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "component bit-depth out of range [1..256]"}; } - writer.write16(component.component_index); + if (component.component_index > 0xFFFF) { + return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "component index must be 16 bit"}; + } + writer.write16(static_cast(component.component_index)); writer.write8(uint8_t(component.component_bit_depth - 1)); writer.write8(component.component_format); writer.write8(component.component_align_size); @@ -407,38 +463,286 @@ } -uint64_t Box_uncC::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const +void fill_uncC_and_cmpd_from_profile(const std::shared_ptr& uncC, + std::shared_ptr& cmpd) { - if (m_profile != 0) { - switch (m_profile) { - case fourcc("rgba"): - return 4 * uint64_t{tile_width} * tile_height; - - case fourcc("rgb3"): - return 3 * uint64_t{tile_width} * tile_height; - - default: - assert(false); - return 0; - } + if (uncC->get_version() != 1 || cmpd) { + return; } - switch (m_interleave_type) { - case interleave_mode_component: - case interleave_mode_pixel: { - uint32_t bytes_per_pixel = 0; - - for (const auto& comp : m_components) { - assert(comp.component_bit_depth % 8 == 0); // TODO: component sizes that are no multiples of bytes - bytes_per_pixel += comp.component_bit_depth / 8; - } - - return bytes_per_pixel * uint64_t{tile_width} * tile_height; - } - default: - assert(false); - return 0; + // Return cached synthetic cmpd if we already created one. + if (auto synthetic = uncC->get_synthetic_cmpd()) { + cmpd = synthetic; + return; + } + + uint32_t profile = uncC->get_profile(); + cmpd = std::make_shared(); + + // Profiles from ISO/IEC 23001-17 Table 5. + // Format: {profile, [{component_type, bit_depth_minus_1}, ...], sampling_type, interleave_type} + // The implicit cmpd is the unique component_types in order of first appearance. + // The uncC component_index refers into that cmpd. + + if (profile == fourcc("rgb3")) { + // {'rgb3', [{4,7},{5,7},{6,7}], 0, 1} + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_red}); + cmpd->add_component({heif_cmpd_component_type_green}); + cmpd->add_component({heif_cmpd_component_type_blue}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + } + else if (profile == fourcc("rgba")) { + // {'rgba', [{4,7},{5,7},{6,7},{7,7}], 0, 1} + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + uncC->add_component({3, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_red}); + cmpd->add_component({heif_cmpd_component_type_green}); + cmpd->add_component({heif_cmpd_component_type_blue}); + cmpd->add_component({heif_cmpd_component_type_alpha}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + } + else if (profile == fourcc("abgr")) { + // {'abgr', [{7,7},{6,7},{5,7},{4,7}], 0, 1} + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + uncC->add_component({3, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_alpha}); + cmpd->add_component({heif_cmpd_component_type_blue}); + cmpd->add_component({heif_cmpd_component_type_green}); + cmpd->add_component({heif_cmpd_component_type_red}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + } + else if (profile == fourcc("2vuy")) { + // {'2vuy', [{2,7},{1,7},{3,7},{1,7}], 1, 5} Cb Y0 Cr Y1 + // cmpd: Cb(0) Y(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + } + else if (profile == fourcc("yuv2")) { + // {'yuv2', [{1,7},{2,7},{1,7},{3,7}], 1, 5} Y0 Cb Y1 Cr + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + } + else if (profile == fourcc("yvyu")) { + // {'yvyu', [{1,7},{3,7},{1,7},{2,7}], 1, 5} Y0 Cr Y1 Cb + // cmpd: Y(0) Cr(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + } + else if (profile == fourcc("vyuy")) { + // {'vyuy', [{3,7},{1,7},{2,7},{1,7}], 1, 5} Cr Y0 Cb Y1 + // cmpd: Cr(0) Y(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + } + else if (profile == fourcc("yuv1")) { + // {'yuv1', [{1,7},{1,7},{2,7},{1,7},{1,7},{3,7}], 3, 5} Y0 Y1 Cb Y2 Y3 Cr + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_411); + uncC->set_interleave_type(interleave_mode_multi_y); + } + else if (profile == fourcc("v308")) { + // {'v308', [{3,7},{1,7},{2,7}], 0, 1} Cr Y Cb + // cmpd: Cr(0) Y(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + } + else if (profile == fourcc("v408")) { + // {'v408', [{2,7},{1,7},{3,7},{7,7}], 0, 1} Cb Y Cr A + // cmpd: Cb(0) Y(1) Cr(2) alpha(3) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + uncC->add_component({3, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_alpha}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + } + else if (profile == fourcc("y210")) { + // {'y210', [{1,9},{2,9},{1,9},{3,9}], 1, 5} Y0 Cb Y1 Cr + // block_size=2, block_little_endian=1, block_pad_lsb=1 + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 10, component_format_unsigned, 0}); + uncC->add_component({1, 10, component_format_unsigned, 0}); + uncC->add_component({0, 10, component_format_unsigned, 0}); + uncC->add_component({2, 10, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + uncC->set_block_size(2); + uncC->set_block_little_endian(true); + uncC->set_block_pad_lsb(true); + } + else if (profile == fourcc("v410")) { + // {'v410', [{2,9},{1,9},{3,9}], 0, 1} Cb Y Cr + // block_size=4, block_little_endian=1, block_pad_lsb=1, block_reversed=1 + // cmpd: Cb(0) Y(1) Cr(2) + uncC->add_component({0, 10, component_format_unsigned, 0}); + uncC->add_component({1, 10, component_format_unsigned, 0}); + uncC->add_component({2, 10, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_no_subsampling); + uncC->set_interleave_type(interleave_mode_pixel); + uncC->set_block_size(4); + uncC->set_block_little_endian(true); + uncC->set_block_pad_lsb(true); + uncC->set_block_reversed(true); + } + else if (profile == fourcc("v210")) { + // {'v210', [{2,9},{1,9},{3,9},{1,9}], 1, 5} Cb Y0 Cr Y1 + // block_size=4, block_little_endian=1, block_reversed=1 + // cmpd: Cb(0) Y(1) Cr(2) + uncC->add_component({0, 10, component_format_unsigned, 0}); + uncC->add_component({1, 10, component_format_unsigned, 0}); + uncC->add_component({2, 10, component_format_unsigned, 0}); + uncC->add_component({1, 10, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_multi_y); + uncC->set_block_size(4); + uncC->set_block_little_endian(true); + uncC->set_block_reversed(true); + } + else if (profile == fourcc("i420")) { + // {'i420', [{1,7},{2,7},{3,7}], 2, 0} planar YCbCr + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_420); + uncC->set_interleave_type(interleave_mode_component); + } + else if (profile == fourcc("nv12")) { + // {'nv12', [{1,7},{2,7},{3,7}], 2, 2} semi-planar YCbCr + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_420); + uncC->set_interleave_type(interleave_mode_mixed); + } + else if (profile == fourcc("nv21")) { + // {'nv21', [{1,7},{3,7},{2,7}], 2, 2} semi-planar YCrCb + // cmpd: Y(0) Cr(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_420); + uncC->set_interleave_type(interleave_mode_mixed); + } + else if (profile == fourcc("yu22")) { + // {'yu22', [{1,7},{2,7},{3,7}], 1, 0} planar YCbCr + // cmpd: Y(0) Cb(1) Cr(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_component); + } + else if (profile == fourcc("yv22")) { + // {'yv22', [{1,7},{3,7},{2,7}], 1, 0} planar YCrCb + // cmpd: Y(0) Cr(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_422); + uncC->set_interleave_type(interleave_mode_component); + } + else if (profile == fourcc("yv20")) { + // {'yv20', [{1,7},{3,7},{2,7}], 2, 0} planar YCrCb + // cmpd: Y(0) Cr(1) Cb(2) + uncC->add_component({0, 8, component_format_unsigned, 0}); + uncC->add_component({1, 8, component_format_unsigned, 0}); + uncC->add_component({2, 8, component_format_unsigned, 0}); + cmpd->add_component({heif_cmpd_component_type_Y}); + cmpd->add_component({heif_cmpd_component_type_Cr}); + cmpd->add_component({heif_cmpd_component_type_Cb}); + uncC->set_sampling_type(sampling_mode_420); + uncC->set_interleave_type(interleave_mode_component); + } + else { + cmpd.reset(); + return; } + + uncC->set_synthetic_cmpd(cmpd); } @@ -518,6 +822,15 @@ // --- check if box is large enough for all the data + // range.get_remaining_bytes() is bounded by the real file size: Box::read() rejects any box whose + // declared size exceeds either the bytes actually available in the file (wait_for_available_bytes()) + // or the parent box's remaining bytes. So this check already prevents an unbounded allocation: + // num_compressed_units cannot exceed the bytes present in the file. + // It is, however, not sufficient on its own. With unit_offset_code==0 and unit_size_code==0 a unit + // occupies only 1 byte in the file but allocates a 16-byte CompressedUnitInfo, a ~16x amplification + // that bypasses the configured memory budget. The alloc() below therefore additionally enforces the + // security limits (mirroring Box_snuc). + uint64_t data_size_bytes = static_cast(num_compressed_units) * (unit_offset_bits + unit_size_bits) / 8; if (data_size_bytes > range.get_remaining_bytes()) { uint64_t contained_units = range.get_remaining_bytes() / ((unit_offset_bits + unit_size_bits) * 8); @@ -529,14 +842,17 @@ sstr.str()}; } - // TODO: should we impose some security limit? - // --- read box content + if (auto err = m_memory_handle.alloc(num_compressed_units, sizeof(CompressedUnitInfo), + limits, "icef box compressed unit infos")) { + return err; + } + m_unit_infos.resize(num_compressed_units); for (uint32_t r = 0; r < num_compressed_units; r++) { - struct CompressedUnitInfo unitInfo; + CompressedUnitInfo unitInfo; if (unit_offset_code == 0) { unitInfo.unit_offset = implied_offset; } else { @@ -545,13 +861,18 @@ unitInfo.unit_size = range.read_uint(unit_size_bits); - if (unitInfo.unit_size >= UINT64_MAX - implied_offset) { + // Reject unit_offset + unit_size wrapping past the 64 bit range. + if (unitInfo.unit_size >= UINT64_MAX - unitInfo.unit_offset) { return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, - "cumulative offsets too large for 64 bit file size"}; + "icef unit offset + size exceeds 64 bit range"}; } - implied_offset += unitInfo.unit_size; + // implied_offset is only used as unit_offset when unit_offset_code==0. + // Accumulating it for other offset codes would be unused and could overflow, so only do it if needed. + if (unit_offset_code == 0) { + implied_offset += unitInfo.unit_size; + } if (range.get_error() != Error::Ok) { return range.get_error(); @@ -697,31 +1018,30 @@ return unsupported_version_error("cpat"); } - m_pattern_width = range.read16(); - m_pattern_height = range.read16(); + m_pattern.pattern_width = range.read16(); + m_pattern.pattern_height = range.read16(); - if (m_pattern_width == 0 || m_pattern_height == 0) { + if (m_pattern.pattern_width == 0 || m_pattern.pattern_height == 0) { return {heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "Zero Bayer pattern size."}; } auto max_bayer_pattern_size = limits->max_bayer_pattern_pixels; - if (max_bayer_pattern_size && m_pattern_height > max_bayer_pattern_size / m_pattern_width) { + if (max_bayer_pattern_size && m_pattern.pattern_height > max_bayer_pattern_size / m_pattern.pattern_width) { return {heif_error_Invalid_input, heif_suberror_Security_limit_exceeded, "Maximum Bayer pattern size exceeded."}; } - m_components.resize(size_t{m_pattern_width} * m_pattern_height); + size_t num_pixels = size_t{m_pattern.pattern_width} * m_pattern.pattern_height; + m_pattern.pixels.resize(num_pixels); - for (uint16_t i = 0; i < m_pattern_height; i++) { - for (uint16_t j = 0; j < m_pattern_width; j++) { - PatternComponent component{}; - component.component_index = range.read32(); - component.component_gain = range.read_float32(); - m_components[i] = component; - } + for (size_t i = 0; i < num_pixels; i++) { + BayerPatternPixelCmpd pixel{}; + pixel.cmpd_index = range.read32(); + pixel.component_gain = range.read_float32(); + m_pattern.pixels[i] = pixel; } return range.get_error(); @@ -736,8 +1056,8 @@ sstr << indent << "pattern_width: " << get_pattern_width() << "\n"; sstr << indent << "pattern_height: " << get_pattern_height() << "\n"; - for (const auto& component : m_components) { - sstr << indent << "component index: " << component.component_index << ", gain: " << component.component_gain << "\n"; + for (const auto& pixel : m_pattern.pixels) { + sstr << indent << "component index: " << pixel.cmpd_index << ", gain: " << pixel.component_gain << "\n"; } return sstr.str(); } @@ -747,22 +1067,475 @@ { size_t box_start = reserve_box_header_space(writer); - if (m_pattern_width * size_t{m_pattern_height} != m_components.size()) { - // needs to be rectangular + if (m_pattern.pattern_width * size_t{m_pattern.pattern_height} != m_pattern.pixels.size()) { return {heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "incorrect number of pattern components"}; } - writer.write16(m_pattern_width); - writer.write16(m_pattern_height); + writer.write16(m_pattern.pattern_width); + writer.write16(m_pattern.pattern_height); - for (const auto& component : m_components) { - writer.write32(component.component_index); - writer.write_float32(component.component_gain); + for (const auto& pixel : m_pattern.pixels) { + writer.write32(pixel.cmpd_index); + writer.write_float32(pixel.component_gain); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_splz::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("splz"); + } + + uint32_t component_count = range.read32(); + if (limits->max_components && component_count > limits->max_components) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "Number of components in splz box exceeds the security limits."}; + } + + m_pattern.component_ids.resize(component_count); + for (uint32_t i = 0; i < component_count; i++) { + m_pattern.component_ids[i] = range.read32(); + } + + m_pattern.pattern_width = range.read16(); + m_pattern.pattern_height = range.read16(); + + if (m_pattern.pattern_width == 0 || m_pattern.pattern_height == 0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "Zero polarization pattern size."}; + } + + auto max_pattern_size = limits->max_bayer_pattern_pixels; + if (max_pattern_size && m_pattern.pattern_height > max_pattern_size / m_pattern.pattern_width) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "Maximum polarization pattern size exceeded."}; + } + + size_t num_pixels = size_t{m_pattern.pattern_width} * m_pattern.pattern_height; + m_pattern.polarization_angles.resize(num_pixels); + + for (size_t i = 0; i < num_pixels; i++) { + m_pattern.polarization_angles[i] = range.read_float32(); + } + + return range.get_error(); +} + + +std::string Box_splz::dump(Indent& indent) const +{ + std::ostringstream sstr; + + sstr << FullBox::dump(indent); + + sstr << indent << "component_count: " << m_pattern.component_ids.size() << "\n"; + for (size_t i = 0; i < m_pattern.component_ids.size(); i++) { + sstr << indent << " component_index[" << i << "]: " << m_pattern.component_ids[i] << "\n"; + } + + sstr << indent << "pattern_width: " << m_pattern.pattern_width << "\n"; + sstr << indent << "pattern_height: " << m_pattern.pattern_height << "\n"; + + for (uint16_t y = 0; y < m_pattern.pattern_height; y++) { + for (uint16_t x = 0; x < m_pattern.pattern_width; x++) { + float angle = m_pattern.polarization_angles[y * m_pattern.pattern_width + x]; + if (heif_polarization_angle_is_no_filter(angle)) { + sstr << indent << " [" << x << "," << y << "]: no filter\n"; + } + else { + sstr << indent << " [" << x << "," << y << "]: " << angle << " degrees\n"; + } + } + } + + return sstr.str(); +} + + +Error Box_splz::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (m_pattern.pattern_width * size_t{m_pattern.pattern_height} != m_pattern.polarization_angles.size()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "incorrect number of polarization pattern angles"}; + } + + writer.write32(static_cast(m_pattern.component_ids.size())); + for (uint32_t idx : m_pattern.component_ids) { + writer.write32(idx); + } + + writer.write16(m_pattern.pattern_width); + writer.write16(m_pattern.pattern_height); + + for (float angle : m_pattern.polarization_angles) { + writer.write_float32(angle); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_sbpm::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("sbpm"); + } + + uint32_t component_count = range.read32(); + + if (limits->max_components && component_count > limits->max_components) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "sbpm component_count exceeds security limit."}; + } + + m_map.component_ids.resize(component_count); + for (uint32_t i = 0; i < component_count; i++) { + m_map.component_ids[i] = range.read32(); + } + + uint8_t flags = range.read8(); + m_map.correction_applied = !!(flags & 0x80); + + uint32_t num_bad_rows = range.read32(); + uint32_t num_bad_cols = range.read32(); + uint32_t num_bad_pixels = range.read32(); + + // Security check: limit total number of entries + uint64_t total_entries = static_cast(num_bad_rows) + num_bad_cols + num_bad_pixels; + if (limits->max_bad_pixels && total_entries > limits->max_bad_pixels) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "sbpm total bad pixel entries exceed security limit."}; + } + + m_map.bad_rows.resize(num_bad_rows); + for (uint32_t i = 0; i < num_bad_rows; i++) { + m_map.bad_rows[i] = range.read32(); + } + + m_map.bad_columns.resize(num_bad_cols); + for (uint32_t i = 0; i < num_bad_cols; i++) { + m_map.bad_columns[i] = range.read32(); + } + + m_map.bad_pixels.resize(num_bad_pixels); + for (uint32_t i = 0; i < num_bad_pixels; i++) { + m_map.bad_pixels[i].row = range.read32(); + m_map.bad_pixels[i].column = range.read32(); + } + + return range.get_error(); +} + + +std::string Box_sbpm::dump(Indent& indent) const +{ + std::ostringstream sstr; + + sstr << FullBox::dump(indent); + + sstr << indent << "component_count: " << m_map.component_ids.size() << "\n"; + for (size_t i = 0; i < m_map.component_ids.size(); i++) { + sstr << indent << " component_index[" << i << "]: " << m_map.component_ids[i] << "\n"; + } + + sstr << indent << "correction_applied: " << m_map.correction_applied << "\n"; + + sstr << indent << "num_bad_rows: " << m_map.bad_rows.size() << "\n"; + for (size_t i = 0; i < m_map.bad_rows.size(); i++) { + sstr << indent << " bad_row[" << i << "]: " << m_map.bad_rows[i] << "\n"; + } + + sstr << indent << "num_bad_columns: " << m_map.bad_columns.size() << "\n"; + for (size_t i = 0; i < m_map.bad_columns.size(); i++) { + sstr << indent << " bad_column[" << i << "]: " << m_map.bad_columns[i] << "\n"; + } + + sstr << indent << "num_bad_pixels: " << m_map.bad_pixels.size() << "\n"; + for (size_t i = 0; i < m_map.bad_pixels.size(); i++) { + sstr << indent << " bad_pixel[" << i << "]: row=" << m_map.bad_pixels[i].row + << ", column=" << m_map.bad_pixels[i].column << "\n"; + } + + return sstr.str(); +} + + +Error Box_sbpm::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_map.component_ids.size())); + for (uint32_t idx : m_map.component_ids) { + writer.write32(idx); + } + + uint8_t flags = m_map.correction_applied ? 0x80 : 0; + writer.write8(flags); + + writer.write32(static_cast(m_map.bad_rows.size())); + writer.write32(static_cast(m_map.bad_columns.size())); + writer.write32(static_cast(m_map.bad_pixels.size())); + + for (uint32_t row : m_map.bad_rows) { + writer.write32(row); + } + + for (uint32_t col : m_map.bad_columns) { + writer.write32(col); + } + + for (const auto& pixel : m_map.bad_pixels) { + writer.write32(pixel.row); + writer.write32(pixel.column); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_snuc::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("snuc"); + } + + uint32_t component_count = range.read32(); + + if (limits->max_components && component_count > limits->max_components) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "snuc component_count exceeds security limit."}; + } + + m_nuc.component_ids.resize(component_count); + for (uint32_t i = 0; i < component_count; i++) { + m_nuc.component_ids[i] = range.read32(); + } + + uint8_t flags = range.read8(); + m_nuc.nuc_is_applied = !!(flags & 0x80); + + m_nuc.image_width = range.read32(); + m_nuc.image_height = range.read32(); + + if (m_nuc.image_width == 0 || m_nuc.image_height == 0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "snuc image width and height must be non-zero."}; + } + + uint64_t num_pixels = static_cast(m_nuc.image_width) * m_nuc.image_height; + + if (limits->max_image_size_pixels && num_pixels > limits->max_image_size_pixels) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "snuc image dimensions exceed security limit."}; + } + + Error err = m_memory_handle.alloc(num_pixels, 2 * sizeof(float), limits, "snuc box"); + if (err) { + return err; + } + + m_nuc.nuc_gains.resize(num_pixels); + for (uint64_t i = 0; i < num_pixels; i++) { + m_nuc.nuc_gains[i] = range.read_float32(); + } + + m_nuc.nuc_offsets.resize(num_pixels); + for (uint64_t i = 0; i < num_pixels; i++) { + m_nuc.nuc_offsets[i] = range.read_float32(); + } + + return range.get_error(); +} + + +std::string Box_snuc::dump(Indent& indent) const +{ + std::ostringstream sstr; + + sstr << FullBox::dump(indent); + + sstr << indent << "component_count: " << m_nuc.component_ids.size() << "\n"; + for (size_t i = 0; i < m_nuc.component_ids.size(); i++) { + sstr << indent << " component_index[" << i << "]: " << m_nuc.component_ids[i] << "\n"; + } + + sstr << indent << "nuc_is_applied: " << m_nuc.nuc_is_applied << "\n"; + sstr << indent << "image_width: " << m_nuc.image_width << "\n"; + sstr << indent << "image_height: " << m_nuc.image_height << "\n"; + + uint64_t num_pixels = static_cast(m_nuc.image_width) * m_nuc.image_height; + sstr << indent << "nuc_gains: " << num_pixels << " values\n"; + sstr << indent << "nuc_offsets: " << num_pixels << " values\n"; + + return sstr.str(); +} + + +Error Box_snuc::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_nuc.component_ids.size())); + for (uint32_t idx : m_nuc.component_ids) { + writer.write32(idx); + } + + uint8_t flags = m_nuc.nuc_is_applied ? 0x80 : 0; + writer.write8(flags); + + writer.write32(m_nuc.image_width); + writer.write32(m_nuc.image_height); + + for (float gain : m_nuc.nuc_gains) { + writer.write_float32(gain); + } + + for (float offset : m_nuc.nuc_offsets) { + writer.write_float32(offset); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_cloc::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() != 0) { + return unsupported_version_error("cloc"); + } + + m_chroma_location = range.read8(); + + if (m_chroma_location > 6) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "cloc chroma_location value out of range (must be 0-6)."}; + } + + return range.get_error(); +} + + +std::string Box_cloc::dump(Indent& indent) const +{ + std::ostringstream sstr; + + sstr << FullBox::dump(indent); + + static const char* location_names[] = { + "h=0, v=0.5", // 0 + "h=0.5, v=0.5", // 1 + "h=0, v=0", // 2 + "h=0.5, v=0", // 3 + "h=0, v=1", // 4 + "h=0.5, v=1", // 5 + "Cr:0,0 / Cb:1,0" // 6 + }; + + sstr << indent << "chroma_location: " << static_cast(m_chroma_location); + if (m_chroma_location <= 6) { + sstr << " (" << location_names[m_chroma_location] << ")"; + } + sstr << "\n"; + + return sstr.str(); +} + + +Error Box_cloc::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write8(m_chroma_location); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_gimi_component_content_ids::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + uint32_t number_of_components = range.read32(); + + if (limits->max_components && number_of_components > limits->max_components) { + std::stringstream sstr; + sstr << "GIMI component content IDs box contains " << number_of_components + << " components, but security limit is set to " << limits->max_components << " components"; + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + + for (uint32_t i = 0; i < number_of_components; i++) { + if (range.eof()) { + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + "Not enough data for all component content IDs"}; + } + m_content_ids.push_back(range.read_string()); + } + + return range.get_error(); +} + + +Error Box_gimi_component_content_ids::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_content_ids.size())); + + for (const auto& id : m_content_ids) { + writer.write(id); } prepend_header(writer, box_start); return Error::Ok; } + + +std::string Box_gimi_component_content_ids::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + + for (size_t i = 0; i < m_content_ids.size(); i++) { + sstr << indent << "[" << i << "] content ID: " << m_content_ids[i] << "\n"; + } + + return sstr.str(); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_boxes.h libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -24,12 +24,15 @@ #include "box.h" #include "bitstream.h" +#include "image/pixelimage.h" #include "unc_types.h" +#include "sequences/seq_boxes.h" #include #include #include #include +#include /** @@ -47,6 +50,8 @@ Error write(StreamWriter& writer) const override; + bool is_essential() const override { return true; } + struct Component { uint16_t component_type; @@ -59,11 +64,17 @@ const std::vector& get_components() const { return m_components; } - void add_component(const Component& component) + bool has_component(heif_cmpd_component_type) const; + + uint16_t add_component(const Component& component) { + auto index = static_cast(m_components.size()); m_components.push_back(component); + return index; } + void set_components(const std::vector&); + protected: Error parse(BitstreamRange& range, const heif_security_limits* limits) override; @@ -82,7 +93,15 @@ bool is_essential() const override { return true; } - void derive_box_version() override {}; + bool is_minimized() const + { + return m_profile != 0 && m_num_tile_cols==1 && m_num_tile_rows==1; + } + + void derive_box_version() override + { + set_version(is_minimized() ? 1 : 0); + } std::string dump(Indent&) const override; @@ -90,7 +109,7 @@ struct Component { - uint16_t component_index; + uint32_t component_index; uint16_t component_bit_depth; // range [1..256] uint8_t component_format; uint8_t component_align_size; @@ -203,7 +222,9 @@ uint32_t get_number_of_tiles() const { return m_num_tile_rows * m_num_tile_rows; } - uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const; + std::shared_ptr get_synthetic_cmpd() const { return m_synthetic_cmpd; } + + void set_synthetic_cmpd(std::shared_ptr cmpd) { m_synthetic_cmpd = std::move(cmpd); } protected: Error parse(BitstreamRange& range, const heif_security_limits* limits) override; @@ -224,6 +245,8 @@ uint32_t m_tile_align_size = 0; uint32_t m_num_tile_cols = 1; uint32_t m_num_tile_rows = 1; + + std::shared_ptr m_synthetic_cmpd; }; @@ -312,6 +335,7 @@ Error parse(BitstreamRange& range, const heif_security_limits* limits) override; std::vector m_unit_infos; + MemoryHandle m_memory_handle; private: const uint8_t get_required_offset_code(uint64_t offset) const; @@ -336,32 +360,186 @@ set_short_type(fourcc("cpat")); } - struct PatternComponent - { - uint32_t component_index; - float component_gain; - }; + uint16_t get_pattern_width() const { return m_pattern.pattern_width; } + + uint16_t get_pattern_height() const { return m_pattern.pattern_height; } + + const BayerPatternCmpd& get_pattern() const { return m_pattern; } + + void set_pattern(const BayerPatternCmpd& pattern) { m_pattern = pattern; } + + std::string dump(Indent&) const override; + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + + BayerPatternCmpd m_pattern; +}; + + +/** + * Polarization pattern definition box (splz). + * + * Describes the polarization filter array pattern on an image sensor. + * Multiple splz boxes can exist (one per set of components with + * different polarization filters). + * + * This is from ISO/IEC 23001-17 Section 6.1.5. + */ +class Box_splz : public FullBox +{ +public: + Box_splz() { set_short_type(fourcc("splz")); } + + const PolarizationPattern& get_pattern() const { return m_pattern; } + + void set_pattern(const PolarizationPattern& pattern) { m_pattern = pattern; } + + std::string dump(Indent&) const override; + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + + PolarizationPattern m_pattern; +}; + + +/** + * Sensor bad pixels map box (sbpm). + * + * Identifies bad pixels on a sensor for which at least one component + * value is corrupted. Supports bad rows, bad columns, and individual + * bad pixel coordinates. + * + * This is from ISO/IEC 23001-17 Section 6.1.7. + */ +class Box_sbpm : public FullBox +{ +public: + Box_sbpm() { set_short_type(fourcc("sbpm")); } + + const SensorBadPixelsMap& get_bad_pixels_map() const { return m_map; } + void set_bad_pixels_map(const SensorBadPixelsMap& map) { m_map = map; } + + std::string dump(Indent&) const override; + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; - uint16_t get_pattern_width() const + SensorBadPixelsMap m_map; +}; + + +/** + * Sensor non-uniformity correction box (snuc). + * + * Provides per-pixel gain and offset tables for sensor non-uniformity + * correction. The correction equation is: y = nuc_gain * x + nuc_offset. + * + * This is from ISO/IEC 23001-17 Section 6.1.6. + */ +class Box_snuc : public FullBox +{ +public: + Box_snuc() { set_short_type(fourcc("snuc")); } + + const SensorNonUniformityCorrection& get_nuc() const { return m_nuc; } + void set_nuc(const SensorNonUniformityCorrection& nuc) { m_nuc = nuc; } + + std::string dump(Indent&) const override; + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + + MemoryHandle m_memory_handle; + SensorNonUniformityCorrection m_nuc; +}; + + +/** + * Chroma location box (cloc). + * + * Signals the chroma sample position for subsampled images. + * + * This is from ISO/IEC 23001-17 Section 6.1.4. + */ +class Box_cloc : public FullBox +{ +public: + Box_cloc() { set_short_type(fourcc("cloc")); } + + uint8_t get_chroma_location() const { return m_chroma_location; } + void set_chroma_location(uint8_t loc) { m_chroma_location = loc; } + + std::string dump(Indent&) const override; + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + + uint8_t m_chroma_location = 0; +}; + + +void fill_uncC_and_cmpd_from_profile(const std::shared_ptr& uncC, + std::shared_ptr& cmpd); + + +class Box_uncv : public Box_VisualSampleEntry +{ +public: + Box_uncv() { - return m_pattern_width; + set_short_type(fourcc("uncv")); } +}; - uint16_t get_pattern_height() const + +/** + * GIMI ItemComponentContentIDProperty. + * + * A UUID-type item property that assigns a unique Content ID string + * to each cmpd component of an image item. + * + * UUID: 9db9dd6e-373c-5a4e-8110-21fc83a911fd + */ +class Box_gimi_component_content_ids : public Box +{ +public: + Box_gimi_component_content_ids() { - return m_pattern_height; + set_uuid_type(std::vector{0x9d, 0xb9, 0xdd, 0x6e, 0x37, 0x3c, 0x5a, 0x4e, + 0x81, 0x10, 0x21, 0xfc, 0x83, 0xa9, 0x11, 0xfd}); } + bool is_essential() const override { return false; } + + bool is_transformative_property() const override { return false; } + std::string dump(Indent&) const override; - Error write(StreamWriter& writer) const override; + const char* debug_box_name() const override { return "GIMI Component Content IDs"; } + + const std::vector& get_content_ids() const { return m_content_ids; } + + void set_content_ids(const std::vector& ids) { m_content_ids = ids; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::ignorable; } protected: - Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + Error parse(BitstreamRange& range, const heif_security_limits*) override; - uint16_t m_pattern_width = 0; - uint16_t m_pattern_height = 0; - std::vector m_components; + Error write(StreamWriter& writer) const override; + +private: + std::vector m_content_ids; }; + #endif //LIBHEIF_UNC_BOXES_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_codec.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_codec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,216 +27,16 @@ #include "unc_types.h" #include "unc_boxes.h" -#include "decoder_abstract.h" -#include "decoder_component_interleave.h" -#include "decoder_pixel_interleave.h" -#include "decoder_mixed_interleave.h" -#include "decoder_row_interleave.h" -#include "decoder_tile_component_interleave.h" +#include "unc_decoder.h" +#include "codecs/decoder.h" #include -#include -#include #include +#include +#include +#include #include "security_limits.h" - - -bool isKnownUncompressedFrameConfigurationBoxProfile(const std::shared_ptr& uncC) -{ - return ((uncC != nullptr) && (uncC->get_version() == 1) && ((uncC->get_profile() == fourcc("rgb3")) || (uncC->get_profile() == fourcc("rgba")) || (uncC->get_profile() == fourcc("abgr")))); -} - - -static Error uncompressed_image_type_is_supported(const std::shared_ptr& uncC, - const std::shared_ptr& cmpd) -{ - if (isKnownUncompressedFrameConfigurationBoxProfile(uncC)) { - return Error::Ok; - } - if (!cmpd) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Missing required cmpd box (no match in uncC box) for uncompressed codec"); - } - - for (Box_uncC::Component component : uncC->get_components()) { - uint16_t component_index = component.component_index; - uint16_t component_type = cmpd->get_components()[component_index].component_type; - if ((component_type > 7) && (component_type != component_type_padded) && (component_type != component_type_filter_array)) { - std::stringstream sstr; - sstr << "Uncompressed image with component_type " << ((int) component_type) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - - if ((component.component_bit_depth > 16)) { - std::stringstream sstr; - sstr << "Uncompressed image with component_bit_depth " << ((int) component.component_bit_depth) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - if (component.component_format != component_format_unsigned) { - std::stringstream sstr; - sstr << "Uncompressed image with component_format " << ((int) component.component_format) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - if (component.component_align_size > 2) { - std::stringstream sstr; - sstr << "Uncompressed image with component_align_size " << ((int) component.component_align_size) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - } - if ((uncC->get_sampling_type() != sampling_mode_no_subsampling) - && (uncC->get_sampling_type() != sampling_mode_422) - && (uncC->get_sampling_type() != sampling_mode_420) - ) { - std::stringstream sstr; - sstr << "Uncompressed sampling_type of " << ((int) uncC->get_sampling_type()) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - if ((uncC->get_interleave_type() != interleave_mode_component) - && (uncC->get_interleave_type() != interleave_mode_pixel) - && (uncC->get_interleave_type() != interleave_mode_mixed) - && (uncC->get_interleave_type() != interleave_mode_row) - && (uncC->get_interleave_type() != interleave_mode_tile_component) - ) { - std::stringstream sstr; - sstr << "Uncompressed interleave_type of " << ((int) uncC->get_interleave_type()) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - // Validity checks per ISO/IEC 23001-17 Section 5.2.1.5.3 - if (uncC->get_sampling_type() == sampling_mode_422) { - // We check Y Cb and Cr appear in the chroma test - // TODO: error for tile width not multiple of 2 - if ((uncC->get_interleave_type() != interleave_mode_component) - && (uncC->get_interleave_type() != interleave_mode_mixed) - && (uncC->get_interleave_type() != interleave_mode_multi_y)) { - std::stringstream sstr; - sstr << "YCbCr 4:2:2 subsampling is only valid with component, mixed or multi-Y interleave mode (ISO/IEC 23001-17 5.2.1.5.3)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - if ((uncC->get_row_align_size() != 0) && (uncC->get_interleave_type() == interleave_mode_component)) { - if (uncC->get_row_align_size() % 2 != 0) { - std::stringstream sstr; - sstr << "YCbCr 4:2:2 subsampling with component interleave requires row_align_size to be a multiple of 2 (ISO/IEC 23001-17 5.2.1.5.3)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - } - if (uncC->get_tile_align_size() != 0) { - if (uncC->get_tile_align_size() % 2 != 0) { - std::stringstream sstr; - sstr << "YCbCr 4:2:2 subsampling requires tile_align_size to be a multiple of 2 (ISO/IEC 23001-17 5.2.1.5.3)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - } - } - // Validity checks per ISO/IEC 23001-17 Section 5.2.1.5.4 - if (uncC->get_sampling_type() == sampling_mode_422) { - // We check Y Cb and Cr appear in the chroma test - // TODO: error for tile width not multiple of 2 - if ((uncC->get_interleave_type() != interleave_mode_component) - && (uncC->get_interleave_type() != interleave_mode_mixed)) { - std::stringstream sstr; - sstr << "YCbCr 4:2:0 subsampling is only valid with component or mixed interleave mode (ISO/IEC 23001-17 5.2.1.5.4)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - if ((uncC->get_row_align_size() != 0) && (uncC->get_interleave_type() == interleave_mode_component)) { - if (uncC->get_row_align_size() % 2 != 0) { - std::stringstream sstr; - sstr << "YCbCr 4:2:2 subsampling with component interleave requires row_align_size to be a multiple of 2 (ISO/IEC 23001-17 5.2.1.5.4)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - } - if (uncC->get_tile_align_size() != 0) { - if (uncC->get_tile_align_size() % 4 != 0) { - std::stringstream sstr; - sstr << "YCbCr 4:2:2 subsampling requires tile_align_size to be a multiple of 4 (ISO/IEC 23001-17 5.2.1.5.3)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - } - } - if ((uncC->get_interleave_type() == interleave_mode_mixed) && (uncC->get_sampling_type() == sampling_mode_no_subsampling)) { - std::stringstream sstr; - sstr << "Interleave interleave mode is not valid with subsampling mode (ISO/IEC 23001-17 5.2.1.6.4)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - if ((uncC->get_interleave_type() == interleave_mode_multi_y) - && ((uncC->get_sampling_type() != sampling_mode_422) && (uncC->get_sampling_type() != sampling_mode_411))) { - std::stringstream sstr; - sstr << "Multi-Y interleave mode is only valid with 4:2:2 and 4:1:1 subsampling modes (ISO/IEC 23001-17 5.2.1.6.7)."; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - // TODO: throw error if mixed and Cb and Cr are not adjacent. - - if (uncC->get_block_size() != 0) { - std::stringstream sstr; - sstr << "Uncompressed block_size of " << ((int) uncC->get_block_size()) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); - } - - if (uncC->is_components_little_endian()) { - const auto& comps = uncC->get_components(); - bool all_8_bit = std::all_of(comps.begin(), comps.end(), - [](const Box_uncC::Component& c) { return c.component_bit_depth==8; }); - if (!all_8_bit) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Uncompressed components_little_endian == 1 is not implemented yet"); - } - } - - if (uncC->is_block_pad_lsb()) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Uncompressed block_pad_lsb == 1 is not implemented yet"); - } - if (uncC->is_block_little_endian()) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Uncompressed block_little_endian == 1 is not implemented yet"); - } - if (uncC->is_block_reversed()) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Uncompressed block_reversed == 1 is not implemented yet"); - } - if ((uncC->get_pixel_size() != 0) && ((uncC->get_interleave_type() != interleave_mode_pixel) && (uncC->get_interleave_type() != interleave_mode_multi_y))) { - std::stringstream sstr; - sstr << "Uncompressed pixel_size of " << ((int) uncC->get_pixel_size()) << " is only valid with interleave_type 1 or 5 (ISO/IEC 23001-17 5.2.1.7)"; - return Error(heif_error_Invalid_input, - heif_suberror_Invalid_parameter_value, - sstr.str()); - } - return Error::Ok; -} +#include Error UncompressedImageCodec::get_heif_chroma_uncompressed(const std::shared_ptr& uncC, @@ -285,30 +85,30 @@ uint16_t componentSet = 0; for (Box_uncC::Component component : uncC->get_components()) { - uint16_t component_index = component.component_index; + uint32_t component_index = component.component_index; uint16_t component_type = cmpd->get_components()[component_index].component_type; - if (component_type > component_type_max_valid) { + if (component_type > heif_cmpd_component_type_max_valid) { std::stringstream sstr; - sstr << "a component_type > " << component_type_max_valid << " is not supported"; + sstr << "a component_type > " << heif_cmpd_component_type_max_valid << " is not supported"; return {heif_error_Unsupported_feature, heif_suberror_Invalid_parameter_value, sstr.str()}; } - if (component_type == component_type_padded) { + if (component_type == heif_cmpd_component_type_padded) { // not relevant for determining chroma continue; } componentSet |= (1 << component_type); } - *out_has_alpha = (componentSet & (1 << component_type_alpha)) != 0; + *out_has_alpha = (componentSet & (1 << heif_cmpd_component_type_alpha)) != 0; - if (componentSet == ((1 << component_type_red) | (1 << component_type_green) | (1 << component_type_blue)) || - componentSet == ((1 << component_type_red) | (1 << component_type_green) | (1 << component_type_blue) | (1 << component_type_alpha))) { + if (componentSet == ((1 << heif_cmpd_component_type_red) | (1 << heif_cmpd_component_type_green) | (1 << heif_cmpd_component_type_blue)) || + componentSet == ((1 << heif_cmpd_component_type_red) | (1 << heif_cmpd_component_type_green) | (1 << heif_cmpd_component_type_blue) | (1 << heif_cmpd_component_type_alpha))) { *out_chroma = heif_chroma_444; *out_colourspace = heif_colorspace_RGB; } - if (componentSet == ((1 << component_type_Y) | (1 << component_type_Cb) | (1 << component_type_Cr))) { + if (componentSet == ((1 << heif_cmpd_component_type_Y) | (1 << heif_cmpd_component_type_Cb) | (1 << heif_cmpd_component_type_Cr))) { switch (uncC->get_sampling_type()) { case sampling_mode_no_subsampling: *out_chroma = heif_chroma_444; @@ -323,153 +123,82 @@ *out_colourspace = heif_colorspace_YCbCr; } - if (componentSet == ((1 << component_type_monochrome)) || componentSet == ((1 << component_type_monochrome) | (1 << component_type_alpha))) { + if (componentSet == (1 << heif_cmpd_component_type_monochrome) || componentSet == ((1 << heif_cmpd_component_type_monochrome) | (1 << heif_cmpd_component_type_alpha)) || + componentSet == (1 << heif_cmpd_component_type_Y) || componentSet == ((1 << heif_cmpd_component_type_Y) | (1 << heif_cmpd_component_type_alpha))) { // mono or mono + alpha input, mono output. *out_chroma = heif_chroma_monochrome; *out_colourspace = heif_colorspace_monochrome; } - if (componentSet == (1 << component_type_filter_array)) { + if (componentSet == (1 << heif_cmpd_component_type_filter_array)) { // TODO - we should look up the components - *out_chroma = heif_chroma_monochrome; - *out_colourspace = heif_colorspace_monochrome; + *out_chroma = heif_chroma_planar; + *out_colourspace = heif_colorspace_filter_array; } // TODO: more combinations - if (*out_chroma == heif_chroma_undefined) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Could not determine chroma"); - } - else if (*out_colourspace == heif_colorspace_undefined) { + // out_colourspace remains heif_colorspace_undefined iff no branch above + // matched any known component set. + if (*out_colourspace == heif_colorspace_undefined) { return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Could not determine colourspace"); } - else { - return Error::Ok; - } + + return Error::Ok; } bool map_uncompressed_component_to_channel(const std::shared_ptr& cmpd, - const std::shared_ptr& uncC, Box_uncC::Component component, heif_channel* channel) { - uint16_t component_index = component.component_index; - if (isKnownUncompressedFrameConfigurationBoxProfile(uncC)) { - if (uncC->get_profile() == fourcc("rgb3")) { - switch (component_index) { - case 0: - *channel = heif_channel_R; - return true; - case 1: - *channel = heif_channel_G; - return true; - case 2: - *channel = heif_channel_B; - return true; - } - } - else if (uncC->get_profile() == fourcc("rgba")) { - switch (component_index) { - case 0: - *channel = heif_channel_R; - return true; - case 1: - *channel = heif_channel_G; - return true; - case 2: - *channel = heif_channel_B; - return true; - case 3: - *channel = heif_channel_Alpha; - return true; - } - } - else if (uncC->get_profile() == fourcc("abgr")) { - switch (component_index) { - case 0: - *channel = heif_channel_Alpha; - return true; - case 1: - *channel = heif_channel_B; - return true; - case 2: - *channel = heif_channel_G; - return true; - case 3: - *channel = heif_channel_R; - return true; - } - } - } + uint32_t component_index = component.component_index; uint16_t component_type = cmpd->get_components()[component_index].component_type; - switch (component_type) { - case component_type_monochrome: - *channel = heif_channel_Y; - return true; - case component_type_Y: - *channel = heif_channel_Y; - return true; - case component_type_Cb: - *channel = heif_channel_Cb; - return true; - case component_type_Cr: - *channel = heif_channel_Cr; - return true; - case component_type_red: - *channel = heif_channel_R; - return true; - case component_type_green: - *channel = heif_channel_G; - return true; - case component_type_blue: - *channel = heif_channel_B; - return true; - case component_type_alpha: - *channel = heif_channel_Alpha; - return true; - case component_type_filter_array: - // TODO: this is just a temporary hack - *channel = heif_channel_Y; - return true; - case component_type_padded: - return false; - default: - return false; - } + *channel = map_uncompressed_component_to_channel(component_type); + return true; } +heif_component_datatype unc_component_format_to_datatype(uint8_t format) +{ + // heif_component_datatype values are aligned with ISO/IEC 23001-17 Table 2. + // is_valid_component_format() in unc_boxes.cc rejects out-of-range bytes + // before they reach here, so any spec-defined byte casts cleanly. + if (format > component_format_max_valid) { + return heif_component_datatype_undefined; + } + return static_cast(format); +} + -static AbstractDecoder* makeDecoder(uint32_t width, uint32_t height, const std::shared_ptr& cmpd, const std::shared_ptr& uncC) +static Error validate_component_indices(const std::vector& indices, + size_t cmpd_size, + const char* box_name) { - switch (uncC->get_interleave_type()) { - case interleave_mode_component: - return new ComponentInterleaveDecoder(width, height, cmpd, uncC); - case interleave_mode_pixel: - return new PixelInterleaveDecoder(width, height, cmpd, uncC); - case interleave_mode_mixed: - return new MixedInterleaveDecoder(width, height, cmpd, uncC); - case interleave_mode_row: - return new RowInterleaveDecoder(width, height, cmpd, uncC); - case interleave_mode_tile_component: - return new TileComponentInterleaveDecoder(width, height, cmpd, uncC); - default: - return nullptr; + for (uint32_t idx : indices) { + if (idx >= cmpd_size) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + std::string(box_name) + " component index out of range of cmpd table"}; + } } + return Error::Ok; } -Result> UncompressedImageCodec::create_image(const std::shared_ptr cmpd, - const std::shared_ptr uncC, +Result> UncompressedImageCodec::create_image(const unci_properties& properties, uint32_t width, uint32_t height, + std::vector& uncC_index_to_comp_ids, const heif_security_limits* limits) { + auto cmpd = properties.cmpd; + auto uncC = properties.uncC; + + const auto& components = cmpd->get_components(); + auto img = std::make_shared(); heif_chroma chroma = heif_chroma_undefined; heif_colorspace colourspace = heif_colorspace_undefined; @@ -482,27 +211,185 @@ colourspace, chroma); + + // Clone the per-component descriptions (id, channel, type, format, datatype, + // bit_depth) from the source ImageItem, populated at parse time. This is + // what makes component IDs stable across the handle and decoded image. + if (properties.source_extra_data) { + img->clone_component_descriptions_from(*properties.source_extra_data); + + // Source descriptions carry the full-ispe plane sizes populated at parse + // time. When this call is for a single tile, width/height are smaller, so + // recompute the per-plane sizes from the actual target dimensions before + // allocate_buffer_for_component() reads desc->width/height. + for (const auto& desc_view : img->get_component_descriptions()) { + if (!desc_view.has_data_plane) { + continue; + } + ComponentDescription* desc = img->find_component_description(desc_view.component_id); + desc->width = channel_width(width, chroma, desc->channel); + desc->height = channel_height(height, chroma, desc->channel); + } + } + + // Remember which components reference which cmpd indices. + // There can be several component ids referencing the same cmpd index. + std::vector> cmpd_index_to_comp_ids(components.size()); + + // Walk uncC. populate_component_descriptions() emitted one description per + // uncC entry first, in order, so the first N descriptions in m_components + // (where N == uncC->get_components().size()) correspond positionally to + // the uncC entries we're walking here. + // + // Capture the number of pre-populated descriptions ONCE, before the loop. + // The fallback branch below calls add_component(), which appends a new + // description to the image; comparing desc_idx against the live + // get_component_descriptions().size() would let those freshly added + // descriptions flip later iterations onto the pre-populated branch. That + // made a 3-component YCbCr sequence (no pre-populated descriptions) create + // the Y plane, then mistake it for a pre-populated description on the next + // iteration and re-reference the Y component instead of creating Cb, so the + // Cb plane was dropped entirely. + const size_t num_prepopulated_descriptions = img->get_component_descriptions().size(); + uint32_t desc_idx = 0; for (Box_uncC::Component component : uncC->get_components()) { - heif_channel channel; - if (map_uncompressed_component_to_channel(cmpd, uncC, component, &channel)) { - if (img->has_channel(channel)) { - return Error{heif_error_Unsupported_feature, - heif_suberror_Unspecified, - "Cannot generate image with several similar heif_channels."}; + if (component.component_index >= components.size()) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Component index out of range." + }; + } + + if (desc_idx >= num_prepopulated_descriptions) { + // Fallback: source did not populate descriptions. This is the path taken + // by the sequence ('uncv') decoder, which builds the properties directly + // from the sample-entry boxes and does not clone descriptions from an + // ImageItem. + auto component_type = components[component.component_index].component_type; + uint32_t plane_w = width; + uint32_t plane_h = height; + if (component_type == heif_cmpd_component_type_Cb || + component_type == heif_cmpd_component_type_Cr) { + // Round the chroma plane size UP so that it covers the full logical + // image, matching channel_width()/channel_height() used on the + // description path above and the round-up chroma extent that the RGB + // conversion (e.g. Op_YCbCr420_to_RGB24, which indexes chroma with + // y/2 for every luma row) and primary_planes_have_size() expect. Floor + // division here undersized the Cb/Cr planes by one row/column for odd + // 4:2:0/4:2:2 dimensions, causing a heap-buffer-overflow read during + // RGB conversion of sequence frames (GHSA-4h82-g446-83fm). + plane_w = channel_width(width, chroma, heif_channel_Cb); + plane_h = channel_height(height, chroma, heif_channel_Cb); + } + auto result = img->add_component(plane_w, plane_h, component_type, + unc_component_format_to_datatype(component.component_format), + component.component_bit_depth, limits); + if (result.is_error()) { + return result.error(); } + cmpd_index_to_comp_ids[component.component_index].push_back(*result); + uncC_index_to_comp_ids.push_back(*result); + continue; + } - if ((channel == heif_channel_Cb) || (channel == heif_channel_Cr)) { - if (auto err = img->add_plane(channel, (width / chroma_h_subsampling(chroma)), (height / chroma_v_subsampling(chroma)), component.component_bit_depth, - limits)) { - return err; - } + // Pre-populated description path: the cloned description already has + // the correct (chroma-subsampled) dimensions from + // populate_component_descriptions(). Just allocate the buffer. + uint32_t comp_id = img->get_component_descriptions()[desc_idx].component_id; + if (auto err = img->allocate_buffer_for_component(comp_id, limits)) { + return err; + } + + cmpd_index_to_comp_ids[component.component_index].push_back(comp_id); + uncC_index_to_comp_ids.push_back(comp_id); + desc_idx++; + } + + + // --- assign the metadata boxes + + size_t cmpd_size = cmpd ? cmpd->get_components().size() : 0; + + if (properties.cpat) { + const auto& pattern_cmpd = properties.cpat->get_pattern(); + std::vector cpat_indices; + cpat_indices.reserve(pattern_cmpd.pixels.size()); + for (const auto& pixel : pattern_cmpd.pixels) { + cpat_indices.push_back(pixel.cmpd_index); + } + Error err = validate_component_indices(cpat_indices, cmpd_size, "cpat"); + if (err) { + return err; + } + + // Build BayerPattern. populate_component_descriptions added one + // reference component per UNIQUE cmpd_index referenced by the pattern, + // in first-occurrence order, immediately after the uncC components. + // We rebuild the cmpd_index -> comp_id map in the same way and reuse + // the existing IDs. (If descriptions weren't populated — fallback + // path — we fall back to minting reference components here.) + BayerPattern pattern; + pattern.pattern_width = pattern_cmpd.pattern_width; + pattern.pattern_height = pattern_cmpd.pattern_height; + std::map cpat_cmpd_idx_to_comp_id; + for (auto p : pattern_cmpd.pixels) { + uint32_t comp_id; + auto it = cpat_cmpd_idx_to_comp_id.find(p.cmpd_index); + if (it != cpat_cmpd_idx_to_comp_id.end()) { + comp_id = it->second; + } + else if (desc_idx < num_prepopulated_descriptions) { + comp_id = img->get_component_descriptions()[desc_idx].component_id; + desc_idx++; + cpat_cmpd_idx_to_comp_id[p.cmpd_index] = comp_id; } else { - if (auto err = img->add_plane(channel, width, height, component.component_bit_depth, limits)) { - return err; - } + comp_id = img->add_component_without_data(components[p.cmpd_index].component_type); + cpat_cmpd_idx_to_comp_id[p.cmpd_index] = comp_id; } + pattern.pixels.push_back({comp_id, p.component_gain}); + cmpd_index_to_comp_ids[p.cmpd_index].push_back(comp_id); } + + img->set_bayer_pattern(pattern); + } + + for (const auto& splz_box : properties.splz) { + const auto& pattern_cmpd = splz_box->get_pattern(); + Error err = validate_component_indices(pattern_cmpd.component_ids, cmpd_size, "splz"); + if (err) { + return err; + } + PolarizationPattern pattern = pattern_cmpd; + pattern.component_ids = map_cmpd_to_component_ids(pattern_cmpd.component_ids, cmpd_index_to_comp_ids); + img->add_polarization_pattern(pattern); + } + + for (const auto& sbpm_box : properties.sbpm) { + const auto& bad_pixels_map_cmpd = sbpm_box->get_bad_pixels_map(); + Error err = validate_component_indices(bad_pixels_map_cmpd.component_ids, cmpd_size, "sbpm"); + if (err) { + return err; + } + SensorBadPixelsMap bad_pixels_map = bad_pixels_map_cmpd; + bad_pixels_map.component_ids = map_cmpd_to_component_ids(bad_pixels_map_cmpd.component_ids, cmpd_index_to_comp_ids); + img->add_sensor_bad_pixels_map(bad_pixels_map); + } + + for (const auto& snuc_box : properties.snuc) { + const auto& nuc_cmpd = snuc_box->get_nuc(); + Error err = validate_component_indices(nuc_cmpd.component_ids, cmpd_size, "snuc"); + if (err) { + return err; + } + SensorNonUniformityCorrection nuc = nuc_cmpd; + nuc.component_ids = map_cmpd_to_component_ids(nuc_cmpd.component_ids, cmpd_index_to_comp_ids); + img->add_sensor_nuc(nuc); + } + + if (properties.cloc) { + img->set_chroma_location(properties.cloc->get_chroma_location()); } return img; @@ -521,42 +408,66 @@ heif_suberror_Nonexisting_item_referenced}; } - std::shared_ptr ispe = image->get_property(); - std::shared_ptr cmpd = image->get_property(); - std::shared_ptr uncC = image->get_property(); + UncompressedImageCodec::unci_properties properties; + properties.fill_from_image_item(image); + + auto ispe = properties.ispe; + auto uncC = properties.uncC; + auto cmpd = properties.cmpd; Error error = check_header_validity(ispe, cmpd, uncC); if (error) { return error; } + // Same uncC layout validation the full-image path runs in + // unc_decoder::decode_full_image(). Without it, a per-tile decode could reach + // a decoder with a component packing the full-image path rejects (e.g. a + // block-pixel layout whose component bit depths sum to more than the block + // width, which produces an undefined shift in + // unc_decoder_block_pixel_interleave::decode_tile). The whole-image size is + // deliberately NOT checked here: tiled images larger than the security limit + // must still be decodable tile by tile. + error = check_hard_limits(uncC); + if (error) { + return error; + } + uint32_t tile_width = ispe->get_width() / uncC->get_number_of_tile_columns(); uint32_t tile_height = ispe->get_height() / uncC->get_number_of_tile_rows(); - Result> createImgResult = create_image(cmpd, uncC, tile_width, tile_height, context->get_security_limits()); - if (createImgResult.error) { - return createImgResult.error; - } - img = createImgResult.value; + // Remember which components reference which cmpd indices. + // There can be several component ids referencing the same cmpd index. + std::vector> cmpd_index_to_comp_ids; + std::vector uncC_index_to_comp_ids; + + Result> createImgResult = create_image(properties, tile_width, tile_height, uncC_index_to_comp_ids, context->get_security_limits()); + if (!createImgResult) { + return createImgResult.error(); + } + img = *createImgResult; - AbstractDecoder* decoder = makeDecoder(ispe->get_width(), ispe->get_height(), cmpd, uncC); - if (decoder == nullptr) { - std::stringstream sstr; - sstr << "Uncompressed interleave_type of " << ((int) uncC->get_interleave_type()) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); + auto decoderResult = unc_decoder_factory::get_unc_decoder(ispe->get_width(), ispe->get_height(), cmpd, uncC, uncC_index_to_comp_ids); + if (!decoderResult) { + return decoderResult.error(); } - decoder->buildChannelList(img); + auto& decoder = *decoderResult; + + DataExtent dataExtent; + dataExtent.set_from_image_item(file, ID); + + decoder->ensure_channel_list(img); - Error result = decoder->decode_tile(context, ID, img, 0, 0, - ispe->get_width(), ispe->get_height(), - tile_x0, tile_y0); - delete decoder; - return result; + std::vector tile_data; + Error err = decoder->fetch_tile_data(dataExtent, properties, tile_x0, tile_y0, tile_data); + if (err) { + return err; + } + + return decoder->decode_tile(tile_data, img, 0, 0); } @@ -580,11 +491,20 @@ if (cmpd) { for (const auto& comp : uncC->get_components()) { - if (comp.component_index > cmpd->get_components().size()) { + if (comp.component_index >= cmpd->get_components().size()) { return {heif_error_Invalid_input, heif_suberror_Unspecified, "Invalid component index in uncC box"}; } + + uint16_t component_type = cmpd->get_components()[comp.component_index].component_type; + if (component_type > 7 && component_type != heif_cmpd_component_type_padded && component_type != heif_cmpd_component_type_filter_array) { + std::stringstream sstr; + sstr << "Uncompressed image with component_type " << ((int) component_type) << " is not implemented yet"; + return {heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + sstr.str()}; + } } } @@ -615,6 +535,7 @@ } +// TODO: this should be deprecated and replaced with the function taking unci_properties/DataExtent Error UncompressedImageCodec::decode_uncompressed_image(const HeifContext* context, heif_item_id ID, std::shared_ptr& img) @@ -633,18 +554,14 @@ heif_suberror_Nonexisting_item_referenced}; } - std::shared_ptr ispe = image->get_property(); - std::shared_ptr cmpd = image->get_property(); - std::shared_ptr uncC = image->get_property(); + UncompressedImageCodec::unci_properties properties; + properties.fill_from_image_item(image); - error = check_header_validity(ispe, cmpd, uncC); - if (error) { - return error; - } + auto ispe = properties.ispe; + auto uncC = properties.uncC; + auto cmpd = properties.cmpd; - // check if we support the type of image - - error = uncompressed_image_type_is_supported(uncC, cmpd); // TODO TODO TODO + error = check_header_validity(ispe, cmpd, uncC); if (error) { return error; } @@ -657,224 +574,107 @@ return error; } - Result> createImgResult = create_image(cmpd, uncC, width, height, context->get_security_limits()); - if (createImgResult.error) { - return createImgResult.error; - } - else { - img = *createImgResult; + if (uncC->get_pixel_size() > 0 && + UINT32_MAX / uncC->get_pixel_size() / width < height) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Aligned total image size exceeds maximum integer range" + }; } - AbstractDecoder* decoder = makeDecoder(width, height, cmpd, uncC); - if (decoder == nullptr) { - std::stringstream sstr; - sstr << "Uncompressed interleave_type of " << ((int) uncC->get_interleave_type()) << " is not implemented yet"; - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - sstr.str()); + if (uncC->get_row_align_size() > 0 && + UINT32_MAX / uncC->get_row_align_size() < 8) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Aligned row size larger than supported maximum" + }; } - decoder->buildChannelList(img); - - uint32_t tile_width = width / uncC->get_number_of_tile_columns(); - uint32_t tile_height = height / uncC->get_number_of_tile_rows(); + DataExtent dataExtent; + dataExtent.set_from_image_item(context->get_heif_file(), ID); - for (uint32_t tile_y0 = 0; tile_y0 < height; tile_y0 += tile_height) - for (uint32_t tile_x0 = 0; tile_x0 < width; tile_x0 += tile_width) { - error = decoder->decode_tile(context, ID, img, tile_x0, tile_y0, - width, height, - tile_x0 / tile_width, tile_y0 / tile_height); - if (error) { - delete decoder; - return error; - } - } + auto result = unc_decoder::decode_full_image(properties, dataExtent, context->get_security_limits()); + if (!result) { + return result.error(); + } - //Error result = decoder->decode(source_data, img); - delete decoder; + img = *result; return Error::Ok; } -Error fill_cmpd_and_uncC(std::shared_ptr& cmpd, - std::shared_ptr& uncC, - const std::shared_ptr& image, - const heif_unci_image_parameters* parameters) + +void UncompressedImageCodec::unci_properties::fill_from_image_item(const std::shared_ptr& image) { - uint32_t nTileColumns = parameters->image_width / parameters->tile_width; - uint32_t nTileRows = parameters->image_height / parameters->tile_height; + ispe = image->get_property(); + auto cmpd_mut = image->get_property(); + auto uncC_mut = image->get_property(); + if (uncC_mut) { + fill_uncC_and_cmpd_from_profile(uncC_mut, cmpd_mut); + } + cmpd = cmpd_mut; + uncC = uncC_mut; + cmpC = image->get_property(); + icef = image->get_property(); + cpat = image->get_property(); + splz = image->get_all_properties(); + sbpm = image->get_all_properties(); + snuc = image->get_all_properties(); + cloc = image->get_property(); + + // The ImageItem already populated its ImageDescription::m_components in + // ImageItem_uncompressed::populate_component_descriptions(). The decoder + // clones those descriptions into the new HeifPixelImage instead of minting + // ids itself. + source_extra_data = image.get(); +} - const heif_colorspace colourspace = image->get_colorspace(); - if (colourspace == heif_colorspace_YCbCr) { - if (!(image->has_channel(heif_channel_Y) && image->has_channel(heif_channel_Cb) && image->has_channel(heif_channel_Cr))) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Invalid colourspace / channel combination - YCbCr"); - } - Box_cmpd::Component yComponent = {component_type_Y}; - cmpd->add_component(yComponent); - Box_cmpd::Component cbComponent = {component_type_Cb}; - cmpd->add_component(cbComponent); - Box_cmpd::Component crComponent = {component_type_Cr}; - cmpd->add_component(crComponent); - uint8_t bpp_y = image->get_bits_per_pixel(heif_channel_Y); - Box_uncC::Component component0 = {0, bpp_y, component_format_unsigned, 0}; - uncC->add_component(component0); - uint8_t bpp_cb = image->get_bits_per_pixel(heif_channel_Cb); - Box_uncC::Component component1 = {1, bpp_cb, component_format_unsigned, 0}; - uncC->add_component(component1); - uint8_t bpp_cr = image->get_bits_per_pixel(heif_channel_Cr); - Box_uncC::Component component2 = {2, bpp_cr, component_format_unsigned, 0}; - uncC->add_component(component2); - if (image->get_chroma_format() == heif_chroma_444) { - uncC->set_sampling_type(sampling_mode_no_subsampling); - } - else if (image->get_chroma_format() == heif_chroma_422) { - uncC->set_sampling_type(sampling_mode_422); - } - else if (image->get_chroma_format() == heif_chroma_420) { - uncC->set_sampling_type(sampling_mode_420); - } - else { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Unsupported YCbCr sub-sampling type"); - } - uncC->set_interleave_type(interleave_mode_component); - uncC->set_block_size(0); - uncC->set_components_little_endian(false); - uncC->set_block_pad_lsb(false); - uncC->set_block_little_endian(false); - uncC->set_block_reversed(false); - uncC->set_pad_unknown(false); - uncC->set_pixel_size(0); - uncC->set_row_align_size(0); - uncC->set_tile_align_size(0); - uncC->set_number_of_tile_columns(nTileColumns); - uncC->set_number_of_tile_rows(nTileRows); - } - else if (colourspace == heif_colorspace_RGB) { - if (!((image->get_chroma_format() == heif_chroma_444) || - (image->get_chroma_format() == heif_chroma_interleaved_RGB) || - (image->get_chroma_format() == heif_chroma_interleaved_RGBA) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE))) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Unsupported colourspace / chroma combination - RGB"); - } - Box_cmpd::Component rComponent = {component_type_red}; - cmpd->add_component(rComponent); - Box_cmpd::Component gComponent = {component_type_green}; - cmpd->add_component(gComponent); - Box_cmpd::Component bComponent = {component_type_blue}; - cmpd->add_component(bComponent); - if ((image->get_chroma_format() == heif_chroma_interleaved_RGBA) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE) || - (image->has_channel(heif_channel_Alpha))) { - Box_cmpd::Component alphaComponent = {component_type_alpha}; - cmpd->add_component(alphaComponent); - } - if ((image->get_chroma_format() == heif_chroma_interleaved_RGB) || - (image->get_chroma_format() == heif_chroma_interleaved_RGBA) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE)) { - uncC->set_interleave_type(interleave_mode_pixel); - int bpp = image->get_bits_per_pixel(heif_channel_interleaved); - uint8_t component_align = 1; - if (bpp == 8) { - component_align = 0; - } - else if (bpp > 8) { - component_align = 2; - } - Box_uncC::Component component0 = {0, (uint8_t) (bpp), component_format_unsigned, component_align}; - uncC->add_component(component0); - Box_uncC::Component component1 = {1, (uint8_t) (bpp), component_format_unsigned, component_align}; - uncC->add_component(component1); - Box_uncC::Component component2 = {2, (uint8_t) (bpp), component_format_unsigned, component_align}; - uncC->add_component(component2); - if ((image->get_chroma_format() == heif_chroma_interleaved_RGBA) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE)) { - Box_uncC::Component component3 = { - 3, (uint8_t) (bpp), component_format_unsigned, component_align}; - uncC->add_component(component3); - } - } - else { - uncC->set_interleave_type(interleave_mode_component); - int bpp_red = image->get_bits_per_pixel(heif_channel_R); - Box_uncC::Component component0 = {0, (uint8_t) (bpp_red), component_format_unsigned, 0}; - uncC->add_component(component0); - int bpp_green = image->get_bits_per_pixel(heif_channel_G); - Box_uncC::Component component1 = {1, (uint8_t) (bpp_green), component_format_unsigned, 0}; - uncC->add_component(component1); - int bpp_blue = image->get_bits_per_pixel(heif_channel_B); - Box_uncC::Component component2 = {2, (uint8_t) (bpp_blue), component_format_unsigned, 0}; - uncC->add_component(component2); - if (image->has_channel(heif_channel_Alpha)) { - int bpp_alpha = image->get_bits_per_pixel(heif_channel_Alpha); - Box_uncC::Component component3 = {3, (uint8_t) (bpp_alpha), component_format_unsigned, 0}; - uncC->add_component(component3); - } - } - uncC->set_sampling_type(sampling_mode_no_subsampling); - uncC->set_block_size(0); - if ((image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE) || - (image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE)) { - uncC->set_components_little_endian(true); - } - else { - uncC->set_components_little_endian(false); - } - uncC->set_block_pad_lsb(false); - uncC->set_block_little_endian(false); - uncC->set_block_reversed(false); - uncC->set_pad_unknown(false); - uncC->set_pixel_size(0); - uncC->set_row_align_size(0); - uncC->set_tile_align_size(0); - uncC->set_number_of_tile_columns(nTileColumns); - uncC->set_number_of_tile_rows(nTileRows); - } - else if (colourspace == heif_colorspace_monochrome) { - Box_cmpd::Component monoComponent = {component_type_monochrome}; - cmpd->add_component(monoComponent); - if (image->has_channel(heif_channel_Alpha)) { - Box_cmpd::Component alphaComponent = {component_type_alpha}; - cmpd->add_component(alphaComponent); - } - int bpp = image->get_bits_per_pixel(heif_channel_Y); - Box_uncC::Component component0 = {0, (uint8_t) (bpp), component_format_unsigned, 0}; - uncC->add_component(component0); - if (image->has_channel(heif_channel_Alpha)) { - bpp = image->get_bits_per_pixel(heif_channel_Alpha); - Box_uncC::Component component1 = {1, (uint8_t) (bpp), component_format_unsigned, 0}; - uncC->add_component(component1); - } - uncC->set_sampling_type(sampling_mode_no_subsampling); - uncC->set_interleave_type(interleave_mode_component); - uncC->set_block_size(0); - uncC->set_components_little_endian(false); - uncC->set_block_pad_lsb(false); - uncC->set_block_little_endian(false); - uncC->set_block_reversed(false); - uncC->set_pad_unknown(false); - uncC->set_pixel_size(0); - uncC->set_row_align_size(0); - uncC->set_tile_align_size(0); - uncC->set_number_of_tile_columns(nTileColumns); - uncC->set_number_of_tile_rows(nTileRows); + +Result> +UncompressedImageCodec::decode_uncompressed_image(const UncompressedImageCodec::unci_properties& properties, + const DataExtent& extent, + const heif_security_limits* securityLimits) +{ + const std::shared_ptr& ispe = properties.ispe; + const std::shared_ptr& cmpd = properties.cmpd; + const std::shared_ptr& uncC = properties.uncC; + + Error error = check_header_validity(ispe, cmpd, uncC); + if (error) { + return error; } - else { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Unsupported colourspace"); + + assert(ispe); + uint32_t width = ispe->get_width(); + uint32_t height = ispe->get_height(); + error = check_for_valid_image_size(securityLimits, width, height); + if (error) { + return error; } - return Error::Ok; + + if (uncC->get_pixel_size() > 0 && + UINT32_MAX / uncC->get_pixel_size() / width < height) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Aligned total image size exceeds maximum integer range" + }; + } + + // Same up-front row-alignment sanity check as the still-image decode path + // (decode_uncompressed_image(context, ID, img)). The sequence path was missing + // it, letting a huge row_align_size flow into the tile decoders. The tile + // decoders themselves are now overflow-safe, but reject the nonsensical value + // here too so both paths behave identically. + if (uncC->get_row_align_size() > 0 && + UINT32_MAX / uncC->get_row_align_size() < 8) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Aligned row size larger than supported maximum" + }; + } + + return unc_decoder::decode_full_image(properties, extent, securityLimits); } diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_codec.h libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_codec.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.h 2026-09-06 14:45:17.000000000 +0000 @@ -22,9 +22,14 @@ #ifndef LIBHEIF_UNC_CODEC_H #define LIBHEIF_UNC_CODEC_H -#include "pixelimage.h" +#include "image/pixelimage.h" #include "file.h" #include "context.h" +#include "libheif/heif_uncompressed.h" + +#if WITH_UNCOMPRESSED_CODEC +#include "unc_boxes.h" +#endif #include #include @@ -35,18 +40,12 @@ class HeifContext; -bool isKnownUncompressedFrameConfigurationBoxProfile(const std::shared_ptr& uncC); - -Error fill_cmpd_and_uncC(std::shared_ptr& cmpd, - std::shared_ptr& uncC, - const std::shared_ptr& image, - const heif_unci_image_parameters* parameters); - bool map_uncompressed_component_to_channel(const std::shared_ptr &cmpd, - const std::shared_ptr &uncC, Box_uncC::Component component, heif_channel *channel); +heif_component_datatype unc_component_format_to_datatype(uint8_t format); + class UncompressedImageCodec { @@ -60,16 +59,42 @@ std::shared_ptr& img, uint32_t tile_x0, uint32_t tile_y0); + struct unci_properties { + std::shared_ptr ispe; + std::shared_ptr cmpd; + std::shared_ptr uncC; + std::shared_ptr cmpC; + std::shared_ptr icef; + std::shared_ptr cpat; + std::vector> splz; + std::vector> sbpm; + std::vector> snuc; + std::shared_ptr cloc; + + // Source ImageDescription (typically the ImageItem) to clone the + // pre-populated component descriptions from. Owned externally (we hold + // the shared_ptr to the item via the boxes above keeping the file alive, + // and the caller passes a stable pointer). + const ImageDescription* source_extra_data = nullptr; + + void fill_from_image_item(const std::shared_ptr&); + }; + + static Result> decode_uncompressed_image(const unci_properties& properties, + const struct DataExtent& extent, + const heif_security_limits*); + + static Error get_heif_chroma_uncompressed(const std::shared_ptr& uncC, const std::shared_ptr& cmpd, heif_chroma* out_chroma, heif_colorspace* out_colourspace, bool* out_has_alpha); - static Result> create_image(std::shared_ptr, - std::shared_ptr, + static Result> create_image(const unci_properties& properties, uint32_t width, uint32_t height, + std::vector& uncC_index_to_comp_ids, const heif_security_limits* limits); static Error check_header_validity(std::optional>, diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_dec.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,12 +19,27 @@ */ #include "codecs/uncompressed/unc_dec.h" +#include "codecs/uncompressed/unc_boxes.h" #include "codecs/uncompressed/unc_codec.h" #include "error.h" #include "context.h" #include #include +#include + + +Decoder_uncompressed::Decoder_uncompressed(std::shared_ptr uncC, + std::shared_ptr cmpd, + std::shared_ptr ispe) + : m_ispe(std::move(ispe)) +{ + if (uncC) { + fill_uncC_and_cmpd_from_profile(uncC, cmpd); + } + m_uncC = std::move(uncC); + m_cmpd = std::move(cmpd); +} Result> Decoder_uncompressed::read_bitstream_configuration_data() const @@ -38,31 +53,26 @@ assert(m_uncC); if (!m_cmpd) { - if (isKnownUncompressedFrameConfigurationBoxProfile(m_uncC)) { - return 8; - } - else { - return -1; - } + return -1; } int luma_bits = 0; int alternate_channel_bits = 0; for (Box_uncC::Component component : m_uncC->get_components()) { - uint16_t component_index = component.component_index; + uint32_t component_index = component.component_index; if (component_index >= m_cmpd->get_components().size()) { return -1; } auto component_type = m_cmpd->get_components()[component_index].component_type; switch (component_type) { - case component_type_monochrome: - case component_type_red: - case component_type_green: - case component_type_blue: - case component_type_filter_array: + case heif_cmpd_component_type_monochrome: + case heif_cmpd_component_type_red: + case heif_cmpd_component_type_green: + case heif_cmpd_component_type_blue: + case heif_cmpd_component_type_filter_array: alternate_channel_bits = std::max(alternate_channel_bits, (int) component.component_bit_depth); break; - case component_type_Y: + case heif_cmpd_component_type_Y: luma_bits = std::max(luma_bits, (int) component.component_bit_depth); break; // TODO: there are other things we'll need to handle eventually, like palette. @@ -93,21 +103,21 @@ int chroma_bits = 0; int alternate_channel_bits = 0; for (Box_uncC::Component component : m_uncC->get_components()) { - uint16_t component_index = component.component_index; + uint32_t component_index = component.component_index; if (component_index >= m_cmpd->get_components().size()) { return -1; } auto component_type = m_cmpd->get_components()[component_index].component_type; switch (component_type) { - case component_type_monochrome: - case component_type_red: - case component_type_green: - case component_type_blue: - case component_type_filter_array: + case heif_cmpd_component_type_monochrome: + case heif_cmpd_component_type_red: + case heif_cmpd_component_type_green: + case heif_cmpd_component_type_blue: + case heif_cmpd_component_type_filter_array: alternate_channel_bits = std::max(alternate_channel_bits, (int) component.component_bit_depth); break; - case component_type_Cb: - case component_type_Cr: + case heif_cmpd_component_type_Cb: + case heif_cmpd_component_type_Cr: chroma_bits = std::max(chroma_bits, (int) component.component_bit_depth); break; // TODO: there are other things we'll need to handle eventually, like palette. @@ -161,3 +171,58 @@ return has_alpha; } + + +Result> +Decoder_uncompressed::decode_single_frame_from_compressed_data(const struct heif_decoding_options& options, + const struct heif_security_limits* limits) +{ + UncompressedImageCodec::unci_properties properties; + properties.uncC = m_uncC; + properties.cmpd = m_cmpd; + properties.ispe = m_ispe; + properties.cpat = m_cpat; + properties.cmpC = m_cmpC; + properties.icef = m_icef; + properties.splz = m_splz; + properties.sbpm = m_sbpm; + properties.snuc = m_snuc; + properties.cloc = m_cloc; + + auto decodeResult = UncompressedImageCodec::decode_uncompressed_image(properties, + get_data_extent(), + limits); + return decodeResult; +} + + +Error +Decoder_uncompressed::decode_sequence_frame_from_compressed_data(bool upload_configuration_NALs, + const heif_decoding_options& options, + uintptr_t user_data, + const heif_security_limits* limits) +{ + auto decodingResult = decode_single_frame_from_compressed_data(options, limits); + if (decodingResult.error()) { + return decodingResult.error(); + } + + m_decoded_image = *decodingResult; + m_decoded_image_user_data = user_data; + + return {}; +} + +Result > Decoder_uncompressed::get_decoded_frame(const heif_decoding_options& options, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + auto img = std::move(m_decoded_image); + m_decoded_image.reset(); + + if (out_user_data) { + *out_user_data = m_decoded_image_user_data; + } + + return img; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_dec.h libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_dec.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_dec.h 2026-09-06 14:45:17.000000000 +0000 @@ -28,16 +28,38 @@ #include #include #include "codecs/decoder.h" +#include class Box_uncC; class Box_cmpd; +class Box_cpat; +class Box_cmpC; +class Box_icef; +class Box_splz; +class Box_sbpm; +class Box_snuc; +class Box_cloc; class Decoder_uncompressed : public Decoder { public: - explicit Decoder_uncompressed(const std::shared_ptr& uncC, - const std::shared_ptr& cmpd) : m_uncC(uncC), m_cmpd(cmpd) {} + explicit Decoder_uncompressed(std::shared_ptr uncC, + std::shared_ptr cmpd, + std::shared_ptr ispe); + + void set_cpat(std::shared_ptr cpat) { m_cpat = std::move(cpat); } + void set_cmpC(std::shared_ptr cmpC) { m_cmpC = std::move(cmpC); } + void set_icef(std::shared_ptr icef) { m_icef = std::move(icef); } + void set_cloc(std::shared_ptr cloc) { m_cloc = std::move(cloc); } + void set_splz(std::vector> splz) { m_splz = std::move(splz); } + void set_sbpm(std::vector> sbpm) { m_sbpm = std::move(sbpm); } + void set_snuc(std::vector> snuc) { m_snuc = std::move(snuc); } + + // Overloads accepting non-const shared_ptrs (from get_child_boxes) + void set_splz(std::vector> splz) { m_splz.assign(splz.begin(), splz.end()); } + void set_sbpm(std::vector> sbpm) { m_sbpm.assign(sbpm.begin(), sbpm.end()); } + void set_snuc(std::vector> snuc) { m_snuc.assign(snuc.begin(), snuc.end()); } heif_compression_format get_compression_format() const override { return heif_compression_uncompressed; } @@ -51,9 +73,40 @@ Result> read_bitstream_configuration_data() const override; + Result> + decode_single_frame_from_compressed_data(const struct heif_decoding_options& options, + const struct heif_security_limits* limits) override; + + + // Push data for one frame into decoder. + Error + decode_sequence_frame_from_compressed_data(bool upload_configuration_NALs, + const heif_decoding_options& options, + uintptr_t user_data, + const heif_security_limits* limits) override; + + Error flush_decoder() override { return {}; } + + // Get a decoded frame from the decoder. + // It may return NULL when there is buffering in the codec. + Result > get_decoded_frame(const heif_decoding_options& options, + uintptr_t* out_user_data, + const heif_security_limits* limits) override; + private: - const std::shared_ptr m_uncC; - const std::shared_ptr m_cmpd; + std::shared_ptr m_uncC; + std::shared_ptr m_cmpd; + std::shared_ptr m_ispe; + std::shared_ptr m_cpat; + std::shared_ptr m_cmpC; + std::shared_ptr m_icef; + std::vector> m_splz; + std::vector> m_sbpm; + std::vector> m_snuc; + std::shared_ptr m_cloc; + + std::shared_ptr m_decoded_image; + uintptr_t m_decoded_image_user_data; }; #endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,513 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include +#include +#include + +#include "unc_decoder.h" +#include "unc_decoder_component_interleave.h" +#include "unc_decoder_pixel_interleave.h" +#include "unc_decoder_mixed_interleave.h" +#include "unc_decoder_row_interleave.h" +#include "unc_decoder_block_pixel_interleave.h" +#include "unc_decoder_bytealign_component_interleave.h" +#include "unc_decoder_block_component_interleave.h" +#include "unc_codec.h" +#include "unc_boxes.h" +#include "compression.h" +#include "codecs/decoder.h" +#include "security_limits.h" +#include + + +// --- unc_decoder --- + +unc_decoder::unc_decoder(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) + : m_width(width), + m_height(height), + m_cmpd(cmpd), + m_uncC(uncC), + m_uncC_index_to_comp_ids(uncC_index_to_comp_ids) +{ + m_tile_height = m_height / m_uncC->get_number_of_tile_rows(); + m_tile_width = m_width / m_uncC->get_number_of_tile_columns(); + + assert(m_tile_width > 0); + assert(m_tile_height > 0); +} + + +Error unc_decoder::fetch_tile_data(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + uint32_t tile_x, uint32_t tile_y, + std::vector& tile_data) +{ + if (m_tile_width == 0 || m_tile_height == 0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Internal error: unc_decoder tile dimensions are 0"}; + } + + auto sizesResult = get_tile_data_sizes(); + if (sizesResult.is_error()) { + return sizesResult.error(); + } + const auto& sizes = *sizesResult; + uint32_t tileIdx = tile_x + tile_y * (m_width / m_tile_width); + + if (sizes.size() == 1) { + // Single contiguous read (component, pixel, mixed, row interleave) + uint64_t tile_start_offset = sizes[0] * tileIdx; + return get_compressed_image_data_uncompressed(dataExtent, properties, &tile_data, tile_start_offset, sizes[0], tileIdx, nullptr); + } + else { + // Scattered per-component reads (tile_component interleave) + uint32_t num_tiles = (m_width / m_tile_width) * (m_height / m_tile_height); + uint64_t component_offset = 0; + + for (uint64_t size : sizes) { + uint64_t tile_start = component_offset + size * tileIdx; + + std::vector channel_data; + Error err = get_compressed_image_data_uncompressed(dataExtent, properties, &channel_data, tile_start, size, tileIdx, nullptr); + if (err) { + return err; + } + + tile_data.insert(tile_data.end(), channel_data.begin(), channel_data.end()); + component_offset += size * num_tiles; + } + } + + return Error::Ok; +} + + +const Error unc_decoder::get_compressed_image_data_uncompressed(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + std::vector* data, + uint64_t range_start_offset, uint64_t range_size, + uint32_t tile_idx, + const Box_iloc::Item* item) const +{ + // --- get codec configuration + + std::shared_ptr cmpC_box = properties.cmpC; + std::shared_ptr icef_box = properties.icef; + + // Security limits used to bound the (potentially highly amplified) decompressed + // output. May be nullptr for a raw data extent, in which case no limit applies. + const heif_security_limits* limits = dataExtent.m_file ? dataExtent.m_file->get_security_limits() : nullptr; + + if (!cmpC_box) { + // assume no generic compression + auto readResult = dataExtent.read_data(range_start_offset, range_size); + if (!readResult) { + return readResult.error(); + } + + data->insert(data->end(), readResult->begin(), readResult->end()); + + return Error::Ok; + } + + if (icef_box && cmpC_box->get_compressed_unit_type() == heif_cmpC_compressed_unit_type_image_tile) { + const auto& units = icef_box->get_units(); + if (tile_idx >= units.size()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "no icef-box entry for tile index" + }; + } + + const auto unit = units[tile_idx]; + + // get data needed for one tile + Result > readingResult = dataExtent.read_data(unit.unit_offset, unit.unit_size); + if (!readingResult) { + return readingResult.error(); + } + + const std::vector& compressed_bytes = *readingResult; + + // decompress only the unit + auto dataResult = do_decompress_data(cmpC_box, compressed_bytes, limits); + if (!dataResult) { + return dataResult.error(); + } + + *data = std::move(*dataResult); + } + else if (icef_box) { + // get all data and decode all + Result*> readResult = dataExtent.read_data(); + if (!readResult) { + return readResult.error(); + } + + const std::vector compressed_bytes = std::move(**readResult); + + // Bound the total decompressed output accumulated across all units. The units + // may overlap (nothing forces them to be disjoint), so N units can point at the + // same compressed slice and be decompressed N times into `data`. Without this + // accounting, that amplifies a tiny icef box into an unbounded allocation + // (GHSA-24wx-9w62-c96w). The handle is local, so it only bounds the peak while + // building `data`; the cropped result is accounted by the caller. + MemoryHandle accumulated_memory_handle; + + for (Box_icef::CompressedUnitInfo unit_info : icef_box->get_units()) { + // Use subtraction form to avoid a uint64_t wrap in 'unit_offset + unit_size', + // which could otherwise pass this check and lead to an out-of-bounds read when + // constructing the iterators below (GHSA-r7qj-cg5r-r6vf). + if (unit_info.unit_offset > compressed_bytes.size() || + unit_info.unit_size > compressed_bytes.size() - unit_info.unit_offset) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "incomplete data in unci image" + }; + } + + auto unit_start = compressed_bytes.begin() + unit_info.unit_offset; + auto unit_end = unit_start + unit_info.unit_size; + std::vector compressed_unit_data = std::vector(unit_start, unit_end); + + auto dataResult = do_decompress_data(cmpC_box, compressed_unit_data, limits); + if (!dataResult) { + return dataResult.error(); + } + + const std::vector uncompressed_unit_data = std::move(*dataResult); + + if (Error memErr = accumulated_memory_handle.alloc(uncompressed_unit_data.size(), limits, + "unci icef decompressed units")) { + return memErr; + } + + data->insert(data->end(), uncompressed_unit_data.data(), uncompressed_unit_data.data() + uncompressed_unit_data.size()); + } + + if (range_start_offset > data->size() || + range_size > data->size() - range_start_offset) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Data range out of existing range" + }; + } + + // cut out the range that we actually need + memcpy(data->data(), data->data() + range_start_offset, range_size); + data->resize(range_size); + } + else { + // get all data and decode all + Result*> readResult = dataExtent.read_data(); + if (!readResult) { + return readResult.error(); + } + + std::vector compressed_bytes = std::move(**readResult); + + // Decode as a single blob + auto dataResult = do_decompress_data(cmpC_box, compressed_bytes, limits); + if (!dataResult) { + return dataResult.error(); + } + + *data = std::move(*dataResult); + + // Use subtraction form to avoid a uint64_t wrap in 'range_start_offset + range_size'. + // A crafted tiling can make the requested tile range wrap to zero, passing the + // addition-form check and leading to an out-of-bounds read in the memcpy() below + // (GHSA-hh47-fhqr-cj2r; same root cause as the icef sibling branch above, GHSA-73p7-m7gg-w2jv). + if (range_start_offset > data->size() || + range_size > data->size() - range_start_offset) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Data range out of existing range" + }; + } + + // cut out the range that we actually need + memcpy(data->data(), data->data() + range_start_offset, range_size); + data->resize(range_size); + } + + return Error::Ok; +} + + +Result > unc_decoder::do_decompress_data(std::shared_ptr& cmpC_box, + const std::vector& compressed_data, + const heif_security_limits* limits) const +{ + if (cmpC_box->get_compression_type() == fourcc("brot")) { +#if HAVE_BROTLI + return decompress_brotli(compressed_data, limits); +#else + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_generic_compression_method, + "cannot decode unci item with brotli compression - not enabled"); +#endif + } + else if (cmpC_box->get_compression_type() == fourcc("zlib")) { +#if HAVE_ZLIB + return decompress_zlib(compressed_data, limits); +#else + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_generic_compression_method, + "cannot decode unci item with zlib compression - not enabled"); +#endif + } + else if (cmpC_box->get_compression_type() == fourcc("defl")) { +#if HAVE_ZLIB + return decompress_deflate(compressed_data, limits); +#else + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_generic_compression_method, + "cannot decode unci item with deflate compression - not enabled"); +#endif + } + else { + std::stringstream sstr; + sstr << "cannot decode unci item with unsupported compression type: " << cmpC_box->get_compression_type() << '\n'; + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_generic_compression_method, + sstr.str()); + } +} + + +Error unc_decoder::decode_image(const DataExtent& extent, + const UncompressedImageCodec::unci_properties& properties, + std::shared_ptr& img) +{ + uint32_t tile_width = m_width / m_uncC->get_number_of_tile_columns(); + uint32_t tile_height = m_height / m_uncC->get_number_of_tile_rows(); + + ensure_channel_list(img); + + for (uint32_t tile_y0 = 0; tile_y0 < m_height; tile_y0 += tile_height) + for (uint32_t tile_x0 = 0; tile_x0 < m_width; tile_x0 += tile_width) { + std::vector tile_data; + Error error = fetch_tile_data(extent, properties, tile_x0 / tile_width, tile_y0 / tile_height, tile_data); + if (error) { + return error; + } + + error = decode_tile(tile_data, img, tile_x0, tile_y0); + if (error) { + return error; + } + } + + return Error::Ok; +} + + +// --- unc_decoder_factory --- + +bool unc_decoder_factory::check_common_requirements(const std::shared_ptr& uncC) +{ + for (const auto& component : uncC->get_components()) { + if (component.component_bit_depth > 16) { + return false; + } + if (component.component_format != component_format_unsigned) { + return false; + } + if (component.component_align_size > 2) { + return false; + } + } + + if (uncC->get_block_size() != 0) { + return false; + } + if (uncC->is_block_pad_lsb()) { + return false; + } + if (uncC->is_block_little_endian()) { + return false; + } + if (uncC->is_block_reversed()) { + return false; + } + +#if 0 + if (uncC->is_components_little_endian()) { + const auto& comps = uncC->get_components(); + bool all_8_bit = std::all_of(comps.begin(), comps.end(), + [](const Box_uncC::Component& c) { return c.component_bit_depth == 8; }); + if (!all_8_bit) { + return false; + } + } +#endif + + return true; +} + + +bool unc_decoder_factory::has_any_multi_byte_components(const std::shared_ptr& uncC) +{ + const auto& comps = uncC->get_components(); + return std::any_of(comps.begin(), comps.end(), + [](const Box_uncC::Component& c) { return c.component_bit_depth > 8; }); +} + + +Error check_hard_limits(const std::shared_ptr& uncC) +{ + const auto& components = uncC->get_components(); + + for (const auto& component : components) { + switch (component.component_format) { + case heif_uncompressed_component_format::component_format_signed: + case heif_uncompressed_component_format::component_format_unsigned: + if (component.component_bit_depth > 64) { + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Maximum supported integer bit-depth is 64 bits."}; + } + break; + + case heif_uncompressed_component_format::component_format_float: + if (component.component_bit_depth != 32 && + component.component_bit_depth != 64) { + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Only 32 bit and 64 bit floats are supported."}; + } + break; + + case heif_uncompressed_component_format::component_format_complex: + if (component.component_bit_depth != 64 && + component.component_bit_depth != 128) { + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Only 2x32 bit and 2x64 bit complex values are supported."}; + } + break; + + default: + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Unknown component format."}; + } + } + + if (uncC->get_interleave_type() != 1 && + uncC->get_interleave_type() != 5 && + uncC->get_pixel_size() != 0) { + return Error{heif_error_Invalid_input, heif_suberror_Unspecified, "uncC pixel_size must be 0 for interleave_types other than 1 or 5."}; + } + + if (uncC->get_interleave_type() == interleave_mode_pixel && + uncC->get_pixel_size() != 0) { + uint32_t total_pixel_bits = 0; + for (const auto& component : uncC->get_components()) { + total_pixel_bits += component.component_bit_depth; + } + + // TODO: we do not consider padding or block sizes yet + uint32_t PixelBytes = (total_pixel_bits + 7)/8; + if (PixelBytes > uncC->get_pixel_size()) { + return Error{heif_error_Invalid_input, heif_suberror_Unspecified, "uncC pixel_size smaller than sum of component sizes."}; + } + } + + return {}; +} + + +Result > unc_decoder_factory::get_unc_decoder( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids +) +{ + static unc_decoder_factory_component_interleave dec_component; + static unc_decoder_factory_bytealign_component_interleave dec_bytealign_component; + static unc_decoder_factory_block_component_interleave dec_block_component; + static unc_decoder_factory_pixel_interleave dec_pixel; + static unc_decoder_factory_block_pixel_interleave dec_block_pixel; + static unc_decoder_factory_mixed_interleave dec_mixed; + static unc_decoder_factory_row_interleave dec_row; + + static const unc_decoder_factory* decoders[]{ + &dec_bytealign_component, &dec_block_component, &dec_component, &dec_pixel, &dec_block_pixel, &dec_mixed, &dec_row + }; + + for (const unc_decoder_factory* dec : decoders) { + if (dec->can_decode(uncC)) { + return {dec->create(width, height, cmpd, uncC, uncC_index_to_comp_ids)}; + } + } + + std::stringstream sstr; + sstr << "No decoder found for uncompressed format (interleave_type of " << ((int) uncC->get_interleave_type()) << ")"; + return Error{heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, sstr.str()}; +} + + +// --- decode orchestration --- + +Result > unc_decoder::decode_full_image( + const UncompressedImageCodec::unci_properties& properties, + const DataExtent& extent, + const heif_security_limits* limits) +{ + const std::shared_ptr& ispe = properties.ispe; + const std::shared_ptr& cmpd = properties.cmpd; + const std::shared_ptr& uncC = properties.uncC; + + assert(ispe); + uint32_t width = ispe->get_width(); + uint32_t height = ispe->get_height(); + + Error global_limit_error = check_hard_limits(uncC); + if (global_limit_error) { + return {global_limit_error}; + } + + std::vector uncC_index_to_comp_ids; + + Result > createImgResult = UncompressedImageCodec::create_image(properties, width, height, uncC_index_to_comp_ids, limits); + if (!createImgResult) { + return createImgResult.error(); + } + + auto img = *createImgResult; + + + auto decoderResult = unc_decoder_factory::get_unc_decoder(width, height, cmpd, uncC, uncC_index_to_comp_ids); + if (!decoderResult) { + return decoderResult.error(); + } + + auto& decoder = *decoderResult; + + Error error = decoder->decode_image(extent, properties, img); + if (error) { + return error; + } + + return img; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,123 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_H +#define LIBHEIF_UNC_DECODER_H + +#include +#include +#include + +#include "error.h" +#include "unc_codec.h" +#include "unc_boxes.h" + +class HeifPixelImage; +struct DataExtent; +struct heif_security_limits; + + +// Validate the uncC component formats, bit depths and pixel packing. These are +// image-size independent invariants, so this must run on every decode entry +// point -- both the full-image path (unc_decoder::decode_full_image) and the +// per-tile path (UncompressedImageCodec::decode_uncompressed_image_tile). +Error check_hard_limits(const std::shared_ptr& uncC); + + +class unc_decoder +{ +public: + virtual ~unc_decoder() = default; + + virtual void ensure_channel_list(std::shared_ptr& img) {} + + Error fetch_tile_data(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + uint32_t tile_x, uint32_t tile_y, + std::vector& tile_data); + + virtual Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) = 0; + + Error decode_image(const DataExtent& extent, + const UncompressedImageCodec::unci_properties& properties, + std::shared_ptr& img); + + static Result> decode_full_image( + const UncompressedImageCodec::unci_properties& properties, + const DataExtent& extent, + const heif_security_limits* limits); + +protected: + unc_decoder(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + + virtual Result> get_tile_data_sizes() const = 0; + + const Error get_compressed_image_data_uncompressed(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + std::vector* data, + uint64_t range_start_offset, uint64_t range_size, + uint32_t tile_idx, + const Box_iloc::Item* item) const; + + Result> do_decompress_data(std::shared_ptr& cmpC_box, + const std::vector& compressed_data, + const heif_security_limits* limits) const; + + const uint32_t m_width; + const uint32_t m_height; + const std::shared_ptr m_cmpd; + const std::shared_ptr m_uncC; + const std::vector m_uncC_index_to_comp_ids; + uint32_t m_tile_height; + uint32_t m_tile_width; +}; + + +class unc_decoder_factory +{ +public: + virtual ~unc_decoder_factory() = default; + + static Result> get_unc_decoder( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + +protected: + static bool check_common_requirements(const std::shared_ptr& uncC); + + static bool has_any_multi_byte_components(const std::shared_ptr& uncC); + + virtual bool can_decode(const std::shared_ptr& uncC) const = 0; + + virtual std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const = 0; +}; + +#endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,250 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_block_component_interleave.h" +#include "unc_decoder_legacybase.h" // for skip_to_alignment +#include "unc_codec.h" +#include "unc_types.h" +#include "error.h" + +#include +#include +#include +#include + + +unc_decoder_block_component_interleave::unc_decoder_block_component_interleave( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) + : unc_decoder(width, height, cmpd, uncC, uncC_index_to_comp_ids) +{ +} + + +Result> unc_decoder_block_component_interleave::get_tile_data_sizes() const +{ + const uint32_t block_size = m_uncC->get_block_size(); + assert(block_size > 0); + + if (m_tile_width > UINT32_MAX / block_size) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + + uint64_t total_tile_size = 0; + + for (const auto& component : m_uncC->get_components()) { + (void) component; + uint32_t bytes_per_row = block_size * m_tile_width; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + total_tile_size += static_cast(bytes_per_row) * m_tile_height; + } + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{total_tile_size}; +} + + +Error unc_decoder_block_component_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + const uint32_t block_size = m_uncC->get_block_size(); + const bool little_endian = m_uncC->is_block_little_endian(); + const bool pad_lsb = m_uncC->is_block_pad_lsb(); + + // Build per-component info + struct ComponentInfo { + uint32_t shift; + uint64_t mask; + uint32_t bytes_per_sample; + bool use; + uint8_t* dst_plane; + size_t dst_plane_stride; + }; + + const auto& components = m_uncC->get_components(); + const uint32_t num_components = static_cast(components.size()); + std::vector comp(num_components); + + for (uint32_t i = 0; i < num_components; i++) { + const auto& c = components[i]; + comp[i].bytes_per_sample = (c.component_bit_depth + 7) / 8; + comp[i].mask = (uint64_t{1} << c.component_bit_depth) - 1; + + // Each component is alone in its block, so shift depends only on padding. + if (pad_lsb) { + comp[i].shift = block_size * 8 - c.component_bit_depth; + } + else { + comp[i].shift = 0; + } + + comp[i].dst_plane = img->get_component(m_uncC_index_to_comp_ids[i], &comp[i].dst_plane_stride); + comp[i].use = true; + +#if 0 + heif_channel channel; + comp[i].use = map_uncompressed_component_to_channel(m_cmpd, c, &channel); + if (comp[i].use) { + comp[i].dst_plane = img->get_channel_memory(channel, &comp[i].dst_plane_stride); + } + else { + comp[i].dst_plane = nullptr; + comp[i].dst_plane_stride = 0; + } +#endif + } + + // Address tile_data by 64-bit offset, not by raw pointer. A crafted + // row_align_size can inflate bytes_per_row to hundreds of megabytes, so the + // per-row `src += bytes_per_row - pixel_bytes` skip would overflow a 32-bit + // pointer and wrap to a small in-range address, defeating the src_end check + // and causing an out-of-bounds read on 32-bit builds. Offset arithmetic in + // uint64_t cannot wrap; the bound is checked before the pointer is formed. + const uint8_t* const src_base = tile_data.data(); + const uint64_t src_size = tile_data.size(); + uint64_t src_offset = 0; + + // Process components sequentially (component interleave) + for (uint32_t c = 0; c < num_components; c++) { + uint64_t bytes_per_row = static_cast(block_size) * m_tile_width; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { + const uint64_t row_start_offset = src_offset; + + for (uint32_t tile_x = 0; tile_x < m_tile_width; tile_x++) { + if (src_offset > src_size || block_size > src_size - src_offset) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Block-component interleave: insufficient data"}; + } + + const uint8_t* src = src_base + src_offset; + + if (comp[c].use) { + // Read block_size bytes into a uint64_t. + uint64_t block_val = 0; + if (little_endian) { + for (uint32_t b = 0; b < block_size; b++) { + block_val |= static_cast(src[b]) << (b * 8); + } + } + else { + for (uint32_t b = 0; b < block_size; b++) { + block_val = (block_val << 8) | src[b]; + } + } + + uint32_t value = static_cast((block_val >> comp[c].shift) & comp[c].mask); + uint32_t dst_x = out_x0 + tile_x; + uint32_t dst_y = out_y0 + tile_y; + uint64_t dst_offset = static_cast(dst_y) * comp[c].dst_plane_stride + + static_cast(dst_x) * comp[c].bytes_per_sample; + uint8_t* dst = comp[c].dst_plane + dst_offset; + + if (comp[c].bytes_per_sample == 1) { + *dst = static_cast(value); + } + else { + // Store in native endian (2 bytes). + if constexpr (std::endian::native == std::endian::little) { + dst[0] = static_cast(value & 0xFF); + dst[1] = static_cast((value >> 8) & 0xFF); + } + else { + dst[0] = static_cast((value >> 8) & 0xFF); + dst[1] = static_cast(value & 0xFF); + } + } + } + + src_offset += block_size; + } + + // Skip to the next row (includes any row-alignment padding). + src_offset = row_start_offset + bytes_per_row; + } + } + + return Error::Ok; +} + + +// --- Factory --- + +bool unc_decoder_factory_block_component_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (uncC->get_interleave_type() != interleave_mode_component) { + return false; + } + + if (uncC->get_block_size() == 0 || uncC->get_block_size() > 8) { + return false; + } + + if (uncC->get_pixel_size() != 0) { + return false; + } + + if (uncC->get_sampling_type() != sampling_mode_no_subsampling) { + return false; + } + + if (uncC->is_components_little_endian() == true) { + return false; + } + + uint32_t block_bits = uncC->get_block_size() * 8; + + for (const auto& component : uncC->get_components()) { + if (component.component_bit_depth > 16) { + return false; + } + if (component.component_format != component_format_unsigned) { + return false; + } + // Each component must fit in the block, and must be larger than half the + // block so that exactly one component occupies each block. + // Each component must be larger than half the block so that exactly one + // component occupies each block, and must fit within the block. + if (component.component_bit_depth > block_bits || + component.component_bit_depth <= block_bits / 2) { + return false; + } + } + + return true; +} + + +std::unique_ptr unc_decoder_factory_block_component_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_component_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,57 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_BLOCK_COMPONENT_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_BLOCK_COMPONENT_INTERLEAVE_H + +#include "unc_decoder.h" +#include +#include + + +class unc_decoder_block_component_interleave : public unc_decoder +{ +public: + unc_decoder_block_component_interleave(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; +}; + + +class unc_decoder_factory_block_component_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_BLOCK_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,270 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_block_pixel_interleave.h" +#include "unc_decoder_legacybase.h" // for skip_to_alignment +#include "unc_codec.h" +#include "unc_types.h" +#include "error.h" + +#include +#include +#include +#include + + +unc_decoder_block_pixel_interleave::unc_decoder_block_pixel_interleave( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) + : unc_decoder(width, height, cmpd, uncC, uncC_index_to_comp_ids) +{ +} + + +Result> unc_decoder_block_pixel_interleave::get_tile_data_sizes() const +{ + uint32_t pixel_size = m_uncC->get_pixel_size(); + assert(pixel_size > 0); + + if (m_tile_width > UINT32_MAX / pixel_size) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + uint32_t bytes_per_row = m_tile_width * pixel_size; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + uint64_t tile_size = static_cast(bytes_per_row) * m_tile_height; + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{tile_size}; +} + + +Error unc_decoder_block_pixel_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + uint32_t block_size = m_uncC->get_block_size(); + const uint32_t pixel_size = m_uncC->get_pixel_size(); + const bool little_endian = m_uncC->is_block_little_endian(); + const bool pad_lsb = m_uncC->is_block_pad_lsb(); + const bool reversed = m_uncC->is_block_reversed(); + + if (block_size == 0) { + block_size = pixel_size; + } + + // Build per-component info + struct ComponentInfo { + uint32_t shift; + uint64_t mask; + uint32_t bytes_per_sample; + bool use; + uint8_t* dst_plane; + size_t dst_plane_stride; + }; + + const auto& components = m_uncC->get_components(); + const uint32_t num_components = static_cast(components.size()); + std::vector comp(num_components); + + for (uint32_t i = 0; i < num_components; i++) { + const auto& c = components[i]; + comp[i].bytes_per_sample = (c.component_bit_depth + 7) / 8; + comp[i].mask = (uint64_t{1} << c.component_bit_depth) - 1; +#if 0 + heif_channel channel; + comp[i].use = map_uncompressed_component_to_channel(m_cmpd, c, &channel); + if (comp[i].use) { + comp[i].dst_plane = img->get_channel_memory(channel, &comp[i].dst_plane_stride); + } + else { + comp[i].dst_plane = nullptr; + comp[i].dst_plane_stride = 0; + } +#endif + comp[i].use = true; + comp[i].dst_plane = img->get_component(m_uncC_index_to_comp_ids[i], &comp[i].dst_plane_stride); + } + + // Compute bit shifts within the block. + if (!pad_lsb) { + uint32_t bit_offset = 0; + for (uint32_t i = 0; i < num_components; i++) { + uint32_t idx = reversed ? i : (num_components - 1 - i); + comp[idx].shift = bit_offset; + bit_offset += components[idx].component_bit_depth; + } + } + else { + uint32_t total_bits = block_size * 8; + uint32_t bit_offset = total_bits; + for (uint32_t i = 0; i < num_components; i++) { + uint32_t idx = reversed ? i : (num_components - 1 - i); + bit_offset -= components[idx].component_bit_depth; + comp[idx].shift = bit_offset; + } + } + + // Address tile_data by 64-bit offset, not by raw pointer. A crafted + // row_align_size can inflate bytes_per_row to hundreds of megabytes, so + // `tile_data.data() + tile_y * bytes_per_row` would overflow a 32-bit pointer + // and wrap to a small in-range address, defeating the src_end check below and + // causing an out-of-bounds read on 32-bit builds. Offset arithmetic in + // uint64_t cannot wrap; the bound is checked before the pointer is formed. + const uint8_t* const src_base = tile_data.data(); + const uint64_t src_size = tile_data.size(); + + uint64_t bytes_per_row = static_cast(m_tile_width) * pixel_size; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { + const uint64_t row_start_offset = static_cast(tile_y) * bytes_per_row; + + for (uint32_t tile_x = 0; tile_x < m_tile_width; tile_x++) { + const uint64_t pixel_offset = row_start_offset + static_cast(tile_x) * pixel_size; + + if (pixel_offset > src_size || block_size > src_size - pixel_offset) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Block-pixel interleave: insufficient data"}; + } + + const uint8_t* pixel_ptr = src_base + pixel_offset; + + // Read block_size bytes into a uint64_t. + uint64_t block_val = 0; + if (little_endian) { + for (uint32_t b = 0; b < block_size; b++) { + block_val |= static_cast(pixel_ptr[b]) << (b * 8); + } + } + else { + for (uint32_t b = 0; b < block_size; b++) { + block_val = (block_val << 8) | pixel_ptr[b]; + } + } + + // Extract each component. + for (uint32_t c = 0; c < num_components; c++) { + if (!comp[c].use) { + continue; + } + + uint32_t value = static_cast((block_val >> comp[c].shift) & comp[c].mask); + uint32_t dst_x = out_x0 + tile_x; + uint32_t dst_y = out_y0 + tile_y; + uint64_t dst_offset = static_cast(dst_y) * comp[c].dst_plane_stride + + static_cast(dst_x) * comp[c].bytes_per_sample; + uint8_t* dst = comp[c].dst_plane + dst_offset; + + if (comp[c].bytes_per_sample == 1) { + *dst = static_cast(value); + } + else { + // Store in native endian (2 bytes). + if constexpr (std::endian::native == std::endian::little) { + dst[0] = static_cast(value & 0xFF); + dst[1] = static_cast((value >> 8) & 0xFF); + } + else { + dst[0] = static_cast((value >> 8) & 0xFF); + dst[1] = static_cast(value & 0xFF); + } + } + } + } + } + + return Error::Ok; +} + + +// --- Factory --- + +bool unc_decoder_factory_block_pixel_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (uncC->get_interleave_type() != interleave_mode_pixel) { + return false; + } + + if (uncC->get_pixel_size() == 0) { + return false; + } + + // Block size must be either 0 or equal to the pixel_size. + if (uncC->get_block_size() != 0 && uncC->get_block_size() != uncC->get_pixel_size()) { + return false; + } + + // The decoder packs each block into a uint64_t, so the effective block size + // (block_size, or pixel_size when block_size is 0) must fit in 8 bytes. + const uint32_t effective_block_size = uncC->get_block_size() != 0 ? uncC->get_block_size() + : uncC->get_pixel_size(); + if (effective_block_size > 8) { + return false; + } + + if (uncC->get_sampling_type() != sampling_mode_no_subsampling) { + return false; + } + + if (uncC->is_components_little_endian()) { + return false; + } + + uint64_t total_component_bits = 0; + for (const auto& component : uncC->get_components()) { + if (component.component_bit_depth > 16) { + return false; + } + if (component.component_format != component_format_unsigned) { + return false; + } + total_component_bits += component.component_bit_depth; + } + + // All components are packed into a single block and decode_tile() derives each + // component's bit shift by accumulating the component bit depths. If the depths + // sum to more than the block width, a shift reaches or exceeds 64 and the + // `block_val >> shift` on the uint64_t block (or, for pad_lsb, a uint32_t + // bit-offset underflow) is undefined behaviour. Requiring the components to fit + // within the block keeps every shift in [0, 63]. This mirrors the byte-granular + // check in check_hard_limits(); enforcing it in can_decode() makes the decoder + // safe no matter which decode path selected it. + if (total_component_bits > static_cast(effective_block_size) * 8) { + return false; + } + + return true; +} + + +std::unique_ptr unc_decoder_factory_block_pixel_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_block_pixel_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,57 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_BLOCK_PIXEL_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_BLOCK_PIXEL_INTERLEAVE_H + +#include "unc_decoder.h" +#include +#include + + +class unc_decoder_block_pixel_interleave : public unc_decoder +{ +public: + unc_decoder_block_pixel_interleave(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; +}; + + +class unc_decoder_factory_block_pixel_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_BLOCK_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,301 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_bytealign_component_interleave.h" +#include "unc_decoder_legacybase.h" // for skip_to_alignment +#include "unc_codec.h" +#include "unc_types.h" +#include "error.h" + +#include +#include +#include +#include + + +unc_decoder_bytealign_component_interleave::unc_decoder_bytealign_component_interleave( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) + : unc_decoder(width, height, cmpd, uncC, uncC_index_to_comp_ids) +{ +} + + +Result> unc_decoder_bytealign_component_interleave::get_tile_data_sizes() const +{ + uint64_t total_tile_size = 0; + + for (const auto& component : m_uncC->get_components()) { + uint32_t bytes_per_sample = (component.component_bit_depth + 7) / 8; + if (component.component_align_size > 0) { + skip_to_alignment(bytes_per_sample, component.component_align_size); + } + + if (bytes_per_sample != 0 && m_tile_width > UINT32_MAX / bytes_per_sample) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + uint32_t bytes_per_row = bytes_per_sample * m_tile_width; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + total_tile_size += static_cast(bytes_per_row) * m_tile_height; + } + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{total_tile_size}; +} + + +Error unc_decoder_bytealign_component_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + const bool little_endian = m_uncC->is_components_little_endian(); + + struct ComponentInfo { + uint32_t bytes_per_sample; + bool use; + uint8_t* dst_plane; + size_t dst_plane_stride; + }; + + const auto& components = m_uncC->get_components(); + const uint32_t num_components = static_cast(components.size()); + std::vector comp(num_components); + + for (uint32_t i = 0; i < num_components; i++) { + const auto& c = components[i]; + comp[i].bytes_per_sample = (c.component_bit_depth + 7) / 8; + + comp[i].use = true; // map_uncompressed_component_to_channel(m_cmpd, c, &channel); +#if 0 + if (comp[i].use) { + comp[i].dst_plane = img->get_component(c.component_index, &comp[i].dst_plane_stride); + assert(comp[i].dst_plane != nullptr); + } + else { + comp[i].dst_plane = nullptr; + comp[i].dst_plane_stride = 0; + } +#endif + + comp[i].dst_plane = img->get_component(m_uncC_index_to_comp_ids[i], &comp[i].dst_plane_stride); + } + + const uint8_t* const src_base = tile_data.data(); + const uint64_t src_size = tile_data.size(); + + // Track the read position as a 64-bit offset into tile_data rather than as a + // raw pointer. A crafted row_align_size can push bytes_per_row into the + // hundreds of megabytes, so `row_start + bytes_per_row` would overflow a + // 32-bit pointer and wrap to a small in-range address, defeating the + // src_end bounds check and causing an out-of-bounds read on 32-bit builds + // (OSS-Fuzz i386, sequence_fuzzer). Offset arithmetic in uint64_t cannot wrap. + uint64_t src_offset = 0; + + for (uint32_t c = 0; c < num_components; c++) { + uint32_t aligned_bytes_per_sample = comp[c].bytes_per_sample; + if (components[c].component_align_size > 0) { + skip_to_alignment(aligned_bytes_per_sample, components[c].component_align_size); + } + + uint64_t bytes_per_row = static_cast(aligned_bytes_per_sample) * m_tile_width; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { + const uint64_t row_start_offset = src_offset; + + for (uint32_t tile_x = 0; tile_x < m_tile_width; tile_x++) { + // Subtraction form to avoid any wrap: src_offset may legitimately be + // larger than src_size after a bytes_per_row row skip. + if (src_offset > src_size || aligned_bytes_per_sample > src_size - src_offset) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Bytealign-component interleave: insufficient data"}; + } + + const uint8_t* src = src_base + src_offset; + + if (comp[c].use) { + uint32_t dst_x = out_x0 + tile_x; + uint32_t dst_y = out_y0 + tile_y; + uint64_t dst_offset = static_cast(dst_y) * comp[c].dst_plane_stride + + static_cast(dst_x) * comp[c].bytes_per_sample; + uint8_t* dst = comp[c].dst_plane + dst_offset; + + if (comp[c].bytes_per_sample == 1) { + *dst = src[0]; + } + else if (comp[c].bytes_per_sample == 2) { + uint16_t value; + if (little_endian) { + value = static_cast(src[0] | (src[1] << 8)); + } + else { + value = static_cast((src[0] << 8) | src[1]); + } + std::memcpy(dst, &value, 2); + } + else if (comp[c].bytes_per_sample == 4) { + uint32_t value; + if (little_endian) { + value = static_cast(src[0]) + | (static_cast(src[1]) << 8) + | (static_cast(src[2]) << 16) + | (static_cast(src[3]) << 24); + } + else { + value = (static_cast(src[0]) << 24) + | (static_cast(src[1]) << 16) + | (static_cast(src[2]) << 8) + | static_cast(src[3]); + } + std::memcpy(dst, &value, 4); + } + else if (comp[c].bytes_per_sample == 8) { + // 8-byte sample + uint64_t value; + if (little_endian) { + value = static_cast(src[0]) + | (static_cast(src[1]) << 8) + | (static_cast(src[2]) << 16) + | (static_cast(src[3]) << 24) + | (static_cast(src[4]) << 32) + | (static_cast(src[5]) << 40) + | (static_cast(src[6]) << 48) + | (static_cast(src[7]) << 56); + } + else { + value = (static_cast(src[0]) << 56) + | (static_cast(src[1]) << 48) + | (static_cast(src[2]) << 40) + | (static_cast(src[3]) << 32) + | (static_cast(src[4]) << 24) + | (static_cast(src[5]) << 16) + | (static_cast(src[6]) << 8) + | static_cast(src[7]); + } + std::memcpy(dst, &value, 8); + } + else if (comp[c].bytes_per_sample == 16) { + // 16-byte sample (2* 8 complex) + uint64_t value[2]; + if (little_endian) { + value[0] = static_cast(src[0]) + | (static_cast(src[1]) << 8) + | (static_cast(src[2]) << 16) + | (static_cast(src[3]) << 24) + | (static_cast(src[4]) << 32) + | (static_cast(src[5]) << 40) + | (static_cast(src[6]) << 48) + | (static_cast(src[7]) << 56); + value[1] = static_cast(src[8]) + | (static_cast(src[9]) << 8) + | (static_cast(src[10]) << 16) + | (static_cast(src[11]) << 24) + | (static_cast(src[12]) << 32) + | (static_cast(src[13]) << 40) + | (static_cast(src[14]) << 48) + | (static_cast(src[15]) << 56); + } + else { + value[0] = (static_cast(src[0]) << 56) + | (static_cast(src[1]) << 48) + | (static_cast(src[2]) << 40) + | (static_cast(src[3]) << 32) + | (static_cast(src[4]) << 24) + | (static_cast(src[5]) << 16) + | (static_cast(src[6]) << 8) + | static_cast(src[7]); + value[1] = (static_cast(src[8]) << 56) + | (static_cast(src[9]) << 48) + | (static_cast(src[10]) << 40) + | (static_cast(src[11]) << 32) + | (static_cast(src[12]) << 24) + | (static_cast(src[13]) << 16) + | (static_cast(src[14]) << 8) + | static_cast(src[15]); + } + std::memcpy(dst, &value, 2*8); + } + else { + assert(false); + } + } + + src_offset += aligned_bytes_per_sample; + } + + // Skip row alignment padding + src_offset = row_start_offset + bytes_per_row; + } + } + + return Error::Ok; +} + + +// --- Factory --- + +bool unc_decoder_factory_bytealign_component_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (uncC->get_interleave_type() != interleave_mode_component) { + return false; + } + + if (uncC->get_block_size() != 0) { + return false; + } + + if (uncC->get_pixel_size() != 0) { + return false; + } + + if (uncC->get_sampling_type() != sampling_mode_no_subsampling) { + return false; + } + + for (const auto& component : uncC->get_components()) { + uint32_t d = component.component_bit_depth; + if (d != 8 && d != 16 && d != 32 && d != 64 && d != 128) { + return false; + } + + if (d == 128 && component.component_format != heif_uncompressed_component_format::component_format_complex) { + return false; + } + } + + return true; +} + + +std::unique_ptr unc_decoder_factory_bytealign_component_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_bytealign_component_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,57 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_BYTEALIGN_COMPONENT_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_BYTEALIGN_COMPONENT_INTERLEAVE_H + +#include "unc_decoder.h" +#include +#include + + +class unc_decoder_bytealign_component_interleave : public unc_decoder +{ +public: + unc_decoder_bytealign_component_interleave(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; +}; + + +class unc_decoder_factory_bytealign_component_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_BYTEALIGN_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,197 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_component_interleave.h" +#include "context.h" +#include "error.h" + +#include + + +Result> unc_decoder_component_interleave::get_tile_data_sizes() const +{ + if (m_uncC->get_interleave_type() == interleave_mode_tile_component) { + // Per-component sizes for scattered reads + std::vector sizes; + + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_pixel = entry.bits_per_component_sample; + if (entry.component_alignment > 0) { + uint32_t bytes_per_component = (bits_per_pixel + 7) / 8; + skip_to_alignment(bytes_per_component, entry.component_alignment); + bits_per_pixel = bytes_per_component * 8; + } + + uint32_t bytes_per_row; + if (m_uncC->get_pixel_size() != 0) { + uint32_t bytes_per_pixel = (bits_per_pixel + 7) / 8; + skip_to_alignment(bytes_per_pixel, m_uncC->get_pixel_size()); + if (bytes_per_pixel != 0 && m_tile_width > UINT32_MAX / bytes_per_pixel) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + bytes_per_row = bytes_per_pixel * m_tile_width; + } + else { + if (bits_per_pixel != 0 && m_tile_width > UINT32_MAX / bits_per_pixel) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + bytes_per_row = (bits_per_pixel * m_tile_width + 7) / 8; + } + + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + uint64_t component_tile_size = bytes_per_row * static_cast(m_tile_height); + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(component_tile_size, m_uncC->get_tile_align_size()); + } + + sizes.push_back(component_tile_size); + } + + return sizes; + } + + // interleave_mode_component: single contiguous block + uint64_t total_tile_size = 0; + + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_component = entry.bits_per_component_sample; + if (entry.component_alignment > 0) { + uint32_t bytes_per_component = (bits_per_component + 7) / 8; + skip_to_alignment(bytes_per_component, entry.component_alignment); + bits_per_component = bytes_per_component * 8; + } + + if (bits_per_component != 0 && entry.tile_width > UINT32_MAX / bits_per_component) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + uint32_t bytes_per_tile_row = (bits_per_component * entry.tile_width + 7) / 8; + skip_to_alignment(bytes_per_tile_row, m_uncC->get_row_align_size()); + uint64_t bytes_per_tile = uint64_t{bytes_per_tile_row} * entry.tile_height; + total_tile_size += bytes_per_tile; + } + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{total_tile_size}; +} + + +Error unc_decoder_component_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + UncompressedBitReader srcBits(tile_data); + + bool per_channel_tile_align = (m_uncC->get_interleave_type() == interleave_mode_tile_component); + + // out_x0/out_y0 are in full-resolution image coordinates. For subsampled channels + // (Cb/Cr at 4:2:0 or 4:2:2), entry.tile_width/tile_height and entry.dst_plane_stride + // refer to the subsampled chroma plane, so the destination origin must be scaled + // to the channel's grid. Failing to do so wrote past the chroma plane on any + // non-first tile row/column — see GHSA-5x55-x5pf-9c6g. + uint32_t tile_col = out_x0 / m_tile_width; + uint32_t tile_row = out_y0 / m_tile_height; + + for (ChannelListEntry& entry : channelList) { + srcBits.markTileStart(); + uint64_t channel_x0 = uint64_t{tile_col} * entry.tile_width; + uint64_t channel_y0 = uint64_t{tile_row} * entry.tile_height; + for (uint32_t y = 0; y < entry.tile_height; y++) { + srcBits.markRowStart(); + if (entry.use_channel) { + uint64_t dst_row_offset = (channel_y0 + y) * entry.dst_plane_stride; + processComponentTileRow(entry, srcBits, dst_row_offset + channel_x0 * entry.bytes_per_component_sample); + } + else { + srcBits.skip_bytes(entry.bytes_per_tile_row_src); + } + srcBits.handleRowAlignment(m_uncC->get_row_align_size()); + } + if (per_channel_tile_align) { + srcBits.handleTileAlignment(m_uncC->get_tile_align_size()); + } + } + + return Error::Ok; +} + + +bool unc_decoder_factory_component_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (!check_common_requirements(uncC)) { + return false; + } + + if (uncC->get_interleave_type() != interleave_mode_component && + uncC->get_interleave_type() != interleave_mode_tile_component) { + return false; + } + + auto sampling = uncC->get_sampling_type(); + if (sampling != sampling_mode_no_subsampling && + sampling != sampling_mode_422 && + sampling != sampling_mode_420) { + return false; + } + + if (sampling == sampling_mode_422 || sampling == sampling_mode_420) { + if (uncC->get_row_align_size() != 0 && uncC->get_row_align_size() % 2 != 0) { + return false; + } + } + + if (sampling == sampling_mode_422) { + if (uncC->get_tile_align_size() != 0 && uncC->get_tile_align_size() % 2 != 0) { + return false; + } + } + + if (sampling == sampling_mode_420) { + if (uncC->get_tile_align_size() != 0 && uncC->get_tile_align_size() % 4 != 0) { + return false; + } + } + + if (uncC->get_pixel_size() != 0) { + return false; + } + + if (uncC->is_components_little_endian() && has_any_multi_byte_components(uncC)) { + return false; + } + + return true; +} + +std::unique_ptr unc_decoder_factory_component_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_component_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,59 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_COMPONENT_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_COMPONENT_INTERLEAVE_H + +#include "unc_decoder_legacybase.h" +#include +#include +#include + + +class unc_decoder_component_interleave : public unc_decoder_legacybase +{ +public: + unc_decoder_component_interleave(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) : + unc_decoder_legacybase(width, height, cmpd, uncC, uncC_index_to_comp_ids) {} + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; +}; + + +class unc_decoder_factory_component_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_legacybase.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_legacybase.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,180 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include +#include +#include +#include + +#if ((defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(__PGI)) && __GNUC__ < 9) || (defined(__clang__) && __clang_major__ < 10) +#include +#else +#include +#endif + +#include "common_utils.h" +#include "context.h" +#include "compression.h" +#include "error.h" +#include "libheif/heif.h" +#include "unc_types.h" +#include "unc_boxes.h" +#include "unc_codec.h" +#include "unc_decoder_legacybase.h" +#include "codecs/decoder.h" + + +unc_decoder_legacybase::unc_decoder_legacybase(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) + : unc_decoder(width, height, cmpd, uncC, uncC_index_to_comp_ids) +{ +} + +void unc_decoder_legacybase::ensureChannelList(std::shared_ptr& img) +{ + if (channelList.empty()) { + buildChannelList(img); + } +} + +void unc_decoder_legacybase::buildChannelList(std::shared_ptr& img) +{ + uint32_t uncC_index = 0; + for (Box_uncC::Component component : m_uncC->get_components()) { + ChannelListEntry entry = buildChannelListEntry(uncC_index, component, img); + channelList.push_back(entry); + uncC_index++; + } +} + +void unc_decoder_legacybase::memcpy_to_native_endian(uint8_t* dst, uint32_t value, uint32_t bytes_per_sample) +{ + // TODO: this assumes that the file endianness is always big-endian. The endianness flags in the uncC header are not taken into account yet. + + if (bytes_per_sample==1) { + *dst = static_cast(value); + return; + } + else if (std::endian::native == std::endian::big) { + for (uint32_t i = 0; i < bytes_per_sample; i++) { + dst[bytes_per_sample - 1 - i] = static_cast((value >> (i * 8)) & 0xFF); + } + } + else { + for (uint32_t i = 0; i < bytes_per_sample; i++) { + dst[i] = static_cast((value >> (i * 8)) & 0xFF); + } + } +} + +void unc_decoder_legacybase::processComponentSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_row_offset, uint32_t tile_column, uint32_t tile_x) +{ + uint64_t dst_col_number = static_cast(tile_column) * entry.tile_width + tile_x; + uint64_t dst_column_offset = dst_col_number * entry.bytes_per_component_sample; + int val = srcBits.get_bits(entry.bits_per_component_sample); // get_bits() reads input in big-endian order + memcpy_to_native_endian(entry.dst_plane + dst_row_offset + dst_column_offset, val, entry.bytes_per_component_sample); +} + +// Handles the case where a row consists of a single component type +// Not valid for Pixel interleave +// Not valid for the Cb/Cr channels in Mixed Interleave +// Not valid for multi-Y pixel interleave +void unc_decoder_legacybase::processComponentRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_row_offset, uint32_t tile_column) +{ + for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { + if (entry.component_alignment != 0) { + srcBits.skip_to_byte_boundary(); + int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; + srcBits.skip_bits(numPadBits); + } + processComponentSample(srcBits, entry, dst_row_offset, tile_column, tile_x); + } + srcBits.skip_to_byte_boundary(); +} + +void unc_decoder_legacybase::processComponentTileSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_offset, uint32_t tile_x) +{ + uint64_t dst_sample_offset = uint64_t{tile_x} * entry.bytes_per_component_sample; + int val = srcBits.get_bits(entry.bits_per_component_sample); + memcpy_to_native_endian(entry.dst_plane + dst_offset + dst_sample_offset, val, entry.bytes_per_component_sample); +} + +// Handles the case where a row consists of a single component type +// Not valid for Pixel interleave +// Not valid for the Cb/Cr channels in Mixed Interleave +// Not valid for multi-Y pixel interleave +void unc_decoder_legacybase::processComponentTileRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_offset) +{ + for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { + if (entry.component_alignment != 0) { + srcBits.skip_to_byte_boundary(); + int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; + srcBits.skip_bits(numPadBits); + } + processComponentTileSample(srcBits, entry, dst_offset, tile_x); + } + srcBits.skip_to_byte_boundary(); +} + + +unc_decoder_legacybase::ChannelListEntry unc_decoder_legacybase::buildChannelListEntry(uint32_t component_id, + Box_uncC::Component component, + std::shared_ptr& img) +{ + ChannelListEntry entry; + entry.use_channel = map_uncompressed_component_to_channel(m_cmpd, component, &(entry.channel)); + entry.dst_plane = img->get_component(m_uncC_index_to_comp_ids[component_id], &(entry.dst_plane_stride)); + entry.tile_width = m_tile_width; + entry.tile_height = m_tile_height; + if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) { + if (m_uncC->get_sampling_type() == sampling_mode_422) { + entry.tile_width /= 2; + } + else if (m_uncC->get_sampling_type() == sampling_mode_420) { + entry.tile_width /= 2; + entry.tile_height /= 2; + } + + // chroma[0] is this entry's own plane; chroma[1] is the paired plane + // (Cr if this entry is Cb, or vice versa). Each plane's width is read + // from that plane itself -- Cb and Cr can be declared with different bit + // depths, and reusing chroma[0]'s width for chroma[1] overruns it + // (GHSA-x8r2-mggj-j6wr). + heif_channel paired_channel = (entry.channel == heif_channel_Cb) ? heif_channel_Cr : heif_channel_Cb; + + entry.chroma_dst_plane[0] = entry.dst_plane; + entry.chroma_dst_plane_stride[0] = entry.dst_plane_stride; + entry.chroma_bytes_per_component_sample[0] = (component.component_bit_depth + 7) / 8; + + entry.chroma_dst_plane[1] = img->get_channel_memory(paired_channel, &(entry.chroma_dst_plane_stride[1])); + entry.chroma_bytes_per_component_sample[1] = img->get_storage_bits_per_pixel(paired_channel) / 8; + } + entry.bits_per_component_sample = component.component_bit_depth; + entry.component_alignment = component.component_align_size; + entry.bytes_per_component_sample = (component.component_bit_depth + 7) / 8; + entry.bytes_per_tile_row_src = entry.tile_width * entry.bytes_per_component_sample; + return entry; +} + + + diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_legacybase.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_legacybase.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,213 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_LEGACYBASE_H +#define LIBHEIF_UNC_DECODER_LEGACYBASE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common_utils.h" +#include "context.h" +#include "compression.h" +#include "error.h" +#include "libheif/heif.h" +#include "unc_types.h" +#include "unc_boxes.h" +#include "unc_codec.h" +#include "unc_decoder.h" + + +class UncompressedBitReader : public BitReader +{ +public: + UncompressedBitReader(const std::vector& data) : BitReader(data.data(), data.size()) {} + + void markPixelStart() + { + m_pixelStartOffset = get_current_byte_index(); + } + + void markRowStart() + { + m_rowStartOffset = get_current_byte_index(); + } + + void markTileStart() + { + m_tileStartOffset = get_current_byte_index(); + } + + inline Error handlePixelAlignment(uint32_t pixel_size) + { + if (pixel_size != 0) { + uint32_t bytes_in_pixel = static_cast(get_current_byte_index() - m_pixelStartOffset); + if (pixel_size > bytes_in_pixel) { + uint32_t padding = pixel_size - bytes_in_pixel; + skip_bytes(padding); + } + else if (pixel_size == bytes_in_pixel) { + // NOP + } + else { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Uncompressed image: invalid 'pixel_size'" + }; + } + } + + return {}; + } + + void handleRowAlignment(uint32_t alignment) + { + skip_to_byte_boundary(); + if (alignment != 0) { + uint32_t bytes_in_row = static_cast(get_current_byte_index() - m_rowStartOffset); + uint32_t residual = bytes_in_row % alignment; + if (residual != 0) { + uint32_t padding = alignment - residual; + skip_bytes(padding); + } + } + } + + void handleTileAlignment(uint32_t alignment) + { + if (alignment != 0) { + uint32_t bytes_in_tile = static_cast(get_current_byte_index() - m_tileStartOffset); + uint32_t residual = bytes_in_tile % alignment; + if (residual != 0) { + uint32_t tile_padding = alignment - residual; + skip_bytes(tile_padding); + } + } + } + +private: + size_t m_pixelStartOffset = 0; + size_t m_rowStartOffset = 0; + size_t m_tileStartOffset = 0; +}; + + +template void skip_to_alignment(T& position, uint32_t alignment) +{ + if (alignment == 0) { + return; + } + + T residual = position % alignment; + if (residual == 0) { + return; + } + + position += alignment - residual; +} + + +class unc_decoder_legacybase : public unc_decoder +{ +public: + ~unc_decoder_legacybase() override = default; + + void ensure_channel_list(std::shared_ptr& img) override { ensureChannelList(img); } + +protected: + unc_decoder_legacybase(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids); + + class ChannelListEntry + { + public: + uint32_t get_bytes_per_tile() const + { + return bytes_per_tile_row_src * tile_height; + } + + inline uint64_t getDestinationRowOffset(uint32_t tile_row, uint32_t tile_y) const + { + uint64_t dst_row_number = uint64_t{tile_row} * tile_height + tile_y; + return dst_row_number * dst_plane_stride; + } + + heif_channel channel = heif_channel_Y; + uint8_t* dst_plane = nullptr; + size_t dst_plane_stride; + uint32_t tile_width; + uint32_t tile_height; + uint32_t bytes_per_component_sample; + // Mixed interleave only: the Cb and Cr planes, indexed in the order the + // two components are declared in the uncC component list (index 0 is + // whichever comes first there -- not "Cb" specifically), since that is + // the order their samples are actually interleaved in the bitstream. + // Each plane is allocated according to its own component's bit depth, so + // the two can have different byte widths; using one plane's width for + // the other's write overruns it (GHSA-x8r2-mggj-j6wr). + uint8_t* chroma_dst_plane[2] = {nullptr, nullptr}; + size_t chroma_dst_plane_stride[2] = {0, 0}; + uint32_t chroma_bytes_per_component_sample[2] = {0, 0}; + uint16_t bits_per_component_sample; + uint8_t component_alignment; + uint32_t bytes_per_tile_row_src; + bool use_channel; + }; + + std::vector channelList; + + // TODO: refactor so that the channel list is passed explicitly instead of being lazily initialized as member state. + void ensureChannelList(std::shared_ptr& img); + + void processComponentSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_row_offset, uint32_t tile_column, uint32_t tile_x); + + // Handles the case where a row consists of a single component type + // Not valid for Pixel interleave + // Not valid for the Cb/Cr channels in Mixed Interleave + // Not valid for multi-Y pixel interleave + void processComponentRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_row_offset, uint32_t tile_column); + + void processComponentTileSample(UncompressedBitReader& srcBits, const ChannelListEntry& entry, uint64_t dst_offset, uint32_t tile_x); + + // Handles the case where a row consists of a single component type + // Not valid for Pixel interleave + // Not valid for the Cb/Cr channels in Mixed Interleave + // Not valid for multi-Y pixel interleave + void processComponentTileRow(ChannelListEntry& entry, UncompressedBitReader& srcBits, uint64_t dst_offset); + +protected: + void memcpy_to_native_endian(uint8_t* dst, uint32_t value, uint32_t bytes_per_sample); + +private: + void buildChannelList(std::shared_ptr& img); + ChannelListEntry buildChannelListEntry(uint32_t component_idx, Box_uncC::Component component, std::shared_ptr& img); +}; + +#endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,177 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_mixed_interleave.h" +#include "context.h" +#include "error.h" + +#include +#include + + +Result> unc_decoder_mixed_interleave::get_tile_data_sizes() const +{ + uint64_t tile_size = 0; + + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_component = entry.bits_per_component_sample; + if (entry.channel != heif_channel_Cb && entry.channel != heif_channel_Cr + && entry.component_alignment > 0) { + uint32_t bytes_per_component = (bits_per_component + 7) / 8; + skip_to_alignment(bytes_per_component, entry.component_alignment); + bits_per_component = bytes_per_component * 8; + } + + if (bits_per_component != 0 && entry.tile_width > UINT32_MAX / bits_per_component) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + uint32_t bits_per_row = bits_per_component * entry.tile_width; + bits_per_row = (bits_per_row + 7) & ~7U; // align to byte boundary + + tile_size += uint64_t{bits_per_row} / 8 * entry.tile_height; + } + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{tile_size}; +} + + +Error unc_decoder_mixed_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + UncompressedBitReader srcBits(tile_data); + + processTile(srcBits, out_x0, out_y0); + + return Error::Ok; +} + + +void unc_decoder_mixed_interleave::processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0) +{ + // out_x0/out_y0 are in full-resolution image coordinates. For subsampled chroma + // channels (4:2:0 or 4:2:2), entry.tile_width/tile_height and entry.dst_plane_stride + // refer to the subsampled chroma plane, so the destination origin must be scaled + // to each channel's grid. Failing to do so wrote past the chroma plane on any + // non-first tile row/column — see GHSA-5x55-x5pf-9c6g. + uint32_t tile_col = out_x0 / m_tile_width; + uint32_t tile_row = out_y0 / m_tile_height; + + bool haveProcessedChromaForThisTile = false; + for (ChannelListEntry& entry : channelList) { + if (entry.use_channel) { + uint64_t channel_x0 = uint64_t{tile_col} * entry.tile_width; + uint64_t channel_y0 = uint64_t{tile_row} * entry.tile_height; + if ((entry.channel == heif_channel_Cb) || (entry.channel == heif_channel_Cr)) { + if (!haveProcessedChromaForThisTile) { + for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { + // TODO: row padding + uint64_t dst_row_number = tile_y + channel_y0; + + uint64_t chroma_dst_row_offset[2]; + chroma_dst_row_offset[0] = dst_row_number * entry.chroma_dst_plane_stride[0]; + chroma_dst_row_offset[1] = dst_row_number * entry.chroma_dst_plane_stride[1]; + + for (uint32_t tile_x = 0; tile_x < entry.tile_width; tile_x++) { + uint64_t dst_column_number = channel_x0 + tile_x; + + // The two chroma samples for this pixel are read in the same order + // as their components are declared in uncC (chroma[0] then chroma[1]). + // Each is written using its own plane's byte width -- Cb and Cr can be + // declared with different bit depths, so reusing one plane's width for + // the other overruns it (GHSA-x8r2-mggj-j6wr). + for (int c = 0; c < 2; c++) { + uint32_t bytes_per_sample = entry.chroma_bytes_per_component_sample[c]; + uint64_t dst_column_offset = dst_column_number * bytes_per_sample; + int val = srcBits.get_bits(bytes_per_sample * 8); + memcpy_to_native_endian(entry.chroma_dst_plane[c] + chroma_dst_row_offset[c] + dst_column_offset, val, bytes_per_sample); + } + } + haveProcessedChromaForThisTile = true; + } + } + } + else { + for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { + uint64_t dst_row_offset = (channel_y0 + tile_y) * entry.dst_plane_stride; + processComponentTileRow(entry, srcBits, dst_row_offset + channel_x0 * entry.bytes_per_component_sample); + } + } + } + else { + // skip over the data we are not using + srcBits.skip_bytes(entry.get_bytes_per_tile()); + continue; + } + } +} + + +bool unc_decoder_factory_mixed_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (!check_common_requirements(uncC)) { + return false; + } + + if (uncC->get_interleave_type() != interleave_mode_mixed) { + return false; + } + + auto sampling = uncC->get_sampling_type(); + if (sampling != sampling_mode_422 && sampling != sampling_mode_420) { + return false; + } + + if (sampling == sampling_mode_422) { + if (uncC->get_tile_align_size() != 0 && uncC->get_tile_align_size() % 2 != 0) { + return false; + } + } + + if (sampling == sampling_mode_420) { + if (uncC->get_tile_align_size() != 0 && uncC->get_tile_align_size() % 4 != 0) { + return false; + } + } + + if (uncC->get_pixel_size() != 0) { + return false; + } + + if (uncC->is_components_little_endian() && has_any_multi_byte_components(uncC)) { + return false; + } + + return true; +} + +std::unique_ptr unc_decoder_factory_mixed_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,58 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_MIXED_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_MIXED_INTERLEAVE_H + +#include "unc_decoder_legacybase.h" +#include +#include +#include + + +class unc_decoder_mixed_interleave : public unc_decoder_legacybase +{ +public: + unc_decoder_mixed_interleave(uint32_t width, uint32_t height, const std::shared_ptr& cmpd, const std::shared_ptr& uncC, const std::vector& uncC_index_to_comp_ids) : + unc_decoder_legacybase(width, height, cmpd, uncC, uncC_index_to_comp_ids) {} + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; + + void processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0); +}; + + +class unc_decoder_factory_mixed_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_MIXED_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,144 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_pixel_interleave.h" +#include "context.h" +#include "error.h" + +#include + + +Result> unc_decoder_pixel_interleave::get_tile_data_sizes() const +{ + uint32_t bits_per_row = 0; + for (uint32_t x = 0; x < m_tile_width; x++) { + uint32_t bits_per_pixel = 0; + + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_component = entry.bits_per_component_sample; + if (entry.component_alignment > 0) { + // start at byte boundary + bits_per_row = (bits_per_row + 7) & ~7U; + + uint32_t bytes_per_component = (bits_per_component + 7) / 8; + skip_to_alignment(bytes_per_component, entry.component_alignment); + bits_per_component = bytes_per_component * 8; + } + + bits_per_pixel += bits_per_component; + } + + if (m_uncC->get_pixel_size() != 0) { + uint32_t bytes_per_pixel = (bits_per_pixel + 7) / 8; + skip_to_alignment(bytes_per_pixel, m_uncC->get_pixel_size()); + bits_per_pixel = bytes_per_pixel * 8; + } + + if (bits_per_pixel > UINT32_MAX - bits_per_row) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + bits_per_row += bits_per_pixel; + } + + uint32_t bytes_per_row = (bits_per_row + 7) / 8; + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + uint64_t total_tile_size = bytes_per_row * static_cast(m_tile_height); + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{total_tile_size}; +} + + +Error unc_decoder_pixel_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + UncompressedBitReader srcBits(tile_data); + + return processTile(srcBits, out_x0, out_y0); +} + + +Error unc_decoder_pixel_interleave::processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0) +{ + for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { + srcBits.markRowStart(); + for (uint32_t tile_x = 0; tile_x < m_tile_width; tile_x++) { + srcBits.markPixelStart(); + for (ChannelListEntry& entry : channelList) { + if (entry.use_channel) { + uint64_t dst_row_offset = entry.getDestinationRowOffset(0, tile_y + out_y0); + if (entry.component_alignment != 0) { + srcBits.skip_to_byte_boundary(); + int numPadBits = (entry.component_alignment * 8) - entry.bits_per_component_sample; + srcBits.skip_bits(numPadBits); + } + processComponentSample(srcBits, entry, dst_row_offset, 0, out_x0 + tile_x); + } + else { + srcBits.skip_bytes(entry.bytes_per_component_sample); + } + } + auto err = srcBits.handlePixelAlignment(m_uncC->get_pixel_size()); + if (err) { + return err; + } + } + + srcBits.handleRowAlignment(m_uncC->get_row_align_size()); + } + + return {}; +} + + +bool unc_decoder_factory_pixel_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (!check_common_requirements(uncC)) { + return false; + } + + if (uncC->get_interleave_type() != interleave_mode_pixel) { + return false; + } + + if (uncC->get_sampling_type() != sampling_mode_no_subsampling) { + return false; + } + + if (uncC->is_components_little_endian()) { + return false; + } + + return true; +} + +std::unique_ptr unc_decoder_factory_pixel_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,58 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_PIXEL_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_PIXEL_INTERLEAVE_H + +#include "unc_decoder_legacybase.h" +#include +#include +#include + + +class unc_decoder_pixel_interleave : public unc_decoder_legacybase +{ +public: + unc_decoder_pixel_interleave(uint32_t width, uint32_t height, const std::shared_ptr& cmpd, const std::shared_ptr& uncC, const std::vector& uncC_index_to_comp_ids) : + unc_decoder_legacybase(width, height, cmpd, uncC, uncC_index_to_comp_ids) {} + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; + + [[nodiscard]] Error processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0); +}; + + +class unc_decoder_factory_pixel_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_row_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_row_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,150 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_decoder_row_interleave.h" +#include "context.h" +#include "error.h" + +#include + + +Result> unc_decoder_row_interleave::get_tile_data_sizes() const +{ + uint32_t bits_per_row = 0; + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_component = entry.bits_per_component_sample; + if (entry.component_alignment > 0) { + // start at byte boundary + bits_per_row = (bits_per_row + 7) & ~7U; + + uint32_t bytes_per_component = (bits_per_component + 7) / 8; + skip_to_alignment(bytes_per_component, entry.component_alignment); + bits_per_component = bytes_per_component * 8; + } + + uint32_t row_bits; + if (bits_per_component != 0 && m_tile_width > UINT32_MAX / bits_per_component) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + if (m_uncC->get_row_align_size() != 0) { + uint32_t bytes_this_row = (bits_per_component * m_tile_width + 7) / 8; + skip_to_alignment(bytes_this_row, m_uncC->get_row_align_size()); + if (bytes_this_row > UINT32_MAX / 8) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + row_bits = bytes_this_row * 8; + } + else { + row_bits = bits_per_component * m_tile_width; + } + + if (row_bits > UINT32_MAX - bits_per_row) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + bits_per_row += row_bits; + + if (bits_per_row > UINT32_MAX - 7) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; + } + bits_per_row = (bits_per_row + 7) & ~7U; + } + + uint32_t bytes_per_row = (bits_per_row + 7) / 8; + if (m_uncC->get_row_align_size()) { + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + } + + uint64_t total_tile_size = bytes_per_row * static_cast(m_tile_height); + + if (m_uncC->get_tile_align_size() != 0) { + skip_to_alignment(total_tile_size, m_uncC->get_tile_align_size()); + } + + return std::vector{total_tile_size}; +} + + +Error unc_decoder_row_interleave::decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) +{ + UncompressedBitReader srcBits(tile_data); + + processTile(srcBits, out_x0, out_y0); + + return Error::Ok; +} + + +void unc_decoder_row_interleave::processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0) +{ + for (uint32_t tile_y = 0; tile_y < m_tile_height; tile_y++) { + for (ChannelListEntry& entry : channelList) { + srcBits.markRowStart(); + if (entry.use_channel) { + uint64_t dst_row_offset = entry.getDestinationRowOffset(0, tile_y + out_y0); + processComponentRow(entry, srcBits, dst_row_offset + out_x0 * entry.bytes_per_component_sample, 0); + } + else { + srcBits.skip_bytes(entry.bytes_per_tile_row_src); + } + srcBits.handleRowAlignment(m_uncC->get_row_align_size()); + } + } +} + + +bool unc_decoder_factory_row_interleave::can_decode(const std::shared_ptr& uncC) const +{ + if (!check_common_requirements(uncC)) { + return false; + } + + if (uncC->get_interleave_type() != interleave_mode_row) { + return false; + } + + if (uncC->get_sampling_type() != sampling_mode_no_subsampling) { + return false; + } + + if (uncC->get_pixel_size() != 0) { + return false; + } + + if (uncC->is_components_little_endian() && has_any_multi_byte_components(uncC)) { + return false; + } + + return true; +} + +std::unique_ptr unc_decoder_factory_row_interleave::create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const +{ + return std::make_unique(width, height, cmpd, uncC, uncC_index_to_comp_ids); +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_row_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_decoder_row_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_row_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,61 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_DECODER_ROW_INTERLEAVE_H +#define LIBHEIF_UNC_DECODER_ROW_INTERLEAVE_H + +#include "unc_decoder_legacybase.h" +#include +#include +#include + + +class unc_decoder_row_interleave : public unc_decoder_legacybase +{ +public: + unc_decoder_row_interleave(uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, const std::shared_ptr& uncC, const std::vector& uncC_index_to_comp_ids) : + unc_decoder_legacybase(width, height, cmpd, uncC, uncC_index_to_comp_ids) {} + + + Result> get_tile_data_sizes() const override; + + Error decode_tile(const std::vector& tile_data, + std::shared_ptr& img, + uint32_t out_x0, uint32_t out_y0) override; + +private: + void processTile(UncompressedBitReader& srcBits, uint32_t out_x0, uint32_t out_y0); +}; + + +class unc_decoder_factory_row_interleave : public unc_decoder_factory +{ +private: + bool can_decode(const std::shared_ptr& uncC) const override; + + std::unique_ptr create( + uint32_t width, uint32_t height, + const std::shared_ptr& cmpd, + const std::shared_ptr& uncC, + const std::vector& uncC_index_to_comp_ids) const override; +}; + +#endif // LIBHEIF_UNC_DECODER_ROW_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_enc.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,83 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_enc.h" +#include "unc_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" +#include +#include + +#include + +#include "unc_encoder.h" + + +Result Encoder_uncompressed::encode(const std::shared_ptr& image, + struct heif_encoder* encoder, + const struct heif_encoding_options& options, + enum heif_image_input_class input_class) +{ + auto uncEncoder = unc_encoder_factory::get_unc_encoder(image, options); + if (uncEncoder.error()) { + return uncEncoder.error(); + } + + auto codingResult = (*uncEncoder)->encode(image, options); + if (!codingResult) { + return codingResult.error(); + } + + Encoder::CodedImageData codedImage = *codingResult; + + // codedImage.bitstream = std::move(vec); + + codedImage.codingConstraints.intra_pred_used = false; + codedImage.codingConstraints.all_ref_pics_intra = true; + codedImage.codingConstraints.max_ref_per_pic = 0; + + return {codedImage}; +} + + +std::shared_ptr Encoder_uncompressed::get_sample_description_box(const CodedImageData& data) const +{ + auto uncv = std::make_shared(); + uncv->get_VisualSampleEntry().compressorname = "iso23001-17"; + + for (const auto& prop : data.properties) { + switch (prop->get_short_type()) { + case fourcc("cmpd"): + case fourcc("uncC"): + case fourcc("cmpC"): + case fourcc("icef"): + case fourcc("cpat"): + case fourcc("splz"): + case fourcc("sbpm"): + case fourcc("snuc"): + case fourcc("cloc"): + uncv->append_child_box(prop); + break; + } + } + + return uncv; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_enc.h libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,86 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_UNCOMPRESSED_H +#define HEIF_ENCODER_UNCOMPRESSED_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_uncompressed : public Encoder { +public: + Result encode(const std::shared_ptr& image, + struct heif_encoder* encoder, + const struct heif_encoding_options& options, + enum heif_image_input_class input_class) override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + + bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override + { + heif_encoding_options dummy_options{}; + + dummy_options.save_alpha_channel = static_cast(options.save_alpha_channel); + + auto encodeResult = encode(image, encoder, dummy_options, input_class); + if (encodeResult.error()) { + return encodeResult.error(); + } + + m_codedImageData = std::move(*encodeResult); + + m_codedImageData->frame_nr = frame_number; + m_codedImageData->is_sync_frame = true; + + return {}; + } + + Error encode_sequence_flush(heif_encoder* encoder) override + { + return {}; + } + + std::optional encode_sequence_get_data() override + { + return std::move(m_codedImageData); + } + +private: + std::optional m_codedImageData; +}; + + +#endif diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,431 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_encoder.h" + +#include +#include +#include +#include + +#include "image/pixelimage.h" +#include "unc_boxes.h" +#include "unc_encoder_component_interleave.h" +#include "unc_encoder_rgb_block_pixel_interleave.h" +#include "unc_encoder_rgb_pixel_interleave.h" +#include "unc_encoder_rgb_bytealign_pixel_interleave.h" +#include "libheif/heif_uncompressed.h" +#include "compression.h" +#include +#include + + +heif_cmpd_component_type heif_channel_to_component_type(heif_channel channel) +{ + switch (channel) { + case heif_channel_Y: return heif_cmpd_component_type_Y; + case heif_channel_Cb: return heif_cmpd_component_type_Cb; + case heif_channel_Cr: return heif_cmpd_component_type_Cr; + case heif_channel_R: return heif_cmpd_component_type_red; + case heif_channel_G: return heif_cmpd_component_type_green; + case heif_channel_B: return heif_cmpd_component_type_blue; + case heif_channel_Alpha: return heif_cmpd_component_type_alpha; + case heif_channel_interleaved: assert(false); + break; + case heif_channel_filter_array: return heif_cmpd_component_type_filter_array; + case heif_channel_depth: return heif_cmpd_component_type_depth; + case heif_channel_disparity: return heif_cmpd_component_type_disparity; + case heif_channel_unknown: return heif_cmpd_component_type_padded; + } + + return heif_cmpd_component_type_padded; +} + + +heif_uncompressed_component_format to_unc_component_format(heif_component_datatype channel_datatype) +{ + // heif_component_datatype values are aligned with ISO/IEC 23001-17 Table 2. + if (channel_datatype == heif_component_datatype_undefined) { + return component_format_unsigned; + } + return static_cast(channel_datatype); +} + + +unc_encoder::unc_encoder(const std::shared_ptr& image) +{ + m_cmpd = std::make_shared(); + m_uncC = std::make_shared(); + + // --- fill component-id to cmpd-index map + + std::vector ids = image->get_used_component_ids(); + + for (size_t i = 0; i < ids.size(); i++) { + m_map_id_to_cmpd_index[ids[i]] = static_cast(i); + } + + // --- remember the image configuration we are generating this encoder for + + m_prototype_colorspace = image->get_colorspace(); + m_prototype_chroma = image->get_chroma_format(); + + for (const auto& desc : image->get_component_descriptions()) { + m_prototype_components.push_back({desc.component_id, + desc.channel, + desc.component_type, + desc.bit_depth, + desc.datatype, + desc.has_data_plane}); + } + + // TODO: we could combine component_ids with similar types if they are also used in the same way in the metadata boxes + + // --- create cmpd component types + + uint32_t max_cmpd_index=0; + for (auto iter : m_map_id_to_cmpd_index) { + max_cmpd_index = std::max(max_cmpd_index, iter.second); + } + + std::vector cmpd_types(static_cast(max_cmpd_index) + 1); + + for (auto iter : m_map_id_to_cmpd_index) { + uint16_t comp_type = image->get_component_type(iter.first); + cmpd_types[iter.second] = comp_type; + } + + m_cmpd->set_components(cmpd_types); + + // --- Bayer pattern: add reference components to cmpd and generate cpat box + + if (image->has_any_bayer_pattern()) { + const BayerPattern& bayer = image->get_any_bayer_pattern(); + + // Build cpat box with resolved cmpd indices + + m_cpat = std::make_shared(); + m_cpat->set_pattern(bayer.resolve_to_cmpd(m_map_id_to_cmpd_index)); + } + + if (image->has_polarization_patterns()) { + for (const auto& pol : image->get_polarization_patterns()) { + PolarizationPattern polCmpd = pol; + polCmpd.component_ids = map_component_ids_to_cmpd(pol.component_ids, m_map_id_to_cmpd_index); + auto splz = std::make_shared(); + splz->set_pattern(polCmpd); + m_splz.push_back(splz); + } + } + + if (image->has_sensor_bad_pixels_maps()) { + for (const auto& bpm : image->get_sensor_bad_pixels_maps()) { + SensorBadPixelsMap bpmCmpd = bpm; + bpmCmpd.component_ids = map_component_ids_to_cmpd(bpm.component_ids, m_map_id_to_cmpd_index); + auto sbpm = std::make_shared(); + sbpm->set_bad_pixels_map(bpmCmpd); + m_sbpm.push_back(sbpm); + } + } + + if (image->has_sensor_nuc()) { + for (const auto& nuc : image->get_sensor_nuc()) { + SensorNonUniformityCorrection nucCmpd = nuc; + nucCmpd.component_ids = map_component_ids_to_cmpd(nuc.component_ids, m_map_id_to_cmpd_index); + auto snuc = std::make_shared(); + snuc->set_nuc(nucCmpd); + m_snuc.push_back(snuc); + } + } + + if (image->has_chroma_location()) { + m_cloc = std::make_shared(); + m_cloc->set_chroma_location(image->get_chroma_location()); + } +} + + +Result > unc_encoder_factory::get_unc_encoder(const std::shared_ptr& prototype_image, + const heif_encoding_options& options) +{ + // The encoders access the pixel planes implied by the image's colorspace and chroma format. + // An image that never received those planes (heif_image_create() without a matching + // heif_image_add_plane()) would make them dereference a plane that does not exist, so reject + // it before we pick an encoder for it. + + if (num_interleaved_components_per_plane(prototype_image->get_chroma_format()) > 1) { + if (!prototype_image->has_channel(heif_channel_interleaved)) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image has an interleaved chroma format, but no interleaved pixel plane."}; + } + } + else if (prototype_image->get_used_planar_component_ids().empty()) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image has no pixel planes."}; + } + + static unc_encoder_factory_rgb_pixel_interleave enc_rgb_pixel_interleave; + static unc_encoder_factory_rgb_block_pixel_interleave enc_rgb_block_pixel_interleave; + static unc_encoder_factory_rgb_bytealign_pixel_interleave enc_rgb_bytealign_pixel_interleave; + static unc_encoder_factory_component_interleave enc_component_interleave; + + static const unc_encoder_factory* encoders[]{ + &enc_rgb_pixel_interleave, + &enc_rgb_block_pixel_interleave, + &enc_rgb_bytealign_pixel_interleave, + &enc_component_interleave + }; + + for (const unc_encoder_factory* enc : encoders) { + if (enc->can_encode(prototype_image, options)) { + return {enc->create(prototype_image, options)}; + } + } + + return Error{ + heif_error_Unsupported_filetype, + heif_suberror_Unspecified, + "Input image configuration unsupported by uncompressed codec." + }; +} + + +heif_uncompressed_component_format to_unc_component_format(const std::shared_ptr& image, heif_channel channel) +{ + heif_component_datatype datatype = image->get_datatype(channel); + heif_uncompressed_component_format component_format = to_unc_component_format(datatype); + return component_format; +} + + +Error unc_encoder::check_component_sizes(const std::shared_ptr& src_image) +{ + uint32_t image_width = src_image->get_width(); + uint32_t image_height = src_image->get_height(); + heif_chroma chroma = src_image->get_chroma_format(); + + // Iterate the component descriptions rather than get_used_planar_component_ids() so that + // components of an interleaved plane are covered too. Reference components of a cpat pattern + // carry no pixels and are skipped. + + for (const auto& desc : src_image->get_component_descriptions()) { + if (!desc.has_data_plane) { + continue; + } + + uint32_t expected_width = image_width; + uint32_t expected_height = image_height; + + if (desc.channel == heif_channel_Cb || desc.channel == heif_channel_Cr) { + if (chroma == heif_chroma_420) { + expected_width = (image_width + 1) / 2; + expected_height = (image_height + 1) / 2; + } + else if (chroma == heif_chroma_422) { + expected_width = (image_width + 1) / 2; + } + } + + if (desc.width != expected_width || + desc.height != expected_height) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image component plane size does not match the image dimensions"}; + } + } + + return Error::Ok; +} + + +static Error incompatible_image_error() +{ + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image does not match the component configuration of the uncompressed image it is added to"}; +} + + +Error unc_encoder::check_image_compatibility(const std::shared_ptr& image) const +{ + // The subsampling factors and the number of bytes per pixel are taken from the prototype image. + + if (image->get_colorspace() != m_prototype_colorspace || + image->get_chroma_format() != m_prototype_chroma) { + return incompatible_image_error(); + } + + // The encoders address the component planes by the component ids of the prototype image and use + // its bit depths and data types to compute the output size. All of these have to match. + // + // We look the components up by id instead of comparing the description lists position by + // position, because that is how the encoders access them (find_component_description() searches + // by id). The order of the description list is not part of the contract: descriptions are + // appended by whoever builds the image and transfer_channel_from_image_as() even erases entries + // from the middle of the list, so equal images may well list their components in a different + // order. + + if (image->get_component_descriptions().size() != m_prototype_components.size()) { + return incompatible_image_error(); + } + + for (const prototype_component& proto : m_prototype_components) { + const ComponentDescription* desc = image->find_component_description(proto.component_id); + + if (desc == nullptr || + desc->channel != proto.channel || + desc->component_type != proto.component_type || + desc->bit_depth != proto.bit_depth || + desc->datatype != proto.datatype || + desc->has_data_plane != proto.has_data_plane) { + return incompatible_image_error(); + } + } + + // Every prototype component was found and the lists are equally long, so the tile image has + // exactly the components the encoder will write and no extra ones that would be silently + // dropped. + + // The encoders size their output buffer from the primary image dimensions, but copy each + // component plane using that plane's actual size. If a component plane is larger than the primary + // image (e.g. an alpha plane that does not match the color planes), this writes out of bounds. + // Reject any image whose component planes are inconsistent with the primary size. Chroma (Cb/Cr) + // planes are legitimately subsampled, so they are checked against the subsampled size. + + return check_component_sizes(image); +} + + +Result> unc_encoder::encode_tile(const std::shared_ptr& image) const +{ + if (Error err = check_image_compatibility(image)) { + return err; + } + + return encode_tile_impl(image); +} + + +Result unc_encoder::encode(const std::shared_ptr& src_image, + const heif_encoding_options& in_options) const +{ + // Fail before generating any boxes. encode_tile() checks this again. + if (Error err = check_image_compatibility(src_image)) { + return err; + } + + auto parameters = std::unique_ptr(heif_unci_image_parameters_alloc(), + heif_unci_image_parameters_release); + + parameters->image_width = src_image->get_width(); + parameters->image_height = src_image->get_height(); + parameters->tile_width = parameters->image_width; + parameters->tile_height = parameters->image_height; + + + heif_encoding_options options = in_options; + if (src_image->has_alpha() && !options.save_alpha_channel) { + // TODO: drop alpha channel + } + + + // --- generate configuration property boxes + + auto uncC = this->get_uncC(); + uncC->derive_box_version(); + + Encoder::CodedImageData codedImageData; + codedImageData.properties.push_back(uncC); + if (!uncC->is_minimized()) { + codedImageData.properties.push_back(this->get_cmpd()); + } + + if (m_cpat) { + codedImageData.properties.push_back(m_cpat); + } + + for (const auto& splz : m_splz) { + codedImageData.properties.push_back(splz); + } + + for (const auto& sbpm : m_sbpm) { + codedImageData.properties.push_back(sbpm); + } + + for (const auto& snuc : m_snuc) { + codedImageData.properties.push_back(snuc); + } + + if (m_cloc) { + codedImageData.properties.push_back(m_cloc); + } + + + // --- encode image + + Result > codedBitstreamResult = this->encode_tile(src_image); + if (!codedBitstreamResult) { + return codedBitstreamResult.error(); + } + + // --- optionally compress + + heif_unci_compression compression = heif_unci_compression_off; + if (in_options.version >= 8 && in_options.unci_parameters != nullptr) { + compression = in_options.unci_parameters->compression; + } + + if (compression != heif_unci_compression_off) { + uint32_t compr_fourcc = unci_compression_to_fourcc(compression); + + auto compressed = compress_unci_fourcc(compr_fourcc, + codedBitstreamResult->data(), + codedBitstreamResult->size()); + if (!compressed) { + return compressed.error(); + } + + auto cmpC = std::make_shared(); + cmpC->set_compression_type(compr_fourcc); + cmpC->set_compressed_unit_type(heif_cmpC_compressed_unit_type_image_tile); + codedImageData.properties.push_back(cmpC); + + /* Generating an icef for a single unit is redundant because when no icef is present, + * the whole data extent will automatically be used. + + auto icef = std::make_shared(); + Box_icef::CompressedUnitInfo info; + info.unit_offset = 0; + info.unit_size = compressed->size(); + icef->add_component(info); + codedImageData.properties.push_back(icef); + */ + + codedImageData.bitstream = std::move(*compressed); + } + else { + codedImageData.bitstream = std::move(*codedBitstreamResult); + } + + return codedImageData; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder.h libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,139 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_ENCODER_H +#define LIBHEIF_UNC_ENCODER_H + +#include +#include +#include + +#include "error.h" +#include "codecs/encoder.h" +#include "libheif/heif_encoding.h" +#include "unc_types.h" + +class Box_uncC; +class Box_cmpd; +class Box_cpat; +class Box_splz; +class Box_sbpm; +class Box_snuc; +class Box_cloc; +class HeifPixelImage; + +heif_cmpd_component_type heif_channel_to_component_type(heif_channel channel); + +heif_uncompressed_component_format to_unc_component_format(heif_component_datatype channel_datatype); + + +class unc_encoder +{ +public: + explicit unc_encoder(const std::shared_ptr& image); + + virtual ~unc_encoder() = default; + + std::shared_ptr get_cmpd() const { return m_cmpd; } + std::shared_ptr get_uncC() const { return m_uncC; } + std::shared_ptr get_cpat() const { return m_cpat; } + std::vector> get_splz() const { return m_splz; } + std::vector> get_sbpm() const { return m_sbpm; } + std::vector> get_snuc() const { return m_snuc; } + std::shared_ptr get_cloc() const { return m_cloc; } + + // const std::map& map_id_to_cmpd_index() { return m_map_id_to_cmpd_index; } // do we need this ? + + + virtual uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const = 0; + + // Encode one tile. This validates 'image' against the encoder configuration before handing it + // to the actual encoder implementation. Do not bypass it by calling encode_tile_impl() directly. + [[nodiscard]] Result> encode_tile(const std::shared_ptr& image) const; + + Result encode(const std::shared_ptr& src_image, + const heif_encoding_options& options) const; + + // Check that 'image' may be passed to encode_tile(). + // + // An encoder freezes its configuration (component list, bit depths, subsampling, bytes per pixel) + // when it is constructed from a prototype image. The tiled writing path + // (ImageItem_uncompressed::add_image_tile()) hands us independently constructed images later on, + // so every one of them has to be checked against that frozen configuration. Otherwise + // encode_tile_impl() sizes its output buffer according to the configuration while copying from + // planes with a different layout, which reads and writes outside the buffers. + Error check_image_compatibility(const std::shared_ptr& image) const; + + // Verify that every component plane has the size implied by the primary image dimensions + // (chroma planes may be subsampled). The encoders assume this when sizing their output buffer, + // so a mismatched plane would otherwise overflow the buffer during encoding. + static Error check_component_sizes(const std::shared_ptr& src_image); + +protected: + // Encode one tile. Only called through encode_tile(), which has verified that 'image' matches + // the configuration this encoder was constructed with. + [[nodiscard]] virtual std::vector encode_tile_impl(const std::shared_ptr& image) const = 0; + + std::shared_ptr m_cmpd; + std::shared_ptr m_uncC; + std::shared_ptr m_cpat; + std::vector> m_splz; + std::vector> m_sbpm; + std::vector> m_snuc; + std::shared_ptr m_cloc; + + std::map m_map_id_to_cmpd_index; + +private: + // Configuration of the prototype image this encoder was constructed for. Tile images passed to + // encode_tile() have to match it. See check_image_compatibility(). + struct prototype_component + { + uint32_t component_id; + heif_channel channel; + uint16_t component_type; + uint16_t bit_depth; + heif_component_datatype datatype; + bool has_data_plane; + }; + + heif_colorspace m_prototype_colorspace = heif_colorspace_undefined; + heif_chroma m_prototype_chroma = heif_chroma_undefined; + std::vector m_prototype_components; +}; + + +class unc_encoder_factory +{ +public: + virtual ~unc_encoder_factory() = default; + + static Result > get_unc_encoder(const std::shared_ptr& prototype_image, + const heif_encoding_options& options); + +private: + virtual bool can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const = 0; + + virtual std::unique_ptr create(const std::shared_ptr& prototype_image, + const heif_encoding_options& options) const = 0; +}; + +#endif //LIBHEIF_UNC_ENCODER_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,259 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_encoder_component_interleave.h" + +#include +#include + +#include "image/pixelimage.h" +#include "unc_boxes.h" + + +bool unc_encoder_factory_component_interleave::can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + if (image->has_channel(heif_channel_interleaved)) { + return false; + } + + // Only consider components with an actual data plane. cpat reference + // components have bpp=0 and no buffer; they're included in + // get_used_component_ids() but do not affect what the encoder writes. + // (Matches what the encoder constructor below uses to build m_components.) + auto component_ids = image->get_used_planar_component_ids(); + + // If any component is not byte-aligned, we use the bit-packing path which + // reads samples as uint32_t, limiting all components to 32 bpp. + bool any_non_aligned = false; + for (uint32_t id : component_ids) { + uint16_t bpp = image->get_component_bits_per_pixel(id); + if (bpp % 8 != 0) { + any_non_aligned = true; + } + } + + if (any_non_aligned) { + for (uint32_t id : component_ids) { + if (image->get_component_bits_per_pixel(id) > 32) { + return false; + } + } + } + + if (!any_non_aligned) { + // All components are byte-aligned. Only accept typical integer widths. + for (uint32_t id : component_ids) { + uint16_t bpp = image->get_component_bits_per_pixel(id); + switch (bpp) { + case 8: + case 16: + case 32: + case 64: + case 128: + case 256: + break; + default: + return false; + } + } + } + + return true; +} + + +std::unique_ptr unc_encoder_factory_component_interleave::create(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + return std::make_unique(image, options); +} + + +unc_encoder_component_interleave::unc_encoder_component_interleave(const std::shared_ptr& image, + const heif_encoding_options& options) + : unc_encoder(image) +{ + bool is_custom = (image->get_colorspace() == heif_colorspace_custom); + //uint32_t num_components = image->get_number_of_used_components(); + + auto componentIds = image->get_used_planar_component_ids(); + + for (uint32_t id : componentIds) { + heif_cmpd_component_type comp_type; + heif_channel ch = heif_channel_Y; // default for nonvisual + + if (is_custom) { + comp_type = static_cast(image->get_component_type(id)); + } + else { + ch = image->get_component_channel(id); + if (ch == heif_channel_Y && !image->has_channel(heif_channel_Cb)) { + comp_type = heif_cmpd_component_type_monochrome; + } + else { + comp_type = heif_channel_to_component_type(ch); + } + } + + uint16_t bpp = image->get_component_bits_per_pixel(id); + auto comp_format = to_unc_component_format(image->get_component_datatype(id)); + bool aligned = (bpp % 8 == 0); + + m_components.push_back({id, ch, comp_type, comp_format, bpp, aligned}); + } + + // Build cmpd/uncC boxes + m_use_memcpy = true; + + for (const auto& comp : m_components) { + if (!comp.byte_aligned) { + m_use_memcpy = false; + break; + } + } + + for (const auto& comp : m_components) { + m_uncC->add_component({m_map_id_to_cmpd_index[comp.component_id], comp.bpp, comp.component_format, 0}); + } + + m_uncC->set_interleave_type(interleave_mode_component); + if (m_use_memcpy) { + m_uncC->set_components_little_endian(std::endian::native == std::endian::little); + } + else { + // we use dense packing + m_uncC->set_components_little_endian(false); + } + m_uncC->set_block_size(0); + + if (image->get_chroma_format() == heif_chroma_420) { + m_uncC->set_sampling_type(sampling_mode_420); + } + else if (image->get_chroma_format() == heif_chroma_422) { + m_uncC->set_sampling_type(sampling_mode_422); + } + else { + m_uncC->set_sampling_type(sampling_mode_no_subsampling); + } +} + + +uint64_t unc_encoder_component_interleave::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const +{ + uint64_t total = 0; + for (const auto& comp : m_components) { + uint32_t plane_width = tile_width; + uint32_t plane_height = tile_height; + + if (comp.channel == heif_channel_Cb || comp.channel == heif_channel_Cr) { + // Adjust for chroma subsampling + if (m_uncC->get_sampling_type() == sampling_mode_420) { + plane_width = (plane_width + 1) / 2; + plane_height = (plane_height + 1) / 2; + } + else if (m_uncC->get_sampling_type() == sampling_mode_422) { + plane_width = (plane_width + 1) / 2; + } + } + + uint64_t row_bytes; + if (comp.byte_aligned) { + row_bytes = static_cast(plane_width) * ((comp.bpp + 7) / 8); + } + else { + row_bytes = (static_cast(plane_width) * comp.bpp + 7) / 8; + } + total += row_bytes * plane_height; + } + return total; +} + + +std::vector unc_encoder_component_interleave::encode_tile_impl(const std::shared_ptr& src_image) const +{ + uint64_t total_size = compute_tile_data_size_bytes(src_image->get_width(), src_image->get_height()); + std::vector data; + data.resize(total_size); + + uint64_t out_pos = 0; + + for (const auto& comp : m_components) { + uint32_t plane_width = src_image->get_component_width(comp.component_id); + uint32_t plane_height = src_image->get_component_height(comp.component_id); + uint16_t bpp = comp.bpp; + + size_t src_stride; + const uint8_t* src_data = src_image->get_component(comp.component_id, &src_stride); + + if (m_use_memcpy) { + assert(comp.byte_aligned); + + // Byte-aligned path: memcpy per row + int bytes_per_pixel = (bpp + 7) / 8; + + for (uint32_t y = 0; y < plane_height; y++) { + memcpy(data.data() + out_pos, + src_data + src_stride * y, + plane_width * bytes_per_pixel); + out_pos += plane_width * bytes_per_pixel; + } + } + else { + // Bit-packed path: bit accumulator with row-end flush + for (uint32_t y = 0; y < plane_height; y++) { + const uint8_t* row = src_data + src_stride * y; + + uint64_t accumulator = 0; + int accumulated_bits = 0; + + for (uint32_t x = 0; x < plane_width; x++) { + uint32_t sample; + + if (bpp <= 8) { + sample = row[x]; + } + else if (bpp <= 16) { + sample = reinterpret_cast(row)[x]; + } + else { + sample = reinterpret_cast(row)[x]; + } + + accumulator = (accumulator << bpp) | sample; + accumulated_bits += bpp; + + while (accumulated_bits >= 8) { + accumulated_bits -= 8; + data[out_pos++] = static_cast(accumulator >> accumulated_bits); + accumulator &= (uint64_t{1} << accumulated_bits) - 1; + } + } + + // Flush partial byte at row end (pad with zeros in LSBs) + if (accumulated_bits > 0) { + data[out_pos++] = static_cast(accumulator << (8 - accumulated_bits)); + } + } + } + } + + return data; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_component_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_component_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_component_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,68 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_ENCODER_COMPONENT_INTERLEAVE_H +#define LIBHEIF_UNC_ENCODER_COMPONENT_INTERLEAVE_H + +#include "unc_encoder.h" +#include "unc_types.h" + +#include +#include + +class unc_encoder_component_interleave : public unc_encoder +{ +public: + unc_encoder_component_interleave(const std::shared_ptr& image, + const heif_encoding_options& options); + + uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const override; + + [[nodiscard]] std::vector encode_tile_impl(const std::shared_ptr& image) const override; + +private: + struct channel_component + { + uint32_t component_id; + heif_channel channel; + heif_cmpd_component_type component_type; + heif_uncompressed_component_format component_format; + uint16_t bpp; + bool byte_aligned; + }; + + std::vector m_components; + bool m_use_memcpy = false; +}; + + +class unc_encoder_factory_component_interleave : public unc_encoder_factory +{ +public: + +private: + [[nodiscard]] bool can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const override; + + std::unique_ptr create(const std::shared_ptr& image, + const heif_encoding_options& options) const override; +}; + +#endif //LIBHEIF_UNC_ENCODER_COMPONENT_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,124 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_encoder_rgb_block_pixel_interleave.h" + +#include "image/pixelimage.h" +#include "unc_boxes.h" +#include "unc_types.h" + + +bool unc_encoder_factory_rgb_block_pixel_interleave::can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + if (image->get_colorspace() != heif_colorspace_RGB) { + return false; + } + + switch (image->get_chroma_format()) { + case heif_chroma_interleaved_RRGGBB_LE: + case heif_chroma_interleaved_RRGGBB_BE: + break; + default: + return false; + } + + if (image->get_bits_per_pixel(heif_channel_interleaved) >= 14) { + return false; + } + + return true; +} + + +std::unique_ptr unc_encoder_factory_rgb_block_pixel_interleave::create(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + return std::make_unique(image, options); +} + + +unc_encoder_rgb_block_pixel_interleave::unc_encoder_rgb_block_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options) + : unc_encoder(image) +{ + auto ids = image->get_component_ids_interleaved(); + assert(ids.size() == 3); + + uint16_t bpp = image->get_bits_per_pixel(heif_channel_interleaved); + + uint16_t nBits = 3 * bpp; + assert(nBits <= 256-8); + m_bytes_per_pixel = static_cast((nBits + 7) / 8); + + m_uncC->set_interleave_type(interleave_mode_pixel); + m_uncC->set_pixel_size(m_bytes_per_pixel); + m_uncC->set_block_size(m_bytes_per_pixel); + m_uncC->set_sampling_type(sampling_mode_no_subsampling); + m_uncC->set_block_little_endian(false); + + m_uncC->add_component({m_map_id_to_cmpd_index.find(ids[0])->second, bpp, component_format_unsigned, 0}); + m_uncC->add_component({m_map_id_to_cmpd_index.find(ids[1])->second, bpp, component_format_unsigned, 0}); + m_uncC->add_component({m_map_id_to_cmpd_index.find(ids[2])->second, bpp, component_format_unsigned, 0}); +} + + +uint64_t unc_encoder_rgb_block_pixel_interleave::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const +{ + return static_cast(tile_width) * tile_height * m_bytes_per_pixel; +} + + +std::vector unc_encoder_rgb_block_pixel_interleave::encode_tile_impl(const std::shared_ptr& src_image) const +{ + std::vector data; + + uint16_t bpp = src_image->get_bits_per_pixel(heif_channel_interleaved); + + size_t src_stride; + const auto* src_data = reinterpret_cast(src_image->get_channel_memory(heif_channel_interleaved, &src_stride)); + src_stride /= 2; + + uint64_t out_size = static_cast(src_image->get_height()) * src_image->get_width() * m_bytes_per_pixel; + data.resize(out_size); + + uint8_t* p = data.data(); + + for (uint32_t y = 0; y < src_image->get_height(); y++) { + for (uint32_t x = 0; x < src_image->get_width(); x++) { + uint16_t r = src_data[src_stride * y + 3 * x + 0]; + uint16_t g = src_data[src_stride * y + 3 * x + 1]; + uint16_t b = src_data[src_stride * y + 3 * x + 2]; + + uint64_t combined_pixel = (static_cast(r) << (2 * bpp)) | (static_cast(g) << bpp) | b; + + if (m_bytes_per_pixel > 4) { + *p++ = static_cast((combined_pixel >> 32) & 0xFF); + } + + *p++ = static_cast((combined_pixel >> 24) & 0xFF); + *p++ = static_cast((combined_pixel >> 16) & 0xFF); + *p++ = static_cast((combined_pixel >> 8) & 0xFF); + *p++ = static_cast(combined_pixel & 0xFF); + } + } + + return data; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_block_pixel_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,58 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_ENCODER_RGB_BLOCK_PIXEL_INTERLEAVE_H +#define LIBHEIF_UNC_ENCODER_RGB_BLOCK_PIXEL_INTERLEAVE_H + +#include "unc_encoder.h" + +#include +#include +#include + + +class unc_encoder_rgb_block_pixel_interleave : public unc_encoder +{ +public: + unc_encoder_rgb_block_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options); + + uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const override; + + [[nodiscard]] std::vector encode_tile_impl(const std::shared_ptr& image) const override; + +private: + uint8_t m_bytes_per_pixel = 0; +}; + + +class unc_encoder_factory_rgb_block_pixel_interleave : public unc_encoder_factory +{ +public: + +private: + [[nodiscard]] bool can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const override; + + std::unique_ptr create(const std::shared_ptr& image, + const heif_encoding_options& options) const override; +}; + +#endif //LIBHEIF_UNC_ENCODER_RGB_BLOCK_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,136 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_encoder_rgb_bytealign_pixel_interleave.h" + +#include + +#include "image/pixelimage.h" +#include "unc_boxes.h" +#include "unc_types.h" + + +bool unc_encoder_factory_rgb_bytealign_pixel_interleave::can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + if (image->get_colorspace() != heif_colorspace_RGB) { + return false; + } + + switch (image->get_chroma_format()) { + case heif_chroma_interleaved_RRGGBB_LE: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + case heif_chroma_interleaved_RRGGBBAA_BE: + break; + default: + return false; + } + + return true; +} + + +std::unique_ptr unc_encoder_factory_rgb_bytealign_pixel_interleave::create(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + return std::make_unique(image, options); +} + + +unc_encoder_rgb_bytealign_pixel_interleave::unc_encoder_rgb_bytealign_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options) + : unc_encoder(image) +{ + auto cmpd_ids = image->get_component_ids_interleaved(); + + bool save_alpha = image->has_alpha(); + + m_bytes_per_pixel = save_alpha ? 8 : 6; + assert(cmpd_ids.size() == m_bytes_per_pixel/2); + + bool little_endian = (image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE || + image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE); + + uint16_t bpp = image->get_bits_per_pixel(heif_channel_interleaved); + + uint8_t component_align_size = 2; + if (bpp == 16) { + component_align_size = 0; + } + + // make sure that we always save as big-endian so that we can read it with out own reader (unc_decoder_pixel_interleave, which only supports big-endian) + m_swap_endianess = little_endian; + + m_uncC->set_interleave_type(interleave_mode_pixel); + m_uncC->set_sampling_type(sampling_mode_no_subsampling); + m_uncC->set_components_little_endian(false); // little_endian); + m_uncC->set_pixel_size(m_bytes_per_pixel); + + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[0]], bpp, component_format_unsigned, component_align_size}); + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[1]], bpp, component_format_unsigned, component_align_size}); + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[2]], bpp, component_format_unsigned, component_align_size}); + if (save_alpha) { + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[3]], bpp, component_format_unsigned, component_align_size}); + } +} + + +uint64_t unc_encoder_rgb_bytealign_pixel_interleave::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const +{ + return static_cast(tile_width) * tile_height * m_bytes_per_pixel; +} + + +void *memcpy_swap16(uint8_t *dst, const uint8_t *src, size_t n) +{ + assert(n % 2 == 0); + + /* swap pairs */ + for (size_t i = 0; i + 1 < n; i += 2) { + dst[i] = src[i + 1]; + dst[i + 1] = src[i]; + } + + return dst; +} + + +std::vector unc_encoder_rgb_bytealign_pixel_interleave::encode_tile_impl(const std::shared_ptr& src_image) const +{ + std::vector data; + + size_t src_stride; + const uint8_t* src_data = src_image->get_channel_memory(heif_channel_interleaved, &src_stride); + + uint64_t out_size = static_cast(src_image->get_height()) * src_image->get_width() * m_bytes_per_pixel; + data.resize(out_size); + + for (uint32_t y = 0; y < src_image->get_height(); y++) { + if (m_swap_endianess) { + memcpy_swap16(data.data() + y * src_image->get_width() * m_bytes_per_pixel, src_data + src_stride * y, src_image->get_width() * m_bytes_per_pixel); + } + else { + memcpy(data.data() + y * src_image->get_width() * m_bytes_per_pixel, src_data + src_stride * y, src_image->get_width() * m_bytes_per_pixel); + } + } + + return data; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,58 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_ENCODER_RGB_BYTEALIGN_PIXEL_INTERLEAVE_H +#define LIBHEIF_UNC_ENCODER_RGB_BYTEALIGN_PIXEL_INTERLEAVE_H + +#include "unc_encoder.h" + +#include +#include + +class unc_encoder_rgb_bytealign_pixel_interleave : public unc_encoder +{ +public: + unc_encoder_rgb_bytealign_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options); + + uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const override; + + [[nodiscard]] std::vector encode_tile_impl(const std::shared_ptr& image) const override; + +private: + uint8_t m_bytes_per_pixel = 0; + bool m_swap_endianess = false; +}; + + +class unc_encoder_factory_rgb_bytealign_pixel_interleave : public unc_encoder_factory +{ +public: + +private: + [[nodiscard]] bool can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const override; + + std::unique_ptr create(const std::shared_ptr& image, + const heif_encoding_options& options) const override; +}; + + +#endif //LIBHEIF_UNC_ENCODER_RGB_BYTEALIGN_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,113 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "unc_encoder_rgb_pixel_interleave.h" + +#include + +#include "image/pixelimage.h" +#include "unc_boxes.h" +#include "unc_types.h" + + +bool unc_encoder_factory_rgb_pixel_interleave::can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + if (image->get_colorspace() != heif_colorspace_RGB) { + return false; + } + + switch (image->get_chroma_format()) { + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RGBA: + return true; + default: + return false; + } +} + + +std::unique_ptr unc_encoder_factory_rgb_pixel_interleave::create(const std::shared_ptr& image, + const heif_encoding_options& options) const +{ + return std::make_unique(image, options); +} + + +unc_encoder_rgb_pixel_interleave::unc_encoder_rgb_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options) + : unc_encoder(image) +{ + auto cmpd_ids = image->get_component_ids_interleaved(); + bool save_alpha = image->has_alpha(); + + m_bytes_per_pixel = save_alpha ? 4 : 3; + assert(cmpd_ids.size() == m_bytes_per_pixel); + + uint16_t bpp = image->get_bits_per_pixel(heif_channel_interleaved); + + if (bpp == 8) { + if (save_alpha) { + m_uncC->set_profile(fourcc("rgba")); + } + else { + m_uncC->set_profile(fourcc("rgb3")); + } + } + + uint8_t component_align_size = 0; + if (bpp != 8) { + component_align_size = 1; + } + + m_uncC->set_interleave_type(interleave_mode_pixel); + m_uncC->set_sampling_type(sampling_mode_no_subsampling); + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[0]], bpp, component_format_unsigned, component_align_size}); + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[1]], bpp, component_format_unsigned, component_align_size}); + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[2]], bpp, component_format_unsigned, component_align_size}); + if (save_alpha) { + m_uncC->add_component({m_map_id_to_cmpd_index[cmpd_ids[3]], bpp, component_format_unsigned, component_align_size}); + } +} + + + +uint64_t unc_encoder_rgb_pixel_interleave::compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const +{ + return static_cast(tile_width) * tile_height * m_bytes_per_pixel; +} + + +std::vector unc_encoder_rgb_pixel_interleave::encode_tile_impl(const std::shared_ptr& src_image) const +{ + std::vector data; + + size_t src_stride; + const uint8_t* src_data = src_image->get_channel_memory(heif_channel_interleaved, &src_stride); + + uint64_t out_size = static_cast(src_image->get_height()) * src_image->get_width() * m_bytes_per_pixel; + data.resize(out_size); + + for (uint32_t y = 0; y < src_image->get_height(); y++) { + memcpy(data.data() + y * src_image->get_width() * m_bytes_per_pixel, src_data + src_stride * y, src_image->get_width() * m_bytes_per_pixel); + } + + return data; +} diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,56 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_UNC_ENCODER_RGB_PIXEL_INTERLEAVE_H +#define LIBHEIF_UNC_ENCODER_RGB_PIXEL_INTERLEAVE_H + +#include "unc_encoder.h" + +#include +#include + +class unc_encoder_rgb_pixel_interleave : public unc_encoder +{ +public: + unc_encoder_rgb_pixel_interleave(const std::shared_ptr& image, + const heif_encoding_options& options); + + uint64_t compute_tile_data_size_bytes(uint32_t tile_width, uint32_t tile_height) const override; + + [[nodiscard]] std::vector encode_tile_impl(const std::shared_ptr& image) const override; + +private: + uint8_t m_bytes_per_pixel = 0; +}; + + +class unc_encoder_factory_rgb_pixel_interleave : public unc_encoder_factory +{ +public: + +private: + [[nodiscard]] bool can_encode(const std::shared_ptr& image, + const heif_encoding_options& options) const override; + + std::unique_ptr create(const std::shared_ptr& image, + const heif_encoding_options& options) const override; +}; + +#endif //LIBHEIF_UNC_ENCODER_RGB_PIXEL_INTERLEAVE_H diff -Nru libheif-1.19.8/libheif/codecs/uncompressed/unc_types.h libheif-1.23.4/libheif/codecs/uncompressed/unc_types.h --- libheif-1.19.8/libheif/codecs/uncompressed/unc_types.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/uncompressed/unc_types.h 2026-09-06 14:45:17.000000000 +0000 @@ -23,106 +23,10 @@ #define LIBHEIF_UNC_TYPES_H #include +#include -/** - * Component type. - * - * See ISO/IEC 23001-17 Table 1. -*/ -enum heif_uncompressed_component_type -{ - /** - * Monochrome component. - */ - component_type_monochrome = 0, - - /** - * Luma component (Y). - */ - component_type_Y = 1, - - /** - * Chroma component (Cb / U). - */ - component_type_Cb = 2, - - /** - * Chroma component (Cr / V). - */ - component_type_Cr = 3, - - /** - * Red component (R). - */ - component_type_red = 4, - - /** - * Green component (G). - */ - component_type_green = 5, - - /** - * Blue component (B). - */ - component_type_blue = 6, - - /** - * Alpha / transparency component (A). - */ - component_type_alpha = 7, - - /** - * Depth component (D). - */ - component_type_depth = 8, - - /** - * Disparity component (Disp). - */ - component_type_disparity = 9, - - /** - * Palette component (P). - * - * The {@code component_format} value for this component shall be 0. - */ - component_type_palette = 10, - - /** - * Filter Array (FA) component such as Bayer, RGBW, etc. - */ - component_type_filter_array = 11, - - /** - * Padded component (unused bits/bytes). - */ - component_type_padded = 12, - - /** - * Cyan component (C). - */ - component_type_cyan = 13, - - /** - * Magenta component (M). - */ - component_type_magenta = 14, - - /** - * Yellow component (Y). - */ - component_type_yellow = 15, - - /** - * Key (black) component (K). - */ - component_type_key_black = 16, - - /** - * Maximum valid component type value. - */ - component_type_max_valid = component_type_key_black -}; +// Update this when adding new values to heif_uncompressed_component_type. +#define heif_cmpd_component_type_max_valid heif_cmpd_component_type_key_black /** * HEIF uncompressed component format. @@ -132,7 +36,7 @@ * * See ISO/IEC 23001-17 Table 2. */ -enum heif_uncompressed_component_format +enum heif_uncompressed_component_format : uint8_t { /** * Unsigned integer. @@ -176,9 +80,16 @@ component_format_complex = 2, /** + * Signed integer. + * + * The component value is a two's complement signed integer. + */ + component_format_signed = 3, + + /** * Maximum valid component format identifier. */ - component_format_max_valid = component_format_complex + component_format_max_valid = component_format_signed }; @@ -280,6 +191,4 @@ }; - - #endif //LIBHEIF_UNC_TYPES_H diff -Nru libheif-1.19.8/libheif/codecs/vvc_boxes.cc libheif-1.23.4/libheif/codecs/vvc_boxes.cc --- libheif-1.19.8/libheif/codecs/vvc_boxes.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,13 +19,15 @@ */ #include "vvc_boxes.h" +#include "hevc_boxes.h" +#include "codecs/decoder.h" #include "file.h" #include #include #include #include #include -#include +#include "api_structs.h" Error Box_vvcC::parse(BitstreamRange& range, const heif_security_limits* limits) @@ -119,7 +121,13 @@ c.max_picture_height = range.read16(); c.avg_frame_rate = range.read16(); } - + else { + return Error{ + heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Reading vvcC configuration with ptl_present_flag=0 is not supported." + }; + } // read NAL arrays @@ -206,6 +214,18 @@ } +const std::vector* Box_vvcC::get_first_nal_of_type(uint8_t nal_type) const +{ + for (const auto& arr : m_nal_array) { + if (arr.m_NAL_unit_type == nal_type && !arr.m_nal_units.empty()) { + return &arr.m_nal_units[0]; + } + } + + return nullptr; +} + + void Box_vvcC::append_nal_data(const uint8_t* data, size_t size) { std::vector nal; @@ -263,7 +283,7 @@ byte = 0; if (c.num_sublayers > 1) { - uint8_t mask=0x80; + uint8_t mask = 0x80; for (int i = c.num_sublayers - 2; i >= 0; i--) { if (ptl.ptl_sublayer_level_present_flag[i]) { @@ -271,8 +291,9 @@ } mask >>= 1; } + + writer.write8(byte); } - writer.write8(byte); for (int i=c.num_sublayers-2; i >= 0; i--) { if (ptl.ptl_sublayer_level_present_flag[i]) { @@ -391,32 +412,11 @@ return sstr.str(); } -static std::vector remove_start_code_emulation(const uint8_t* sps, size_t size) -{ - std::vector out_data; - - for (size_t i = 0; i < size; i++) { - if (i + 2 < size && - sps[i] == 0 && - sps[i + 1] == 0 && - sps[i + 2] == 3) { - out_data.push_back(0); - out_data.push_back(0); - i += 2; - } - else { - out_data.push_back(sps[i]); - } - } - - return out_data; -} - - Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size, Box_vvcC::configuration* config, - int* width, int* height) + uint32_t* width, uint32_t* height, + ImageSize* coded_size) { // remove start-code emulation bytes from SPS header stream @@ -425,7 +425,7 @@ sps = sps_no_emul.data(); size = sps_no_emul.size(); - BitReader reader(sps, (int) size); + BitReader reader(sps, size); // skip NAL header reader.skip_bits(2 * 8); @@ -460,7 +460,10 @@ bool gci_present_flag = reader.get_bits(1); if (gci_present_flag) { - assert(false); + // TODO + return {heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + "VVC SPS with general_constraints_info is not supported yet"}; } else { ptl.num_bytes_constraint_info = 1; @@ -500,18 +503,28 @@ reader.skip_bits(1); // sps_res_change_in_clvs_allowed_flag } - int sps_pic_width_max_in_luma_samples; - int sps_pic_height_max_in_luma_samples; - - bool success; - success = reader.get_uvlc(&sps_pic_width_max_in_luma_samples); - (void)success; - success = reader.get_uvlc(&sps_pic_height_max_in_luma_samples); - (void)success; + Error invalidUVLC{ + heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "Invalid variable length code in VVC SPS header" + }; + + uint32_t sps_pic_width_max_in_luma_samples; + uint32_t sps_pic_height_max_in_luma_samples; + + if (!reader.get_uvlc(&sps_pic_width_max_in_luma_samples) || + !reader.get_uvlc(&sps_pic_height_max_in_luma_samples)) { + return invalidUVLC; + } *width = sps_pic_width_max_in_luma_samples; *height = sps_pic_height_max_in_luma_samples; + if (coded_size) { + coded_size->width = *width; + coded_size->height = *height; + } + if (sps_pic_width_max_in_luma_samples > 0xFFFF || sps_pic_height_max_in_luma_samples > 0xFFFF) { return {heif_error_Encoding_error, @@ -524,21 +537,45 @@ int sps_conformance_window_flag = reader.get_bits(1); if (sps_conformance_window_flag) { - int left,right,top,bottom; - reader.get_uvlc(&left); - reader.get_uvlc(&right); - reader.get_uvlc(&top); - reader.get_uvlc(&bottom); + uint32_t left, right, top, bottom; + if (!reader.get_uvlc(&left) || + !reader.get_uvlc(&right) || + !reader.get_uvlc(&top) || + !reader.get_uvlc(&bottom)) { + return invalidUVLC; + } + + // SubWidthC / SubHeightC per ITU-T H.266 Table 5-1, indexed by chroma_format_idc. + uint32_t subWidthC = 1, subHeightC = 1; + switch (config->chroma_format_idc) { + case 1: subWidthC = 2; subHeightC = 2; break; // 4:2:0 + case 2: subWidthC = 2; subHeightC = 1; break; // 4:2:2 + default: break; // mono / 4:4:4 + } + + const uint64_t crop_w = (uint64_t)subWidthC * ((uint64_t)left + (uint64_t)right); + const uint64_t crop_h = (uint64_t)subHeightC * ((uint64_t)top + (uint64_t)bottom); + if (crop_w > *width || crop_h > *height) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS conformance window exceeds image dimensions"}; + } + *width -= (uint32_t)crop_w; + *height -= (uint32_t)crop_h; } bool sps_subpic_info_present_flag = reader.get_bits(1); if (sps_subpic_info_present_flag) { - assert(false); // TODO + // TODO + return {heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + "VVC SPS with subpicture info is not supported yet"}; } - int bitDepth_minus8; - success = reader.get_uvlc(&bitDepth_minus8); - (void)success; + uint32_t bitDepth_minus8; + if (!reader.get_uvlc(&bitDepth_minus8)) { + return invalidUVLC; + } if (bitDepth_minus8 > 0xFF - 8) { return {heif_error_Encoding_error, heif_suberror_Unspecified, "VCC bit depth out of range."}; diff -Nru libheif-1.19.8/libheif/codecs/vvc_boxes.h libheif-1.23.4/libheif/codecs/vvc_boxes.h --- libheif-1.19.8/libheif/codecs/vvc_boxes.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -26,6 +26,7 @@ #include #include "image-items/image_item.h" #include +#include "sequences/seq_boxes.h" class Box_vvcC : public FullBox @@ -39,12 +40,12 @@ bool is_essential() const override { return true; } struct VvcPTLRecord { - uint8_t num_bytes_constraint_info; // 6 bits - uint8_t general_profile_idc; // 7 bits - uint8_t general_tier_flag; // 1 bit - uint8_t general_level_idc; // 8 bits - uint8_t ptl_frame_only_constraint_flag; // 1 bit - uint8_t ptl_multi_layer_enabled_flag; // 1 bit + uint8_t num_bytes_constraint_info = 0; // 6 bits + uint8_t general_profile_idc = 0; // 7 bits + uint8_t general_tier_flag = 0; // 1 bit + uint8_t general_level_idc = 0; // 8 bits + uint8_t ptl_frame_only_constraint_flag = 0; // 1 bit + uint8_t ptl_multi_layer_enabled_flag = 0; // 1 bit std::vector general_constraint_info; std::vector ptl_sublayer_level_present_flag; // TODO: should we save this here or can we simply derive it on the fly? @@ -58,16 +59,16 @@ uint8_t LengthSizeMinusOne = 3; // 0,1,3 default: 4 bytes for NAL unit lengths bool ptl_present_flag = true; - // only of PTL present - uint16_t ols_idx; // 9 bits - uint8_t num_sublayers; // 3 bits - uint8_t constant_frame_rate; // 2 bits - uint8_t chroma_format_idc; // 2 bits - uint8_t bit_depth_minus8; // 3 bits - struct VvcPTLRecord native_ptl; - uint16_t max_picture_width; - uint16_t max_picture_height; - uint16_t avg_frame_rate; + // only if PTL present + uint16_t ols_idx = 0; // 9 bits + uint8_t num_sublayers = 0; // 3 bits + uint8_t constant_frame_rate = 0; // 2 bits + uint8_t chroma_format_idc = 0; // 2 bits + uint8_t bit_depth_minus8 = 0; // 3 bits + struct VvcPTLRecord native_ptl{}; + uint16_t max_picture_width = 0; + uint16_t max_picture_height = 0; + uint16_t avg_frame_rate = 0; }; @@ -79,9 +80,15 @@ const configuration& get_configuration() const { return m_configuration; } + configuration& get_configuration() { return m_configuration; } + void append_nal_data(const std::vector& nal); void append_nal_data(const uint8_t* data, size_t size); + // Returns the bytes of the first NAL unit of the requested type stored in + // the configuration record, or nullptr if none is present. + const std::vector* get_first_nal_of_type(uint8_t nal_type) const; + Error write(StreamWriter& writer) const override; protected: @@ -101,8 +108,24 @@ }; +class Box_vvc1 : public Box_VisualSampleEntry +{ +public: + Box_vvc1() + { + set_short_type(fourcc("vvc1")); + } +}; + + +struct ImageSize; + +// Parses a VVC SPS NAL unit. *width / *height return the post-conformance- +// window cropping (display) dimensions. If non-null, *coded_size receives the +// pre-cropping dimensions actually allocated by the decoder. Error parse_sps_for_vvcC_configuration(const uint8_t* sps, size_t size, Box_vvcC::configuration* inout_config, - int* width, int* height); + uint32_t* width, uint32_t* height, + ImageSize* coded_size = nullptr); #endif // LIBHEIF_VVC_BOXES_H diff -Nru libheif-1.19.8/libheif/codecs/vvc_dec.cc libheif-1.23.4/libheif/codecs/vvc_dec.cc --- libheif-1.19.8/libheif/codecs/vvc_dec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_dec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,6 +22,7 @@ #include "vvc_boxes.h" #include "error.h" #include "context.h" +#include "plugins/nalu_utils.h" #include @@ -56,6 +57,26 @@ } +Result> Decoder_VVC::get_coded_image_size_from_config() const +{ + const std::vector* sps = m_vvcC->get_first_nal_of_type(VVC_NAL_UNIT_SPS_NUT); + if (!sps || sps->empty()) { + return std::optional{}; + } + + Box_vvcC::configuration scratch = m_vvcC->get_configuration(); + uint32_t cropped_w = 0, cropped_h = 0; + ImageSize coded{}; + Error e = parse_sps_for_vvcC_configuration(sps->data(), sps->size(), &scratch, + &cropped_w, &cropped_h, &coded); + if (e) { + return e; + } + + return std::optional{coded}; +} + + Error Decoder_VVC::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { *out_chroma = (heif_chroma) (m_vvcC->get_configuration().chroma_format_idc); diff -Nru libheif-1.19.8/libheif/codecs/vvc_dec.h libheif-1.23.4/libheif/codecs/vvc_dec.h --- libheif-1.19.8/libheif/codecs/vvc_dec.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_dec.h 2026-09-06 14:45:17.000000000 +0000 @@ -47,6 +47,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_coded_image_size_from_config() const override; + private: const std::shared_ptr m_vvcC; }; diff -Nru libheif-1.19.8/libheif/codecs/vvc_enc.cc libheif-1.23.4/libheif/codecs/vvc_enc.cc --- libheif-1.19.8/libheif/codecs/vvc_enc.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_enc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,297 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "vvc_enc.h" +#include "vvc_boxes.h" +#include "error.h" +#include "context.h" +#include "api_structs.h" + +#include + +#include "plugins/nalu_utils.h" + + +Result Encoder_VVC::encode(const std::shared_ptr& image, + struct heif_encoder* encoder, + const struct heif_encoding_options& options, + enum heif_image_input_class input_class) +{ + Encoder::CodedImageData codedImage; + + auto vvcC = std::make_shared(); + codedImage.properties.push_back(vvcC); + + + heif_image c_api_image; + c_api_image.image = image; + + struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); + if (err.code) { + return Error(err.code, + err.subcode, + err.message); + } + + uint32_t encoded_width = 0; + uint32_t encoded_height = 0; + + for (;;) { + uint8_t* data; + int size; + + err = encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, NULL); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == NULL) { + break; + } + + + const uint8_t NAL_SPS = 15; + + uint8_t nal_type = 0; + if (size>=2) { + nal_type = (data[1] >> 3) & 0x1F; + } + + if (nal_type == NAL_SPS) { + Box_vvcC::configuration config; + + parse_sps_for_vvcC_configuration(data, size, &config, &encoded_width, &encoded_height); + + vvcC->set_configuration(config); + } + + switch (nal_type) { + case 14: // VPS + case 15: // SPS + case 16: // PPS + vvcC->append_nal_data(data, size); + break; + + default: + codedImage.append_with_4bytes_size(data, size); + } + } + + codedImage.codingConstraints.intra_pred_used = true; + codedImage.codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + codedImage.codingConstraints.max_ref_per_pic = 0; + + return codedImage; +} + + +std::shared_ptr Encoder_VVC::get_sample_description_box(const CodedImageData& data) const +{ + auto vvc1 = std::make_shared(); + vvc1->get_VisualSampleEntry().compressorname = "VVC"; + + for (const auto& prop : data.properties) { + if (prop->get_short_type() == fourcc("vvcC")) { + vvc1->append_child_box(prop); + return vvc1; + } + } + + // box not yet available + return nullptr; +} + + +Error Encoder_VVC::encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) +{ + heif_image c_api_image; + c_api_image.image = image; + + if (!m_encoder_active) { + heif_error err = encoder->plugin->start_sequence_encoding(encoder->encoder, &c_api_image, input_class, + framerate_num, framerate_denom, + &options); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + m_vvcC = std::make_shared(); + m_encoder_active = true; + uint64_t avg_fr = framerate_denom ? (256ULL * framerate_num + framerate_denom / 2) / framerate_denom : 0; + m_avg_frame_rate = avg_fr <= UINT16_MAX ? static_cast(avg_fr) : 0; + } + + Error dataErr = get_data(encoder); + if (dataErr) { + return dataErr; + } + + heif_error err = encoder->plugin->encode_sequence_frame(encoder->encoder, &c_api_image, frame_number); + if (err.code) { + return { + err.code, + err.subcode, + err.message + }; + } + + return get_data(encoder); +} + + +Error Encoder_VVC::encode_sequence_flush(heif_encoder* encoder) +{ + encoder->plugin->end_sequence_encoding(encoder->encoder); + m_encoder_active = false; + m_end_of_sequence_reached = true; + + return get_data(encoder); +} + + +std::optional Encoder_VVC::encode_sequence_get_data() +{ + return std::move(m_current_output_data); +} + +Error Encoder_VVC::get_data(heif_encoder* encoder) +{ + //CodedImageData codedImage; + + bool got_some_data = false; + + for (;;) { + uint8_t* data; + int size; + + uintptr_t frameNr=0; + int more_frame_packets = 1; + struct heif_error err = encoder->plugin->get_compressed_data2(encoder->encoder, &data, &size, &frameNr, nullptr, &more_frame_packets); + if (err.code) { + return Error(err.code, err.subcode, err.message); + } + + if (data == nullptr) { + break; + } + + got_some_data = true; + + const uint8_t nal_type = (data[1] >> 3); + const bool is_sync = (nal_type == 7 || nal_type == 8 || nal_type == 9); + const bool is_image_data = (nal_type >= 0 && nal_type <= VVC_NAL_UNIT_MAX_VCL); + + // std::cout << "received frameNr=" << frameNr << " nal_type:" << ((int)nal_type) << " size: " << size << "\n"; + + if (nal_type == VVC_NAL_UNIT_SPS_NUT && m_vvcC) { + parse_sps_for_vvcC_configuration(data, size, + &m_vvcC->get_configuration(), + &m_encoded_image_width, &m_encoded_image_height); + m_vvcC->get_configuration().avg_frame_rate = m_avg_frame_rate; + } + + switch (nal_type) { + case VVC_NAL_UNIT_VPS_NUT: + if (m_vvcC && !m_vvcC_has_VPS) m_vvcC->append_nal_data(data, size); + m_vvcC_has_VPS = true; + break; + + case VVC_NAL_UNIT_SPS_NUT: + if (m_vvcC && !m_vvcC_has_SPS) m_vvcC->append_nal_data(data, size); + m_vvcC_has_SPS = true; + break; + + case VVC_NAL_UNIT_PPS_NUT: + if (m_vvcC && !m_vvcC_has_PPS) m_vvcC->append_nal_data(data, size); + m_vvcC_has_PPS = true; + break; + + default: + if (!m_current_output_data) { + m_current_output_data = CodedImageData{}; + } + m_current_output_data->append_with_4bytes_size(data, size); + + if (is_image_data) { + m_current_output_data->is_sync_frame = is_sync; + m_current_output_data->frame_nr = frameNr; + } + } + + if (!more_frame_packets) { + break; + } + } + + if (!got_some_data) { + return {}; + } + + if (!m_encoded_image_width || !m_encoded_image_height) { + return Error(heif_error_Encoder_plugin_error, + heif_suberror_Invalid_image_size); + } + + + // --- return hvcC when all headers are included and it was not returned yet + // TODO: it's maybe better to return this at the end so that we are sure to have all headers + // and also complete codingConstraints. + + //if (hvcC_has_VPS && m_hvcC_has_SPS && m_hvcC_has_PPS && !m_hvcC_returned) { + if (m_end_of_sequence_reached && m_vvcC && !m_vvcC_sent) { + m_current_output_data->properties.push_back(m_vvcC); + m_vvcC = nullptr; + m_vvcC_sent = true; + } + + m_current_output_data->encoded_image_width = m_encoded_image_width; + m_current_output_data->encoded_image_height = m_encoded_image_height; + + + // Make sure that the encoder plugin works correctly and the encoded image has the correct size. +#if 0 + if (encoder->plugin->plugin_api_version >= 3 && + encoder->plugin->query_encoded_size != nullptr) { + uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); + + encoder->plugin->query_encoded_size(encoder->encoder, + image->get_width(), image->get_height(), + &check_encoded_width, + &check_encoded_height); + + assert((int)check_encoded_width == encoded_width); + assert((int)check_encoded_height == encoded_height); + } +#endif + + m_current_output_data->codingConstraints.intra_pred_used = true; + m_current_output_data->codingConstraints.all_ref_pics_intra = true; // TODO: change when we use predicted frames + + return {}; +} diff -Nru libheif-1.19.8/libheif/codecs/vvc_enc.h libheif-1.23.4/libheif/codecs/vvc_enc.h --- libheif-1.19.8/libheif/codecs/vvc_enc.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/codecs/vvc_enc.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,80 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef HEIF_ENCODER_VVC_H +#define HEIF_ENCODER_VVC_H + +#include "libheif/heif.h" +#include "box.h" +#include "error.h" +#include "file.h" + +#include +#include +#include +#include +#include "codecs/encoder.h" + + +class Encoder_VVC : public Encoder { +public: + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + std::shared_ptr get_sample_description_box(const CodedImageData&) const override; + + + bool encode_sequence_started() const override { return m_encoder_active; } + + Error encode_sequence_frame(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options& options, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + uintptr_t frame_number) override; + + Error encode_sequence_flush(heif_encoder* encoder) override; + + std::optional encode_sequence_get_data() override; + +private: + bool m_encoder_active = false; + bool m_end_of_sequence_reached = false; + + // Whether the vvcC is complete and was returned in an encode_sequence_get_data() call. + bool m_vvcC_has_VPS = false; + bool m_vvcC_has_SPS = false; + bool m_vvcC_has_PPS = false; + std::shared_ptr m_vvcC; + bool m_vvcC_sent = false; + + uint32_t m_encoded_image_width = 0; + uint32_t m_encoded_image_height = 0; + uint16_t m_avg_frame_rate = 0; + + std::optional m_current_output_data; + + Error get_data(heif_encoder*); +}; + + +#endif diff -Nru libheif-1.19.8/libheif/color-conversion/alpha.cc libheif-1.23.4/libheif/color-conversion/alpha.cc --- libheif-1.19.8/libheif/color-conversion/alpha.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/alpha.cc 2026-09-06 14:45:17.000000000 +0000 @@ -18,13 +18,52 @@ * along with libheif. If not, see . */ +#include +#include +#include #include "alpha.h" +namespace { + // Expand an `in_bits`-bit sample to `out_bits` bits by bit replication + // (repeating the source bit pattern); requires out_bits >= in_bits >= 1. + // + // For out_bits <= 2*in_bits a single extra copy of the top bits suffices: + // (in << (out_bits - in_bits)) | (in >> (2*in_bits - out_bits)) + // both shift counts are non-negative in that range. + // + // For a wider expansion (out_bits > 2*in_bits) the right-shift exponent above + // would go negative, which is undefined behaviour. Instead we lay down the + // required number of copies with a single multiply: multiplying by a constant + // whose set bits sit at 0, in_bits, 2*in_bits, ... places ceil(out_bits/in_bits) + // non-overlapping copies of the pattern, and one final (non-negative) right + // shift keeps the top out_bits. All arithmetic fits in 32 bits for the sample + // widths used here (out_bits <= 16). + inline uint32_t replicate_sample_bits(uint32_t in, int in_bits, int out_bits) + { + assert(in_bits >= 1 && out_bits >= in_bits); + + if (out_bits <= 2 * in_bits) { + return (in << (out_bits - in_bits)) | (in >> (2 * in_bits - out_bits)); + } + + int num_copies = (out_bits + in_bits - 1) / in_bits; // ceil(out_bits / in_bits) + uint32_t replication_const = 0; + for (int j = 0; j < num_copies; j++) { + replication_const |= static_cast(1) << (j * in_bits); + } + + uint32_t replicated = in * replication_const; + return replicated >> (num_copies * in_bits - out_bits); + } +} + + std::vector Op_drop_alpha_plane::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // only drop alpha plane if it is not needed in output @@ -37,6 +76,10 @@ return {}; } + if (options_ext.alpha_composition_mode != heif_alpha_composition_mode_none) { + return {}; + } + std::vector states; ColorState output_state; @@ -57,6 +100,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -75,7 +119,395 @@ heif_channel_G, heif_channel_B}) { if (input->has_channel(channel)) { - outimg->copy_new_plane_from(input, channel, channel, limits); + outimg->copy_new_channel_from(input, channel, channel, limits); + } + } + + return outimg; +} + + +template +std::vector +Op_flatten_alpha_plane::state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const +{ + bool hdr = !std::is_same::value; + + // TODO: this Op only works when all channels are either HDR or all are SDR. + // But there is currently no easy way to check that. + + if ((input_state.bits_per_pixel > 8) != hdr) { + return {}; + } + + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + + // only drop alpha plane if it is not needed in output + + if ((input_state.chroma != heif_chroma_monochrome && + input_state.chroma != heif_chroma_420 && + input_state.chroma != heif_chroma_422 && + input_state.chroma != heif_chroma_444) || + input_state.has_alpha == false || + target_state.has_alpha == true) { + return {}; + } + + if (options_ext.alpha_composition_mode == heif_alpha_composition_mode_none) { + return {}; + } + + std::vector states; + + ColorState output_state; + + // --- drop alpha plane + + output_state = input_state; + output_state.has_alpha = false; + + states.emplace_back(output_state, SpeedCosts_Trivial); + + return states; +} + + +template +Result> +Op_flatten_alpha_plane::convert_colorspace(const std::shared_ptr& input_raw, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const +{ + std::shared_ptr input = input_raw; + + heif_color_conversion_options_ext options_ext_skip_alpha = options_ext; + options_ext_skip_alpha.alpha_composition_mode = heif_alpha_composition_mode_none; + + if (options_ext.alpha_composition_mode != heif_alpha_composition_mode_none) { + Result> convInput = ::convert_colorspace(input, + heif_colorspace_RGB, + heif_chroma_444, + input_state.nclx, + input_state.bits_per_pixel, + options, &options_ext_skip_alpha, + limits); + if (!convInput) { + return convInput.error(); + } + else { + input = *convInput; + } + } + + uint32_t width = input->get_width(); + uint32_t height = input->get_height(); + + auto outimg = std::make_shared(); + + outimg->create(width, height, + input->get_colorspace(), + input->get_chroma_format()); + + for (heif_channel channel : {heif_channel_R, + heif_channel_G, + heif_channel_B}) { + outimg->add_channel(channel, width, height, target_state.bits_per_pixel, limits); + + const Pixel* p_alpha; + size_t stride_alpha; + p_alpha = (const Pixel*)input->get_channel_memory(heif_channel_Alpha, &stride_alpha); + int bpp_alpha = input->get_bits_per_pixel(heif_channel_Alpha); + Pixel alpha_max = (Pixel)((1 << bpp_alpha) - 1); + + // The composite below (p_in*a + bkg*(alpha_max-a)) is a weighted sum bounded by + // (2^bpp-1) * (2^bpp_alpha-1). For 8-bit samples that fits a plain int; for + // wider samples it can exceed a signed 32-bit int (65535*65535 overflows), so + // accumulate in an unsigned 32-bit integer, which is exact as long as both the + // colour and alpha samples are <= 16 bits (bound < 2^32). Using uint32_t rather + // than a 64-bit type keeps the hot path efficient on non-64-bit architectures. + // Reject anything wider instead of overflowing. + if (bpp_alpha > 16 || input->get_bits_per_pixel(channel) > 16) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Alpha compositing is not supported for images with more than 16 bits per sample."}; + } + using composite_t = std::conditional_t<(sizeof(Pixel) > 1), uint32_t, int>; + + const Pixel* p_in; + size_t stride_in; + p_in = (const Pixel*)input->get_channel_memory(channel, &stride_in); + + Pixel* p_out; + size_t stride_out; + p_out = (Pixel*)outimg->get_channel_memory(channel, &stride_out); + + if (sizeof(Pixel) == 2) { + stride_alpha /= 2; + stride_in /= 2; + stride_out /= 2; + } + + if (options_ext.alpha_composition_mode == heif_alpha_composition_mode_solid_color || + (options_ext.alpha_composition_mode == heif_alpha_composition_mode_checkerboard && options_ext.checkerboard_square_size == 0)) { + uint16_t bkg16; + + switch (channel) { + case heif_channel_R: + bkg16 = options_ext.background_red; + break; + case heif_channel_G: + bkg16 = options_ext.background_green; + break; + case heif_channel_B: + bkg16 = options_ext.background_blue; + break; + default: + assert(false); + bkg16 = 0; + } + + Pixel bkg = static_cast(bkg16 >> (16 - input->get_bits_per_pixel(channel))); + + for (uint32_t y = 0; y < height; y++) + for (uint32_t x = 0; x < width; x++) { + int a = p_alpha[y * stride_alpha + x]; + composite_t composite = static_cast(p_in[y * stride_in + x]) * a + + static_cast(bkg) * (alpha_max - a); + p_out[y * stride_out + x] = static_cast(composite >> bpp_alpha); + } + } + else { + uint16_t bkg16_1, bkg16_2; + + switch (channel) { + case heif_channel_R: + bkg16_1 = options_ext.background_red; + bkg16_2 = options_ext.secondary_background_red; + break; + case heif_channel_G: + bkg16_1 = options_ext.background_green; + bkg16_2 = options_ext.secondary_background_green; + break; + case heif_channel_B: + bkg16_1 = options_ext.background_blue; + bkg16_2 = options_ext.secondary_background_blue; + break; + default: + assert(false); + bkg16_1 = bkg16_2 = 0; + } + + Pixel bkg1 = static_cast(bkg16_1 >> (16 - input->get_bits_per_pixel(channel))); + Pixel bkg2 = static_cast(bkg16_2 >> (16 - input->get_bits_per_pixel(channel))); + + for (uint32_t y = 0; y < height; y++) + for (uint32_t x = 0; x < width; x++) { + uint8_t parity = (x / options_ext.checkerboard_square_size + y / options_ext.checkerboard_square_size) % 2; + Pixel bkg = parity ? bkg1 : bkg2; + + int a = p_alpha[y * stride_alpha + x]; + composite_t composite = static_cast(p_in[y * stride_in + x]) * a + + static_cast(bkg) * (alpha_max - a); + p_out[y * stride_out + x] = static_cast(composite >> bpp_alpha); + } + } + + } + + if (options_ext.alpha_composition_mode != heif_alpha_composition_mode_none) { + Result> convOutput = ::convert_colorspace(outimg, + input_raw->get_colorspace(), + input_raw->get_chroma_format(), + input_state.nclx, + input_state.bits_per_pixel, + options, &options_ext_skip_alpha, + limits); + if (!convOutput) { + return convOutput.error(); + } + else { + return convOutput; + } + } + else { + return outimg; + } +} + +template class Op_flatten_alpha_plane; +template class Op_flatten_alpha_plane; + + +std::vector +Op_adjust_alpha_bit_depth::state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const +{ + // Only applicable when alpha BPP differs from color BPP + if (!input_state.has_alpha || + input_state.get_alpha_bits_per_pixel() == input_state.bits_per_pixel) { + return {}; + } + + // Only for planar formats with alpha + if (input_state.chroma != heif_chroma_monochrome && + input_state.chroma != heif_chroma_420 && + input_state.chroma != heif_chroma_422 && + input_state.chroma != heif_chroma_444) { + return {}; + } + + // Rewrites the alpha plane from its own bit depth to the colour bit depth, so both + // ends have to be accessible as 8- or 16-bit samples. + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + std::vector states; + + ColorState output_state = input_state; + output_state.alpha_bits_per_pixel = input_state.bits_per_pixel; + + states.emplace_back(output_state, SpeedCosts_Unoptimized); + + return states; +} + + +Result> +Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr& input, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const +{ + uint32_t width = input->get_width(); + uint32_t height = input->get_height(); + + auto outimg = std::make_shared(); + outimg->create(width, height, input->get_colorspace(), input->get_chroma_format()); + + // Copy all non-alpha channels unchanged + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, + heif_channel_R, heif_channel_G, heif_channel_B}) { + if (input->has_channel(channel)) { + outimg->copy_new_channel_from(input, channel, channel, limits); + } + } + + if (!input->has_channel(heif_channel_Alpha)) { + return outimg; + } + + int input_alpha_bpp = input->get_bits_per_pixel(heif_channel_Alpha); + int target_bpp = input_state.bits_per_pixel; + + uint32_t alpha_width = input->get_width(heif_channel_Alpha); + uint32_t alpha_height = input->get_height(heif_channel_Alpha); + + if (auto err = outimg->add_channel(heif_channel_Alpha, alpha_width, alpha_height, target_bpp, limits)) { + return err; + } + + if (input_alpha_bpp <= 8 && target_bpp > 8) { + // Upscale: 8-bit alpha -> HDR using bit replication + const uint8_t* p_in; + size_t stride_in; + p_in = input->get_channel_memory(heif_channel_Alpha, &stride_in); + + uint16_t* p_out; + size_t stride_out; + p_out = (uint16_t*) outimg->get_channel_memory(heif_channel_Alpha, &stride_out); + stride_out /= 2; + + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + int in = p_in[y * stride_in + x]; + p_out[y * stride_out + x] = (uint16_t) replicate_sample_bits(in, input_alpha_bpp, target_bpp); + } + } + else if (input_alpha_bpp > 8 && target_bpp <= 8) { + // Downscale: HDR alpha -> 8-bit + const uint16_t* p_in; + size_t stride_in; + p_in = (const uint16_t*) input->get_channel_memory(heif_channel_Alpha, &stride_in); + stride_in /= 2; + + uint8_t* p_out; + size_t stride_out; + p_out = outimg->get_channel_memory(heif_channel_Alpha, &stride_out); + + int shift = input_alpha_bpp - 8; + + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift); + } + } + else if (input_alpha_bpp > 8 && target_bpp > 8) { + // HDR alpha -> different HDR: rescale within uint16_t + const uint16_t* p_in; + size_t stride_in; + p_in = (const uint16_t*) input->get_channel_memory(heif_channel_Alpha, &stride_in); + stride_in /= 2; + + uint16_t* p_out; + size_t stride_out; + p_out = (uint16_t*) outimg->get_channel_memory(heif_channel_Alpha, &stride_out); + stride_out /= 2; + + if (target_bpp > input_alpha_bpp) { + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + int in = p_in[y * stride_in + x]; + p_out[y * stride_out + x] = (uint16_t) replicate_sample_bits(in, input_alpha_bpp, target_bpp); + } + } + else { + int shift = input_alpha_bpp - target_bpp; + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + p_out[y * stride_out + x] = (uint16_t) (p_in[y * stride_in + x] >> shift); + } + } + } + else { + // SDR alpha -> different SDR (both <= 8) + const uint8_t* p_in; + size_t stride_in; + p_in = input->get_channel_memory(heif_channel_Alpha, &stride_in); + + uint8_t* p_out; + size_t stride_out; + p_out = outimg->get_channel_memory(heif_channel_Alpha, &stride_out); + + if (target_bpp > input_alpha_bpp) { + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + int in = p_in[y * stride_in + x]; + p_out[y * stride_out + x] = (uint8_t) replicate_sample_bits(in, input_alpha_bpp, target_bpp); + } + } + else { + int shift = input_alpha_bpp - target_bpp; + for (uint32_t y = 0; y < alpha_height; y++) + for (uint32_t x = 0; x < alpha_width; x++) { + p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift); + } } } diff -Nru libheif-1.19.8/libheif/color-conversion/alpha.h libheif-1.23.4/libheif/color-conversion/alpha.h --- libheif-1.19.8/libheif/color-conversion/alpha.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/alpha.h 2026-09-06 14:45:17.000000000 +0000 @@ -32,13 +32,53 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const override; +}; + + +template +class Op_flatten_alpha_plane : public ColorConversionOperation +{ +public: + std::vector + state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; + + Result> + convert_colorspace(const std::shared_ptr& input, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const override; +}; + +class Op_adjust_alpha_bit_depth : public ColorConversionOperation +{ +public: + std::vector + state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; + + Result> + convert_colorspace(const std::shared_ptr& input, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/bayer_bilinear.cc libheif-1.23.4/libheif/color-conversion/bayer_bilinear.cc --- libheif-1.19.8/libheif/color-conversion/bayer_bilinear.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/bayer_bilinear.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,213 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "bayer_bilinear.h" +#include +#include +#include +#include + + +std::vector +Op_bayer_bilinear_to_RGB24_32::state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const +{ + if (input_state.colorspace != heif_colorspace_filter_array || + input_state.chroma != heif_chroma_planar) { + return {}; + } + + std::vector states; + + ColorState output_state; + output_state.colorspace = heif_colorspace_RGB; + output_state.has_alpha = false; + + if (input_state.bits_per_pixel == 8) { + output_state.chroma = heif_chroma_interleaved_RGB; + output_state.bits_per_pixel = 8; + } + else if (input_state.bits_per_pixel > 8 && input_state.bits_per_pixel <= 16) { + output_state.chroma = heif_chroma_interleaved_RRGGBB_LE; + output_state.bits_per_pixel = input_state.bits_per_pixel; + } + else { + return {}; + } + + states.emplace_back(output_state, SpeedCosts_Unoptimized); + + return states; +} + + +// Map uncompressed component types to R/G/B output channel indices. +// Returns -1 for unknown component types. +static int component_type_to_rgb_index(uint16_t component_type) +{ + switch (component_type) { + case heif_cmpd_component_type_red: + return 0; + case heif_cmpd_component_type_green: + return 1; + case heif_cmpd_component_type_blue: + return 2; + default: + return -1; + } +} + + +Result> +Op_bayer_bilinear_to_RGB24_32::convert_colorspace(const std::shared_ptr& input, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const +{ + uint32_t width = input->get_width(); + uint32_t height = input->get_height(); + + if (!input->has_any_bayer_pattern()) { + return Error::InternalError; + } + + const BayerPattern& pattern = input->get_any_bayer_pattern(); + uint16_t pw = pattern.pattern_width; + uint16_t ph = pattern.pattern_height; + + if (pw == 0 || ph == 0) { + return Error::InternalError; + } + + int bpp = input->get_bits_per_pixel(heif_channel_filter_array); + bool hdr = bpp > 8; + + heif_chroma out_chroma = hdr ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB; + + auto outimg = std::make_shared(); + + outimg->create(width, height, heif_colorspace_RGB, out_chroma); + + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, bpp, limits)) { + return err; + } + + size_t in_stride = 0; + const uint8_t* in_p = input->get_channel_memory(heif_channel_filter_array, &in_stride); + + size_t out_stride = 0; + uint8_t* out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_stride); + + // Build a lookup table: for each pattern position, which RGB channel (0=R,1=G,2=B) does it provide? + std::vector pattern_channel(pw * ph); + for (int i = 0; i < pw * ph; i++) { + uint16_t comp_type = input->get_component_type(pattern.pixels[i].component_id); + pattern_channel[i] = component_type_to_rgb_index(comp_type); + if (pattern_channel[i] < 0) { + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + "Bayer pattern contains component types that we currently cannot convert to RGB"); + } + } + + // Precompute neighbor offset tables for each pattern position and channel. + // neighbor_offsets[py * pw + px][ch] = list of (dx, dy) offsets to average. + // For the channel this position directly provides: single entry (0, 0). + // For other channels: all neighbor offsets within the search radius that provide that channel. + std::vector>, 3>> neighbor_offsets(pw * ph); + + for (int py = 0; py < ph; py++) { + for (int px = 0; px < pw; px++) { + int this_ch = pattern_channel[py * pw + px]; + auto& offsets = neighbor_offsets[py * pw + px]; + + // The channel this position directly provides: just read from (0,0) + offsets[this_ch].emplace_back(0, 0); + + // For the other two channels: collect neighbor offsets + int search_radius_x = pw - 1; + int search_radius_y = ph - 1; + + for (int dy = -search_radius_y; dy <= search_radius_y; dy++) { + for (int dx = -search_radius_x; dx <= search_radius_x; dx++) { + if (dx == 0 && dy == 0) { + continue; + } + + int npx = (((px + dx) % pw) + pw) % pw; + int npy = (((py + dy) % ph) + ph) % ph; + int neighbor_ch = pattern_channel[npy * pw + npx]; + + if (neighbor_ch != this_ch) { + offsets[neighbor_ch].emplace_back(dx, dy); + } + } + } + } + } + + // Bilinear demosaicing using precomputed offset tables. + auto demosaic = [&](const Pixel* in, Pixel* out, + size_t in_str, size_t out_str) { + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + const auto& offsets = neighbor_offsets[(y % ph) * pw + (x % pw)]; + + Pixel* out_pixel = &out[y * out_str + x * 3]; + + for (int ch = 0; ch < 3; ch++) { + const auto& ch_offsets = offsets[ch]; + int sum = 0; + int count = 0; + + for (const auto& [dx, dy] : ch_offsets) { + int nx = static_cast(x) + dx; + int ny = static_cast(y) + dy; + + if (nx < 0 || nx >= static_cast(width) || + ny < 0 || ny >= static_cast(height)) { + continue; + } + + sum += in[ny * in_str + nx]; + count++; + } + + out_pixel[ch] = count > 0 ? static_cast((sum + count / 2) / count) : 0; + } + } + } + }; + + if (hdr) { + demosaic(reinterpret_cast(in_p), + reinterpret_cast(out_p), + in_stride / 2, out_stride / 2); + } + else { + demosaic(in_p, out_p, in_stride, out_stride); + } + + return outimg; +} diff -Nru libheif-1.19.8/libheif/color-conversion/bayer_bilinear.h libheif-1.23.4/libheif/color-conversion/bayer_bilinear.h --- libheif-1.19.8/libheif/color-conversion/bayer_bilinear.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/bayer_bilinear.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,47 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H +#define LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H + +#include "colorconversion.h" +#include +#include + + +class Op_bayer_bilinear_to_RGB24_32 : public ColorConversionOperation +{ +public: + std::vector + state_after_conversion(const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; + + Result> + convert_colorspace(const std::shared_ptr& input, + const ColorState& input_state, + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, + const heif_security_limits* limits) const override; +}; + +#endif //LIBHEIF_COLORCONVERSION_BAYER_BILINEAR_H diff -Nru libheif-1.19.8/libheif/color-conversion/chroma_sampling.cc libheif-1.23.4/libheif/color-conversion/chroma_sampling.cc --- libheif-1.19.8/libheif/color-conversion/chroma_sampling.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/chroma_sampling.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,7 +26,8 @@ std::vector Op_YCbCr444_to_YCbCr420_average::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_YCbCr) { return {}; @@ -48,7 +49,11 @@ return {}; } - if (input_state.nclx_profile.get_matrix_coefficients() == 0) { + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.nclx.get_matrix_coefficients() == 0) { return {}; } @@ -66,7 +71,8 @@ output_state.chroma = heif_chroma_420; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = input_state.nclx_profile; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -80,6 +86,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -118,8 +125,6 @@ } - auto colorProfile = input->get_color_profile_nclx(); - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -130,14 +135,14 @@ uint32_t cwidth = (width + 1) / 2; uint32_t cheight = (height + 1) / 2; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_Cb, cwidth, cheight, bpp_cb, limits) || - outimg->add_plane(heif_channel_Cr, cwidth, cheight, bpp_cr, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_Cb, cwidth, cheight, bpp_cb, limits) || + outimg->add_channel(heif_channel_Cr, cwidth, cheight, bpp_cr, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp_a, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp_a, limits)) { return err; } } @@ -148,12 +153,12 @@ Pixel* out_y, * out_cb, * out_cr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_y = (const Pixel*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (const Pixel*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (const Pixel*) input->get_plane(heif_channel_Cr, &in_cr_stride); - out_y = (Pixel*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (Pixel*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (Pixel*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_y = (const Pixel*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (const Pixel*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (const Pixel*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_y = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); if (hdr) { in_y_stride /= 2; @@ -170,8 +175,8 @@ uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -224,7 +229,7 @@ } } - // TODO: check whether we can use HeifPixelImage::transfer_plane_from_image_as() instead of copying Y and Alpha + // TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha for (y = 0; y < height; y++) { uint32_t copyWidth = (hdr ? width * 2 : width); @@ -249,7 +254,8 @@ std::vector Op_YCbCr444_to_YCbCr422_average::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_YCbCr) { return {}; @@ -271,7 +277,11 @@ return {}; } - if (input_state.nclx_profile.get_matrix_coefficients() == 0) { + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.nclx.get_matrix_coefficients() == 0) { return {}; } @@ -289,7 +299,8 @@ output_state.chroma = heif_chroma_422; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = input_state.nclx_profile; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -303,6 +314,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -341,8 +353,6 @@ } - auto colorProfile = input->get_color_profile_nclx(); - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -353,14 +363,14 @@ uint32_t cwidth = (width + 1) / 2; uint32_t cheight = height; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_Cb, cwidth, cheight, bpp_cb, limits) || - outimg->add_plane(heif_channel_Cr, cwidth, cheight, bpp_cr, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_Cb, cwidth, cheight, bpp_cb, limits) || + outimg->add_channel(heif_channel_Cr, cwidth, cheight, bpp_cr, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp_a, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp_a, limits)) { return err; } } @@ -371,19 +381,19 @@ Pixel* out_y, * out_cb, * out_cr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_y = (const Pixel*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (const Pixel*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (const Pixel*) input->get_plane(heif_channel_Cr, &in_cr_stride); - out_y = (Pixel*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (Pixel*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (Pixel*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_y = (const Pixel*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (const Pixel*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (const Pixel*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_y = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); const uint8_t* in_a; uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -425,7 +435,7 @@ } } - // TODO: check whether we can use HeifPixelImage::transfer_plane_from_image_as() instead of copying Y and Alpha + // TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha for (y = 0; y < height; y++) { uint32_t copyWidth = (hdr ? width * 2 : width); @@ -450,7 +460,8 @@ std::vector Op_YCbCr420_bilinear_to_YCbCr444::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_YCbCr) { return {}; @@ -472,7 +483,11 @@ return {}; } - if (input_state.nclx_profile.get_matrix_coefficients() == 0) { + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.nclx.get_matrix_coefficients() == 0) { return {}; } @@ -486,7 +501,8 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = input_state.nclx_profile; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -500,6 +516,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -538,8 +555,6 @@ } - auto colorProfile = input->get_color_profile_nclx(); - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -547,14 +562,14 @@ outimg->create(width, height, heif_colorspace_YCbCr, heif_chroma_444); - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_Cb, width, height, bpp_cb, limits) || - outimg->add_plane(heif_channel_Cr, width, height, bpp_cr, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_Cb, width, height, bpp_cb, limits) || + outimg->add_channel(heif_channel_Cr, width, height, bpp_cr, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp_a, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp_a, limits)) { return err; } } @@ -565,19 +580,19 @@ Pixel* out_y, * out_cb, * out_cr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_y = (const Pixel*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (const Pixel*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (const Pixel*) input->get_plane(heif_channel_Cr, &in_cr_stride); - out_y = (Pixel*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (Pixel*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (Pixel*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_y = (const Pixel*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (const Pixel*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (const Pixel*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_y = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); const uint8_t* in_a; uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -704,7 +719,7 @@ } } - // TODO: check whether we can use HeifPixelImage::transfer_plane_from_image_as() instead of copying Y and Alpha + // TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha for (y = 0; y < height; y++) { uint32_t copyWidth = (hdr ? width * 2 : width); @@ -730,7 +745,8 @@ std::vector Op_YCbCr422_bilinear_to_YCbCr444::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_YCbCr) { return {}; @@ -752,7 +768,11 @@ return {}; } - if (input_state.nclx_profile.get_matrix_coefficients() == 0) { + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.nclx.get_matrix_coefficients() == 0) { return {}; } @@ -766,7 +786,8 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = input_state.nclx_profile; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -780,6 +801,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -818,8 +840,6 @@ } - auto colorProfile = input->get_color_profile_nclx(); - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -827,14 +847,14 @@ outimg->create(width, height, heif_colorspace_YCbCr, heif_chroma_444); - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_Cb, width, height, bpp_cb, limits) || - outimg->add_plane(heif_channel_Cr, width, height, bpp_cr, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_Cb, width, height, bpp_cb, limits) || + outimg->add_channel(heif_channel_Cr, width, height, bpp_cr, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp_a, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp_a, limits)) { return err; } } @@ -845,19 +865,19 @@ Pixel* out_y, * out_cb, * out_cr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_y = (const Pixel*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (const Pixel*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (const Pixel*) input->get_plane(heif_channel_Cr, &in_cr_stride); - out_y = (Pixel*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (Pixel*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (Pixel*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_y = (const Pixel*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (const Pixel*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (const Pixel*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_y = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); const uint8_t* in_a; uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -930,7 +950,7 @@ } } - // TODO: check whether we can use HeifPixelImage::transfer_plane_from_image_as() instead of copying Y and Alpha + // TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha for (y = 0; y < height; y++) { uint32_t copyWidth = (hdr ? width * 2 : width); diff -Nru libheif-1.19.8/libheif/color-conversion/chroma_sampling.h libheif-1.23.4/libheif/color-conversion/chroma_sampling.h --- libheif-1.19.8/libheif/color-conversion/chroma_sampling.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/chroma_sampling.h 2026-09-06 14:45:17.000000000 +0000 @@ -35,13 +35,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -53,13 +55,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -73,13 +77,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -90,13 +96,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/colorconversion.cc libheif-1.23.4/libheif/color-conversion/colorconversion.cc --- libheif-1.19.8/libheif/color-conversion/colorconversion.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/colorconversion.cc 2026-09-06 14:45:17.000000000 +0000 @@ -39,6 +39,7 @@ #include "alpha.h" #include "hdr_sdr.h" #include "chroma_sampling.h" +#include "bayer_bilinear.h" #if ENABLE_MULTITHREADING_SUPPORT @@ -67,6 +68,9 @@ case heif_colorspace_undefined: ostr << "undefined"; break; + case heif_colorspace_filter_array: + ostr << "filter_array"; + break; default: assert(false); } @@ -154,15 +158,21 @@ return false; } - if (colorspace == heif_colorspace_YCbCr) { - bool ycbcr_parameters_match = (nclx_profile.get_full_range_flag() == b.nclx_profile.get_full_range_flag() && - nclx_profile.get_matrix_coefficients() == b.nclx_profile.get_matrix_coefficients() && - nclx_profile.get_colour_primaries() == b.nclx_profile.get_colour_primaries()); - return ycbcr_parameters_match; + if (has_alpha && b.has_alpha) { + if (get_alpha_bits_per_pixel() != b.get_alpha_bits_per_pixel()) { + return false; + } } - else { - return true; + + if (colorspace == heif_colorspace_YCbCr) { + bool ycbcr_parameters_match = nclx.equal_except_transfer_curve(b.nclx); + + if (!ycbcr_parameters_match) { + return false; + } } + + return true; } @@ -196,11 +206,15 @@ << " bpp(R)=" << state.bits_per_pixel << " alpha=" << (state.has_alpha ? "yes" : "no"); + if (state.has_alpha && state.get_alpha_bits_per_pixel() != state.bits_per_pixel) { + ostr << " alpha_bpp=" << state.get_alpha_bits_per_pixel(); + } + if (state.colorspace == heif_colorspace_YCbCr) { - ostr << " matrix-coefficients=" << state.nclx_profile.get_matrix_coefficients() - << " colour-primaries=" << state.nclx_profile.get_colour_primaries() - << " transfer-characteristics=" << state.nclx_profile.get_transfer_characteristics() - << " full-range=" << (state.nclx_profile.get_full_range_flag() ? "yes" : "no"); + ostr << " matrix-coefficients=" << state.nclx.get_matrix_coefficients() + << " colour-primaries=" << state.nclx.get_colour_primaries() + << " transfer-characteristics=" << state.nclx.get_transfer_characteristics() + << " full-range=" << (state.nclx.get_full_range_flag() ? "yes" : "no"); } return ostr; @@ -230,6 +244,7 @@ ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); + ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); @@ -238,6 +253,9 @@ ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); + ops.emplace_back(std::make_shared>()); + ops.emplace_back(std::make_shared>()); + ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared()); ops.emplace_back(std::make_shared>()); @@ -260,11 +278,13 @@ bool ColorConversionPipeline::construct_pipeline(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) { m_conversion_steps.clear(); m_options = options; + m_options_ext = options_ext; if (input_state == target_state) { return true; @@ -360,7 +380,7 @@ auto out_states = op_ptr->state_after_conversion(processed_states.back().output_state, target_state, - options); + options, options_ext); for (const auto& out_state : out_states) { int new_op_costs = out_state.speed_costs + processed_states.back().speed_costs; #if DEBUG_PIPELINE_CREATION @@ -440,36 +460,20 @@ print_spec(std::cerr, in); #endif - auto outResult = step.operation->convert_colorspace(in, step.input_state, step.output_state, m_options, limits); - if (outResult.error) { - return outResult.error; + auto outResult = step.operation->convert_colorspace(in, step.input_state, step.output_state, m_options, m_options_ext, limits); + if (!outResult) { + return outResult.error(); } else { out = *outResult; } - // --- pass the color profiles to the new image - - auto output_nclx = std::make_shared(step.output_state.nclx_profile); - out->set_color_profile_nclx(output_nclx); - out->set_color_profile_icc(in->get_color_profile_icc()); + // copy metadata over to new image + out->copy_metadata_from(*in); - out->set_premultiplied_alpha(in->is_premultiplied_alpha()); - - // pass through HDR information - if (in->has_clli()) { - out->set_clli(in->get_clli()); - } - - if (in->has_mdcv()) { - out->set_mdcv(in->get_mdcv()); - } + // overwrite color profile nclx from color conversion + out->set_color_profile_nclx(step.output_state.nclx); - if (in->has_nonsquare_pixel_ratio()) { - uint32_t h, v; - in->get_pixel_ratio(&h, &v); - out->set_pixel_ratio(h, v); - } const auto& warnings = in->get_warnings(); for (const auto& warning : warnings) { @@ -486,11 +490,18 @@ Result> convert_colorspace(const std::shared_ptr& input, heif_colorspace target_colorspace, heif_chroma target_chroma, - const std::shared_ptr& target_profile, + const nclx_profile& target_profile, int output_bpp, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext* options_ext_optional, const heif_security_limits* limits) { + std::unique_ptr + options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free); + + heif_color_conversion_options_ext_copy(options_ext.get(), options_ext_optional); + + // --- check that input image is valid uint32_t width = input->get_width(); @@ -521,46 +532,53 @@ input_state.colorspace = input->get_colorspace(); input_state.chroma = input->get_chroma_format(); input_state.has_alpha = input->has_channel(heif_channel_Alpha) || is_interleaved_with_alpha(input->get_chroma_format()); - if (input->get_color_profile_nclx()) { - input_state.nclx_profile = *input->get_color_profile_nclx(); + if (input->has_nclx_color_profile()) { + input_state.nclx = input->get_color_profile_nclx(); } - input_state.nclx_profile.replace_undefined_values_with_sRGB_defaults(); + input_state.nclx.replace_undefined_values_with_sRGB_defaults(); std::set channels = input->get_channel_set(); assert(!channels.empty()); input_state.bits_per_pixel = input->get_bits_per_pixel(*(channels.begin())); + if (input_state.has_alpha && input->has_channel(heif_channel_Alpha)) { + input_state.alpha_bits_per_pixel = input->get_bits_per_pixel(heif_channel_Alpha); + } + ColorState output_state = input_state; output_state.colorspace = target_colorspace; output_state.chroma = target_chroma; - if (target_profile) { - output_state.nclx_profile = *target_profile; - } + output_state.nclx = target_profile; // If some output nclx values are unspecified, set them to the same as the input. - if (output_state.nclx_profile.get_matrix_coefficients() == heif_matrix_coefficients_unspecified) { - output_state.nclx_profile.set_matrix_coefficients(input_state.nclx_profile.get_matrix_coefficients()); + if (output_state.nclx.get_matrix_coefficients() == heif_matrix_coefficients_unspecified) { + output_state.nclx.set_matrix_coefficients(input_state.nclx.get_matrix_coefficients()); } - if (output_state.nclx_profile.get_colour_primaries() == heif_color_primaries_unspecified) { - output_state.nclx_profile.set_colour_primaries(input_state.nclx_profile.get_colour_primaries()); + if (output_state.nclx.get_colour_primaries() == heif_color_primaries_unspecified) { + output_state.nclx.set_colour_primaries(input_state.nclx.get_colour_primaries()); } - if (output_state.nclx_profile.get_transfer_characteristics() == heif_transfer_characteristic_unspecified) { - output_state.nclx_profile.set_transfer_characteristics(input_state.nclx_profile.get_transfer_characteristics()); + if (output_state.nclx.get_transfer_characteristics() == heif_transfer_characteristic_unspecified) { + output_state.nclx.set_transfer_characteristics(input_state.nclx.get_transfer_characteristics()); } // If we convert to an interleaved format, we want alpha only if present in the // interleaved output format. // For planar formats, we include an alpha plane when included in the input. - if (num_interleaved_pixels_per_plane(target_chroma) > 1) { + if (num_interleaved_components_per_plane(target_chroma) > 1) { output_state.has_alpha = is_interleaved_with_alpha(target_chroma); } else { - output_state.has_alpha = input_state.has_alpha; + if (options_ext->alpha_composition_mode != heif_alpha_composition_mode_none) { + output_state.has_alpha = false; + } + else { + output_state.has_alpha = input_state.has_alpha; + } } if (output_bpp) { @@ -586,8 +604,11 @@ output_state.bits_per_pixel = 10; } + // Output alpha should always match the output color BPP + output_state.alpha_bits_per_pixel = output_state.bits_per_pixel; + ColorConversionPipeline pipeline; - bool success = pipeline.construct_pipeline(input_state, output_state, options); + bool success = pipeline.construct_pipeline(input_state, output_state, options, *options_ext); if (!success) { return Error{heif_error_Unsupported_feature, heif_suberror_Unsupported_color_conversion}; @@ -597,6 +618,54 @@ return input; } else { + // Every color-conversion operator is written for 8-bit or 16-bit integer samples. + // They access the planes through uint8_t* / uint16_t* and derive shift amounts and + // midpoint values from the bit depth (e.g. '128 << (bpp - 8)' in Op_mono_to_YCbCr420). + // An 'unci' component may however declare a bit depth of up to 256 bits, of which we + // accept up to 128 (64-bit integers, 32/64-bit floats, complex numbers). Those are + // stored by HeifPixelImage so that they can be read through the component API. A + // 64-bit monochrome component reached Op_mono_to_YCbCr420 and shifted an 'int' by + // 56 (OSS-Fuzz 5154611212910592). A nop conversion is handled above and still hands + // the image through untouched, so wide components stay accessible to the caller. + // + // This is a backstop, not the primary defence. The constraint belongs in each + // operator's state_after_conversion(), and every operator now declares it there + // (most through has_samples_wider_than_16bit()), so construct_pipeline() above + // already fails for a wider input and a real conversion never reaches this loop. + // Keep it until an operator actually supports more than 16 bits per component, + // then remove it together with that operator's call to the helper. + + for (heif_channel channel : channels) { + if (input->get_bits_per_pixel(channel) > 16) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Color conversion of images with more than 16 bits per component is not supported."}; + } + } + + // The YCbCr color-conversion operators assume that luma and chroma share a + // single bit depth: several of them read the chroma planes with a sample width + // derived from the luma bit depth. A file may however declare per-component bit + // depths (e.g. 'unci'), so we reject a real (non-nop) conversion of any YCbCr + // image whose Y/Cb/Cr channels do not agree, rather than over-reading a narrower + // chroma plane (GHSA-w7mc-p8jc-p853). An identity decode (is_nop() above) + // returns the image untouched and is unaffected. RGB is intentionally not + // restricted here: the RGB operators support differing per-channel bit depths + // (e.g. 5/6/5). Alpha is handled separately by the individual operators. + + if (input->get_colorspace() == heif_colorspace_YCbCr && + input->has_channel(heif_channel_Y) && + input->has_channel(heif_channel_Cb) && + input->has_channel(heif_channel_Cr)) { + int bpp_y = input->get_bits_per_pixel(heif_channel_Y); + if (input->get_bits_per_pixel(heif_channel_Cb) != bpp_y || + input->get_bits_per_pixel(heif_channel_Cr) != bpp_y) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Color conversion of YCbCr images with differing luma and chroma bit depths is not supported."}; + } + } + return pipeline.convert_image(input, limits); } } @@ -605,16 +674,17 @@ Result> convert_colorspace(const std::shared_ptr& input, heif_colorspace colorspace, heif_chroma chroma, - const std::shared_ptr& target_profile, + const nclx_profile& target_profile, int output_bpp, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext* options_ext, const heif_security_limits* limits) { std::shared_ptr non_const_input = std::const_pointer_cast(input); - auto result = convert_colorspace(non_const_input, colorspace, chroma, target_profile, output_bpp, options, limits); - if (result.error) { - return result.error; + auto result = convert_colorspace(non_const_input, colorspace, chroma, target_profile, output_bpp, options, options_ext, limits); + if (!result) { + return result.error(); } else { // TODO: can we simplify this? It's a bit awkward to do these assignments just to get a "const HeifPixelImage". diff -Nru libheif-1.19.8/libheif/color-conversion/colorconversion.h libheif-1.23.4/libheif/color-conversion/colorconversion.h --- libheif-1.19.8/libheif/color-conversion/colorconversion.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/colorconversion.h 2026-09-06 14:45:17.000000000 +0000 @@ -21,7 +21,7 @@ #ifndef LIBHEIF_COLORCONVERSION_H #define LIBHEIF_COLORCONVERSION_H -#include "pixelimage.h" +#include "image/pixelimage.h" #include #include #include @@ -34,21 +34,46 @@ heif_chroma chroma = heif_chroma_undefined; bool has_alpha = false; int bits_per_pixel = 8; + int alpha_bits_per_pixel = 0; // 0 = not set, treated as bits_per_pixel // ColorConversionOperations can assume that the input and target nclx has no 'unspecified' values // if the colorspace is heif_colorspace_YCbCr. Otherwise, the values should preferably be 'unspecified'. - color_profile_nclx nclx_profile; + nclx_profile nclx; ColorState() = default; ColorState(heif_colorspace colorspace, heif_chroma chroma, bool has_alpha, int bits_per_pixel) : colorspace(colorspace), chroma(chroma), has_alpha(has_alpha), bits_per_pixel(bits_per_pixel) {} + // Returns effective alpha BPP (treats 0 as bits_per_pixel for backward compatibility) + int get_alpha_bits_per_pixel() const { return alpha_bits_per_pixel ? alpha_bits_per_pixel : bits_per_pixel; } + bool operator==(const ColorState&) const; }; std::ostream& operator<<(std::ostream& ostr, const ColorState& state); + +// True if 'state' has a colour or alpha component wider than 16 bits. +// +// Every conversion operator reads and writes sample data through uint8_t* or uint16_t* +// and derives shift amounts from the bit depth, so none of them can handle a wider +// component. Images with wider components do exist: 'unci' components may be up to 256 +// bits and we store up to 128 of them (64-bit integers, 32/64-bit floats, complex +// numbers) so that they can be read through the component API. +// +// An operator that cannot handle a bit depth must not offer itself to the pipeline for +// it, so each operator states its own supported range in state_after_conversion(). This +// helper spells out the upper bound they currently all share; operators with a tighter +// or different range (an exact 8 bits, or an explicit list) say so themselves instead. +// When an operator gains support for wider samples it simply stops calling this, and the +// catch-all in convert_colorspace() can go away. +inline bool has_samples_wider_than_16bit(const ColorState& state) +{ + return state.bits_per_pixel > 16 || + (state.has_alpha && state.get_alpha_bits_per_pixel() > 16); +} + // These are some integer constants for typical color conversion Op speed costs. // The integer value is the speed cost. Any other integer can be assigned to the speed cost. enum SpeedCosts @@ -63,7 +88,7 @@ struct ColorStateWithCost { - ColorStateWithCost(ColorState c, int s) : color_state(std::move(c)), speed_costs(s) {} + ColorStateWithCost(ColorState c, int s) : color_state(c), speed_costs(s) {} ColorState color_state; @@ -83,13 +108,15 @@ virtual std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const = 0; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const = 0; virtual Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const = 0; }; @@ -104,7 +131,8 @@ bool construct_pipeline(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options); + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext); Result> convert_image(const std::shared_ptr& input, const heif_security_limits* limits); @@ -123,6 +151,7 @@ std::vector m_conversion_steps; heif_color_conversion_options m_options; + heif_color_conversion_options_ext m_options_ext; }; @@ -131,17 +160,19 @@ Result> convert_colorspace(const std::shared_ptr& input, heif_colorspace colorspace, heif_chroma chroma, - const std::shared_ptr& target_profile, + const nclx_profile& target_profile, int output_bpp, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext* options_ext, const heif_security_limits* limits); Result> convert_colorspace(const std::shared_ptr& input, heif_colorspace colorspace, heif_chroma chroma, - const std::shared_ptr& target_profile, + const nclx_profile& target_profile, int output_bpp, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext* options_ext, const heif_security_limits* limits); #endif diff -Nru libheif-1.19.8/libheif/color-conversion/hdr_sdr.cc libheif-1.23.4/libheif/color-conversion/hdr_sdr.cc --- libheif-1.19.8/libheif/color-conversion/hdr_sdr.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/hdr_sdr.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,8 @@ std::vector Op_to_hdr_planes::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if ((input_state.chroma != heif_chroma_monochrome && input_state.chroma != heif_chroma_420 && @@ -35,6 +36,16 @@ return {}; } + // This operation increases the bit depth of an 8-bit input by replicating + // the input bit pattern: out = (in << (m-8)) | (in >> (16-m)). That identity + // only holds for target bit depths m in (8, 16]; a larger m would both make + // the right shift exponent negative (undefined behavior) and exceed the range + // of the uint16_t output plane. Only offer the conversion within that range. + if (target_state.bits_per_pixel <= 8 || + target_state.bits_per_pixel > 16) { + return {}; + } + std::vector states; ColorState output_state; @@ -43,6 +54,7 @@ output_state = input_state; output_state.bits_per_pixel = target_state.bits_per_pixel; + output_state.alpha_bits_per_pixel = target_state.bits_per_pixel; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -55,6 +67,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { auto outimg = std::make_shared(); @@ -74,23 +87,34 @@ if (input->has_channel(channel)) { uint32_t width = input->get_width(channel); uint32_t height = input->get_height(channel); - if (auto err = outimg->add_plane(channel, width, height, target_state.bits_per_pixel, limits)) { + if (auto err = outimg->add_channel(channel, width, height, target_state.bits_per_pixel, limits)) { return err; } int input_bits = input->get_bits_per_pixel(channel); int output_bits = target_state.bits_per_pixel; + // Guard against unsupported bit-depth combinations. state_after_conversion() + // only offers this operation for 8-bit input and 8 < output <= 16, but a + // caller may invoke convert_colorspace() directly. Outside that range the + // bit-replication formula below would use a negative or oversized shift + // exponent (undefined behavior), so reject it instead. + if (input_bits != 8 || output_bits <= input_bits || output_bits > 2 * input_bits) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + "Op_to_hdr_planes: unsupported bit depth conversion"}; + } + int shift1 = output_bits - input_bits; int shift2 = 2 * input_bits - output_bits; const uint8_t* p_in; size_t stride_in; - p_in = input->get_plane(channel, &stride_in); + p_in = input->get_channel_memory(channel, &stride_in); uint16_t* p_out; size_t stride_out; - p_out = (uint16_t*) outimg->get_plane(channel, &stride_out); + p_out = (uint16_t*) outimg->get_channel_memory(channel, &stride_out); stride_out /= 2; for (uint32_t y = 0; y < height; y++) @@ -109,7 +133,8 @@ std::vector Op_to_sdr_planes::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if ((input_state.chroma != heif_chroma_monochrome && input_state.chroma != heif_chroma_420 && @@ -123,6 +148,12 @@ return {}; } + // Every channel, alpha included, is read through a uint16_t* and shifted down by + // (bits_per_pixel - 8). + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + std::vector states; ColorState output_state; @@ -131,6 +162,7 @@ output_state = input_state; output_state.bits_per_pixel = 8; + output_state.alpha_bits_per_pixel = 8; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -143,6 +175,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { @@ -166,7 +199,7 @@ if (input_bits > 8) { uint32_t width = input->get_width(channel); uint32_t height = input->get_height(channel); - if (auto err = outimg->add_plane(channel, width, height, 8, limits)) { + if (auto err = outimg->add_channel(channel, width, height, 8, limits)) { return err; } @@ -174,12 +207,12 @@ const uint16_t* p_in; size_t stride_in; - p_in = (uint16_t*) input->get_plane(channel, &stride_in); + p_in = (uint16_t*) input->get_channel_memory(channel, &stride_in); stride_in /= 2; uint8_t* p_out; size_t stride_out; - p_out = outimg->get_plane(channel, &stride_out); + p_out = outimg->get_channel_memory(channel, &stride_out); for (uint32_t y = 0; y < height; y++) for (uint32_t x = 0; x < width; x++) { @@ -189,7 +222,7 @@ } else if (input_bits < 8) { uint32_t width = input->get_width(channel); uint32_t height = input->get_height(channel); - if (auto err = outimg->add_plane(channel, width, height, 8, limits)) { + if (auto err = outimg->add_channel(channel, width, height, 8, limits)) { return err; } @@ -218,10 +251,10 @@ } size_t stride_in; - const uint8_t* p_in = input->get_plane(channel, &stride_in); + const uint8_t* p_in = input->get_channel_memory(channel, &stride_in); size_t stride_out; - uint8_t* p_out = outimg->get_plane(channel, &stride_out); + uint8_t* p_out = outimg->get_channel_memory(channel, &stride_out); for (uint32_t y = 0; y < height; y++) for (uint32_t x = 0; x < width; x++) { @@ -229,7 +262,7 @@ p_out[y * stride_out + x] = (uint8_t) ((in * mulFactor) >> 8); } } else { - outimg->copy_new_plane_from(input, channel, channel, limits); + outimg->copy_new_channel_from(input, channel, channel, limits); } } } diff -Nru libheif-1.19.8/libheif/color-conversion/hdr_sdr.h libheif-1.23.4/libheif/color-conversion/hdr_sdr.h --- libheif-1.19.8/libheif/color-conversion/hdr_sdr.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/hdr_sdr.h 2026-09-06 14:45:17.000000000 +0000 @@ -32,13 +32,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -49,13 +51,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/monochrome.cc libheif-1.23.4/libheif/color-conversion/monochrome.cc --- libheif-1.19.8/libheif/color-conversion/monochrome.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/monochrome.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,13 +25,18 @@ std::vector Op_mono_to_YCbCr420::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_monochrome || input_state.chroma != heif_chroma_monochrome) { return {}; } + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + std::vector states; ColorState output_state; @@ -42,6 +47,7 @@ output_state.chroma = heif_chroma_420; output_state.has_alpha = input_state.has_alpha; output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; states.emplace_back(output_state, SpeedCosts_OptimizedSoftware); @@ -54,6 +60,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { auto outimg = std::make_shared(); @@ -68,9 +75,9 @@ uint32_t chroma_width = (width + 1) / 2; uint32_t chroma_height = (height + 1) / 2; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, input_bpp, limits) || - outimg->add_plane(heif_channel_Cb, chroma_width, chroma_height, input_bpp, limits) || - outimg->add_plane(heif_channel_Cr, chroma_width, chroma_height, input_bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, input_bpp, limits) || + outimg->add_channel(heif_channel_Cb, chroma_width, chroma_height, input_bpp, limits) || + outimg->add_channel(heif_channel_Cr, chroma_width, chroma_height, input_bpp, limits)) { return err; } @@ -78,7 +85,7 @@ bool has_alpha = input->has_channel(heif_channel_Alpha); if (has_alpha) { alpha_bpp = input->get_bits_per_pixel(heif_channel_Alpha); - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, alpha_bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, alpha_bpp, limits)) { return err; } } @@ -91,11 +98,11 @@ const uint8_t* in_y; size_t in_y_stride = 0; - in_y = input->get_plane(heif_channel_Y, &in_y_stride); + in_y = input->get_channel_memory(heif_channel_Y, &in_y_stride); - out_y = outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = outimg->get_plane(heif_channel_Cr, &out_cr_stride); + out_y = outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); auto chroma_value = static_cast(1 << (input_bpp - 1)); @@ -115,11 +122,11 @@ const uint16_t* in_y; size_t in_y_stride = 0; - in_y = (const uint16_t*) input->get_plane(heif_channel_Y, &in_y_stride); + in_y = (const uint16_t*) input->get_channel_memory(heif_channel_Y, &in_y_stride); - out_y = (uint16_t*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (uint16_t*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (uint16_t*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + out_y = (uint16_t*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (uint16_t*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (uint16_t*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); in_y_stride /= 2; out_y_stride /= 2; @@ -145,8 +152,8 @@ size_t in_a_stride = 0; size_t out_a_stride = 0; - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); uint32_t memory_width = (alpha_bpp > 8 ? width * 2 : width); @@ -162,7 +169,8 @@ std::vector Op_mono_to_RGB24_32::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -172,6 +180,10 @@ return {}; } + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + std::vector states; ColorState output_state; @@ -206,6 +218,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -226,7 +239,7 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_interleaved_24bit); } - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, 8, limits)) { return err; } @@ -236,12 +249,12 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_y = input->get_plane(heif_channel_Y, &in_y_stride); + in_y = input->get_channel_memory(heif_channel_Y, &in_y_stride); if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); uint32_t x, y; for (y = 0; y < height; y++) { diff -Nru libheif-1.19.8/libheif/color-conversion/monochrome.h libheif-1.23.4/libheif/color-conversion/monochrome.h --- libheif-1.19.8/libheif/color-conversion/monochrome.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/monochrome.h 2026-09-06 14:45:17.000000000 +0000 @@ -32,13 +32,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -49,13 +51,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2rgb.cc libheif-1.23.4/libheif/color-conversion/rgb2rgb.cc --- libheif-1.19.8/libheif/color-conversion/rgb2rgb.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2rgb.cc 2026-09-06 14:45:17.000000000 +0000 @@ -28,7 +28,8 @@ std::vector Op_RGB_to_RGB24_32::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { if (input_state.colorspace != heif_colorspace_RGB || input_state.chroma != heif_chroma_444 || @@ -36,6 +37,10 @@ return {}; } + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + std::vector states; ColorState output_state; @@ -68,6 +73,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool has_alpha = input->has_channel(heif_channel_Alpha); @@ -91,7 +97,7 @@ outimg->create(width, height, heif_colorspace_RGB, want_alpha ? heif_chroma_interleaved_32bit : heif_chroma_interleaved_24bit); - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, 8, limits)) { return err; } @@ -101,13 +107,13 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_r = input->get_plane(heif_channel_R, &in_r_stride); - in_g = input->get_plane(heif_channel_G, &in_g_stride); - in_b = input->get_plane(heif_channel_B, &in_b_stride); - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + in_r = input->get_channel_memory(heif_channel_R, &in_r_stride); + in_g = input->get_channel_memory(heif_channel_G, &in_g_stride); + in_b = input->get_channel_memory(heif_channel_B, &in_b_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } uint32_t x, y; @@ -147,7 +153,8 @@ std::vector Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -157,6 +164,14 @@ return {}; } + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + std::vector states; ColorState output_state; @@ -192,6 +207,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { if (input->get_bits_per_pixel(heif_channel_R) <= 8 || @@ -223,7 +239,7 @@ outimg->create(width, height, heif_colorspace_RGB, output_has_alpha ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE); - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, bpp, limits)) { return err; } @@ -233,13 +249,13 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_r = (uint16_t*) input->get_plane(heif_channel_R, &in_r_stride); - in_g = (uint16_t*) input->get_plane(heif_channel_G, &in_g_stride); - in_b = (uint16_t*) input->get_plane(heif_channel_B, &in_b_stride); - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + in_r = (uint16_t*) input->get_channel_memory(heif_channel_R, &in_r_stride); + in_g = (uint16_t*) input->get_channel_memory(heif_channel_G, &in_g_stride); + in_b = (uint16_t*) input->get_channel_memory(heif_channel_B, &in_b_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); if (input_has_alpha) { - in_a = (uint16_t*) input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = (uint16_t*) input->get_channel_memory(heif_channel_Alpha, &in_a_stride); assert(in_a != nullptr); // should never happen, but makes clang-tidy happy @@ -283,16 +299,33 @@ std::vector Op_RGB_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. + // DISABLED for the moment. This operator targets the RRGGBB(AA)_BE interleaved + // container, whose samples are 16 bits per channel, yet it only accepts 8-bit + // planar RGB input (checked below) and then allocates the interleaved output + // plane at 8 bits per channel. That combination (RRGGBB(AA)_BE with 8 bits per + // channel) is not valid and is now rejected by HeifPixelImage::add_channel(), so + // the operator always failed at runtime before writing anything. Offer no + // conversion state so the pipeline never selects it; the 8-bit RGB -> 16-bit + // RRGGBB_BE route is handled via Op_to_hdr_planes + Op_RGB_HDR_to_RRGGBBaa_BE + // instead. Re-enable once this operator widens the output bit depth before the + // add_channel() call. + return {}; + if (input_state.colorspace != heif_colorspace_RGB || input_state.chroma != heif_chroma_444 || input_state.bits_per_pixel != 8) { return {}; } + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + std::vector states; ColorState output_state; @@ -327,6 +360,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { if (input->get_bits_per_pixel(heif_channel_R) != 8 || @@ -352,7 +386,7 @@ outimg->create(width, height, heif_colorspace_RGB, output_has_alpha ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE); - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, input->get_bits_per_pixel(heif_channel_R), limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, input->get_bits_per_pixel(heif_channel_R), limits)) { return err; } @@ -362,13 +396,13 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_r = input->get_plane(heif_channel_R, &in_r_stride); - in_g = input->get_plane(heif_channel_G, &in_g_stride); - in_b = input->get_plane(heif_channel_B, &in_b_stride); - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + in_r = input->get_channel_memory(heif_channel_R, &in_r_stride); + in_g = input->get_channel_memory(heif_channel_G, &in_g_stride); + in_b = input->get_channel_memory(heif_channel_B, &in_b_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); if (input_has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } const int pixelsize = (output_has_alpha ? 8 : 6); @@ -412,7 +446,8 @@ std::vector Op_RRGGBBaa_BE_to_RGB_HDR::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -423,6 +458,10 @@ return {}; } + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + std::vector states; ColorState output_state; @@ -433,6 +472,7 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = target_state.has_alpha; output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.alpha_bits_per_pixel = input_state.bits_per_pixel; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -445,6 +485,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE || @@ -459,14 +500,14 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_444); - if (auto err = outimg->add_plane(heif_channel_R, width, height, bpp, limits) || - outimg->add_plane(heif_channel_G, width, height, bpp, limits) || - outimg->add_plane(heif_channel_B, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_R, width, height, bpp, limits) || + outimg->add_channel(heif_channel_G, width, height, bpp, limits) || + outimg->add_channel(heif_channel_B, width, height, bpp, limits)) { return err; } if (want_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp, limits)) { return err; } } @@ -478,14 +519,14 @@ uint16_t* out_r, * out_g, * out_b, * out_a = nullptr; size_t out_r_stride = 0, out_g_stride = 0, out_b_stride = 0, out_a_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_p_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_p_stride); - out_r = (uint16_t*) outimg->get_plane(heif_channel_R, &out_r_stride); - out_g = (uint16_t*) outimg->get_plane(heif_channel_G, &out_g_stride); - out_b = (uint16_t*) outimg->get_plane(heif_channel_B, &out_b_stride); + out_r = (uint16_t*) outimg->get_channel_memory(heif_channel_R, &out_r_stride); + out_g = (uint16_t*) outimg->get_channel_memory(heif_channel_G, &out_g_stride); + out_b = (uint16_t*) outimg->get_channel_memory(heif_channel_B, &out_b_stride); if (want_alpha) { - out_a = (uint16_t*) outimg->get_plane(heif_channel_Alpha, &out_a_stride); + out_a = (uint16_t*) outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } out_r_stride /= 2; @@ -525,8 +566,9 @@ std::vector Op_RGB24_32_to_RGB::state_after_conversion(const ColorState& input_state, - const ColorState& target_state, - const heif_color_conversion_options& options) const + const ColorState& target_state, + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -547,6 +589,7 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = target_state.has_alpha; output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.alpha_bits_per_pixel = input_state.bits_per_pixel; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -559,6 +602,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool has_alpha = input->get_chroma_format() == heif_chroma_interleaved_RGBA; @@ -571,14 +615,14 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_444); - if (auto err = outimg->add_plane(heif_channel_R, width, height, 8, limits) || - outimg->add_plane(heif_channel_G, width, height, 8, limits) || - outimg->add_plane(heif_channel_B, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_R, width, height, 8, limits) || + outimg->add_channel(heif_channel_G, width, height, 8, limits) || + outimg->add_channel(heif_channel_B, width, height, 8, limits)) { return err; } if (want_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, 8, limits)) { return err; } } @@ -590,14 +634,14 @@ uint8_t* out_r, * out_g, * out_b, * out_a = nullptr; size_t out_r_stride = 0, out_g_stride = 0, out_b_stride = 0, out_a_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_p_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_p_stride); - out_r = outimg->get_plane(heif_channel_R, &out_r_stride); - out_g = outimg->get_plane(heif_channel_G, &out_g_stride); - out_b = outimg->get_plane(heif_channel_B, &out_b_stride); + out_r = outimg->get_channel_memory(heif_channel_R, &out_r_stride); + out_g = outimg->get_channel_memory(heif_channel_G, &out_g_stride); + out_b = outimg->get_channel_memory(heif_channel_B, &out_b_stride); if (want_alpha) { - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } uint32_t x, y; @@ -622,7 +666,8 @@ std::vector Op_RRGGBBaa_swap_endianness::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -634,6 +679,12 @@ return {}; } + // Swaps the two bytes of each component, which is only meaningful for components + // that are stored in 16 bits. + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + std::vector states; ColorState output_state; @@ -686,6 +737,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { auto outimg = std::make_shared(); @@ -710,7 +762,7 @@ return Error::InternalError; } - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, input->get_bits_per_pixel(heif_channel_interleaved), limits)) { return err; } @@ -721,8 +773,8 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_p_stride); - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_p_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); size_t n_bytes = std::min(in_p_stride, out_p_stride); diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2rgb.h libheif-1.23.4/libheif/color-conversion/rgb2rgb.h --- libheif-1.19.8/libheif/color-conversion/rgb2rgb.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2rgb.h 2026-09-06 14:45:17.000000000 +0000 @@ -32,13 +32,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -49,13 +51,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -66,13 +70,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -83,13 +89,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -100,13 +108,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -117,13 +127,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2yuv.cc libheif-1.23.4/libheif/color-conversion/rgb2yuv.cc --- libheif-1.19.8/libheif/color-conversion/rgb2yuv.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2yuv.cc 2026-09-06 14:45:17.000000000 +0000 @@ -31,7 +31,8 @@ std::vector Op_RGB_to_YCbCr::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { bool hdr = !std::is_same::value; @@ -39,6 +40,10 @@ return {}; } + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + // TODO: add support for <8 bpp if (input_state.bits_per_pixel < 8) { return {}; @@ -49,10 +54,17 @@ return {}; } - int matrix = target_state.nclx_profile.get_matrix_coefficients(); - if (matrix == 8 || matrix == 11 || matrix == 14) { + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + + int matrix = target_state.nclx.get_matrix_coefficients(); + if (matrix == 11 || matrix == 14) { return {}; } + // TODO: matrix == 10 (BT.2020 CL) currently falls through and is encoded as if it were + // BT.2020 NCL. A correct CL path needs OETF application before deriving Y'C, not the + // linear matrix below. std::vector states; @@ -70,7 +82,7 @@ output_state.chroma = target_state.chroma; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -81,7 +93,7 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -95,6 +107,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -111,6 +124,14 @@ return Error::InternalError; } + // All three colour planes are reinterpreted as the same 'Pixel' type below, so their + // bit depths (and hence plane strides) must match. Differing bit depths would make the + // shared stride assumption read past the smaller plane. + if (input->get_bits_per_pixel(heif_channel_G) != bpp || + input->get_bits_per_pixel(heif_channel_B) != bpp) { + return Error::InternalError; + } + bool has_alpha = input->has_channel(heif_channel_Alpha); if (has_alpha && input->get_bits_per_pixel(heif_channel_Alpha) != bpp) { @@ -124,14 +145,14 @@ uint32_t cwidth = (width + subH - 1) / subH; uint32_t cheight = (height + subV - 1) / subV; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp, limits) || - outimg->add_plane(heif_channel_Cb, cwidth, cheight, bpp, limits) || - outimg->add_plane(heif_channel_Cr, cwidth, cheight, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp, limits) || + outimg->add_channel(heif_channel_Cb, cwidth, cheight, bpp, limits) || + outimg->add_channel(heif_channel_Cr, cwidth, cheight, bpp, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp, limits)) { return err; } } @@ -142,19 +163,19 @@ Pixel* out_y, * out_cb, * out_cr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_r = (const Pixel*) input->get_plane(heif_channel_R, &in_r_stride); - in_g = (const Pixel*) input->get_plane(heif_channel_G, &in_g_stride); - in_b = (const Pixel*) input->get_plane(heif_channel_B, &in_b_stride); - out_y = (Pixel*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (Pixel*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (Pixel*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_r = (const Pixel*) input->get_channel_memory(heif_channel_R, &in_r_stride); + in_g = (const Pixel*) input->get_channel_memory(heif_channel_G, &in_g_stride); + in_b = (const Pixel*) input->get_channel_memory(heif_channel_B, &in_b_stride); + out_y = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); const uint8_t* in_a; uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -177,10 +198,10 @@ int matrix_coeffs = 2; RGB_to_YCbCr_coefficients coeffs = RGB_to_YCbCr_coefficients::defaults(); bool full_range_flag = true; - full_range_flag = target_state.nclx_profile.get_full_range_flag(); - matrix_coeffs = target_state.nclx_profile.get_matrix_coefficients(); - coeffs = get_RGB_to_YCbCr_coefficients(target_state.nclx_profile.get_matrix_coefficients(), - target_state.nclx_profile.get_colour_primaries()); + full_range_flag = target_state.nclx.get_full_range_flag(); + matrix_coeffs = target_state.nclx.get_matrix_coefficients(); + coeffs = get_RGB_to_YCbCr_coefficients(target_state.nclx.get_matrix_coefficients(), + target_state.nclx.get_colour_primaries()); uint32_t x, y; @@ -195,6 +216,11 @@ out_y[y * out_y_stride + x] = (Pixel) clip_f_u16(v, fullRange); } } + else if (matrix_coeffs == 8) { + // Note: this is the YCgCo transform for equal Y/C bit depths, H.273(2024) Eq.51-57. + // To avoid a loss in accuracy, input bit-depth must be extended by two bits. + out_y[y * out_y_stride + x] = static_cast(in_g[y * in_g_stride + x] / 2 + (in_r[y * in_r_stride + x] + in_b[y * in_b_stride + x]) / 4); + } else { float r = in_r[y * in_r_stride + x]; float g = in_g[y * in_g_stride + x]; @@ -217,15 +243,24 @@ if (matrix_coeffs == 0) { if (full_range_flag) { out_cb[(y / subV) * out_cb_stride + (x / subH)] = in_b[y * in_b_stride + x]; - out_cr[(y / subV) * out_cb_stride + (x / subH)] = in_r[y * in_b_stride + x]; + out_cr[(y / subV) * out_cr_stride + (x / subH)] = in_r[y * in_r_stride + x]; } else { out_cb[(y / subV) * out_cb_stride + (x / subH)] = (Pixel) clip_f_u16( ((in_b[y * in_b_stride + x] * 224.0f) / 256) + limited_range_offset, fullRange); - out_cr[(y / subV) * out_cb_stride + (x / subH)] = (Pixel) clip_f_u16( - ((in_r[y * in_b_stride + x] * 224.0f) / 256) + limited_range_offset, fullRange); + out_cr[(y / subV) * out_cr_stride + (x / subH)] = (Pixel) clip_f_u16( + ((in_r[y * in_r_stride + x] * 224.0f) / 256) + limited_range_offset, fullRange); } } + else if (matrix_coeffs == 8) { + // Note: this is the YCgCo transform for equal Y/C bit depths, H.273(2024) Eq.51-57. + // To avoid a loss in accuracy, input bit-depth must be extended by two bits. + out_cb[(y / subV) * out_cb_stride + (x / subH)] = static_cast(clip_int_u16(in_g[y * in_g_stride + x] / 2 + - (in_r[y * in_r_stride + x] + in_b[y * in_b_stride + x]) / 4 + + halfRange, (uint16_t) fullRange)); + out_cr[(y / subV) * out_cr_stride + (x / subH)] = static_cast(clip_int_u16((in_r[y * in_r_stride + x] - in_b[y * in_b_stride + x]) / 2 + + halfRange, (uint16_t) fullRange)); + } else { float r = in_r[y * in_r_stride + x]; float g = in_g[y * in_g_stride + x]; @@ -288,7 +323,8 @@ std::vector Op_RRGGBBxx_HDR_to_YCbCr420::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -308,11 +344,15 @@ return {}; } - int matrix = target_state.nclx_profile.get_matrix_coefficients(); + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + int matrix = target_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } - if (!target_state.nclx_profile.get_full_range_flag()) { + if (!target_state.nclx.get_full_range_flag()) { return {}; } @@ -331,7 +371,7 @@ output_state.chroma = heif_chroma_420; output_state.has_alpha = input_state.has_alpha; // we generate an alpha plane if the source contains data output_state.bits_per_pixel = input_state.bits_per_pixel; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -344,6 +384,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -363,14 +404,14 @@ uint32_t cwidth = (width + 1) / 2; uint32_t cheight = (height + 1) / 2; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, bpp, limits) || - outimg->add_plane(heif_channel_Cb, cwidth, cheight, bpp, limits) || - outimg->add_plane(heif_channel_Cr, cwidth, cheight, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, bpp, limits) || + outimg->add_channel(heif_channel_Cb, cwidth, cheight, bpp, limits) || + outimg->add_channel(heif_channel_Cr, cwidth, cheight, bpp, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp, limits)) { return err; } } @@ -381,13 +422,13 @@ uint16_t* out_y, * out_cb, * out_cr, * out_a = nullptr; size_t out_y_stride = 0, out_cb_stride = 0, out_cr_stride = 0, out_a_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_p_stride); - out_y = (uint16_t*) outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = (uint16_t*) outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = (uint16_t*) outimg->get_plane(heif_channel_Cr, &out_cr_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_p_stride); + out_y = (uint16_t*) outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = (uint16_t*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = (uint16_t*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); if (has_alpha) { - out_a = (uint16_t*) outimg->get_plane(heif_channel_Alpha, &out_a_stride); + out_a = (uint16_t*) outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } @@ -405,10 +446,10 @@ int le = (input->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE || input->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE) ? 1 : 0; - bool full_range_flag = target_state.nclx_profile.get_full_range_flag(); + bool full_range_flag = target_state.nclx.get_full_range_flag(); RGB_to_YCbCr_coefficients coeffs = get_RGB_to_YCbCr_coefficients( - target_state.nclx_profile.get_matrix_coefficients(), - target_state.nclx_profile.get_colour_primaries()); + target_state.nclx.get_matrix_coefficients(), + target_state.nclx.get_colour_primaries()); for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < width; x++) { @@ -481,7 +522,8 @@ std::vector Op_RGB24_32_to_YCbCr::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -500,13 +542,19 @@ return {}; } + // The interleaved input is indexed as bytes and the output is hard-coded to 8 bits, + // so this operator handles 8-bit input only. + if (input_state.bits_per_pixel != 8) { + return {}; + } + if (target_state.chroma != heif_chroma_420 && target_state.chroma != heif_chroma_422 && target_state.chroma != heif_chroma_444) { return {}; } - int matrix = target_state.nclx_profile.get_matrix_coefficients(); + int matrix = target_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } @@ -519,7 +567,7 @@ output_state.chroma = target_state.chroma; output_state.has_alpha = target_state.has_alpha; output_state.bits_per_pixel = 8; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -551,6 +599,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -570,14 +619,14 @@ const bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_32bit); const bool want_alpha = target_state.has_alpha; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, 8, limits) || - outimg->add_plane(heif_channel_Cb, chroma_width, chroma_height, 8, limits) || - outimg->add_plane(heif_channel_Cr, chroma_width, chroma_height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, 8, limits) || + outimg->add_channel(heif_channel_Cb, chroma_width, chroma_height, 8, limits) || + outimg->add_channel(heif_channel_Cr, chroma_width, chroma_height, 8, limits)) { return err; } if (want_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, 8, limits)) { return err; } } @@ -588,14 +637,14 @@ const uint8_t* in_p; size_t in_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_stride); - out_y = outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = outimg->get_plane(heif_channel_Cr, &out_cr_stride); + out_y = outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); if (want_alpha) { - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { out_a = nullptr; @@ -604,9 +653,9 @@ RGB_to_YCbCr_coefficients coeffs = RGB_to_YCbCr_coefficients::defaults(); bool full_range_flag = true; - full_range_flag = target_state.nclx_profile.get_full_range_flag(); - coeffs = get_RGB_to_YCbCr_coefficients(target_state.nclx_profile.get_matrix_coefficients(), - target_state.nclx_profile.get_colour_primaries()); + full_range_flag = target_state.nclx.get_full_range_flag(); + coeffs = get_RGB_to_YCbCr_coefficients(target_state.nclx.get_matrix_coefficients(), + target_state.nclx.get_colour_primaries()); int bytes_per_pixel = (has_alpha ? 4 : 3); @@ -785,7 +834,8 @@ std::vector Op_RGB24_32_to_YCbCr444_GBR::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // Note: no input alpha channel required. It will be filled up with 0xFF. @@ -795,11 +845,17 @@ return {}; } - if (target_state.nclx_profile.get_matrix_coefficients() != 0) { + // The interleaved input is indexed as bytes and the output is hard-coded to 8 bits, + // so this operator handles 8-bit input only. + if (input_state.bits_per_pixel != 8) { + return {}; + } + + if (target_state.nclx.get_matrix_coefficients() != 0) { return {}; } - if (!target_state.nclx_profile.get_full_range_flag()) { + if (!target_state.nclx.get_full_range_flag()) { return {}; } @@ -811,7 +867,7 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = target_state.has_alpha; output_state.bits_per_pixel = 8; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -824,6 +880,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -836,14 +893,14 @@ const bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_32bit); const bool want_alpha = target_state.has_alpha; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, 8, limits) || - outimg->add_plane(heif_channel_Cb, width, height, 8, limits) || - outimg->add_plane(heif_channel_Cr, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, 8, limits) || + outimg->add_channel(heif_channel_Cb, width, height, 8, limits) || + outimg->add_channel(heif_channel_Cr, width, height, 8, limits)) { return err; } if (want_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, 8, limits)) { return err; } } @@ -854,18 +911,18 @@ const uint8_t* in_p; size_t in_stride = 0; - in_p = input->get_plane(heif_channel_interleaved, &in_stride); + in_p = input->get_channel_memory(heif_channel_interleaved, &in_stride); - out_y = outimg->get_plane(heif_channel_Y, &out_y_stride); - out_cb = outimg->get_plane(heif_channel_Cb, &out_cb_stride); - out_cr = outimg->get_plane(heif_channel_Cr, &out_cr_stride); + out_y = outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + out_cb = outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + out_cr = outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); if (want_alpha) { - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } - assert(target_state.nclx_profile.get_matrix_coefficients() == 0); + assert(target_state.nclx.get_matrix_coefficients() == 0); int bytes_per_pixel = (has_alpha ? 4 : 3); for (uint32_t y = 0; y < height; y++) { diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2yuv.h libheif-1.23.4/libheif/color-conversion/rgb2yuv.h --- libheif-1.19.8/libheif/color-conversion/rgb2yuv.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2yuv.h 2026-09-06 14:45:17.000000000 +0000 @@ -33,13 +33,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -51,13 +53,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -68,13 +72,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -85,13 +91,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2yuv_sharp.cc libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.cc --- libheif-1.19.8/libheif/color-conversion/rgb2yuv_sharp.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.cc 2026-09-06 14:45:17.000000000 +0000 @@ -55,7 +55,8 @@ std::vector Op_Any_RGB_to_YCbCr_420_Sharp::state_after_conversion( const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { #ifdef HAVE_LIBSHARPYUV // this Op only implements the sharp_yuv algorithm @@ -95,11 +96,24 @@ return {}; } + // The alpha plane is read with the sample width and step derived from the color + // channels (input_bytes_per_sample below comes from heif_channel_R), so a planar + // input whose alpha plane has a different bit depth would be indexed with the wrong + // stride and read two bytes per 1-byte sample: a heap over-read past the alpha plane + // (OSS-Fuzz 6503781601443840). Such a state is reachable because Op_YCbCr_to_RGB + // deliberately copies the alpha plane through at its own depth while converting the + // color channels, so 10-bit R/G/B next to an 8-bit alpha is normal here. Decline it, + // exactly as every other RGB operator does; the pipeline then inserts + // Op_adjust_alpha_bit_depth first and hands us a matched-depth image. + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + if (target_state.chroma != heif_chroma_420) { return {}; } - int matrix = target_state.nclx_profile.get_matrix_coefficients(); + int matrix = target_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } @@ -112,7 +126,7 @@ output_state.chroma = heif_chroma_420; output_state.has_alpha = target_state.has_alpha; output_state.bits_per_pixel = target_state.bits_per_pixel; - output_state.nclx_profile = target_state.nclx_profile; + output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Slow); return states; @@ -127,6 +141,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { #ifdef HAVE_LIBSHARPYUV @@ -155,14 +170,14 @@ bool want_alpha = target_state.has_alpha; int output_bits = target_state.bits_per_pixel; - if (auto err = outimg->add_plane(heif_channel_Y, width, height, output_bits, limits) || - outimg->add_plane(heif_channel_Cb, chroma_width, chroma_height, output_bits, limits) || - outimg->add_plane(heif_channel_Cr, chroma_width, chroma_height, output_bits, limits)) { + if (auto err = outimg->add_channel(heif_channel_Y, width, height, output_bits, limits) || + outimg->add_channel(heif_channel_Cb, chroma_width, chroma_height, output_bits, limits) || + outimg->add_channel(heif_channel_Cr, chroma_width, chroma_height, output_bits, limits)) { return err; } if (want_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, output_bits, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, output_bits, limits)) { return err; } } @@ -182,9 +197,9 @@ int input_bits = 0; if (planar_input) { size_t in_r_stride = 0, in_g_stride = 0, in_b_stride = 0; - in_r = input->get_plane(heif_channel_R, &in_r_stride); - in_g = input->get_plane(heif_channel_G, &in_g_stride); - in_b = input->get_plane(heif_channel_B, &in_b_stride); + in_r = input->get_channel_memory(heif_channel_R, &in_r_stride); + in_g = input->get_channel_memory(heif_channel_G, &in_g_stride); + in_b = input->get_channel_memory(heif_channel_B, &in_b_stride); // The stride must be the same for all channels. if (in_r_stride != in_g_stride || in_r_stride != in_b_stride) { return Error::InternalError; @@ -197,11 +212,11 @@ return Error::InternalError; } if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } } else { - const uint8_t* in_p = input->get_plane(heif_channel_interleaved, &in_stride); + const uint8_t* in_p = input->get_channel_memory(heif_channel_interleaved, &in_stride); input_bits = input->get_bits_per_pixel(heif_channel_interleaved); in_r = &in_p[input_bytes_per_sample * 0]; in_g = &in_p[input_bytes_per_sample * 1]; @@ -213,16 +228,16 @@ } size_t out_cb_stride = 0, out_cr_stride = 0, out_y_stride = 0; - uint8_t* out_y = outimg->get_plane(heif_channel_Y, &out_y_stride); - uint8_t* out_cb = outimg->get_plane(heif_channel_Cb, &out_cb_stride); - uint8_t* out_cr = outimg->get_plane(heif_channel_Cr, &out_cr_stride); + uint8_t* out_y = outimg->get_channel_memory(heif_channel_Y, &out_y_stride); + uint8_t* out_cb = outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride); + uint8_t* out_cr = outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride); bool full_range_flag = true; Kr_Kb kr_kb = Kr_Kb::defaults(); - full_range_flag = target_state.nclx_profile.get_full_range_flag(); + full_range_flag = target_state.nclx.get_full_range_flag(); kr_kb = - get_Kr_Kb(target_state.nclx_profile.get_matrix_coefficients(), - target_state.nclx_profile.get_colour_primaries()); + get_Kr_Kb(target_state.nclx.get_matrix_coefficients(), + target_state.nclx.get_colour_primaries()); SharpYuvColorSpace color_space = { kr_kb.Kr, kr_kb.Kb, output_bits, @@ -251,16 +266,20 @@ : 0; size_t out_a_stride; - uint8_t* out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + uint8_t* out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); uint16_t alpha_max = static_cast((1 << input_bits) - 1); for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < width; x++) { - const uint8_t* in = has_alpha ? &in_a[y * in_a_stride + x * rgb_step] : nullptr; - uint16_t a = has_alpha - ? ((input_bits == 8) - ? in[0] - : (uint16_t) ((in[0 + le] << 8) | in[1 - le])) - : alpha_max; + uint16_t a; + if (has_alpha) { + const uint8_t* in = &in_a[y * in_a_stride + x * rgb_step]; + a = (input_bits == 8) + ? in[0] + : (uint16_t) ((in[0 + le] << 8) | in[1 - le]); + } + else { + a = alpha_max; + } if (output_bits == 8) { out_a[y * out_a_stride + x] = (uint8_t) Shift(a, input_bits, output_bits); } diff -Nru libheif-1.19.8/libheif/color-conversion/rgb2yuv_sharp.h libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.h --- libheif-1.19.8/libheif/color-conversion/rgb2yuv_sharp.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.h 2026-09-06 14:45:17.000000000 +0000 @@ -32,13 +32,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/color-conversion/yuv2rgb.cc libheif-1.23.4/libheif/color-conversion/yuv2rgb.cc --- libheif-1.19.8/libheif/color-conversion/yuv2rgb.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/yuv2rgb.cc 2026-09-06 14:45:17.000000000 +0000 @@ -29,7 +29,8 @@ std::vector Op_YCbCr_to_RGB::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -47,10 +48,14 @@ return {}; } - int matrix = input_state.nclx_profile.get_matrix_coefficients(); - if ( matrix == 11 || matrix == 14) { + int matrix = input_state.nclx.get_matrix_coefficients(); + if (matrix == 11 || matrix == 14 || matrix == 17) { return {}; } + // TODO: matrix == 17 (YCgCo-Ro) is not implemented. Without the rejection above, it would + // fall through to the Kr/Kb-based conversion and silently decode with BT.601 coefficients. + // TODO: matrix == 10 (BT.2020 CL) currently falls through and is decoded as if it were + // BT.2020 NCL. A correct CL path needs EOTF inversion on Cb/Cr, not the linear matrix below. bool hdr = !std::is_same::value; @@ -64,6 +69,17 @@ return {}; } + // The YCgCo-Re conversion computes with int16_t intermediates. 14 bpp input is the + // maximum for which these cannot overflow. + if (matrix == 16 && input_state.bits_per_pixel > 14) { + return {}; + } + + // Also covers the alpha plane, which is copied through at its own bit depth below. + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + std::vector states; ColorState output_state; @@ -74,6 +90,7 @@ output_state.chroma = heif_chroma_444; output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -87,6 +104,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { bool hdr = !std::is_same::value; @@ -127,8 +145,6 @@ } - auto colorProfile = input->get_color_profile_nclx(); - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -136,14 +152,14 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_444); - if (auto err = outimg->add_plane(heif_channel_R, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_G, width, height, bpp_y, limits) || - outimg->add_plane(heif_channel_B, width, height, bpp_y, limits)) { + if (auto err = outimg->add_channel(heif_channel_R, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_G, width, height, bpp_y, limits) || + outimg->add_channel(heif_channel_B, width, height, bpp_y, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp_a, limits)) { + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp_a, limits)) { return err; } } @@ -154,12 +170,12 @@ Pixel* out_r, * out_g, * out_b; size_t out_r_stride = 0, out_g_stride = 0, out_b_stride = 0, out_a_stride = 0; - in_y = (const Pixel*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (const Pixel*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (const Pixel*) input->get_plane(heif_channel_Cr, &in_cr_stride); - out_r = (Pixel*) outimg->get_plane(heif_channel_R, &out_r_stride); - out_g = (Pixel*) outimg->get_plane(heif_channel_G, &out_g_stride); - out_b = (Pixel*) outimg->get_plane(heif_channel_B, &out_b_stride); + in_y = (const Pixel*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (const Pixel*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (const Pixel*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_r = (Pixel*) outimg->get_channel_memory(heif_channel_R, &out_r_stride); + out_g = (Pixel*) outimg->get_channel_memory(heif_channel_G, &out_g_stride); + out_b = (Pixel*) outimg->get_channel_memory(heif_channel_B, &out_b_stride); // We only copy the alpha, do not access it as 16 bit @@ -167,8 +183,8 @@ uint8_t* out_a; if (has_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); - out_a = outimg->get_plane(heif_channel_Alpha, &out_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); + out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride); } else { in_a = nullptr; @@ -176,7 +192,9 @@ } - uint16_t halfRange = (uint16_t) (1 << (bpp_y - 1)); + uint16_t halfRange = static_cast(1 << (bpp_y - 1)); + int16_t halfRange_chroma = static_cast(1 << (bpp_cb - 1)); + int32_t fullRange = (1 << bpp_y) - 1; int limited_range_offset_int = 16 << (bpp_y - 8); float limited_range_offset = static_cast(limited_range_offset_int); @@ -196,11 +214,18 @@ int matrix_coeffs = 2; bool full_range_flag = true; YCbCr_to_RGB_coefficients coeffs = YCbCr_to_RGB_coefficients::defaults(); - if (colorProfile) { - matrix_coeffs = colorProfile->get_matrix_coefficients(); - full_range_flag = colorProfile->get_full_range_flag(); - coeffs = get_YCbCr_to_RGB_coefficients(colorProfile->get_matrix_coefficients(), - colorProfile->get_colour_primaries()); + if (input->has_nclx_color_profile()) { + nclx_profile colorProfile = input->get_color_profile_nclx(); + + matrix_coeffs = colorProfile.get_matrix_coefficients(); + full_range_flag = colorProfile.get_full_range_flag(); + coeffs = get_YCbCr_to_RGB_coefficients(colorProfile.get_matrix_coefficients(), + colorProfile.get_colour_primaries()); + } + + // The YCgCo-Re path below computes with int16_t intermediates, which overflow above 14 bpp. + if (matrix_coeffs == 16 && bpp_y > 14) { + return Error::InternalError; } @@ -231,9 +256,28 @@ int cb = in_cb[cy * in_cb_stride + cx] - halfRange; int cr = in_cr[cy * in_cr_stride + cx] - halfRange; - out_r[y * out_r_stride + x] = (Pixel) (clip_int_u8(yv - cb + cr)); - out_g[y * out_g_stride + x] = (Pixel) (clip_int_u8(yv + cb)); - out_b[y * out_b_stride + x] = (Pixel) (clip_int_u8(yv - cb - cr)); + uint16_t max_rgb = static_cast(fullRange); + out_r[y * out_r_stride + x] = (Pixel) (clip_int_u16(yv - cb + cr, max_rgb)); + out_g[y * out_g_stride + x] = (Pixel) (clip_int_u16(yv + cb, max_rgb)); + out_b[y * out_b_stride + x] = (Pixel) (clip_int_u16(yv - cb - cr, max_rgb)); + } + else if (matrix_coeffs == 16) { + int16_t yy = in_y[y * in_y_stride + x]; + int16_t cb = static_cast(in_cb[cy * in_cb_stride + cx]) - halfRange_chroma; + int16_t cr = static_cast(in_cr[cy * in_cr_stride + cx]) - halfRange_chroma; + + int16_t t = yy - (cb >> 1); + int16_t g = t + cb; + int16_t b = t - (cr>>1); + int16_t r = b + cr; + + // TODO: we are extending the output RGB bpp by 2 bits because this function cannot do bit-depth + // conversion yet. This should be ultimately replaced by a new function with implicit bit-depth reduction. + + uint16_t max_rgb = static_cast((1<(clip_int_u16(r * 4, max_rgb)); + out_g[y * out_g_stride + x] = static_cast(clip_int_u16(g * 4, max_rgb)); + out_b[y * out_b_stride + x] = static_cast(clip_int_u16(b * 4, max_rgb)); } else { // TODO: matrix_coefficients = 11,14 float yv, cb, cr; @@ -269,7 +313,8 @@ std::vector Op_YCbCr420_to_RGB24::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -287,11 +332,11 @@ return {}; } - int matrix = input_state.nclx_profile.get_matrix_coefficients(); + int matrix = input_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } - if (!input_state.nclx_profile.get_full_range_flag()) { + if (!input_state.nclx.get_full_range_flag()) { return {}; } @@ -317,6 +362,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { if (input->get_bits_per_pixel(heif_channel_Y) != 8 || @@ -332,15 +378,15 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_interleaved_24bit); - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, 8, limits)) { return err; } - auto colorProfile = input->get_color_profile_nclx(); YCbCr_to_RGB_coefficients coeffs = YCbCr_to_RGB_coefficients::defaults(); - if (colorProfile) { - coeffs = get_YCbCr_to_RGB_coefficients(colorProfile->get_matrix_coefficients(), - colorProfile->get_colour_primaries()); + if (input->has_nclx_color_profile()) { + auto colorProfile = input->get_color_profile_nclx(); + coeffs = get_YCbCr_to_RGB_coefficients(colorProfile.get_matrix_coefficients(), + colorProfile.get_colour_primaries()); } int r_cr = static_cast(std::lround(256 * coeffs.r_cr)); @@ -354,21 +400,40 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_y = input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = input->get_plane(heif_channel_Cr, &in_cr_stride); - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + in_y = input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = input->get_channel_memory(heif_channel_Cr, &in_cr_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); uint32_t x, y; for (y = 0; y < height; y++) { + // Row pointers for input and output + const uint8_t* y_row = &in_y[y * in_y_stride]; + const uint8_t* cb_row = &in_cb[(y / 2) * in_cb_stride]; + const uint8_t* cr_row = &in_cr[(y / 2) * in_cr_stride]; + uint8_t* out_row = &out_p[y * out_p_stride]; + + int cb = 0; + int cr = 0; + int r_offset = 0; + int g_offset = 0; + int b_offset = 0; + for (x = 0; x < width; x++) { - int yv = (in_y[y * in_y_stride + x]); - int cb = (in_cb[y / 2 * in_cb_stride + x / 2] - 128); - int cr = (in_cr[y / 2 * in_cr_stride + x / 2] - 128); + // Update color offsets every other pixel + if ((x & 1) == 0) { + cb = cb_row[x / 2] - 128; + cr = cr_row[x / 2] - 128; + r_offset = ((r_cr * cr + 128) >> 8); + g_offset = ((g_cb * cb + g_cr * cr + 128) >> 8); + b_offset = ((b_cb * cb + 128) >> 8); + } - out_p[y * out_p_stride + 3 * x + 0] = clip_int_u8(yv + ((r_cr * cr + 128) >> 8)); - out_p[y * out_p_stride + 3 * x + 1] = clip_int_u8(yv + ((g_cb * cb + g_cr * cr + 128) >> 8)); - out_p[y * out_p_stride + 3 * x + 2] = clip_int_u8(yv + ((b_cb * cb + 128) >> 8)); + int yv = y_row[x]; + uint8_t* rgb = &out_row[3 * x]; + rgb[0] = clip_int_u8(yv + r_offset); + rgb[1] = clip_int_u8(yv + g_offset); + rgb[2] = clip_int_u8(yv + b_offset); } } @@ -379,7 +444,8 @@ std::vector Op_YCbCr420_to_RGB32::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -398,11 +464,15 @@ return {}; } - int matrix = input_state.nclx_profile.get_matrix_coefficients(); + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + + int matrix = input_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } - if (!input_state.nclx_profile.get_full_range_flag()) { + if (!input_state.nclx.get_full_range_flag()) { return {}; } @@ -428,6 +498,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { if (input->get_bits_per_pixel(heif_channel_Y) != 8 || @@ -443,18 +514,18 @@ outimg->create(width, height, heif_colorspace_RGB, heif_chroma_interleaved_32bit); - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, 8, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, 8, limits)) { return err; } // --- get conversion coefficients - auto colorProfile = input->get_color_profile_nclx(); YCbCr_to_RGB_coefficients coeffs = YCbCr_to_RGB_coefficients::defaults(); - if (colorProfile) { - coeffs = get_YCbCr_to_RGB_coefficients(colorProfile->get_matrix_coefficients(), - colorProfile->get_colour_primaries()); + if (input->has_nclx_color_profile()) { + nclx_profile colorProfile = input->get_color_profile_nclx(); + coeffs = get_YCbCr_to_RGB_coefficients(colorProfile.get_matrix_coefficients(), + colorProfile.get_colour_primaries()); } int r_cr = static_cast(std::lround(256 * coeffs.r_cr)); @@ -471,14 +542,14 @@ uint8_t* out_p; size_t out_p_stride = 0; - in_y = input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = input->get_plane(heif_channel_Cr, &in_cr_stride); + in_y = input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = input->get_channel_memory(heif_channel_Cr, &in_cr_stride); if (with_alpha) { - in_a = input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); uint32_t x, y; for (y = 0; y < height; y++) { @@ -509,7 +580,8 @@ std::vector Op_YCbCr420_to_RRGGBBaa::state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const { // this Op only implements the nearest-neighbor algorithm @@ -526,7 +598,15 @@ return {}; } - int matrix = input_state.nclx_profile.get_matrix_coefficients(); + if (has_samples_wider_than_16bit(input_state)) { + return {}; + } + + if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + return {}; + } + + int matrix = input_state.nclx.get_matrix_coefficients(); if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) { return {}; } @@ -563,6 +643,7 @@ const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { uint32_t width = input->get_width(); @@ -571,6 +652,18 @@ int bpp = input->get_bits_per_pixel(heif_channel_Y); bool has_alpha = input->has_channel(heif_channel_Alpha); + // Defense in depth: the Cb/Cr planes are read below through a uint16_t* using a + // sample width taken from the Y channel (bpp). convert_colorspace() already + // rejects YCbCr with mismatched luma/chroma bit depths (GHSA-w7mc-p8jc-p853), + // but guard here as well so this operator is self-contained and a future direct + // caller or pipeline change cannot reintroduce the out-of-bounds chroma read. + if (input->get_bits_per_pixel(heif_channel_Cb) != bpp || + input->get_bits_per_pixel(heif_channel_Cr) != bpp) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Color conversion cannot handle YCbCr images with chroma bit depth differing from luma."}; + } + int le = (target_state.chroma == heif_chroma_interleaved_RRGGBB_LE || target_state.chroma == heif_chroma_interleaved_RRGGBBAA_LE) ? 1 : 0; @@ -579,12 +672,29 @@ int bytesPerPixel = has_alpha ? 8 : 6; - if (auto err = outimg->add_plane(heif_channel_interleaved, width, height, bpp, limits)) { + if (auto err = outimg->add_channel(heif_channel_interleaved, width, height, bpp, limits)) { return err; } if (has_alpha) { - if (auto err = outimg->add_plane(heif_channel_Alpha, width, height, bpp, limits)) { + if (input->get_width(heif_channel_Alpha) != width || + input->get_height(heif_channel_Alpha) != height) { + return Error{ + heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Color conversion cannot handle alpha images with sizes differing from the main image size." + }; + } + + if (input->get_bits_per_pixel(heif_channel_Alpha) != bpp) { + return Error{ + heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Color conversion cannot handle alpha images with bits-per-pixel differing from the main image." + }; + } + + if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp, limits)) { return err; } } @@ -595,13 +705,13 @@ const uint16_t* in_y, * in_cb, * in_cr, * in_a = nullptr; size_t in_y_stride = 0, in_cb_stride = 0, in_cr_stride = 0, in_a_stride = 0; - out_p = outimg->get_plane(heif_channel_interleaved, &out_p_stride); - in_y = (uint16_t*) input->get_plane(heif_channel_Y, &in_y_stride); - in_cb = (uint16_t*) input->get_plane(heif_channel_Cb, &in_cb_stride); - in_cr = (uint16_t*) input->get_plane(heif_channel_Cr, &in_cr_stride); + out_p = outimg->get_channel_memory(heif_channel_interleaved, &out_p_stride); + in_y = (uint16_t*) input->get_channel_memory(heif_channel_Y, &in_y_stride); + in_cb = (uint16_t*) input->get_channel_memory(heif_channel_Cb, &in_cb_stride); + in_cr = (uint16_t*) input->get_channel_memory(heif_channel_Cr, &in_cr_stride); if (has_alpha) { - in_a = (uint16_t*) input->get_plane(heif_channel_Alpha, &in_a_stride); + in_a = (uint16_t*) input->get_channel_memory(heif_channel_Alpha, &in_a_stride); } int maxval = (1 << bpp) - 1; @@ -609,11 +719,11 @@ bool full_range_flag = true; YCbCr_to_RGB_coefficients coeffs = YCbCr_to_RGB_coefficients::defaults(); - auto colorProfile = input->get_color_profile_nclx(); - if (colorProfile) { - full_range_flag = colorProfile->get_full_range_flag(); - coeffs = get_YCbCr_to_RGB_coefficients(colorProfile->get_matrix_coefficients(), - colorProfile->get_colour_primaries()); + if (input->has_nclx_color_profile()) { + nclx_profile colorProfile = input->get_color_profile_nclx(); + full_range_flag = colorProfile.get_full_range_flag(); + coeffs = get_YCbCr_to_RGB_coefficients(colorProfile.get_matrix_coefficients(), + colorProfile.get_colour_primaries()); } float limited_range_offset = static_cast(16 << (bpp - 8)); diff -Nru libheif-1.19.8/libheif/color-conversion/yuv2rgb.h libheif-1.23.4/libheif/color-conversion/yuv2rgb.h --- libheif-1.19.8/libheif/color-conversion/yuv2rgb.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/color-conversion/yuv2rgb.h 2026-09-06 14:45:17.000000000 +0000 @@ -34,13 +34,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -51,13 +53,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -68,13 +72,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; @@ -85,13 +91,15 @@ std::vector state_after_conversion(const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options) const override; + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) const override; Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const override; }; diff -Nru libheif-1.19.8/libheif/common_utils.cc libheif-1.23.4/libheif/common_utils.cc --- libheif-1.19.8/libheif/common_utils.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/common_utils.cc 2026-09-06 14:45:17.000000000 +0000 @@ -36,8 +36,15 @@ case heif_chroma_interleaved_RGB: case heif_chroma_interleaved_RGBA: default: - assert(false); - return 0; + // Interleaved and undefined chroma have no Cb/Cr subsampling of their own + // (a Cb/Cr channel paired with one of these is already an inconsistent + // image -- Cb/Cr only exist for planar YCbCr -- which other checks reject + // on their own merits). Callers divide by this return value, and this + // function is reachable with attacker-controlled chroma through the + // public API (heif_image_create() + heif_image_add_plane()), so an + // assert() here (compiled out under NDEBUG, i.e. every shipped build) + // is not a guard: return the neutral, unsubsampled value instead of 0. + return 1; } } @@ -56,8 +63,8 @@ case heif_chroma_interleaved_RGB: case heif_chroma_interleaved_RGBA: default: - assert(false); - return 0; + // See chroma_h_subsampling(). + return 1; } } @@ -158,3 +165,27 @@ return str; } + + +Result vector_to_string(const std::vector& vec) +{ + if (vec.empty()) { + return std::string{}; // return empty string + } + + if (vec.back() != 0) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "utf8string not null-terminated"}; + } + + for (size_t i=0;i -#include "libheif/heif.h" #include +#include + +#include "libheif/heif.h" +#include "error.h" #ifdef _MSC_VER #define MAYBE_UNUSED @@ -32,12 +35,26 @@ #endif -constexpr inline uint32_t fourcc(const char* id) +constexpr uint32_t four_bytes_to_uint32(uint8_t msb, uint8_t b, uint8_t c, uint8_t lsb) +{ + return (static_cast(msb << 24) | + static_cast(b << 16) | + static_cast(c << 8) | + static_cast(lsb)); +} + +constexpr uint16_t two_bytes_to_uint16(uint8_t msb, uint8_t lsb) +{ + return (static_cast(msb << 8) | + static_cast(lsb)); +} + +constexpr uint32_t fourcc(const char* id) { - return (((((uint32_t) id[0])&0xFF) << 24) | - ((((uint32_t) id[1])&0xFF) << 16) | - ((((uint32_t) id[2])&0xFF) << 8) | - ((((uint32_t) id[3])&0xFF) << 0)); + return four_bytes_to_uint32(static_cast(id[0]), + static_cast(id[1]), + static_cast(id[2]), + static_cast(id[3])); } std::string fourcc_to_string(uint32_t code); @@ -80,22 +97,30 @@ return static_cast(x); } +inline uint16_t clip_int_u16(int32_t x, uint16_t maxi) +{ + if (x < 0) return 0; + if (x > maxi) return maxi; + return static_cast(x); +} + inline uint16_t clip_f_u16(float fx, int32_t maxi) { - long x = (long int) (fx + 0.5f); + int32_t x = (int32_t) (fx + 0.5f); if (x < 0) return 0; if (x > maxi) return (uint16_t) maxi; return static_cast(x); } - inline uint8_t clip_f_u8(float fx) { - long x = (long int) (fx + 0.5f); + int32_t x = (int32_t) (fx + 0.5f); if (x < 0) return 0; if (x > 255) return 255; return static_cast(x); } +Result vector_to_string(const std::vector& vec); + #endif //LIBHEIF_COMMON_UTILS_H diff -Nru libheif-1.19.8/libheif/compression.cc libheif-1.23.4/libheif/compression.cc --- libheif-1.19.8/libheif/compression.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/compression.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,61 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "compression.h" +#include "common_utils.h" + + +uint32_t unci_compression_to_fourcc(heif_unci_compression method) +{ + switch (method) { + case heif_unci_compression_off: + return 0; + case heif_unci_compression_deflate: + return fourcc("defl"); + case heif_unci_compression_zlib: + return fourcc("zlib"); + case heif_unci_compression_brotli: + return fourcc("brot"); + default: + return 0; + } +} + + +Result> compress_unci_fourcc(uint32_t fourcc_code, + const uint8_t* data, size_t size) +{ + switch (fourcc_code) { +#if HAVE_ZLIB + case fourcc("defl"): + return {compress_deflate(data, size)}; + case fourcc("zlib"): + return {compress_zlib(data, size)}; +#endif +#if HAVE_BROTLI + case fourcc("brot"): + return {compress_brotli(data, size)}; +#endif + default: + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_generic_compression_method, + "Unsupported unci compression method."}; + } +} diff -Nru libheif-1.19.8/libheif/compression.h libheif-1.23.4/libheif/compression.h --- libheif-1.19.8/libheif/compression.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/compression.h 2026-09-06 14:45:17.000000000 +0000 @@ -25,6 +25,28 @@ #include #include +#include + +struct heif_security_limits; + +/** + * Convert heif_unci_compression enum to a fourcc code. + * + * @param method the compression method + * @return the corresponding fourcc code, or 0 for heif_unci_compression_off + */ +uint32_t unci_compression_to_fourcc(heif_unci_compression method); + +/** + * Compress data using the compression method identified by a fourcc code. + * + * @param fourcc_code the fourcc code for the compression method (e.g. "defl", "zlib", "brot") + * @param data pointer to the data to be compressed + * @param size the length of the input array in bytes + * @return the corresponding compressed data, or an error + */ +Result> compress_unci_fourcc(uint32_t fourcc_code, + const uint8_t* data, size_t size); #if HAVE_ZLIB /** @@ -61,7 +83,8 @@ * @sa decompress_deflate * @sa compress_zlib */ -Error decompress_zlib(const std::vector& compressed_input, std::vector* output); +Result> decompress_zlib(const std::vector& compressed_input, + const heif_security_limits* limits); /** * Decompress "deflate" compressed data. @@ -75,7 +98,8 @@ * @sa decompress_zlib * @sa compress_deflate */ -Error decompress_deflate(const std::vector& compressed_input, std::vector* output); +Result> decompress_deflate(const std::vector& compressed_input, + const heif_security_limits* limits); #endif @@ -89,7 +113,8 @@ * @param output pointer to the resulting vector of decompressed data * @return success (Ok) or an error on failure (usually corrupt data) */ -Error decompress_brotli(const std::vector& compressed_input, std::vector* output); +Result> decompress_brotli(const std::vector& compressed_input, + const heif_security_limits* limits); std::vector compress_brotli(const uint8_t* input, size_t size); #endif diff -Nru libheif-1.19.8/libheif/compression_brotli.cc libheif-1.23.4/libheif/compression_brotli.cc --- libheif-1.19.8/libheif/compression_brotli.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/compression_brotli.cc 2026-09-06 14:45:17.000000000 +0000 @@ -31,9 +31,11 @@ #include #include "error.h" +#include "security_limits.h" -Error decompress_brotli(const std::vector &compressed_input, std::vector *output) +Result> decompress_brotli(const std::vector &compressed_input, + const heif_security_limits* limits) { BrotliDecoderResult result = BROTLI_DECODER_RESULT_ERROR; std::vector buffer(BUF_SIZE, 0); @@ -44,19 +46,34 @@ std::unique_ptr state(BrotliDecoderCreateInstance(0, 0, 0), BrotliDecoderDestroyInstance); + std::vector output; + + // Track the growth of `output` against the security limits. Without this, a + // high-ratio "decompression bomb" would expand a few KB of input into GBs of + // output, bypassing max_memory_block_size / max_total_memory (GHSA-24wx-9w62-c96w). + MemoryHandle output_memory_handle; + while (true) { result = BrotliDecoderDecompressStream(state.get(), &available_in, &next_in, &available_out, &next_output, 0); if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { - output->insert(output->end(), buffer.data(), buffer.data() + std::distance(buffer.data(), next_output)); + size_t n_new_bytes = static_cast(std::distance(buffer.data(), next_output)); + if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "brotli decompression output")) { + return memErr; + } + output.insert(output.end(), buffer.data(), buffer.data() + n_new_bytes); available_out = buffer.size(); next_output = buffer.data(); } else if (result == BROTLI_DECODER_RESULT_SUCCESS) { - output->insert(output->end(), buffer.data(), buffer.data() + std::distance(buffer.data(), next_output)); + size_t n_new_bytes = static_cast(std::distance(buffer.data(), next_output)); + if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "brotli decompression output")) { + return memErr; + } + output.insert(output.end(), buffer.data(), buffer.data() + n_new_bytes); break; } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) @@ -81,7 +98,7 @@ } } - return Error::Ok; + return output; } diff -Nru libheif-1.19.8/libheif/compression_zlib.cc libheif-1.23.4/libheif/compression_zlib.cc --- libheif-1.19.8/libheif/compression_zlib.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/compression_zlib.cc 2026-09-06 14:45:17.000000000 +0000 @@ -20,6 +20,7 @@ #include "compression.h" +#include "security_limits.h" #if HAVE_ZLIB @@ -80,13 +81,25 @@ } -Error do_inflate(const std::vector& compressed_input, int windowSize, std::vector *output) +Result> do_inflate(const std::vector& compressed_input, int windowSize, + const heif_security_limits* limits) { + if (compressed_input.empty()) { + return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, + "Empty zlib compressed data."); + } + + std::vector output; + + // Track the growth of `output` against the security limits. Without this, a + // high-ratio "decompression bomb" would expand a few KB of input into GBs of + // output, bypassing max_memory_block_size / max_total_memory (GHSA-24wx-9w62-c96w). + MemoryHandle output_memory_handle; // decompress data with zlib - const int outBufferSize = 8192; - uint8_t dst[outBufferSize]; + std::vector dst; + dst.resize(8192); z_stream strm; memset(&strm, 0, sizeof(z_stream)); @@ -94,8 +107,8 @@ strm.avail_in = (int)compressed_input.size(); strm.next_in = (Bytef*) compressed_input.data(); - strm.avail_out = outBufferSize; - strm.next_out = (Bytef*) dst; + strm.avail_out = (uInt)dst.size(); + strm.next_out = (Bytef*) dst.data(); strm.zalloc = Z_NULL; strm.zfree = Z_NULL; @@ -111,29 +124,56 @@ } do { - strm.next_out = dst; - strm.avail_out = outBufferSize; + strm.avail_out = (uInt)dst.size(); + strm.next_out = (Bytef*) dst.data(); - err = inflate(&strm, Z_FINISH); - if (err == Z_BUF_ERROR || err == Z_OK) { - // this is the usual case when we run out of buffer space - // -> do nothing + err = inflate(&strm, Z_NO_FLUSH); + + if (err == Z_BUF_ERROR) { + if (strm.avail_in == 0) { + // All input consumed; decompression is complete even without Z_STREAM_END + break; + } + + // Grow the scratch buffer so inflate() can make progress. Bound its growth + // against the per-block security limit so a pathological stream cannot force + // an unbounded scratch allocation (the accumulated output is bounded + // separately, via output_memory_handle below). + size_t new_size = dst.size() * 2; + if (limits && limits->max_memory_block_size != 0 && new_size > limits->max_memory_block_size) { + inflateEnd(&strm); + return Error(heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "zlib inflate scratch buffer exceeds the maximum block size"); + } + + dst.resize(new_size); + strm.next_out = dst.data(); + strm.avail_out = (uInt)dst.size(); + continue; } - else if (err == Z_NEED_DICT || err == Z_DATA_ERROR || err == Z_STREAM_ERROR) { + + if (err == Z_NEED_DICT || err == Z_DATA_ERROR || err == Z_STREAM_ERROR) { inflateEnd(&strm); std::stringstream sstr; sstr << "Error performing zlib inflate: " << (strm.msg ? strm.msg : "NULL") << " (" << err << ")\n"; return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, sstr.str()); } - // append decoded data to output - output->insert(output->end(), dst, dst + outBufferSize - strm.avail_out); + // account for and append decoded data to output + + size_t n_new_bytes = dst.size() - strm.avail_out; + if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "zlib/deflate decompression output")) { + inflateEnd(&strm); + return memErr; + } + + output.insert(output.end(), dst.begin(), dst.begin() + n_new_bytes); } while (err != Z_STREAM_END); inflateEnd(&strm); - return Error::Ok; + return output; } std::vector compress_zlib(const uint8_t* input, size_t size) @@ -147,13 +187,15 @@ } -Error decompress_zlib(const std::vector& compressed_input, std::vector *output) +Result> decompress_zlib(const std::vector& compressed_input, + const heif_security_limits* limits) { - return do_inflate(compressed_input, 15, output); + return do_inflate(compressed_input, 15, limits); } -Error decompress_deflate(const std::vector& compressed_input, std::vector *output) +Result> decompress_deflate(const std::vector& compressed_input, + const heif_security_limits* limits) { - return do_inflate(compressed_input, -15, output); + return do_inflate(compressed_input, -15, limits); } #endif diff -Nru libheif-1.19.8/libheif/context.cc libheif-1.23.4/libheif/context.cc --- libheif-1.19.8/libheif/context.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/context.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,6 +22,7 @@ #include "error.h" #include "libheif/heif.h" #include "region.h" +#include "brands.h" #include #include #include @@ -30,8 +31,13 @@ #include #include #include +#include #include "image-items/image_item.h" #include +#include "sequences/track.h" +#include "sequences/track_visual.h" +#include "sequences/track_metadata.h" +#include "libheif/heif_sequences.h" #if ENABLE_PARALLEL_TILE_DECODING #include @@ -39,8 +45,8 @@ #include "context.h" #include "file.h" -#include "pixelimage.h" -#include "libheif/api_structs.h" +#include "image/pixelimage.h" +#include "api_structs.h" #include "security_limits.h" #include "compression.h" #include "color-conversion/colorconversion.h" @@ -58,9 +64,10 @@ #if WITH_UNCOMPRESSED_CODEC #include "image-items/unc_image.h" #endif +#include "text.h" -heif_encoder::heif_encoder(const struct heif_encoder_plugin* _plugin) +heif_encoder::heif_encoder(const heif_encoder_plugin* _plugin) : plugin(_plugin) { @@ -80,30 +87,85 @@ } -struct heif_error heif_encoder::alloc() +heif_error heif_encoder::alloc() { if (encoder == nullptr) { - struct heif_error error = plugin->new_encoder(&encoder); + heif_error error = plugin->new_encoder(&encoder); // TODO: error handling return error; } - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, Error::kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, Error::kSuccess}; } -HeifContext::HeifContext() +void heif_encoder::copy_parameters_from(const heif_encoder& src) +{ + // Copy dedicated quality/lossless/logging parameters + int ival; + plugin->get_parameter_quality(src.encoder, &ival); + plugin->set_parameter_quality(encoder, ival); + + plugin->get_parameter_lossless(src.encoder, &ival); + plugin->set_parameter_lossless(encoder, ival); + + if (plugin->get_parameter_logging_level && plugin->set_parameter_logging_level) { + plugin->get_parameter_logging_level(src.encoder, &ival); + plugin->set_parameter_logging_level(encoder, ival); + } + + // Copy all enumerable plugin parameters + const heif_encoder_parameter* const* params = plugin->list_parameters(src.encoder); + if (!params) return; + + for (; *params; params++) { + const char* name = (*params)->name; + switch ((*params)->type) { + case heif_encoder_parameter_type_integer: { + int v; + if (plugin->get_parameter_integer(src.encoder, name, &v).code == heif_error_Ok) + plugin->set_parameter_integer(encoder, name, v); + break; + } + case heif_encoder_parameter_type_boolean: { + int v; + if (plugin->get_parameter_boolean(src.encoder, name, &v).code == heif_error_Ok) + plugin->set_parameter_boolean(encoder, name, v); + break; + } + case heif_encoder_parameter_type_string: { + char v[256]; + if (plugin->get_parameter_string(src.encoder, name, v, sizeof(v)).code == heif_error_Ok) + plugin->set_parameter_string(encoder, name, v); + break; + } + } + } +} + + +// Selects the initial security limits for a new context. The environment variable +// LIBHEIF_SECURITY_LIMITS=off disables all limits (for trusted input only). +static const heif_security_limits& initial_security_limits() { const char* security_limits_variable = getenv("LIBHEIF_SECURITY_LIMITS"); if (security_limits_variable && (strcmp(security_limits_variable, "off") == 0 || strcmp(security_limits_variable, "OFF") == 0)) { - m_limits = disabled_security_limits; + return disabled_security_limits; } else { - m_limits = global_security_limits; + return global_security_limits; } +} + + +HeifContext::HeifContext() + : m_limits(initial_security_limits()), + m_memory_tracker(&m_limits) +{ + // m_limits must be fully initialized above before its address is passed to the memory tracker. + // (m_limits is declared before m_memory_tracker in context.h, so it is constructed first.) reset_to_empty_heif(); } @@ -121,7 +183,6 @@ static void copy_security_limits(heif_security_limits* dst, const heif_security_limits* src) { - dst->version = 1; dst->max_image_size_pixels = src->max_image_size_pixels; dst->max_number_of_tiles = src->max_number_of_tiles; dst->max_bayer_pattern_pixels = src->max_bayer_pattern_pixels; @@ -136,15 +197,59 @@ dst->max_size_entity_group = src->max_size_entity_group; dst->max_children_per_box = src->max_children_per_box; + + if (src->version >= 2) { + dst->max_total_memory = src->max_total_memory; + dst->max_sample_description_box_entries = src->max_sample_description_box_entries; + dst->max_sample_group_description_box_entries = src->max_sample_group_description_box_entries; + } + + if (src->version >= 3) { + dst->max_sequence_frames = src->max_sequence_frames; + dst->max_number_of_file_brands = src->max_number_of_file_brands; + } + + if (src->version >= 4) { + dst->max_bad_pixels = src->max_bad_pixels; + dst->max_iso23001_17_pixel_size_bytes = src->max_iso23001_17_pixel_size_bytes; + } + + // `parent` is an internal field; user-supplied limits are always treated as + // a root context. dst is HeifContext::m_limits, which is registered. + dst->parent = nullptr; } void HeifContext::set_security_limits(const heif_security_limits* limits) { + // copy default limits + if (limits->version < global_security_limits.version) { + copy_security_limits(&m_limits, &global_security_limits); + } + + // overwrite with input limits copy_security_limits(&m_limits, limits); } +void HeifContext::set_unif(bool flag) +{ + m_heif_file->get_id_creator().set_unif(flag); +} + + +bool HeifContext::get_unif() const +{ + return m_heif_file->get_id_creator().get_unif(); +} + + +IDCreator& HeifContext::get_id_creator() +{ + return m_heif_file->get_id_creator(); +} + + Error HeifContext::read(const std::shared_ptr& reader) { m_heif_file = std::make_shared(); @@ -230,22 +335,34 @@ std::shared_ptr HeifContext::get_primary_image(bool return_error_image) { - if (!return_error_image && m_primary_image->get_item_error()) + if (m_primary_image == nullptr) + return nullptr; + else if (!return_error_image && m_primary_image->get_item_error()) return nullptr; else return m_primary_image; } +std::shared_ptr HeifContext::get_primary_image(bool return_error_image) const +{ + return const_cast(this)->get_primary_image(return_error_image); +} + + bool HeifContext::is_image(heif_item_id ID) const { - return m_all_images.find(ID) != m_all_images.end(); + return m_all_images.contains(ID); } -std::shared_ptr HeifContext::add_region_item(uint32_t reference_width, uint32_t reference_height) +Result> HeifContext::add_region_item(uint32_t reference_width, uint32_t reference_height) { - std::shared_ptr box = m_heif_file->add_new_infe_box(fourcc("rgan")); + auto boxResult = m_heif_file->add_new_infe_box(fourcc("rgan")); + if (!boxResult) { + return boxResult.error(); + } + auto box = *boxResult; box->set_hidden_item(true); auto regionItem = std::make_shared(box->get_item_ID(), reference_width, reference_height); @@ -259,8 +376,77 @@ m_heif_file->add_iref_reference(region_item_id, fourcc("mask"), {mask_item_id}); } -void HeifContext::write(StreamWriter& writer) + +static uint64_t rescale(uint64_t duration, uint32_t old_base, uint32_t new_base) { + // prevent division by zero + // TODO: we might emit an error in this case + if (old_base == 0) { + return 0; + } + + return duration * new_base / old_base; +} + + +Error HeifContext::write(StreamWriter& writer) +{ + // Writing is only implemented for contexts that were built in memory. In a context read + // from a file, the item and sample data still live in the input file and would not be + // copied to the output: the result would be a truncated file that no reader accepts. + if (m_heif_file->get_reader()) { + return Error(heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Writing a context that was read from a file is not supported"); + } + + // --- finalize some parameters + + uint64_t max_sequence_duration = 0; + if (auto mvhd = m_heif_file->get_mvhd_box()) { + for (const auto& track : m_tracks) { + track.second->finalize_track(); + + // rescale track duration to movie timescale units + + uint64_t track_duration_in_media_units = track.second->get_duration_in_media_units(); + uint32_t media_timescale = track.second->get_timescale(); + + uint32_t mvhd_timescale = m_heif_file->get_mvhd_box()->get_time_scale(); + if (mvhd_timescale == 0) { + mvhd_timescale = track.second->get_timescale(); + m_heif_file->get_mvhd_box()->set_time_scale(mvhd_timescale); + } + + uint64_t movie_duration = rescale(track_duration_in_media_units, media_timescale, mvhd_timescale); + uint64_t unrepeated_movie_duration = movie_duration; + + // sequence repetitions + + if (m_sequence_repetitions == heif_sequence_maximum_number_of_repetitions) { + movie_duration = std::numeric_limits::max(); + } + else { + if (std::numeric_limits::max() / m_sequence_repetitions < movie_duration) { + movie_duration = std::numeric_limits::max(); + } + else { + movie_duration *= m_sequence_repetitions; + } + } + + if (m_sequence_repetitions != 1) { + track.second->enable_edit_list_repeat_mode(true); + } + + track.second->set_track_duration_in_movie_units(movie_duration, unrepeated_movie_duration); + + max_sequence_duration = std::max(max_sequence_duration, movie_duration); + } + + mvhd->set_duration(max_sequence_duration); + } + // --- serialize regions for (auto& image : m_all_images) { @@ -273,24 +459,85 @@ for (auto& region : m_region_items) { std::vector data_array; Error err = region->encode(data_array); - // TODO: err + if (err) { + return err; + } m_heif_file->append_iloc_data(region->item_id, data_array, 0); } + // --- serialise text items + + for (auto& image : m_all_images) { + for (auto text_item_id : image.second->get_text_item_ids()) { + m_heif_file->add_iref_reference(text_item_id, fourcc("text"), {image.first}); + } + } + + for (auto& text_item : m_text_items) { + auto encodeResult = text_item->encode(); + if (encodeResult) { + m_heif_file->append_iloc_data(text_item->get_item_id(), *encodeResult, 1); + } + } + // --- post-process images for (auto& img : m_all_images) { - img.second->process_before_write(); + Error err = img.second->process_before_write(); + if (err) { + return err; + } } // --- sort item properties - m_heif_file->get_ipma_box()->sort_properties(m_heif_file->get_ipco_box()); + if (auto ipma = m_heif_file->get_ipma_box()) { + ipma->sort_properties(m_heif_file->get_ipco_box()); + } + + // --- derive box versions + + m_heif_file->derive_box_versions(); + + // --- determine brands + + heif_brand2 main_brand; + std::vector compatible_brands; + compatible_brands = compute_compatible_brands(this, &main_brand); + + // Note: major brand should be repeated in the compatible brands, according to this: + // ISOBMFF (ISO/IEC 14496-12:2020) § K.4: + // NOTE This document requires that the major brand be repeated in the compatible-brands, + // but this requirement is relaxed in the 'profiles' parameter for compactness. + // See https://github.com/strukturag/libheif/issues/478 + + auto ftyp = m_heif_file->get_ftyp_box(); + + // set major brand if not set manually yet + if (ftyp->get_major_brand() == 0) { + ftyp->set_major_brand(main_brand); + } + + // A file without images and without an image sequence (e.g. only metadata items) has no + // brand that describes it. Instead of writing a file with an all-zero 'ftyp' box that no + // reader (including libheif) accepts, refuse to write it. + if (ftyp->get_major_brand() == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Unspecified, + "Cannot write a file that contains neither images nor an image sequence"); + } + + ftyp->set_minor_version(0); + for (auto brand : compatible_brands) { + ftyp->add_compatible_brand(brand); + } // --- write to file m_heif_file->write(writer); + + return {}; } std::string HeifContext::debug_dump_boxes() const @@ -298,6 +545,17 @@ return m_heif_file->debug_dump_boxes(); } +std::string HeifContext::debug_dump_item_data() const +{ + return m_heif_file->debug_dump_item_data(); +} + + +void HeifContext::set_write_mini_format(bool enable) +{ + m_heif_file->set_write_mini_format(enable); +} + static bool item_type_is_image(uint32_t item_type, const std::string& content_type) { @@ -333,6 +591,26 @@ Error HeifContext::interpret_heif_file() { + if (m_heif_file->has_images()) { + Error err = interpret_heif_file_images(); + if (err) { + return err; + } + } + + if (m_heif_file->has_sequences()) { + Error err = interpret_heif_file_sequences(); + if (err) { + return err; + } + } + + return Error::Ok; +} + + +Error HeifContext::interpret_heif_file_images() +{ m_all_images.clear(); m_top_level_images.clear(); m_primary_image.reset(); @@ -349,34 +627,46 @@ continue; } - auto image = ImageItem::alloc_for_infe_box(this, infe_box); - if (!image) { - // It is no image item, skip it. + auto imageItem = ImageItem::alloc_for_infe_box(this, infe_box); + if (!imageItem) { + // It is no imageItem item, skip it. continue; } - m_all_images.insert(std::make_pair(id, image)); - - if (!infe_box->is_hidden_item()) { - if (id == m_heif_file->get_primary_image_ID()) { - image->set_primary(true); - m_primary_image = image; - } - - m_top_level_images.push_back(image); - } - std::vector> properties; Error err = m_heif_file->get_properties(id, properties); if (err) { - return err; + imageItem = std::make_shared(this, imageItem->get_infe_type(), id, err); } - image->set_properties(properties); + imageItem->set_properties(properties); - err = image->on_load_file(); + err = imageItem->initialize_decoder(); if (err) { - return err; + imageItem = std::make_shared(this, imageItem->get_infe_type(), id, err); + imageItem->set_properties(properties); + } else { + // The decoder's input data extent must be set before any codec-config + // query: some decoders (e.g. JPEG, whose jpgC box is optional) read the + // actual bitstream to answer colorspace/bit-depth queries. + imageItem->set_decoder_input_data(); + + // After initialize_decoder, codec-config queries (colorspace, bit depth) + // are available, so visual-codec items can now populate their component + // descriptions. Idempotent for items already populated by set_properties + // (e.g. unci items). + imageItem->populate_component_descriptions(); + } + + m_all_images.insert(std::make_pair(id, imageItem)); + + if (!infe_box->is_hidden_item()) { + if (id == m_heif_file->get_primary_image_ID()) { + imageItem->set_primary(true); + m_primary_image = imageItem; + } + + m_top_level_images.push_back(imageItem); } } @@ -417,6 +707,19 @@ } + // --- Are there any `rref` reference types that we do not process. + // This only makes the affected item undecodable; other items in the file + // can still be decoded, so we do not abort the whole load. + + auto rrefBox = m_heif_file->get_property_for_item(pair.first); + if (rrefBox) { + if (Error err = rrefBox->reference_types_supported_error()) { + image->set_item_error(err); + continue; + } + } + + // --- Are there any parse errors in optional properties? Attach the errors as warnings to the images. bool ignore_nonfatal_parse_errors = false; // TODO: this should be a user option. Where should we put this (heif_decoding_options, or while creating the context) ? @@ -445,9 +748,10 @@ uint32_t width = ispe->get_width(); uint32_t height = ispe->get_height(); - Error sizeError = check_for_valid_image_size(get_security_limits(), width, height); - if (sizeError) { - return sizeError; + if (width == 0 || height == 0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_image_size, + "Zero image width or height"}; } image->set_resolution(width, height); @@ -491,8 +795,16 @@ for (const auto& prop : properties) { auto clap = std::dynamic_pointer_cast(prop); if (clap) { - image->set_resolution(clap->get_width_rounded(), - clap->get_height_rounded()); + int clap_width = clap->get_width_rounded(); + int clap_height = clap->get_height_rounded(); + if (clap_width <= 0 || clap_height <= 0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_clean_aperture, + "Clean aperture (clap) reduces image to zero size"}; + } + + image->set_resolution(static_cast(clap_width), + static_cast(clap_height)); if (image->has_intrinsic_matrix()) { image->get_intrinsic_matrix().apply_clap(clap.get(), image->get_width(), image->get_height()); @@ -524,6 +836,18 @@ // TODO: apply irot to camera extrinsic matrix } } + + + // --- assign GIMI content-ID to image + + if (auto box_gimi_content_id = image->get_property()) { + image->set_gimi_sample_content_id(box_gimi_content_id->get_content_id()); + } + + // add image projection information + if (auto prfr = image->get_property()) { + image->ImageDescription::set_omaf_image_projection(prfr->get_omaf_image_projection()); + } } @@ -731,6 +1055,7 @@ "No vvcC property in vvc1 type image"); } } + // TODO: check for AV1, AVC, JPEG, J2K } @@ -772,8 +1097,8 @@ image->set_color_profile(tile_img->get_color_profile_icc()); } - if (image->get_color_profile_nclx() == nullptr && tile_img->get_color_profile_nclx()) { - image->set_color_profile(tile_img->get_color_profile_nclx()); + if (!image->has_nclx_color_profile() && tile_img->has_nclx_color_profile()) { + image->set_color_profile_nclx(tile_img->get_color_profile_nclx()); } } } @@ -801,18 +1126,37 @@ metadata->content_type = content_type; metadata->item_uri_type = std::move(item_uri_type); - Error err = m_heif_file->get_uncompressed_item_data(id, &(metadata->m_data)); - if (err) { + auto metadataResult = m_heif_file->get_uncompressed_item_data(id); + if (!metadataResult) { + if (metadataResult.error().error_code == heif_error_Unsupported_feature) { + // The item uses a content_encoding that we cannot decode (unknown coding, or the + // decompressor was not compiled in). That is a limitation of this one item, not of + // the file: skip it instead of refusing the whole file. Its raw data stays accessible + // through heif_item_get_item_data(). + continue; + } + if (item_type == fourcc("Exif") || item_type == fourcc("mime")) { // these item types should have data - return err; + return metadataResult.error(); } else { // anything else is probably something that we don't understand yet continue; } } + else { + metadata->m_data = *metadataResult; + // Account the (possibly decompressed) metadata against the total-memory budget + // for the lifetime of the context. This bounds the cumulative memory of up to + // max_items metadata items, which would otherwise bypass the security limits + // (GHSA-24wx-9w62-c96w). + if (Error memErr = metadata->m_memory_handle.alloc(metadata->m_data.size(), &m_limits, + "decompressed item metadata")) { + return memErr; + } + } // --- assign metadata to the image @@ -865,12 +1209,13 @@ std::shared_ptr region_item = std::make_shared(); region_item->item_id = id; - std::vector region_data; - Error err = m_heif_file->get_uncompressed_item_data(id, ®ion_data); - if (err) { - return err; + + Result regionDataResult = m_heif_file->get_uncompressed_item_data(id); + if (!regionDataResult) { + return regionDataResult.error(); } - region_item->parse(region_data); + region_item->parse(*regionDataResult, get_security_limits()); + if (iref_box) { std::vector references = iref_box->get_references_from(id); for (const auto& ref : references) { @@ -934,6 +1279,48 @@ } } + // --- read text item and assign to image(s) + for (heif_item_id id : image_IDs) { + uint32_t item_type = m_heif_file->get_item_type_4cc(id); + if (item_type != fourcc("mime")) { // TODO: && content_type starts with "text/" ? + continue; + } + std::shared_ptr text_item = std::make_shared(); + text_item->set_item_id(id); + + auto textDataResult = m_heif_file->get_uncompressed_item_data(id); + if (!textDataResult) { + if (textDataResult.error().error_code == heif_error_Unsupported_feature) { + // content_encoding that we cannot decode: this is not a text item we can use, but + // that is no reason to reject the file (see the metadata loop above) + continue; + } + + return textDataResult.error(); + } + + text_item->parse(*textDataResult); + if (iref_box) { + std::vector references = iref_box->get_references_from(id); + for (const auto& ref : references) { + if (ref.header.get_short_type() == fourcc("text")) { + std::vector refs = ref.to_item_ID; + for (uint32_t ref : refs) { + uint32_t image_id = ref; + auto img_iter = m_all_images.find(image_id); + if (img_iter == m_all_images.end()) { + return Error(heif_error_Invalid_input, + heif_suberror_Nonexisting_item_referenced, + "Text item assigned to non-existing image"); + } + img_iter->second->add_text_item_id(id); + m_text_items.push_back(text_item); + } + } + } + } + } + return Error::Ok; } @@ -976,14 +1363,14 @@ uint32_t image_type = m_heif_file->get_item_type_4cc(ID); if (image_type == fourcc("grid")) { - std::vector grid_data; - Error error = m_heif_file->get_uncompressed_item_data(ID, &grid_data); - if (error) { + + Result gridDataResult = m_heif_file->get_uncompressed_item_data(ID); + if (!gridDataResult) { return false; } ImageGrid grid; - err = grid.parse(grid_data); + err = grid.parse(*gridDataResult); if (err) { return false; } @@ -1034,47 +1421,54 @@ } -Error HeifContext::get_id_of_non_virtual_child_image(heif_item_id id, heif_item_id& out) const +Result HeifContext::find_first_coded_image_id(heif_item_id id) const { - uint32_t image_type = m_heif_file->get_item_type_4cc(id); - if (image_type == fourcc("grid") || - image_type == fourcc("iden") || - image_type == fourcc("iovl")) { - auto iref_box = m_heif_file->get_iref_box(); - if (!iref_box) { + std::set visited; + + for (;;) { + if (!visited.insert(id).second) { return Error(heif_error_Invalid_input, heif_suberror_No_item_data, - "Derived image does not reference any other image items"); + "Derived image references form a cycle"); } - std::vector image_references = iref_box->get_references(id, fourcc("dimg")); + uint32_t image_type = m_heif_file->get_item_type_4cc(id); + if (image_type == fourcc("grid") || + image_type == fourcc("iden") || + image_type == fourcc("iovl")) { + auto iref_box = m_heif_file->get_iref_box(); + if (!iref_box) { + return Error(heif_error_Invalid_input, + heif_suberror_No_item_data, + "Derived image does not reference any other image items"); + } - // TODO: check whether this really can be recursive (e.g. overlay of grid images) + std::vector image_references = iref_box->get_references(id, fourcc("dimg")); - if (image_references.empty() || image_references[0] == id) { - return Error(heif_error_Invalid_input, - heif_suberror_No_item_data, - "Derived image does not reference any other image items"); + if (image_references.empty()) { + return Error(heif_error_Invalid_input, + heif_suberror_No_item_data, + "Derived image does not reference any other image items"); + } + + // follow the first reference + id = image_references[0]; } else { - return get_id_of_non_virtual_child_image(image_references[0], out); - } - } - else { - if (m_all_images.find(id) == m_all_images.end()) { - std::stringstream sstr; - sstr << "Image item " << id << " referenced, but it does not exist\n"; + if (!m_all_images.contains(id)) { + std::stringstream sstr; + sstr << "Image item " << id << " referenced, but it does not exist\n"; - return Error(heif_error_Invalid_input, - heif_suberror_Nonexisting_item_referenced, - sstr.str()); - } - else if (dynamic_cast(m_all_images.find(id)->second.get())) { - // Should er return an error here or leave it to the follow-up code to detect that? - } + return Error(heif_error_Invalid_input, + heif_suberror_Nonexisting_item_referenced, + sstr.str()); + } + else if (dynamic_cast(m_all_images.find(id)->second.get())) { + // Should we return an error here or leave it to the follow-up code to detect that? + } - out = id; - return Error::Ok; + return id; + } } } @@ -1082,11 +1476,12 @@ Result> HeifContext::decode_image(heif_item_id ID, heif_colorspace out_colorspace, heif_chroma out_chroma, - const struct heif_decoding_options& options, - bool decode_only_tile, uint32_t tx, uint32_t ty) const + const heif_decoding_options& options, + bool decode_only_tile, uint32_t tx, uint32_t ty, + std::set processed_ids) const { std::shared_ptr imgitem; - if (m_all_images.find(ID) != m_all_images.end()) { + if (m_all_images.contains(ID)) { imgitem = m_all_images.find(ID)->second; } @@ -1095,17 +1490,112 @@ return Error(heif_error_Invalid_input, heif_suberror_Nonexisting_item_referenced); } - - auto decodingResult = imgitem->decode_image(options, decode_only_tile, tx, ty); - if (decodingResult.error) { - return decodingResult.error; + // Reject un-decodable inputs before any decoding starts. In particular, a + // cycle in the decode reference graph ('dimg' inputs or the alpha 'auxl' + // edge) would let two parallel grid-tile workers take two item mutexes in + // opposite order and deadlock (GHSA-prgh-72vc-3xmc). Checked once here, at the + // single top-level decode entry, so the recursion below can assume an acyclic + // graph. + if (Error err = imgitem->verify_decodable()) { + return err; } - std::shared_ptr img = decodingResult.value; + // Seed the traversal state for this top-level decode. The cycle-detection set + // is carried over from the caller; the amplification budget (shared across all + // recursion branches and parallel tile-decode threads) is created here, once. + // (GHSA-x8xm-cm2c-cfc8) + DecodeTraversalState decode_state; + decode_state.processed_ids = std::move(processed_ids); + + const heif_security_limits* limits = get_security_limits(); + if (limits && limits->max_items != 0) { + // Bound total sub-image decodes at a modest multiple of max_items. A + // well-formed file decodes each of its (<= max_items) items a small number + // of times, so this only trips on reference-amplification. Computed in 64 + // bit and clamped to avoid overflow when max_items is configured very high. + uint64_t budget = static_cast(limits->max_items) * MAX_DERIVED_IMAGE_DECODE_FACTOR; + decode_state.max_decodes = (budget > UINT32_MAX) ? UINT32_MAX : static_cast(budget); + decode_state.max_overlay_nesting = MAX_OVERLAY_NESTING_LEVEL; + decode_state.decode_count = std::make_shared>(0); + } + + auto decodingResult = imgitem->decode_image(options, decode_only_tile, tx, ty, decode_state); + if (!decodingResult) { + return decodingResult.error(); + } + + std::shared_ptr img = *decodingResult; + + // For visual codecs (HEVC/AVC/AVIF/JPEG/...) the decoder plugin builds the + // returned image via the public C API (heif_image_add_plane_safe), which + // auto-mints component ids. Reconcile those with the canonical description + // list that ImageItem::populate_component_descriptions() produced, so + // handle-side and decoded-image-side ids agree by construction. + // No-op for unci (which already shares ids with the item) or for items + // without a populated description list (grid/overlay/iden). + img->apply_descriptions_from(*imgitem); + // Note: the decoded image is validated against the signaled size inside + // ImageItem::decode_image() (via the per-item check_decoded_image_size()). // --- convert to output chroma format + auto img_result = convert_to_output_colorspace(img, out_colorspace, out_chroma, options); + if (!img_result) { + return img_result.error(); + } + else { + img = *img_result; + } + + img->add_warnings(imgitem->get_decoding_warnings()); + + return img; +} + + +bool nclx_color_profile_equal(std::optional a, + const heif_color_profile_nclx* b) +{ + if (!a && b==nullptr) { + return true; + } + + heif_color_profile_nclx* default_nclx = nullptr; + + if (!a || b==nullptr) { + default_nclx = heif_nclx_color_profile_alloc(); + + if (!a) { + a = nclx_profile::defaults(); + } + + if (b==nullptr) { + b = default_nclx; + } + } + + bool equal = true; + if (a->m_matrix_coefficients != b->matrix_coefficients || + a->m_colour_primaries != b->color_primaries || + a->m_transfer_characteristics != b->transfer_characteristics || + a->m_full_range_flag != b->full_range_flag) { + equal = false; + } + + if (default_nclx) { + heif_nclx_color_profile_free(default_nclx); + } + + return equal; +} + + +Result> HeifContext::convert_to_output_colorspace(std::shared_ptr img, + heif_colorspace out_colorspace, + heif_chroma out_chroma, + const heif_decoding_options& options) const +{ heif_colorspace target_colorspace = (out_colorspace == heif_colorspace_undefined ? img->get_colorspace() : out_colorspace); @@ -1116,30 +1606,47 @@ bool different_chroma = (target_chroma != img->get_chroma_format()); bool different_colorspace = (target_colorspace != img->get_colorspace()); - uint8_t img_bpp = img->get_visual_image_bits_per_pixel(); + uint16_t img_bpp = img->get_visual_image_bits_per_pixel(); uint8_t converted_output_bpp = (options.convert_hdr_to_8bit && img_bpp > 8) ? 8 : 0 /* keep input depth */; + nclx_profile img_nclx = img->get_color_profile_nclx_with_fallback(); + const bool nclx_passthrough = (options.output_image_nclx_profile == nullptr && + options.output_image_nclx_profile_passthrough); + const bool different_nclx = !nclx_passthrough && + !nclx_color_profile_equal(img_nclx, options.output_image_nclx_profile); + if (different_chroma || different_colorspace || - converted_output_bpp) { - - auto img_result = convert_colorspace(img, target_colorspace, target_chroma, nullptr, - converted_output_bpp, options.color_conversion_options, get_security_limits()); - if (img_result.error) { - return img_result.error; + converted_output_bpp || + different_nclx || + (img->has_alpha() && options.color_conversion_options_ext && options.color_conversion_options_ext->alpha_composition_mode != heif_alpha_composition_mode_none)) { + + nclx_profile output_profile; + if (options.output_image_nclx_profile) { + output_profile.set_matrix_coefficients(options.output_image_nclx_profile->matrix_coefficients); + output_profile.set_colour_primaries(options.output_image_nclx_profile->color_primaries); + output_profile.set_full_range_flag(options.output_image_nclx_profile->full_range_flag); + } + else if (nclx_passthrough) { + // Keep input image's NCLX as the conversion target so a chroma/colorspace + // change does not silently re-tag the pixels with sRGB. + output_profile = img_nclx; } else { - img = *img_result; + output_profile.set_sRGB_defaults(); } - } - - img->add_warnings(imgitem->get_decoding_warnings()); - return img; + return convert_colorspace(img, target_colorspace, target_chroma, output_profile, converted_output_bpp, + options.color_conversion_options, options.color_conversion_options_ext, + get_security_limits()); + } + else { + return img; + } } -static Result> +Result> create_alpha_image_from_image_alpha_channel(const std::shared_ptr& image, const heif_security_limits* limits) { @@ -1150,7 +1657,7 @@ heif_colorspace_monochrome, heif_chroma_monochrome); if (image->has_channel(heif_channel_Alpha)) { - alpha_image->copy_new_plane_from(image, heif_channel_Alpha, heif_channel_Y, limits); + alpha_image->copy_new_channel_from(image, heif_channel_Alpha, heif_channel_Y, limits); } else if (image->get_chroma_format() == heif_chroma_interleaved_RGBA) { if (auto err = alpha_image->extract_alpha_from_RGBA(image, limits)) { @@ -1161,9 +1668,8 @@ // --- set nclx profile with full-range flag - auto nclx = std::make_shared(); - nclx->set_undefined(); - nclx->set_full_range_flag(true); // this is the default, but just to be sure in case the defaults change + nclx_profile nclx = nclx_profile::undefined(); + nclx.set_full_range_flag(true); // this is the default, but just to be sure in case the defaults change alpha_image->set_color_profile_nclx(nclx); return alpha_image; @@ -1171,9 +1677,9 @@ Result> HeifContext::encode_image(const std::shared_ptr& pixel_image, - struct heif_encoder* encoder, - const struct heif_encoding_options& in_options, - enum heif_image_input_class input_class) + heif_encoder* encoder, + const heif_encoding_options& in_options, + heif_image_input_class input_class) { std::shared_ptr output_image_item = ImageItem::alloc_for_compression_format(this, encoder->plugin->compression_format); @@ -1204,19 +1710,28 @@ heif_encoding_options options = in_options; - if (const auto* nclx = output_image_item->get_forced_output_nclx()) { - options.output_nclx_profile = const_cast(nclx); - } + std::shared_ptr colorConvertedImage; - Result> srcImageResult = output_image_item->convert_colorspace_for_encoding(pixel_image, - encoder, - options); - if (srcImageResult.error) { - return srcImageResult.error; - } + if (output_image_item->get_encoder()) { + if (const auto* nclx = output_image_item->get_encoder()->get_forced_output_nclx()) { + options.output_nclx_profile = const_cast(nclx); + } - std::shared_ptr colorConvertedImage = srcImageResult.value; + Result> srcImageResult; + srcImageResult = output_image_item->get_encoder()->convert_colorspace_for_encoding(pixel_image, + encoder, + options.output_nclx_profile, + &options.color_conversion_options, + get_security_limits()); + if (!srcImageResult) { + return srcImageResult.error(); + } + colorConvertedImage = *srcImageResult; + } + else { + colorConvertedImage = pixel_image; + } Error err = output_image_item->encode_to_item(this, colorConvertedImage, @@ -1240,24 +1755,34 @@ std::shared_ptr alpha_image; auto alpha_image_result = create_alpha_image_from_image_alpha_channel(colorConvertedImage, get_security_limits()); if (!alpha_image_result) { - return alpha_image_result.error; + return alpha_image_result.error(); } alpha_image = *alpha_image_result; - // --- encode the alpha image + // --- encode the alpha image using a fresh encoder instance to avoid + // leaking codec state from the color encoding pass - auto alphaEncodingResult = encode_image(alpha_image, encoder, options, + heif_encoder alpha_enc(encoder->plugin); + heif_error alloc_err = alpha_enc.alloc(); + if (alloc_err.code) { + return Error(alloc_err.code, alloc_err.subcode, alloc_err.message); + } + alpha_enc.copy_parameters_from(*encoder); + + auto alphaEncodingResult = encode_image(alpha_image, &alpha_enc, options, heif_image_input_class_alpha); - if (alphaEncodingResult.error) { - return alphaEncodingResult.error; + if (!alphaEncodingResult) { + return alphaEncodingResult.error(); } std::shared_ptr heif_alpha_image = *alphaEncodingResult; m_heif_file->add_iref_reference(heif_alpha_image->get_id(), fourcc("auxl"), {output_image_item->get_id()}); - m_heif_file->set_auxC_property(heif_alpha_image->get_id(), output_image_item->get_auxC_alpha_channel_type()); + if (Error err = m_heif_file->set_auxC_property(heif_alpha_image->get_id(), output_image_item->get_auxC_alpha_channel_type())) { + return err; + } if (pixel_image->is_premultiplied_alpha()) { m_heif_file->add_iref_reference(output_image_item->get_id(), fourcc("prem"), {heif_alpha_image->get_id()}); @@ -1271,8 +1796,8 @@ } output_image_item->set_properties(properties); - m_heif_file->set_brand(encoder->plugin->compression_format, - output_image_item->is_miaf_compatible()); + //m_heif_file->set_brand(encoder->plugin->compression_format, + // output_image_item->is_miaf_compatible()); return output_image_item; } @@ -1307,8 +1832,8 @@ Result> HeifContext::encode_thumbnail(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, + heif_encoder* encoder, + const heif_encoding_options& options, int bbox_size) { int orig_width = image->get_width(); @@ -1346,8 +1871,8 @@ auto encodingResult = encode_image(thumbnail_image, encoder, options, heif_image_input_class_thumbnail); - if (encodingResult.error) { - return encodingResult.error; + if (!encodingResult) { + return encodingResult.error(); } return *encodingResult; @@ -1400,7 +1925,11 @@ { // create an infe box describing what kind of data we are storing (this also creates a new ID) - auto metadata_infe_box = m_heif_file->add_new_infe_box(item_type); + auto infe_result = m_heif_file->add_new_infe_box(item_type); + if (!infe_result) { + return infe_result.error(); + } + auto metadata_infe_box = *infe_result; metadata_infe_box->set_hidden_item(true); if (content_type != nullptr) { metadata_infe_box->set_content_type(content_type); @@ -1465,9 +1994,31 @@ } -heif_property_id HeifContext::add_property(heif_item_id targetItem, std::shared_ptr property, bool essential) +Result HeifContext::add_property(heif_item_id targetItem, const std::shared_ptr& property, bool essential) { - heif_property_id id = m_heif_file->add_property(targetItem, property, essential); + // Like writing, adding properties is only implemented for contexts that were built in memory. + // In a context read from a file, the item data still lives in the input file and the context + // cannot be written out again (see HeifContext::write()). + if (m_heif_file->get_reader()) { + return Error(heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Adding a property to a context that was read from a file is not supported"); + } + + heif_property_id id; + + if (auto img = get_image(targetItem, false)) { + id = img->add_property(property, essential); + } + else { + id = m_heif_file->add_property(targetItem, property, essential); + } + + if (id == 0) { + return Error(heif_error_Encoding_error, + heif_suberror_Unspecified, + "Cannot add property to item"); + } return id; } @@ -1547,7 +2098,11 @@ ids.push_back(layer_item->get_id()); } - heif_item_id group_id = m_heif_file->get_unused_item_id(); + auto groupIdResult = m_heif_file->get_id_creator().get_new_id(IDCreator::Namespace::entity_group); + if (!groupIdResult) { + return groupIdResult.error(); + } + heif_item_id group_id = *groupIdResult; pymd->set_group_id(group_id); pymd->set_layers((uint16_t)tile_w, (uint16_t)tile_h, layers, ids); @@ -1562,3 +2117,234 @@ return {group_id}; } + + +Result HeifContext::add_text_property(heif_item_id itemId, const std::string& language) +{ + if (find_property(itemId)) { + return Error{ + heif_error_Usage_error, + heif_suberror_Unspecified, + "Item already has an 'elng' language property." + }; + } + + auto elng = std::make_shared(); + elng->set_lang(std::string(language)); + + return add_property(itemId, elng, false); +} + + +Error HeifContext::interpret_heif_file_sequences() +{ + m_tracks.clear(); + + + // --- reference all non-hidden images + + auto moov = m_heif_file->get_moov_box(); + assert(moov); + + auto mvhd = moov->get_child_box(); + if (!mvhd) { + assert(false); // TODO + } + + auto tracks = moov->get_child_boxes(); + for (const auto& track_box : tracks) { + auto trackResult = Track::alloc_track(this, track_box); + bool skip_track = false; + + if (auto err = trackResult.error()) { + if (err.error_code == heif_error_Unsupported_feature && + err.sub_error_code == heif_suberror_Unsupported_track_type) { + // ignore error, skip track + skip_track = true; + } + else { + return trackResult.error(); + } + } + + if (!skip_track) { + assert(*trackResult); + auto track = *trackResult; + m_tracks.insert({track->get_id(), track}); + + if (track->is_visual_track() && m_visual_track_id == 0) { + m_visual_track_id = track->get_id(); + } + } + } + + // --- post-parsing initialization + + std::vector> all_tracks; + all_tracks.reserve(m_tracks.size()); + for (auto& track : m_tracks) { + all_tracks.push_back(track.second); + } + + for (auto& track : m_tracks) { + Error err = track.second->initialize_after_parsing(this, all_tracks); + if (err) { + return err; + } + } + + return Error::Ok; +} + + +std::vector HeifContext::get_track_IDs() const +{ + std::vector ids; + + ids.reserve(m_tracks.size()); + for (const auto& track : m_tracks) { + ids.push_back(track.first); + } + + return ids; +} + + +Result> HeifContext::get_track(uint32_t track_id) +{ + // The caller is expected to have confirmed (via has_sequence()) that there are + // sequence tracks before requesting one. Guard against an empty track map anyway, + // since this is reachable through the public API (e.g. on a still image file). + if (!has_sequence()) { + return Error{heif_error_Usage_error, + heif_suberror_Unspecified, + "File contains no sequence tracks"}; + } + + if (track_id != 0) { + auto iter = m_tracks.find(track_id); + if (iter == m_tracks.end()) { + return Error{heif_error_Usage_error, + heif_suberror_Unspecified, + "Invalid track id"}; + } + + return iter->second; + } + + if (m_visual_track_id != 0) { + return m_tracks[m_visual_track_id]; + } + + return m_tracks.begin()->second; +} + + +Result> HeifContext::get_track(uint32_t track_id) const +{ + auto result = const_cast(this)->get_track(track_id); + if (!result) { + return result.error(); + } + else { + Result> my_result(*result); + return my_result; + } +} + + +uint32_t HeifContext::get_sequence_timescale() const +{ + auto mvhd = m_heif_file->get_mvhd_box(); + if (!mvhd) { + return 0; + } + + return mvhd->get_time_scale(); +} + + +void HeifContext::set_sequence_timescale(uint32_t timescale) +{ + get_heif_file()->init_for_sequence(); + + auto mvhd = m_heif_file->get_mvhd_box(); + + /* unnecessary, since mvhd duration is set during writing + + uint32_t old_timescale = mvhd->get_time_scale(); + if (old_timescale != 0) { + uint64_t scaled_duration = mvhd->get_duration() * timescale / old_timescale; + mvhd->set_duration(scaled_duration); + } + */ + + mvhd->set_time_scale(timescale); +} + + +void HeifContext::set_number_of_sequence_repetitions(uint32_t repetitions) +{ + m_sequence_repetitions = repetitions; +} + + +uint64_t HeifContext::get_sequence_duration() const +{ + auto mvhd = m_heif_file->get_mvhd_box(); + if (!mvhd) { + return 0; + } + + return mvhd->get_duration(); +} + + +bool HeifContext::is_sequence_duration_indefinite() const +{ + auto mvhd = m_heif_file->get_mvhd_box(); + if (!mvhd) { + return false; + } + + return mvhd->is_duration_indefinite(); +} + + +Result> HeifContext::add_visual_sequence_track(const TrackOptions* options, + uint32_t handler_type, + uint16_t width, uint16_t height) +{ + m_heif_file->init_for_sequence(); + + std::shared_ptr trak = std::make_shared(this, 0, width, height, options, handler_type); + m_tracks.insert({trak->get_id(), trak}); + + return trak; +} + + +Result> HeifContext::add_uri_metadata_sequence_track(const TrackOptions* options, + const std::string& uri) +{ + m_heif_file->init_for_sequence(); + + std::shared_ptr trak = std::make_shared(this, 0, uri, options); + m_tracks.insert({trak->get_id(), trak}); + + return trak; +} + +Result> HeifContext::add_text_item(const char* content_type, const char* text) +{ + auto boxResult = m_heif_file->add_new_infe_box(fourcc("mime")); + if (!boxResult) { + return boxResult.error(); + } + auto box = *boxResult; + box->set_hidden_item(true); + box->set_content_type(std::string(content_type)); + auto textItem = std::make_shared(box->get_item_ID(), text); + add_text_item(textItem); + return textItem; +} diff -Nru libheif-1.19.8/libheif/context.h libheif-1.23.4/libheif/context.h --- libheif-1.19.8/libheif/context.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/context.h 2026-09-06 14:45:17.000000000 +0000 @@ -29,6 +29,7 @@ #include #include "error.h" +#include "security_limits.h" #include "libheif/heif.h" #include "libheif/heif_experimental.h" @@ -36,9 +37,11 @@ #include "bitstream.h" #include "box.h" // only for color_profile, TODO: maybe move the color_profiles to its own header - +#include "file.h" #include "region.h" +#include "text.h" + class HeifFile; class HeifPixelImage; @@ -47,6 +50,15 @@ class ImageItem; +class Track; + +struct TrackOptions; + + +Result> +create_alpha_image_from_image_alpha_channel(const std::shared_ptr& image, + const heif_security_limits* limits); + // This is a higher-level view than HeifFile. // Images are grouped logically into main images and their thumbnails. @@ -58,6 +70,8 @@ ~HeifContext(); + static constexpr int default_max_decoding_threads = 4; + void set_max_decoding_threads(int max_threads) { m_max_decoding_threads = max_threads; } int get_max_decoding_threads() const { return m_max_decoding_threads; } @@ -77,6 +91,12 @@ std::shared_ptr get_heif_file() const { return m_heif_file; } + void set_unif(bool flag); + + bool get_unif() const; + + IDCreator& get_id_creator(); + // === image items === std::vector> get_top_level_images(bool return_error_images); @@ -94,6 +114,8 @@ std::shared_ptr get_primary_image(bool return_error_image); + std::shared_ptr get_primary_image(bool return_error_image) const; + bool is_image(heif_item_id ID) const; bool has_alpha(heif_item_id ID) const; @@ -101,17 +123,27 @@ Result> decode_image(heif_item_id ID, heif_colorspace out_colorspace, heif_chroma out_chroma, - const struct heif_decoding_options& options, - bool decode_only_tile, uint32_t tx, uint32_t ty) const; + const heif_decoding_options& options, + bool decode_only_tile, uint32_t tx, uint32_t ty, + std::set processed_ids) const; + + Result> convert_to_output_colorspace(std::shared_ptr img, + heif_colorspace out_colorspace, + heif_chroma out_chroma, + const heif_decoding_options& options) const; - Error get_id_of_non_virtual_child_image(heif_item_id in, heif_item_id& out) const; + Result find_first_coded_image_id(heif_item_id in) const; std::string debug_dump_boxes() const; + std::string debug_dump_item_data() const; + // === writing === - void write(StreamWriter& writer); + [[nodiscard]] Error write(StreamWriter& writer); + + void set_write_mini_format(bool enable); // Create all boxes necessary for an empty HEIF file. // Note that this is no valid HEIF file, since some boxes (e.g. pitm) are generated, but @@ -119,9 +151,9 @@ void reset_to_empty_heif(); Result> encode_image(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class); + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class); void set_primary_image(const std::shared_ptr& image); @@ -131,8 +163,8 @@ const std::shared_ptr& thumbnail_image); Result> encode_thumbnail(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, + heif_encoder* encoder, + const heif_encoding_options& options, int bbox_size); Error add_exif_metadata(const std::shared_ptr& master_image, const void* data, int size); @@ -143,10 +175,13 @@ uint32_t item_type, const char* content_type, const char* item_uri_type, heif_metadata_compression compression, heif_item_id* out_item_id); - heif_property_id add_property(heif_item_id targetItem, std::shared_ptr property, bool essential); + Result add_property(heif_item_id targetItem, const std::shared_ptr& property, bool essential); Result add_pyramid_group(const std::vector& layers); + Result add_text_property(heif_item_id, const std::string& language); + + // --- region items void add_region_item(std::shared_ptr region_item) @@ -154,7 +189,7 @@ m_region_items.push_back(std::move(region_item)); } - std::shared_ptr add_region_item(uint32_t reference_width, uint32_t reference_height); + Result> add_region_item(uint32_t reference_width, uint32_t reference_height); std::shared_ptr get_region_item(heif_item_id id) const { @@ -168,6 +203,103 @@ void add_region_referenced_mask_ref(heif_item_id region_item_id, heif_item_id mask_item_id); + + // === sequences == + + bool has_sequence() const { return !m_tracks.empty(); } + + int get_number_of_tracks() const { return static_cast(m_tracks.size()); } + + std::vector get_track_IDs() const; + + // If 0 is passed as track_id, the main visual track is returned (we assume that there is only one visual track). + Result> get_track(uint32_t track_id); + + Result> get_track(uint32_t track_id) const; + + uint32_t get_sequence_timescale() const; + + uint64_t get_sequence_duration() const; + + // Returns true if the mvhd box signals an "indefinite" / unknown duration. + // For such files, an editlist in repeat mode means "loop forever". + bool is_sequence_duration_indefinite() const; + + void set_sequence_timescale(uint32_t timescale); + + void set_number_of_sequence_repetitions(uint32_t repetitions); + + Result> add_visual_sequence_track(const TrackOptions*, uint32_t handler_type, + uint16_t width, uint16_t height); + + Result> add_uri_metadata_sequence_track(const TrackOptions*, const std::string& uri); + + void add_text_item(std::shared_ptr text_item) + { + m_text_items.push_back(std::move(text_item)); + } + + Result> add_text_item(const char* content_type, const char* text); + + std::shared_ptr get_text_item(heif_item_id id) const + { + for (auto& item : m_text_items) { + if (item->get_item_id() == id) + return item; + } + + return nullptr; + } + + template + Result> find_property(heif_item_id itemId, heif_property_id propertyId) + { + auto file = this->get_heif_file(); + + // For propertyId == 0, return the first property with this type. + if (propertyId == 0) { + return find_property(itemId); + } + + std::vector> properties; + Error err = file->get_properties(itemId, properties); + if (err) { + return err; + } + + if (propertyId - 1 >= properties.size()) { + return Error(heif_error_Usage_error, heif_suberror_Invalid_property, "property index out of range"); + } + + auto box = properties[propertyId - 1]; + auto box_casted = std::dynamic_pointer_cast(box); + if (!box_casted) { + return Error(heif_error_Usage_error, heif_suberror_Invalid_property, "wrong property type"); + } + + return box_casted; + } + + template + Result> find_property(heif_item_id itemId) { + auto file = this->get_heif_file(); + auto result = file->get_property_for_item(itemId); + if (!result) { + return Error(heif_error_Invalid_input, + heif_suberror_No_properties_assigned_to_item, + "property not found on item"); + } + return result; + } + + template + bool has_property(heif_item_id itemId) const + { + auto file = this->get_heif_file(); + auto result = file->get_property_for_item(itemId); + return result != nullptr; + } + private: std::map> m_all_images; @@ -179,14 +311,26 @@ std::shared_ptr m_heif_file; - int m_max_decoding_threads = 4; + int m_max_decoding_threads = default_max_decoding_threads; heif_security_limits m_limits; + TotalMemoryTracker m_memory_tracker; std::vector> m_region_items; + std::vector> m_text_items; + + // --- sequences + + std::map> m_tracks; + uint32_t m_visual_track_id = 0; + uint32_t m_sequence_repetitions = 1; Error interpret_heif_file(); + Error interpret_heif_file_images(); + + Error interpret_heif_file_sequences(); + void remove_top_level_image(const std::shared_ptr& image); }; diff -Nru libheif-1.19.8/libheif/error.cc libheif-1.23.4/libheif/error.cc --- libheif-1.19.8/libheif/error.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/error.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,6 +21,27 @@ #include "error.h" #include +#include + + +const heif_error heif_error_null_pointer_argument { + heif_error_Usage_error, + heif_suberror_Null_pointer_argument, + "NULL argument passed" +}; + +const heif_error heif_error_out_of_memory { + heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + "Out of memory" +}; + +const heif_error heif_error_internal_exception { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Internal error (unexpected C++ exception)" +}; + // static const char Error::kSuccess[] = "Success"; @@ -47,6 +68,44 @@ } +// Replacement for C++20 std::string::starts_with() +static bool starts_with(const std::string& str, const std::string& prefix) { + if (str.length() < prefix.length()) { + return false; + } + + return str.compare(0, prefix.size(), prefix) == 0; +} + + +Error Error::from_heif_error(const heif_error& c_error) +{ + // unpack the concatenated error message and extract the last part only + + const char* err_string = get_error_string(c_error.code); + const char* sub_err_string = get_error_string(c_error.subcode); + + std::string msg = c_error.message; + if (starts_with(msg, err_string)) { + msg = msg.substr(strlen(err_string)); + + if (starts_with(msg, ": ")) { + msg = msg.substr(2); + } + + if (starts_with(msg, sub_err_string)) { + msg = msg.substr(strlen(sub_err_string)); + + if (starts_with(msg, ": ")) { + msg = msg.substr(2); + } + } + } + + return {c_error.code, c_error.subcode, msg}; +} + + const char* Error::get_error_string(heif_error_code err) { switch (err) { @@ -76,6 +135,8 @@ return "Error while loading plugin"; case heif_error_Canceled: return "Canceled by user"; + case heif_error_End_of_sequence: + return "End of sequence"; } assert(false); @@ -178,6 +239,10 @@ return "Invalid JPEG 2000 codestream"; case heif_suberror_Decompression_invalid_data: return "Invalid data in generic compression inflation"; + case heif_suberror_No_moov_box: + return "No 'moov' box"; + case heif_suberror_NCLX_colr_VUI_mismatch: + return "colr box and bitstream colour signalling disagree"; case heif_suberror_No_icbr_box: return "No 'icbr' box"; case heif_suberror_Invalid_mini_box: @@ -230,6 +295,8 @@ return "Unsupported generic compression method"; case heif_suberror_Unsupported_essential_property: return "Unsupported essential item property"; + case heif_suberror_Unsupported_track_type: + return "Unsupported track type"; // --- Encoder_plugin_error -- diff -Nru libheif-1.19.8/libheif/error.h libheif-1.23.4/libheif/error.h --- libheif-1.19.8/libheif/error.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/error.h 2026-09-06 14:45:17.000000000 +0000 @@ -34,6 +34,16 @@ #include "libheif/heif.h" #include +#include +#include + + +extern const heif_error heif_error_null_pointer_argument; + +// Predefined errors with string-literal messages (static storage, no allocation). +// Safe to return from a std::bad_alloc handler, where the allocator must not be used. +extern const heif_error heif_error_out_of_memory; +extern const heif_error heif_error_internal_exception; class ErrorBuffer @@ -64,11 +74,14 @@ }; -class Error +// The clang static analyzer reports a garbage value assigned in the implicit copy constructor +// when an Error is copied out of the std::variant inside Result. All members have default +// initializers, so this is a false positive of its variant modeling. +class Error // NOLINT(clang-analyzer-core.uninitialized.Assign) { public: - enum heif_error_code error_code = heif_error_Ok; - enum heif_suberror_code sub_error_code = heif_suberror_Unspecified; + heif_error_code error_code = heif_error_Ok; + heif_suberror_code sub_error_code = heif_suberror_Unspecified; std::string message; Error(); @@ -77,6 +90,8 @@ heif_suberror_code sc = heif_suberror_Unspecified, const std::string& msg = ""); + static Error from_heif_error(const heif_error&); + static const Error Ok; static const Error InternalError; @@ -96,7 +111,10 @@ } } - operator bool() const { return error_code != heif_error_Ok; } + // 'explicit' so that an Error can only be tested in a boolean context (if/!/&&/...). + // Without it, `int x = err;` or `return err;` from an int function silently compile + // and yield 0/1 instead of the intended value. + explicit operator bool() const { return error_code != heif_error_Ok; } static const char* get_error_string(heif_error_code err); @@ -118,20 +136,57 @@ public: Result() = default; - Result(const T& v) : value(v), error(Error::Ok) {} + Result(T v) : m_data(std::move(v)) {} + + Result(const Error& e) : m_data(e) {} - Result(const Error& e) : error(e) {} + // True if the Result holds a value, false if it holds an error. + // 'explicit' so that the value can only be extracted with operator*: `id = result;` must + // not compile, because it would store the boolean (this bug shipped several times, see + // https://github.com/strukturag/libheif/pull/1885). + explicit operator bool() const { return std::holds_alternative(m_data); } - operator bool() const { return error.error_code == heif_error_Ok; } + //void set(const T& v) { m_data = v; } + // Pointer-like access for `r->member` + T* operator->() + { + assert(*this); // Uses the operator bool() above + return &std::get(m_data); + } + + // Dereference access for `*r` T& operator*() { - assert(error.error_code == heif_error_Ok); - return value; + assert(*this); + return std::get(m_data); + } + + [[nodiscard]] bool is_error() const { + return std::holds_alternative(m_data); + } + + // Accessor for the error, if it exists + [[nodiscard]] const Error& error() const + { + if (*this) { + return Error::Ok; + } + + return std::get(m_data); + } + + // Directly get the C error struct. + heif_error error_struct(ErrorBuffer* error_buffer) const + { + if (*this) { + return heif_error_success; + } + + return std::get(m_data).error_struct(error_buffer); } - T value{}; - Error error; + std::variant m_data; }; #endif diff -Nru libheif-1.19.8/libheif/file.cc libheif-1.23.4/libheif/file.cc --- libheif-1.19.8/libheif/file.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/file.cc 2026-09-06 14:45:17.000000000 +0000 @@ -23,12 +23,15 @@ #include "libheif/heif.h" #include "libheif/heif_properties.h" #include "compression.h" +#include "image-items/grid.h" #include "image-items/jpeg2000.h" #include "image-items/jpeg.h" +#include "image-items/overlay.h" #include "image-items/vvc.h" #include "codecs/avif_boxes.h" #include "codecs/hevc_boxes.h" -#include "codecs/uncompressed/unc_boxes.h" +#include "sequences/seq_boxes.h" +#include "mini.h" #include #include @@ -39,6 +42,8 @@ #include #include +#include "libheif/heif_cxx.h" + #if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER) #ifndef NOMINMAX @@ -56,6 +61,8 @@ // TODO: make this a decoder option #define STRICT_PARSING false +static const char* const too_many_properties_error = "Too many item properties in file to add another one"; + HeifFile::HeifFile() { @@ -68,6 +75,7 @@ { std::vector IDs; + IDs.reserve(m_infe_boxes.size()); for (const auto& infe : m_infe_boxes) { IDs.push_back(infe.second->get_item_ID()); } @@ -137,7 +145,23 @@ } Error error = parse_heif_file(); - return error; + if (error) { + return error; + } + + seed_id_creator(); + + return Error::Ok; +} + + +bool HeifFile::has_images() const +{ + if (!m_meta_box) { + return false; + } + + return m_hdlr_box && m_hdlr_box->get_handler_type() == fourcc("pict"); } @@ -147,122 +171,164 @@ m_top_level_boxes.clear(); m_ftyp_box = std::make_shared(); - m_hdlr_box = std::make_shared(); - m_meta_box = std::make_shared(); - m_ipco_box = std::make_shared(); - m_ipma_box = std::make_shared(); - m_iloc_box = std::make_shared(); - m_iinf_box = std::make_shared(); - m_iprp_box = std::make_shared(); - m_pitm_box = std::make_shared(); - - m_meta_box->append_child_box(m_hdlr_box); - m_meta_box->append_child_box(m_pitm_box); - m_meta_box->append_child_box(m_iloc_box); - m_meta_box->append_child_box(m_iinf_box); - m_meta_box->append_child_box(m_iprp_box); + m_top_level_boxes.push_back(m_ftyp_box); +} - m_iprp_box->append_child_box(m_ipco_box); - m_iprp_box->append_child_box(m_ipma_box); - m_infe_boxes.clear(); +void HeifFile::init_for_meta_item() +{ + if (!m_meta_box) { + m_meta_box = std::make_shared(); + m_top_level_boxes.push_back(m_meta_box); + } - m_top_level_boxes.push_back(m_ftyp_box); - m_top_level_boxes.push_back(m_meta_box); -#if ENABLE_EXPERIMENTAL_MINI_FORMAT - // TODO: do not create 'mini' box as we cannot write them yet. - // if we include it in the top_level_boxes, it will be written into every file. - //m_mini_box = std::make_shared(); - //m_top_level_boxes.push_back(m_mini_box); -#endif + if (!m_hdlr_box) { + m_hdlr_box = std::make_shared(); + m_hdlr_box->set_handler_type(fourcc("null")); + m_meta_box->append_child_box(m_hdlr_box); + } + + if (!m_iloc_box) { + m_iloc_box = std::make_shared(); + m_meta_box->append_child_box(m_iloc_box); + } + + if (!m_iinf_box) { + m_iinf_box = std::make_shared(); + m_meta_box->append_child_box(m_iinf_box); + } } -void HeifFile::set_brand(heif_compression_format format, bool miaf_compatible) +void HeifFile::init_for_image() { - // Note: major brand should be repeated in the compatible brands, according to this: - // ISOBMFF (ISO/IEC 14496-12:2020) § K.4: - // NOTE This document requires that the major brand be repeated in the compatible-brands, - // but this requirement is relaxed in the 'profiles' parameter for compactness. - // See https://github.com/strukturag/libheif/issues/478 - - switch (format) { - case heif_compression_HEVC: - m_ftyp_box->set_major_brand(heif_brand2_heic); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(heif_brand2_mif1); - m_ftyp_box->add_compatible_brand(heif_brand2_heic); - break; + init_for_meta_item(); - case heif_compression_AV1: - m_ftyp_box->set_major_brand(heif_brand2_avif); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(heif_brand2_avif); - m_ftyp_box->add_compatible_brand(heif_brand2_mif1); - break; + // Upgrade handler from "null" to "pict" if needed + if (m_hdlr_box->get_handler_type() == fourcc("null")) { + m_hdlr_box->set_handler_type(fourcc("pict")); + } - case heif_compression_VVC: - m_ftyp_box->set_major_brand(heif_brand2_vvic); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(heif_brand2_mif1); - m_ftyp_box->add_compatible_brand(heif_brand2_vvic); - break; + if (!m_pitm_box) { + m_pitm_box = std::make_shared(); + m_meta_box->append_child_box(m_pitm_box); + } - case heif_compression_JPEG: - m_ftyp_box->set_major_brand(heif_brand2_jpeg); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(heif_brand2_jpeg); - m_ftyp_box->add_compatible_brand(heif_brand2_mif1); - break; + init_for_item_properties(); +} - case heif_compression_uncompressed: - // Not clear what the correct major brand should be - m_ftyp_box->set_major_brand(heif_brand2_mif2); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(heif_brand2_mif1); - break; - case heif_compression_JPEG2000: - case heif_compression_HTJ2K: - m_ftyp_box->set_major_brand(fourcc("j2ki")); - m_ftyp_box->set_minor_version(0); - m_ftyp_box->add_compatible_brand(fourcc("mif1")); - m_ftyp_box->add_compatible_brand(fourcc("j2ki")); - break; +void HeifFile::init_for_item_properties() +{ + init_for_meta_item(); - default: - break; + if (!m_iprp_box) { + m_iprp_box = std::make_shared(); + m_meta_box->append_child_box(m_iprp_box); + } + + if (!m_ipco_box) { + m_ipco_box = std::make_shared(); + m_iprp_box->append_child_box(m_ipco_box); } - if (miaf_compatible) { - m_ftyp_box->add_compatible_brand(heif_brand2_miaf); + if (!m_ipma_box) { + m_ipma_box = std::make_shared(); + m_iprp_box->append_child_box(m_ipma_box); } +} -#if 0 - // Temporarily disabled, pending resolution of - // https://github.com/strukturag/libheif/issues/888 - if (get_num_images() == 1) { - // This could be overly conservative, but is safe - m_ftyp_box->add_compatible_brand(heif_brand2_1pic); + +void HeifFile::init_for_sequence() +{ + if (m_moov_box) { + return; + } + + m_moov_box = std::make_shared(); + m_top_level_boxes.push_back(m_moov_box); + + m_mvhd_box = std::make_shared(); + m_moov_box->append_child_box(m_mvhd_box); +} + + +size_t HeifFile::append_mdat_data(const std::vector& data) +{ + if (!m_mdat_data) { + m_mdat_data = std::make_unique(); + } + + return m_mdat_data->append_data(data); +} + + +void HeifFile::derive_box_versions() +{ + for (auto& box : m_top_level_boxes) { + box->derive_box_version_recursive(); } -#endif } void HeifFile::write(StreamWriter& writer) { + if (m_write_mini_format) { + std::string reason; + if (Box_mini::can_convert_to_mini(this, reason)) { + auto mini = Box_mini::create_from_heif_file(this); + if (mini) { + // Adjust ftyp for mini format + auto ftyp = get_ftyp_box(); + + // Determine codec brand from primary item type + uint32_t item_type = get_item_type_4cc(get_primary_image_ID()); + heif_brand2 codec_brand = 0; + if (item_type == fourcc("av01")) { + codec_brand = heif_brand2_avif; + } + else if (item_type == fourcc("hvc1")) { + codec_brand = heif_brand2_heic; + } + + ftyp->set_major_brand(fourcc("mif3")); + ftyp->set_minor_version(codec_brand); + ftyp->clear_compatible_brands(); + + // Write ftyp + mini (no mdat needed) + ftyp->write(writer); + mini->write(writer); + return; + } + } + // Fall through to normal write if conversion fails + } + for (auto& box : m_top_level_boxes) { -#if ENABLE_EXPERIMENTAL_MINI_FORMAT if (box == nullptr) { // Either mini or meta will be null, just ignore that one continue; } -#endif - box->derive_box_version_recursive(); - box->write(writer); + Error err = box->write(writer); + (void)err; // TODO: error ? } - m_iloc_box->write_mdat_after_iloc(writer); + if (m_iloc_box) { + // TODO: rewrite to use MdatData class + Error err = m_iloc_box->write_mdat_after_iloc(writer); + (void)err; // TODO: error ? + } + + if (m_mdat_data) { + Result mdatResult = write_mdat(writer); + if (!mdatResult) { + return; // TODO: error ? + } + + for (auto& box : m_top_level_boxes) { + box->patch_file_pointers_recursively(writer, *mdatResult); + } + } } @@ -273,12 +339,10 @@ bool first = true; for (const auto& box : m_top_level_boxes) { -#if ENABLE_EXPERIMENTAL_MINI_FORMAT if (box == nullptr) { // Either mini or meta will be null, just ignore that one continue; } -#endif // dump box content for debugging if (first) { @@ -296,6 +360,74 @@ } +std::string HeifFile::debug_dump_item_data() const +{ + std::stringstream sstr; + bool has_output = false; + + sstr << "=== Item Data ===\n"; + + for (const auto& [id, infe] : m_infe_boxes) { + uint32_t item_type = infe->get_item_type_4cc(); + + if (item_type == fourcc("grid")) { + auto dataResult = get_uncompressed_item_data(id); + if (!dataResult) { + continue; + } + + ImageGrid grid; + Error err = grid.parse(*dataResult); + if (err) { + continue; + } + + has_output = true; + sstr << "\nitem ID " << id << " (grid):\n"; + + std::istringstream lines(grid.dump()); + std::string line; + while (std::getline(lines, line)) { + sstr << " " << line << "\n"; + } + } + else if (item_type == fourcc("iovl")) { + if (!m_iref_box) { + continue; + } + + auto refs = m_iref_box->get_references(id, fourcc("dimg")); + + auto dataResult = get_uncompressed_item_data(id); + if (!dataResult) { + continue; + } + + ImageOverlay overlay; + Error err = overlay.parse(refs.size(), *dataResult); + if (err) { + continue; + } + + has_output = true; + sstr << "\nitem ID " << id << " (iovl):\n"; + + std::istringstream lines(overlay.dump()); + std::string line; + while (std::getline(lines, line)) { + sstr << " " << line << "\n"; + } + } + } + + if (has_output) { + return sstr.str(); + } + + return {}; +} + + Error HeifFile::parse_heif_file() { // --- read all top-level boxes @@ -343,6 +475,11 @@ m_top_level_boxes.push_back(m_ftyp_box); + bool is_sequence_brand = (m_ftyp_box->has_compatible_brand(heif_brand2_msf1) || + m_ftyp_box->has_compatible_brand(heif_brand2_isom) || + m_ftyp_box->has_compatible_brand(heif_brand2_mp41) || + m_ftyp_box->has_compatible_brand(heif_brand2_mp42)); + // --- check whether this is a HEIF file and its structural format if (!m_ftyp_box->has_compatible_brand(heif_brand2_heic) && @@ -350,101 +487,83 @@ !m_ftyp_box->has_compatible_brand(heif_brand2_mif1) && !m_ftyp_box->has_compatible_brand(heif_brand2_avif) && !m_ftyp_box->has_compatible_brand(heif_brand2_1pic) && -#if ENABLE_EXPERIMENTAL_MINI_FORMAT !(m_ftyp_box->get_major_brand() == heif_brand2_mif3) && -#endif - !m_ftyp_box->has_compatible_brand(heif_brand2_jpeg)) { + !m_ftyp_box->has_compatible_brand(heif_brand2_jpeg) && + !m_ftyp_box->has_compatible_brand(heif_brand2_isom) && + !m_ftyp_box->has_compatible_brand(heif_brand2_mp42) && + !m_ftyp_box->has_compatible_brand(heif_brand2_mp41) && + !m_ftyp_box->has_compatible_brand(heif_brand2_msf1)) { std::stringstream sstr; sstr << "File does not include any supported brands.\n"; return Error(heif_error_Unsupported_filetype, heif_suberror_Unspecified, sstr.str()); - } + } -#if ENABLE_EXPERIMENTAL_MINI_FORMAT m_mini_box = m_file_layout->get_mini_box(); - m_top_level_boxes.push_back(m_mini_box); if (m_mini_box) { + m_top_level_boxes.push_back(m_mini_box); + Error err = m_mini_box->create_expanded_boxes(this); if (err) { return err; } return Error::Ok; } -#endif m_meta_box = m_file_layout->get_meta_box(); - m_top_level_boxes.push_back(m_meta_box); - // TODO: we are missing 'mdat' top level boxes + if (m_meta_box) { + m_top_level_boxes.push_back(m_meta_box); + } - // if we didn't find the mini box, meta is required + // TODO: we are missing 'mdat' top level boxes - if (!m_meta_box) { - return Error(heif_error_Invalid_input, - heif_suberror_No_meta_box); + m_moov_box = m_file_layout->get_moov_box(); + if (m_moov_box) { + m_top_level_boxes.push_back(m_moov_box); } + // if we didn't find the mini box, meta is required - m_hdlr_box = m_meta_box->get_child_box(); - if (STRICT_PARSING && !m_hdlr_box) { + if (!m_meta_box && !is_sequence_brand) { return Error(heif_error_Invalid_input, - heif_suberror_No_hdlr_box); + heif_suberror_No_meta_box); } - if (m_hdlr_box && - m_hdlr_box->get_handler_type() != fourcc("pict")) { + if (!m_moov_box && is_sequence_brand) { return Error(heif_error_Invalid_input, - heif_suberror_No_pict_handler); + heif_suberror_No_moov_box); } - - // --- find mandatory boxes needed for image decoding - - m_pitm_box = m_meta_box->get_child_box(); - if (!m_pitm_box) { - return Error(heif_error_Invalid_input, - heif_suberror_No_pitm_box); + if (m_meta_box) { + auto err = parse_heif_images(); + if (err) { + return err; + } } - m_iprp_box = m_meta_box->get_child_box(); - if (!m_iprp_box) { - return Error(heif_error_Invalid_input, - heif_suberror_No_iprp_box); + if (m_moov_box) { + auto err = parse_heif_sequences(); + if (err) { + return err; + } } - m_ipco_box = m_iprp_box->get_child_box(); - if (!m_ipco_box) { - return Error(heif_error_Invalid_input, - heif_suberror_No_ipco_box); - } + return Error::Ok; +} - auto ipma_boxes = m_iprp_box->get_child_boxes(); - if (ipma_boxes.empty()) { - return Error(heif_error_Invalid_input, - heif_suberror_No_ipma_box); - } - for (size_t i=1;iinsert_entries_from_other_ipma_box(*ipma_boxes[i]); - } - m_ipma_box = ipma_boxes[0]; - m_iloc_box = m_meta_box->get_child_box(); - if (!m_iloc_box) { +Error HeifFile::parse_heif_images() +{ + m_hdlr_box = m_meta_box->get_child_box(); + if (STRICT_PARSING && !m_hdlr_box) { return Error(heif_error_Invalid_input, - heif_suberror_No_iloc_box); + heif_suberror_No_hdlr_box); } - m_idat_box = m_meta_box->get_child_box(); - - m_iref_box = m_meta_box->get_child_box(); - if (m_iref_box) { - Error error = check_for_ref_cycle(get_primary_image_ID(), m_iref_box); - if (error) { - return error; - } - } + // --- assign item boxes m_iinf_box = m_meta_box->get_child_box(); if (!m_iinf_box) { @@ -452,11 +571,6 @@ heif_suberror_No_iinf_box); } - m_grpl_box = m_meta_box->get_child_box(); - - - // --- build list of images - std::vector> infe_boxes = m_iinf_box->get_child_boxes(); for (auto& infe_box : infe_boxes) { @@ -468,42 +582,75 @@ m_infe_boxes.insert(std::make_pair(infe_box->get_item_ID(), infe_box)); } - return Error::Ok; -} + if (has_images()) { + // --- find mandatory boxes needed for image decoding -Error HeifFile::check_for_ref_cycle(heif_item_id ID, - const std::shared_ptr& iref_box) const -{ - std::unordered_set parent_items; - return check_for_ref_cycle_recursion(ID, iref_box, parent_items); -} + m_pitm_box = m_meta_box->get_child_box(); + if (!m_pitm_box) { + return Error(heif_error_Invalid_input, + heif_suberror_No_pitm_box); + } + m_iprp_box = m_meta_box->get_child_box(); + if (!m_iprp_box) { + return Error(heif_error_Invalid_input, + heif_suberror_No_iprp_box); + } -Error HeifFile::check_for_ref_cycle_recursion(heif_item_id ID, - const std::shared_ptr& iref_box, - std::unordered_set& parent_items) const { - if (parent_items.find(ID) != parent_items.end()) { + m_ipco_box = m_iprp_box->get_child_box(); + if (!m_ipco_box) { + return Error(heif_error_Invalid_input, + heif_suberror_No_ipco_box); + } + + auto ipma_boxes = m_iprp_box->get_child_boxes(); + if (ipma_boxes.empty()) { + return Error(heif_error_Invalid_input, + heif_suberror_No_ipma_box); + } + for (size_t i=1;iinsert_entries_from_other_ipma_box(*ipma_boxes[i]); + } + m_ipma_box = ipma_boxes[0]; + } + + m_iloc_box = m_meta_box->get_child_box(); + if (!m_iloc_box) { return Error(heif_error_Invalid_input, - heif_suberror_Item_reference_cycle, - "Image reference cycle"); + heif_suberror_No_iloc_box); } - parent_items.insert(ID); - std::vector image_references = iref_box->get_references(ID, fourcc("dimg")); - for (heif_item_id reference_idx : image_references) { - Error error = check_for_ref_cycle_recursion(reference_idx, iref_box, parent_items); - if (error) { - return error; - } + m_idat_box = m_meta_box->get_child_box(); + + m_iref_box = m_meta_box->get_child_box(); + + // Note: reference cycles are not rejected at load. A cycle only matters when + // it is decoded, and it is caught there per item by ImageItem::verify_decodable() + // (following both 'dimg' and 'auxl' edges). Rejecting the whole file at load + // would also make its unaffected, independently valid items undecodable. + + m_grpl_box = m_meta_box->get_child_box(); + + + return Error::Ok; +} + + +Error HeifFile::parse_heif_sequences() +{ + m_mvhd_box = m_moov_box->get_child_box(); + if (!m_mvhd_box) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "No mvhd box in image sequence."}; } - parent_items.erase(ID); return Error::Ok; } -bool HeifFile::image_exists(heif_item_id ID) const +bool HeifFile::item_exists(heif_item_id ID) const { auto image_iter = m_infe_boxes.find(ID); return image_iter != m_infe_boxes.end(); @@ -565,7 +712,7 @@ } -Error HeifFile::get_uncompressed_item_data(heif_item_id ID, std::vector* data) const +Result> HeifFile::get_uncompressed_item_data(heif_item_id ID) const { assert(m_limits); @@ -573,7 +720,11 @@ // std::lock_guard guard(m_read_mutex); // TODO: I think that this is not needed anymore because this function is not used for image data anymore. #endif - if (!image_exists(ID)) { + if (!m_iloc_box) { + return Error(heif_error_Invalid_input, heif_suberror_No_iloc_box); + } + + if (!item_exists(ID)) { return Error(heif_error_Usage_error, heif_suberror_Nonexisting_item_referenced); } @@ -591,75 +742,133 @@ // --- decompress data Error error; - bool read_uncompressed = true; + if (item_type == fourcc("mime")) { std::string encoding = infe_box->get_content_encoding(); - if (encoding == "compress_zlib") { + // Skip the encoding comparisons when no encoding is set; falling through + // to the raw read below is the right behaviour for the uncompressed case. + // Skipping is also necessary to avoid libstdc++'s _S_compare(0, N) which + // computes unsigned `0 - N` and trips UBSan even though the cast result + // is benign. + if (!encoding.empty() && encoding != "identity") { + if (encoding == "compress_zlib") { #if HAVE_ZLIB - read_uncompressed = false; - std::vector compressed_data; - error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); - if (error) { - return error; - } - error = decompress_zlib(compressed_data, data); - if (error) { - return error; - } + std::vector compressed_data; + error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); + if (error) { + return error; + } + + return decompress_zlib(compressed_data, m_limits); #else - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_header_compression_method, - encoding); + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_header_compression_method, + encoding); #endif - } - else if (encoding == "deflate") { -#if HAVE_ZLIB - read_uncompressed = false; - std::vector compressed_data; - error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); - if (error) { - return error; - } - error = decompress_deflate(compressed_data, data); - if (error) { - return error; } + else if (encoding == "deflate") { +#if HAVE_ZLIB + std::vector compressed_data; + error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); + if (error) { + return error; + } + return decompress_deflate(compressed_data, m_limits); #else - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_header_compression_method, - encoding); + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_header_compression_method, + encoding); #endif - } - else if (encoding == "br") { -#if HAVE_BROTLI - read_uncompressed = false; - std::vector compressed_data; - error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); - if (error) { - return error; - } - error = decompress_brotli(compressed_data, data); - if (error) { - return error; } + else if (encoding == "br") { +#if HAVE_BROTLI + std::vector compressed_data; + error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &compressed_data, m_limits); + if (error) { + return error; + } + return decompress_brotli(compressed_data, m_limits); #else - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_header_compression_method, - encoding); + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_header_compression_method, + encoding); #endif + } + else { + return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_codec); + } + } + } + + + // --- read uncompressed + + std::vector data; + error = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &data, m_limits); + if (error) { + return error; + } + else { + return data; + } +} + + +Error HeifFile::append_data_from_file_range(std::vector& out_data, uint64_t offset, uint32_t size) const +{ + auto old_size = out_data.size(); + + // --- check that the requested range does not exceed the file size + + uint64_t end_pos = offset + size; + if (m_input_stream->wait_for_file_size(end_pos) != StreamReader::grow_status::size_reached) { + std::stringstream sstr; + sstr << "File range " << offset << ".." << end_pos << " is beyond end of file."; + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str()}; + } + + // --- check security limit on resulting buffer size + + if (m_limits) { + auto max_memory_block_size = m_limits->max_memory_block_size; + if (max_memory_block_size && max_memory_block_size - old_size < size) { + std::stringstream sstr; + sstr << "Sample data of " << size << " bytes would grow total buffer to " + << (old_size + size) << " bytes, exceeding the security limit of " + << max_memory_block_size << " bytes."; + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; } } - if (read_uncompressed) { - return m_iloc_box->read_data(ID, m_input_stream, m_idat_box, data, m_limits); + if (!m_input_stream->seek(offset)) { + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + "Cannot seek to sample data offset."}; } - return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_codec); + out_data.resize(old_size + size); + + if (!m_input_stream->read(out_data.data() + old_size, size)) { + out_data.resize(old_size); + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + "Failed to read sample data from file."}; + } + + return {}; } Error HeifFile::append_data_from_iloc(heif_item_id ID, std::vector& out_data, uint64_t offset, uint64_t size) const { + if (!m_iloc_box) { + return Error(heif_error_Invalid_input, heif_suberror_No_iloc_box); + } + const auto& items = m_iloc_box->get_items(); const Box_iloc::Item* item = nullptr; for (const auto& i : items) { @@ -681,16 +890,20 @@ } -Error HeifFile::get_item_data(heif_item_id ID, std::vector* out_data, heif_metadata_compression* out_compression) const +Result> HeifFile::get_item_data(heif_item_id ID, heif_metadata_compression* out_compression) const { Error error; assert(m_limits); + if (!m_iloc_box) { + return Error(heif_error_Invalid_input, heif_suberror_No_iloc_box); + } + auto infe_box = get_infe_box(ID); if (!infe_box) { - return {heif_error_Usage_error, - heif_suberror_Nonexisting_item_referenced}; + return Error{heif_error_Usage_error, + heif_suberror_Nonexisting_item_referenced}; } uint32_t item_type = infe_box->get_item_type_4cc(); @@ -703,7 +916,14 @@ *out_compression = heif_metadata_compression_off; } - return m_iloc_box->read_data(ID, m_input_stream, m_idat_box, out_data, m_limits); + std::vector out_data; + Error err = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &out_data, m_limits); + if (err) { + return err; + } + else { + return out_data; + } } @@ -713,14 +933,21 @@ heif_metadata_compression compression; - if (encoding.empty()) { - // shortcut for case of uncompressed mime data + if (encoding.empty() || encoding == "identity") { + // shortcut for case of uncompressed mime data ("identity" is the RFC 2616 no-op coding) if (out_compression) { *out_compression = heif_metadata_compression_off; } - return m_iloc_box->read_data(ID, m_input_stream, m_idat_box, out_data, m_limits); + std::vector out_data; + Error err = m_iloc_box->read_data(ID, m_input_stream, m_idat_box, &out_data, m_limits); + if (err) { + return err; + } + else { + return out_data; + } } else if (encoding == "compress_zlib") { compression = heif_metadata_compression_zlib; @@ -745,11 +972,10 @@ // return compressed data, if we do not want to have it uncompressed - const bool do_decode = (out_compression == nullptr); - if (!do_decode) { + const bool return_compressed = (out_compression != nullptr); + if (return_compressed) { *out_compression = compression; - *out_data = std::move(compressed_data); - return Error::Ok; + return compressed_data; } // decompress the data @@ -757,47 +983,85 @@ switch (compression) { #if HAVE_ZLIB case heif_metadata_compression_zlib: - return decompress_zlib(compressed_data, out_data); + return decompress_zlib(compressed_data, m_limits); case heif_metadata_compression_deflate: - return decompress_deflate(compressed_data, out_data); + return decompress_deflate(compressed_data, m_limits); #endif #if HAVE_BROTLI case heif_metadata_compression_brotli: - return decompress_brotli(compressed_data, out_data); + return decompress_brotli(compressed_data, m_limits); #endif default: - return {heif_error_Unsupported_filetype, heif_suberror_Unsupported_header_compression_method}; + return Error{heif_error_Unsupported_filetype, heif_suberror_Unsupported_header_compression_method}; } } -// TODO: we should use a acquire() / release() approach here so that we can get multiple IDs before actually creating infe boxes -heif_item_id HeifFile::get_unused_item_id() const +Result HeifFile::get_unused_item_id() { - heif_item_id max_id = 0; + return m_id_creator.get_new_id(IDCreator::Namespace::item); +} - // TODO: replace with better algorithm and data-structure + +void HeifFile::seed_id_creator() +{ + // A file with the 'unif' brand uses one ID space for items, tracks and entity groups. + // Keep it that way for everything we add to the file. + if (m_ftyp_box && m_ftyp_box->has_compatible_brand(heif_brand2_unif)) { + m_id_creator.set_unif(true); + } for (const auto& infe : m_infe_boxes) { - max_id = std::max(max_id, infe.second->get_item_ID()); + m_id_creator.mark_id_used(IDCreator::Namespace::item, infe.first); } - assert(max_id != 0xFFFFFFFF); + if (m_grpl_box) { + for (const auto& box : m_grpl_box->get_all_child_boxes()) { + if (auto group = std::dynamic_pointer_cast(box)) { + m_id_creator.mark_id_used(IDCreator::Namespace::entity_group, group->get_group_id()); + } + } + } - return max_id + 1; + if (m_moov_box) { + for (const auto& trak : m_moov_box->get_child_boxes()) { + if (auto tkhd = trak->get_child_box()) { + m_id_creator.mark_id_used(IDCreator::Namespace::track, tkhd->get_track_id()); + } + } + } +} + + +void HeifFile::remove_infe_box(const std::shared_ptr& infe) +{ + m_infe_boxes.erase(infe->get_item_ID()); + + if (m_iinf_box) { + m_iinf_box->remove_child_box(infe); + } } -heif_item_id HeifFile::add_new_image(uint32_t item_type) +Result HeifFile::add_new_image(uint32_t item_type) { - auto box = add_new_infe_box(item_type); - return box->get_item_ID(); + auto result = add_new_infe_box(item_type); + if (!result) { + return result.error(); + } + return (*result)->get_item_ID(); } -std::shared_ptr HeifFile::add_new_infe_box(uint32_t item_type) +Result> HeifFile::add_new_infe_box(uint32_t item_type) { - heif_item_id id = get_unused_item_id(); + init_for_image(); + + auto idResult = get_unused_item_id(); + if (!idResult) { + return idResult.error(); + } + heif_item_id id = *idResult; auto infe = std::make_shared(); infe->set_item_ID(id); @@ -811,40 +1075,87 @@ } -void HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential) +Result> HeifFile::add_new_meta_infe_box(uint32_t item_type) { + init_for_meta_item(); + + auto idResult = get_unused_item_id(); + if (!idResult) { + return idResult.error(); + } + heif_item_id id = *idResult; + + auto infe = std::make_shared(); + infe->set_item_ID(id); + infe->set_hidden_item(false); + infe->set_item_type_4cc(item_type); + + m_infe_boxes[id] = infe; + m_iinf_box->append_child_box(infe); + + return infe; +} + + +Error HeifFile::add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential) +{ + init_for_item_properties(); + auto ispe = std::make_shared(); ispe->set_size(width, height); uint32_t index = m_ipco_box->find_or_append_child_box(ispe); + // 'ipma' stores the property index as a 16 bit value. Rather than truncating it and writing a + // wrong association, refuse to add the property. + if (index + 1 > 0xFFFF) { + return {heif_error_Encoding_error, + heif_suberror_Unspecified, + too_many_properties_error}; + } + m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); + + return Error::Ok; } heif_property_id HeifFile::add_property(heif_item_id id, const std::shared_ptr& property, bool essential) { + init_for_item_properties(); + uint32_t index = m_ipco_box->find_or_append_child_box(property); - m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); + // 'ipma' stores the property index as a 16 bit value. Rather than truncating it and writing a + // wrong association, refuse to add the property. + if (index + 1 > 0xFFFF) { + return 0; + } - return index + 1; + // Note that we have to return the position within the item's property list and not 'index', + // as 'index' is the position in the file-wide 'ipco' box, which is shared by all items. + return m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); } heif_property_id HeifFile::add_property_without_deduplication(heif_item_id id, const std::shared_ptr& property, bool essential) { - uint32_t index = m_ipco_box->append_child_box(property); + init_for_item_properties(); - m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); + uint32_t index = m_ipco_box->append_child_box(property); + if (index + 1 > 0xFFFF) { + return 0; + } - return index + 1; + return m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); } -void HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orientation) +Error HeifFile::add_orientation_properties(heif_item_id id, heif_orientation orientation) { + init_for_item_properties(); + // Note: ISO/IEC 23000-22:2019(E) (MIAF) 7.3.6.7 requires the following order: // clean aperture first, then rotation, then mirror @@ -890,6 +1201,9 @@ irot->set_rotation_ccw(rotation_ccw); uint32_t index = m_ipco_box->find_or_append_child_box(irot); + if (index + 1 > 0xFFFF) { + return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error}; + } m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)}); } @@ -899,27 +1213,36 @@ imir->set_mirror_direction(mirror); uint32_t index = m_ipco_box->find_or_append_child_box(imir); + if (index + 1 > 0xFFFF) { + return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error}; + } m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)}); } + + return Error::Ok; } Result HeifFile::add_infe(uint32_t item_type, const uint8_t* data, size_t size) { - Result result; - // create an infe box describing what kind of data we are storing (this also creates a new ID) - auto infe_box = add_new_infe_box(item_type); + auto infe_result = add_new_meta_infe_box(item_type); + if (!infe_result) { + return infe_result.error(); + } + auto infe_box = *infe_result; infe_box->set_hidden_item(true); heif_item_id metadata_id = infe_box->get_item_ID(); - result.value = metadata_id; - set_item_data(infe_box, data, size, heif_metadata_compression_off); + if (Error err = set_item_data(infe_box, data, size, heif_metadata_compression_off)) { + remove_infe_box(infe_box); + return err; + } - return result; + return metadata_id; } @@ -931,58 +1254,70 @@ Result HeifFile::add_infe_mime(const char* content_type, heif_metadata_compression content_encoding, const uint8_t* data, size_t size) { - Result result; - // create an infe box describing what kind of data we are storing (this also creates a new ID) - auto infe_box = add_new_infe_box(fourcc("mime")); + auto infe_result = add_new_meta_infe_box(fourcc("mime")); + if (!infe_result) { + return infe_result.error(); + } + auto infe_box = *infe_result; infe_box->set_hidden_item(true); infe_box->set_content_type(content_type); heif_item_id metadata_id = infe_box->get_item_ID(); - result.value = metadata_id; - set_item_data(infe_box, data, size, content_encoding); + if (Error err = set_item_data(infe_box, data, size, content_encoding)) { + remove_infe_box(infe_box); + return err; + } - return result; + return metadata_id; } -Result HeifFile::add_precompressed_infe_mime(const char* content_type, std::string content_encoding, const uint8_t* data, size_t size) +Result HeifFile::add_precompressed_infe_mime(const char* content_type, const std::string& content_encoding, const uint8_t* data, size_t size) { - Result result; - // create an infe box describing what kind of data we are storing (this also creates a new ID) - auto infe_box = add_new_infe_box(fourcc("mime")); + auto infe_result = add_new_meta_infe_box(fourcc("mime")); + if (!infe_result) { + return infe_result.error(); + } + auto infe_box = *infe_result; infe_box->set_hidden_item(true); infe_box->set_content_type(content_type); heif_item_id metadata_id = infe_box->get_item_ID(); - result.value = metadata_id; - set_precompressed_item_data(infe_box, data, size, std::move(content_encoding)); + if (Error err = set_precompressed_item_data(infe_box, data, size, content_encoding)) { + remove_infe_box(infe_box); + return err; + } - return result; + return metadata_id; } Result HeifFile::add_infe_uri(const char* item_uri_type, const uint8_t* data, size_t size) { - Result result; - // create an infe box describing what kind of data we are storing (this also creates a new ID) - auto infe_box = add_new_infe_box(fourcc("uri ")); + auto infe_result = add_new_meta_infe_box(fourcc("uri ")); + if (!infe_result) { + return infe_result.error(); + } + auto infe_box = *infe_result; infe_box->set_hidden_item(true); infe_box->set_item_uri_type(item_uri_type); heif_item_id metadata_id = infe_box->get_item_ID(); - result.value = metadata_id; - set_item_data(infe_box, data, size, heif_metadata_compression_off); + if (Error err = set_item_data(infe_box, data, size, heif_metadata_compression_off)) { + remove_infe_box(infe_box); + return err; + } - return result; + return metadata_id; } @@ -997,7 +1332,9 @@ // only set metadata compression for MIME type data which has 'content_encoding' field if (compression != heif_metadata_compression_off && item->get_item_type_4cc() != fourcc("mime")) { - // TODO: error, compression not supported + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Item data compression is only supported for 'mime' items"); } @@ -1014,13 +1351,21 @@ else if (compression == heif_metadata_compression_deflate) { #if HAVE_ZLIB data_array = compress_deflate((const uint8_t*) data, size); - item->set_content_encoding("compress_zlib"); + item->set_content_encoding("deflate"); +#else + return Error(heif_error_Unsupported_feature, + heif_suberror_Unsupported_header_compression_method); +#endif + } + else if (compression == heif_metadata_compression_brotli) { +#if HAVE_BROTLI + data_array = compress_brotli((const uint8_t*)data, size); + item->set_content_encoding("br"); #else return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_header_compression_method); #endif } - // TODO: brotli else { // uncompressed data, plain copy @@ -1038,18 +1383,22 @@ } -Error HeifFile::set_precompressed_item_data(const std::shared_ptr& item, const uint8_t* data, size_t size, std::string content_encoding) +Error HeifFile::set_precompressed_item_data(const std::shared_ptr& item, const uint8_t* data, size_t size, const std::string& content_encoding) { // only set metadata compression for MIME type data which has 'content_encoding' field if (!content_encoding.empty() && item->get_item_type_4cc() != fourcc("mime")) { - // TODO: error, compression not supported + return Error(heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "A content_encoding can only be set for 'mime' items"); } std::vector data_array; - data_array.resize(size); - memcpy(data_array.data(), data, size); + if (size > 0) { // do not memcpy() from a NULL pointer when there is no data + data_array.resize(size); + memcpy(data_array.data(), data, size); + } item->set_content_encoding(content_encoding); @@ -1084,28 +1433,28 @@ } -void HeifFile::set_ipco_box(std::shared_ptr ipco) +void HeifFile::set_ipco_box(const std::shared_ptr& ipco) { m_ipco_box = ipco; m_meta_box->replace_child_box(ipco); } -void HeifFile::set_ipma_box(std::shared_ptr ipma) +void HeifFile::set_ipma_box(const std::shared_ptr& ipma) { m_ipma_box = ipma; m_meta_box->replace_child_box(ipma); } -void HeifFile::set_iloc_box(std::shared_ptr iloc) +void HeifFile::set_iloc_box(const std::shared_ptr& iloc) { m_iloc_box = iloc; m_meta_box->replace_child_box(iloc); } -void HeifFile::set_iref_box(std::shared_ptr iref) +void HeifFile::set_iref_box(const std::shared_ptr& iref) { m_iref_box = iref; m_meta_box->replace_child_box(iref); @@ -1162,14 +1511,21 @@ } -void HeifFile::set_auxC_property(heif_item_id id, const std::string& type) +Error HeifFile::set_auxC_property(heif_item_id id, const std::string& type) { + init_for_item_properties(); + auto auxC = std::make_shared(); auxC->set_aux_type(type); uint32_t index = m_ipco_box->find_or_append_child_box(auxC); + if (index + 1 > 0xFFFF) { + return {heif_error_Encoding_error, heif_suberror_Unspecified, too_many_properties_error}; + } m_ipma_box->add_property_for_item_ID(id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)}); + + return Error::Ok; } #if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER) @@ -1185,3 +1541,29 @@ return ret; } #endif + + +Result HeifFile::write_mdat(StreamWriter& writer) +{ + // --- write mdat box + + size_t mdatSize = m_mdat_data->get_data_size(); + + if (mdatSize <= 0xFFFFFFFF - 8) { + writer.write32((uint32_t) (mdatSize + 8)); + writer.write32(fourcc("mdat")); + } + else { + // box size > 4 GB + + writer.write32(1); + writer.write32(fourcc("mdat")); + writer.write64(mdatSize+8+8); + } + + size_t dataStartPos = writer.get_position(); + + m_mdat_data->write(writer); + + return dataStartPos; +} diff -Nru libheif-1.19.8/libheif/file.h libheif-1.23.4/libheif/file.h --- libheif-1.19.8/libheif/file.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/file.h 2026-09-06 14:45:17.000000000 +0000 @@ -22,11 +22,12 @@ #define LIBHEIF_FILE_H #include "box.h" +#include "id_creator.h" #include "nclx.h" #include "image-items/avif.h" #include "image-items/hevc.h" #include "image-items/vvc.h" -#include "codecs/uncompressed/unc_boxes.h" +//#include "codecs/uncompressed/unc_boxes.h" #include "file_layout.h" #include @@ -37,6 +38,7 @@ #include #include #include +#include "mdat_data.h" #if ENABLE_PARALLEL_TILE_DECODING @@ -49,6 +51,11 @@ class Box_j2kH; +class Box_moov; + +class Box_mvhd; + + class HeifFile { @@ -61,31 +68,52 @@ // You have to make sure that the pointer points to a valid object as long as the HeifFile is used. void set_security_limits(const heif_security_limits* limits) { m_limits = limits; } + const heif_security_limits* get_security_limits() const { return m_limits; } + Error read(const std::shared_ptr& reader); Error read_from_file(const char* input_filename); Error read_from_memory(const void* data, size_t size, bool copy); + bool has_images() const; + + bool has_sequences() const { return m_moov_box != nullptr; } + std::shared_ptr get_reader() { return m_input_stream; } void new_empty_file(); - void set_brand(heif_compression_format format, bool miaf_compatible); + void init_for_meta_item(); + + void init_for_image(); + + // Create the 'iprp'/'ipco'/'ipma' boxes if they do not exist yet. Unlike init_for_image(), + // this does not turn the file into an image file (no 'pict' handler, no 'pitm'). + void init_for_item_properties(); + + void init_for_sequence(); void set_hdlr_box(std::shared_ptr box) { m_hdlr_box = std::move(box); } + size_t append_mdat_data(const std::vector& data); + + void derive_box_versions(); + void write(StreamWriter& writer); + void set_write_mini_format(bool enable) { m_write_mini_format = enable; } + bool get_write_mini_format() const { return m_write_mini_format; } + int get_num_images() const { return static_cast(m_infe_boxes.size()); } - heif_item_id get_primary_image_ID() const { return m_pitm_box->get_item_ID(); } + heif_item_id get_primary_image_ID() const { return m_pitm_box ? m_pitm_box->get_item_ID() : 0; } size_t get_number_of_items() const { return m_infe_boxes.size(); } std::vector get_item_IDs() const; - bool image_exists(heif_item_id ID) const; + bool item_exists(heif_item_id ID) const; bool has_item_with_id(heif_item_id ID) const; @@ -95,7 +123,9 @@ std::string get_item_uri_type(heif_item_id ID) const; - Error get_uncompressed_item_data(heif_item_id ID, std::vector* data) const; + Result> get_uncompressed_item_data(heif_item_id ID) const; + + Error append_data_from_file_range(std::vector& out_data, uint64_t offset, uint32_t size) const; Error append_data_from_iloc(heif_item_id ID, std::vector& out_data, uint64_t offset, uint64_t size) const; @@ -103,7 +133,9 @@ return append_data_from_iloc(ID, out_data, 0, std::numeric_limits::max()); } - Error get_item_data(heif_item_id ID, std::vector *out_data, heif_metadata_compression* out_compression) const; + // If `out_compression` is not NULL, the compression method is returned there and the compressed data is returned. + // If `out_compression` is NULL, the data is returned decompressed. + Result> get_item_data(heif_item_id ID, heif_metadata_compression* out_compression) const; std::shared_ptr get_ftyp_box() { return m_ftyp_box; } @@ -113,7 +145,7 @@ std::shared_ptr get_infe_box(heif_item_id imageID); - void set_iref_box(std::shared_ptr); + void set_iref_box(const std::shared_ptr&); std::shared_ptr get_iref_box() { return m_iref_box; } @@ -121,11 +153,11 @@ std::shared_ptr get_ipco_box() { return m_ipco_box; } - void set_ipco_box(std::shared_ptr); + void set_ipco_box(const std::shared_ptr&); std::shared_ptr get_ipco_box() const { return m_ipco_box; } - void set_ipma_box(std::shared_ptr); + void set_ipma_box(const std::shared_ptr&); std::shared_ptr get_ipma_box() { return m_ipma_box; } @@ -133,6 +165,8 @@ std::shared_ptr get_grpl_box() const { return m_grpl_box; } + std::shared_ptr get_meta_box() const { return m_meta_box; } + std::shared_ptr get_entity_group(heif_entity_group_id id); Error get_properties(heif_item_id imageID, @@ -158,19 +192,27 @@ std::string debug_dump_boxes() const; + std::string debug_dump_item_data() const; + // --- writing --- - heif_item_id get_unused_item_id() const; + IDCreator& get_id_creator() { return m_id_creator; } - heif_item_id add_new_image(uint32_t item_type); + const IDCreator& get_id_creator() const { return m_id_creator; } - std::shared_ptr add_new_infe_box(uint32_t item_type); + Result get_unused_item_id(); - void add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential); + Result add_new_image(uint32_t item_type); + + Result> add_new_infe_box(uint32_t item_type); + + Result> add_new_meta_infe_box(uint32_t item_type); + + Error add_ispe_property(heif_item_id id, uint32_t width, uint32_t height, bool essential); // set irot/imir according to heif_orientation - void add_orientation_properties(heif_item_id id, heif_orientation); + Error add_orientation_properties(heif_item_id id, heif_orientation); // TODO: can we remove the 'essential' parameter and take this from the box? Or is that depending on the context? heif_property_id add_property(heif_item_id id, const std::shared_ptr& property, bool essential); @@ -183,19 +225,19 @@ Result add_infe_mime(const char* content_type, heif_metadata_compression content_encoding, const uint8_t* data, size_t size); - Result add_precompressed_infe_mime(const char* content_type, std::string content_encoding, const uint8_t* data, size_t size); + Result add_precompressed_infe_mime(const char* content_type, const std::string& content_encoding, const uint8_t* data, size_t size); Result add_infe_uri(const char* item_uri_type, const uint8_t* data, size_t size); Error set_item_data(const std::shared_ptr& item, const uint8_t* data, size_t size, heif_metadata_compression compression); - Error set_precompressed_item_data(const std::shared_ptr& item, const uint8_t* data, size_t size, std::string content_encoding); + Error set_precompressed_item_data(const std::shared_ptr& item, const uint8_t* data, size_t size, const std::string& content_encoding); void append_iloc_data(heif_item_id id, const std::vector& nal_packets, uint8_t construction_method); void replace_iloc_data(heif_item_id id, uint64_t offset, const std::vector& data, uint8_t construction_method = 0); - void set_iloc_box(std::shared_ptr); + void set_iloc_box(const std::shared_ptr&); std::shared_ptr get_iloc_box() { return m_iloc_box; } @@ -208,12 +250,19 @@ void add_entity_group_box(const std::shared_ptr& entity_group_box); - void set_auxC_property(heif_item_id id, const std::string& type); + Error set_auxC_property(heif_item_id id, const std::string& type); #if defined(__MINGW32__) || defined(__MINGW64__) || defined(_MSC_VER) static std::wstring convert_utf8_path_to_utf16(std::string pathutf8); #endif + + // --- sequences + + std::shared_ptr get_moov_box() { return m_moov_box; } + + std::shared_ptr get_mvhd_box() { return m_mvhd_box; } + private: #if ENABLE_PARALLEL_TILE_DECODING mutable std::mutex m_read_mutex; @@ -228,33 +277,47 @@ std::shared_ptr m_ftyp_box; std::shared_ptr m_hdlr_box; std::shared_ptr m_meta_box; -#if ENABLE_EXPERIMENTAL_MINI_FORMAT std::shared_ptr m_mini_box; // meta alternative -#endif + bool m_write_mini_format = false; - std::shared_ptr m_ipco_box; - std::shared_ptr m_ipma_box; std::shared_ptr m_iloc_box; std::shared_ptr m_idat_box; std::shared_ptr m_iref_box; - std::shared_ptr m_pitm_box; std::shared_ptr m_iinf_box; std::shared_ptr m_grpl_box; + std::shared_ptr m_pitm_box; // only non-null if has_images()==true + std::shared_ptr m_iprp_box; // only non-null if has_images()==true + std::shared_ptr m_ipco_box; // only non-null if has_images()==true + std::shared_ptr m_ipma_box; // only non-null if has_images()==true - std::shared_ptr m_iprp_box; std::map > m_infe_boxes; + std::unique_ptr m_mdat_data; + + // returns the position of the first data byte in the file + Result write_mdat(StreamWriter& writer); + + // --- sequences + + std::shared_ptr m_moov_box; + std::shared_ptr m_mvhd_box; + const heif_security_limits* m_limits = nullptr; + IDCreator m_id_creator; Error parse_heif_file(); - Error check_for_ref_cycle(heif_item_id ID, - const std::shared_ptr& iref_box) const; + // Advance the ID creator past all item, track and entity-group IDs found in the parsed + // file, so that IDs allocated for new items cannot collide with existing ones. + void seed_id_creator(); + + // Undo add_new_infe_box() / add_new_meta_infe_box() when the item could not be completed. + void remove_infe_box(const std::shared_ptr& infe); + + Error parse_heif_images(); - Error check_for_ref_cycle_recursion(heif_item_id ID, - const std::shared_ptr& iref_box, - std::unordered_set& parent_items) const; + Error parse_heif_sequences(); }; #endif diff -Nru libheif-1.19.8/libheif/file_layout.cc libheif-1.23.4/libheif/file_layout.cc --- libheif-1.19.8/libheif/file_layout.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/file_layout.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,6 +19,8 @@ */ #include "file_layout.h" +#include "sequences/seq_boxes.h" +#include FileLayout::FileLayout() @@ -86,6 +88,9 @@ BitstreamRange ftyp_range(m_stream_reader, 0, ftyp_size); std::shared_ptr ftyp_box; err = Box::read(ftyp_range, &ftyp_box, limits); + if (err) { + return err; + } m_boxes.push_back(ftyp_box); m_ftyp_box = std::dynamic_pointer_cast(ftyp_box); @@ -95,6 +100,10 @@ uint64_t next_box_start = ftyp_size; + bool meta_found = false; + bool mini_found = false; + bool moov_found = false; + for (;;) { // TODO: overflow uint64_t next_box_header_end = next_box_start + MAXIMUM_BOX_HEADER_SIZE; @@ -103,9 +112,14 @@ } if (next_box_header_end > m_max_length) { - return {heif_error_Invalid_input, - heif_suberror_Unspecified, - "Insufficient input data"}; + if (meta_found || mini_found || moov_found) { + return Error::Ok; + } + else { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Insufficient input data"}; + } } BitstreamRange box_range(m_stream_reader, next_box_start, m_max_length); @@ -117,15 +131,29 @@ if (box_header.get_short_type() == fourcc("meta")) { const uint64_t meta_box_start = next_box_start; - if (box_header.get_box_size() == 0) { - // TODO: get file-size from stream and compute box size - return {heif_error_Invalid_input, - heif_suberror_No_meta_box, - "Cannot read meta box with unspecified size"}; + uint64_t end_of_meta_box; + if (box_header.get_box_size() == BoxHeader::size_until_end_of_file) { + // A box size of 0 means the box extends to the end of the file + // (ISO/IEC 14496-12 clause 4.2), which is legal for the last box. + // Resolve it to the file size. + end_of_meta_box = m_stream_reader->request_range(meta_box_start, + std::numeric_limits::max()); + m_max_length = end_of_meta_box; + if (end_of_meta_box <= meta_box_start) { + return {heif_error_Invalid_input, + heif_suberror_No_meta_box, + "Cannot read meta box with unspecified size"}; + } + } + else { + end_of_meta_box = box_header.get_box_size(); + if (end_of_meta_box > std::numeric_limits::max() - meta_box_start) { + return {heif_error_Invalid_input, + heif_suberror_No_meta_box, + "Cannot read meta box with invalid size"}; + } + end_of_meta_box += meta_box_start; } - - // TODO: overflow - uint64_t end_of_meta_box = meta_box_start + box_header.get_box_size(); if (m_max_length < end_of_meta_box) { m_max_length = m_stream_reader->request_range(meta_box_start, end_of_meta_box); } @@ -145,20 +173,35 @@ m_boxes.push_back(meta_box); m_meta_box = std::dynamic_pointer_cast(meta_box); - break; + meta_found = true; } -#if ENABLE_EXPERIMENTAL_MINI_FORMAT // TODO: this is basically the same as the meta box case above, with different error handling. if (box_header.get_short_type() == fourcc("mini")) { const uint64_t mini_box_start = next_box_start; - if (box_header.get_box_size() == 0) { - // TODO: get file-size from stream and compute box size - return {heif_error_Invalid_input, - heif_suberror_Invalid_mini_box, - "Cannot read mini box with unspecified size"}; + uint64_t end_of_mini_box; + if (box_header.get_box_size() == BoxHeader::size_until_end_of_file) { + // A box size of 0 means the box extends to the end of the file + // (ISO/IEC 14496-12 clause 4.2), which is legal for the last box. + // Resolve it to the file size. + end_of_mini_box = m_stream_reader->request_range(mini_box_start, + std::numeric_limits::max()); + m_max_length = end_of_mini_box; + if (end_of_mini_box <= mini_box_start) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Cannot read mini box with unspecified size"}; + } + } + else { + end_of_mini_box = box_header.get_box_size(); + if (end_of_mini_box > std::numeric_limits::max() - mini_box_start) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Cannot read mini box with invalid size"}; + } + end_of_mini_box += mini_box_start; } - uint64_t end_of_mini_box = mini_box_start + box_header.get_box_size(); if (m_max_length < end_of_mini_box) { m_max_length = m_stream_reader->request_range(mini_box_start, end_of_mini_box); } @@ -172,27 +215,82 @@ std::shared_ptr mini_box; err = Box::read(mini_box_range, &mini_box, heif_get_global_security_limits()); if (err) { - std::cout << "error reading mini box" << std::endl; return err; } m_boxes.push_back(mini_box); m_mini_box = std::dynamic_pointer_cast(mini_box); - if (m_mini_box == nullptr) { - std::cout << "error casting mini box" << std::endl; + + mini_found = true; + } + + if (box_header.get_short_type() == fourcc("moov")) { + const uint64_t moov_box_start = next_box_start; + uint64_t end_of_moov_box; + if (box_header.get_box_size() == BoxHeader::size_until_end_of_file) { + // A box size of 0 means the box extends to the end of the file + // (ISO/IEC 14496-12 clause 4.2), which is legal for the last box. + // Resolve it to the file size. + end_of_moov_box = m_stream_reader->request_range(moov_box_start, + std::numeric_limits::max()); + m_max_length = end_of_moov_box; + if (end_of_moov_box <= moov_box_start) { + return {heif_error_Invalid_input, + heif_suberror_No_moov_box, + "Cannot read moov box with unspecified size"}; + } } - return Error::Ok; + else { + end_of_moov_box = box_header.get_box_size(); + if (end_of_moov_box > std::numeric_limits::max() - moov_box_start) { + return {heif_error_Invalid_input, + heif_suberror_No_moov_box, + "Cannot read moov box with invalid size"}; + } + end_of_moov_box += moov_box_start; + } + if (m_max_length < end_of_moov_box) { + m_max_length = m_stream_reader->request_range(moov_box_start, end_of_moov_box); + } + + if (m_max_length < end_of_moov_box) { + return {heif_error_Invalid_input, + heif_suberror_No_moov_box, + "Cannot read full moov box"}; + } + + BitstreamRange moov_box_range(m_stream_reader, moov_box_start, end_of_moov_box); + std::shared_ptr moov_box; + err = Box::read(moov_box_range, &moov_box, limits); + if (err) { + return err; + } + + m_boxes.push_back(moov_box); + m_moov_box = std::dynamic_pointer_cast(moov_box); + + moov_found = true; } -#endif - if (box_header.get_box_size() == 0) { + uint64_t boxSize = box_header.get_box_size(); + if (boxSize == Box::size_until_end_of_file) { + if (meta_found || mini_found || moov_found) { + return Error::Ok; + } + else { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "No meta box found"}; + } + } + + if (std::numeric_limits::max() - boxSize < next_box_start) { return {heif_error_Invalid_input, - heif_suberror_No_meta_box, - "No meta box found"}; + heif_suberror_Unspecified, + "Box size too large, integer overflow"}; } - // TODO: overflow - next_box_start = next_box_start + box_header.get_box_size(); + next_box_start = next_box_start + boxSize; } return Error::Ok; diff -Nru libheif-1.19.8/libheif/file_layout.h libheif-1.23.4/libheif/file_layout.h --- libheif-1.19.8/libheif/file_layout.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/file_layout.h 2026-09-06 14:45:17.000000000 +0000 @@ -24,12 +24,13 @@ #include "error.h" #include "bitstream.h" #include "box.h" -#if ENABLE_EXPERIMENTAL_MINI_FORMAT #include "mini.h" -#endif + #include #include +class Box_moov; + class FileLayout { @@ -58,9 +59,9 @@ std::shared_ptr get_meta_box() { return m_meta_box; } -#if ENABLE_EXPERIMENTAL_MINI_FORMAT std::shared_ptr get_mini_box() { return m_mini_box; } -#endif + + std::shared_ptr get_moov_box() { return m_moov_box; } private: WriteMode m_writeMode = WriteMode::Floating; @@ -74,9 +75,8 @@ std::shared_ptr m_ftyp_box; std::shared_ptr m_meta_box; -#if ENABLE_EXPERIMENTAL_MINI_FORMAT std::shared_ptr m_mini_box; -#endif + std::shared_ptr m_moov_box; uint64_t m_max_length = 0; // Length seen so far. It can grow over time. diff -Nru libheif-1.19.8/libheif/id_creator.cc libheif-1.23.4/libheif/id_creator.cc --- libheif-1.19.8/libheif/id_creator.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/id_creator.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,111 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "id_creator.h" +#include + +namespace { + +// The next free id after 'id', i.e. the value a counter should take once 'id' +// has been handed out or reserved. When 'id' is the maximum representable value +// there is no larger id, so the counter becomes 0 == "exhausted" and +// get_new_id() will refuse to hand out another id. Computing this explicitly +// (rather than 'id + 1') keeps the exhausted transition free of the unsigned +// wrap that the 'integer' sanitizer flags, while guaranteeing the counter always +// moves strictly past 'id' so no id is ever handed out twice. +inline uint32_t next_id_after(uint32_t id) +{ + return id == UINT32_MAX ? 0u : id + 1u; +} + +} + + +Result IDCreator::get_new_id(Namespace ns) +{ + if (m_unif) { + if (m_next_id_global == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Unspecified, + "ID namespace overflow"); + } + uint32_t id = m_next_id_global; + m_next_id_global = next_id_after(id); + return id; + } + + uint32_t* counter = nullptr; + switch (ns) { + case Namespace::item: + counter = &m_next_id_item; + break; + case Namespace::track: + counter = &m_next_id_track; + break; + case Namespace::entity_group: + counter = &m_next_id_entity_group; + break; + } + + if (*counter == 0) { + return Error(heif_error_Usage_error, + heif_suberror_Unspecified, + "ID namespace overflow"); + } + + uint32_t id = *counter; + *counter = next_id_after(id); + + // Keep the global counter ahead of all namespace counters so that switching to + // unif mode later does not reuse an ID that was already handed out. + if (m_next_id_global != 0 && id >= m_next_id_global) { + m_next_id_global = next_id_after(id); + } + + return id; +} + + +void IDCreator::mark_id_used(Namespace ns, uint32_t id) +{ + uint32_t* counter = nullptr; + switch (ns) { + case Namespace::item: + counter = &m_next_id_item; + break; + case Namespace::track: + counter = &m_next_id_track; + break; + case Namespace::entity_group: + counter = &m_next_id_entity_group; + break; + } + + // A counter value of 0 means "exhausted" (see get_new_id). Advancing past a + // just-used id keeps get_new_id() from ever handing out the same id again; + // marking 0xFFFFFFFF used moves the counter to 0 == exhausted. + if (*counter != 0 && id >= *counter) { + *counter = next_id_after(id); + } + + if (m_next_id_global != 0 && id >= m_next_id_global) { + m_next_id_global = next_id_after(id); + } +} diff -Nru libheif-1.19.8/libheif/id_creator.h libheif-1.23.4/libheif/id_creator.h --- libheif-1.19.8/libheif/id_creator.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/id_creator.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,55 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_ID_CREATOR_H +#define LIBHEIF_ID_CREATOR_H + +#include "error.h" +#include + +class IDCreator +{ +public: + enum class Namespace { item, track, entity_group }; + + void set_unif(bool flag) { m_unif = flag; } + + bool get_unif() const { return m_unif; } + + // Returns a new unique ID for the given namespace. + // In non-unif mode: separate counters per namespace. + // In unif mode: single global counter shared across all namespaces. + // Returns error on overflow (counter would exceed 0xFFFFFFFF). + Result get_new_id(Namespace ns); + + // Declare an ID as already taken (e.g. because it was read from an existing file), + // so that get_new_id() will never hand it out again. Advances the namespace counter + // and the global counter past 'id'. + void mark_id_used(Namespace ns, uint32_t id); + +private: + bool m_unif = false; + uint32_t m_next_id_item = 1; + uint32_t m_next_id_track = 1; + uint32_t m_next_id_entity_group = 1; + uint32_t m_next_id_global = 1; // used in unif mode +}; + +#endif diff -Nru libheif-1.19.8/libheif/image/image_description.cc libheif-1.23.4/libheif/image/image_description.cc --- libheif-1.19.8/libheif/image/image_description.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/image/image_description.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,369 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . +*/ + + +#include "image_description.h" + +#include "codecs/uncompressed/unc_types.h" + +#if WITH_UNCOMPRESSED_CODEC +#include "codecs/uncompressed/unc_boxes.h" +#endif + +#include + + +heif_channel map_uncompressed_component_to_channel(uint16_t component_type) +{ + switch (component_type) { + case heif_cmpd_component_type_monochrome: + case heif_cmpd_component_type_Y: + return heif_channel_Y; + case heif_cmpd_component_type_Cb: + return heif_channel_Cb; + case heif_cmpd_component_type_Cr: + return heif_channel_Cr; + case heif_cmpd_component_type_red: + return heif_channel_R; + case heif_cmpd_component_type_green: + return heif_channel_G; + case heif_cmpd_component_type_blue: + return heif_channel_B; + case heif_cmpd_component_type_alpha: + return heif_channel_Alpha; + case heif_cmpd_component_type_filter_array: + return heif_channel_filter_array; + case heif_cmpd_component_type_depth: + return heif_channel_depth; + case heif_cmpd_component_type_disparity: + return heif_channel_disparity; + + case heif_cmpd_component_type_padded: + default: + return heif_channel_unknown; + } +} + + +BayerPatternCmpd BayerPattern::resolve_to_cmpd(std::map comp_id_to_cmpd) const +{ + BayerPatternCmpd cpat; + cpat.pattern_width = pattern_width; + cpat.pattern_height = pattern_height; + + for (auto p : pixels) { + assert(comp_id_to_cmpd.find(p.component_id) != comp_id_to_cmpd.end()); + cpat.pixels.push_back({comp_id_to_cmpd[p.component_id], p.component_gain}); + } + + return cpat; +} + + +static void remove_duplicates(std::vector& v) +{ + std::sort(v.begin(), v.end()); + v.erase(std::unique(v.begin(), v.end()), v.end()); +} + + +std::vector map_component_ids_to_cmpd(const std::vector& component_ids, const std::map& comp_id_to_cmpd) +{ + std::vector cmpd_indices; + + for (uint32_t comp_id : component_ids) { + assert(comp_id_to_cmpd.find(comp_id) != comp_id_to_cmpd.end()); + cmpd_indices.push_back(comp_id_to_cmpd.find(comp_id)->second); + } + + remove_duplicates(cmpd_indices); + + return cmpd_indices; +} + +std::vector map_cmpd_to_component_ids(const std::vector& cmpd_indices, const std::vector>& cmpd_to_comp_ids) +{ + std::vector component_ids; + + for (uint32_t idx : cmpd_indices) { + assert(idx < cmpd_to_comp_ids.size()); + component_ids.insert(component_ids.end(), cmpd_to_comp_ids[idx].begin(), cmpd_to_comp_ids[idx].end()); + } + + remove_duplicates(component_ids); + + return component_ids; +} + + +ImageDescription::~ImageDescription() +{ + heif_tai_timestamp_packet_release(m_tai_timestamp); +} + + +void ImageDescription::copy_metadata_from(const ImageDescription& other) +{ + m_premultiplied_alpha = other.m_premultiplied_alpha; + m_color_profile_nclx = other.m_color_profile_nclx; + m_color_profile_icc = other.m_color_profile_icc; + + m_PixelAspectRatio_h = other.m_PixelAspectRatio_h; + m_PixelAspectRatio_v = other.m_PixelAspectRatio_v; + + m_clli = other.m_clli; + m_mdcv = other.m_mdcv; + m_amve = other.m_amve; + m_nominal_diffuse_white_luminance = other.m_nominal_diffuse_white_luminance; + + heif_tai_timestamp_packet_release(m_tai_timestamp); + m_tai_timestamp = nullptr; + if (other.m_tai_timestamp) { + m_tai_timestamp = heif_tai_timestamp_packet_alloc(); + heif_tai_timestamp_packet_copy(m_tai_timestamp, other.m_tai_timestamp); + } + + m_gimi_sample_content_id = other.m_gimi_sample_content_id; + + m_bayer_pattern = other.m_bayer_pattern; + m_polarization_patterns = other.m_polarization_patterns; + m_sensor_bad_pixels_maps = other.m_sensor_bad_pixels_maps; + m_sensor_nuc = other.m_sensor_nuc; + + m_chroma_location = other.m_chroma_location; + + m_sample_duration = other.m_sample_duration; + + m_omaf_image_projection = other.m_omaf_image_projection; +} + + +bool ImageDescription::has_nclx_color_profile() const +{ + return m_color_profile_nclx != nclx_profile::undefined(); +} + + +nclx_profile ImageDescription::get_color_profile_nclx_with_fallback() const +{ + if (has_nclx_color_profile()) { + return get_color_profile_nclx(); + } + else { + return nclx_profile::defaults(); + } +} + + +std::shared_ptr ImageDescription::create_clli_box() const +{ + if (!has_clli()) { + return {}; + } + + auto clli = std::make_shared(); + clli->clli = get_clli(); + + return clli; +} + + +std::shared_ptr ImageDescription::create_mdcv_box() const +{ + if (!has_mdcv()) { + return {}; + } + + auto mdcv = std::make_shared(); + mdcv->mdcv = get_mdcv(); + + return mdcv; +} + + +std::shared_ptr ImageDescription::create_amve_box() const +{ + if (!has_amve()) { + return {}; + } + + auto amve = std::make_shared(); + amve->amve = get_amve(); + + return amve; +} + + +std::shared_ptr ImageDescription::create_ndwt_box() const +{ + if (!has_nominal_diffuse_white()) { + return {}; + } + + auto ndwt = std::make_shared(); + ndwt->set_diffuse_white_luminance(get_nominal_diffuse_white_luminance()); + + return ndwt; +} + + +std::shared_ptr ImageDescription::create_pasp_box() const +{ + if (!has_nonsquare_pixel_ratio()) { + return {}; + } + + auto pasp = std::make_shared(); + pasp->hSpacing = m_PixelAspectRatio_h; + pasp->vSpacing = m_PixelAspectRatio_v; + + return pasp; +} + + +std::shared_ptr ImageDescription::create_colr_box_nclx() const +{ + if (!has_nclx_color_profile()) { + return {}; + } + + auto colr = std::make_shared(); + colr->set_color_profile(std::make_shared(get_color_profile_nclx())); + return colr; +} + + +std::shared_ptr ImageDescription::create_colr_box_icc() const +{ + if (!has_icc_color_profile()) { + return {}; + } + + auto colr = std::make_shared(); + colr->set_color_profile(get_color_profile_icc()); + return colr; +} + +std::shared_ptr ImageDescription::create_prfr_box() const +{ + if (!has_omaf_image_projection()) { + return {}; + } + + auto prfr = std::make_shared(); + if (prfr->set_image_projection(get_omaf_image_projection())) { + return {}; + } + + return prfr; +} + +std::vector> ImageDescription::generate_property_boxes(bool generate_colr_boxes) const +{ + std::vector> properties; + + // --- write PASP property + + if (has_nonsquare_pixel_ratio()) { + auto pasp = std::make_shared(); + get_pixel_ratio(&pasp->hSpacing, &pasp->vSpacing); + + properties.push_back(pasp); + } + + + // --- write CLLI property + + if (has_clli()) { + properties.push_back(create_clli_box()); + } + + + // --- write MDCV property + + if (has_mdcv()) { + auto mdcv = std::make_shared(); + mdcv->mdcv = get_mdcv(); + + properties.push_back(mdcv); + } + + + // --- write AMVE property + + if (has_amve()) { + properties.push_back(create_amve_box()); + } + + + // --- write NDWT property + + if (has_nominal_diffuse_white()) { + properties.push_back(create_ndwt_box()); + } + + + // --- write TAI property + + if (auto* tai = get_tai_timestamp()) { + auto itai = std::make_shared(); + itai->set_from_tai_timestamp_packet(tai); + + properties.push_back(itai); + } + + // --- write GIMI content ID property + + if (has_gimi_sample_content_id()) { + auto gimi = std::make_shared(); + gimi->set_content_id(get_gimi_sample_content_id()); + properties.push_back(gimi); + } + + if (generate_colr_boxes) { + // --- colr (nclx) + + if (has_nclx_color_profile()) { + properties.push_back(create_colr_box_nclx()); + } + + // --- colr (icc) + + if (has_icc_color_profile()) { + properties.push_back(create_colr_box_icc()); + } + } + + if (has_omaf_image_projection()) { + auto prfr = std::make_shared(); + prfr->set_image_projection(get_omaf_image_projection()); + properties.push_back(prfr); + } + +#if WITH_UNCOMPRESSED_CODEC + if (has_component_content_ids()) { + auto ccid = std::make_shared(); + ccid->set_content_ids(get_component_content_ids()); + properties.push_back(ccid); + } +#endif + + return properties; +} diff -Nru libheif-1.19.8/libheif/image/image_description.h libheif-1.23.4/libheif/image/image_description.h --- libheif-1.19.8/libheif/image/image_description.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/image/image_description.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,519 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . +*/ + +#ifndef LIBHEIF_IMAGE_DESCRIPTION_H +#define LIBHEIF_IMAGE_DESCRIPTION_H + +#include "error.h" +#include "nclx.h" +#include +#include +#include "omaf_boxes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +// === === Bayer pattern + +// --- Bayer pattern expressed as ISO 23001-17 cmpd component indices + +struct BayerPatternPixelCmpd +{ + uint32_t cmpd_index; + float component_gain; +}; + +struct BayerPatternCmpd +{ + uint16_t pattern_width = 0; + uint16_t pattern_height = 0; + std::vector pixels; +}; + +// --- Bayer pattern expressed as libheif components IDs + +struct BayerPattern +{ + uint16_t pattern_width = 0; + uint16_t pattern_height = 0; + std::vector pixels; + + [[nodiscard]] BayerPatternCmpd resolve_to_cmpd(std::map comp_id_to_cmpd) const; +}; + + +// === Polarization pattern (ISO 23001-17) + +struct PolarizationPattern +{ + std::vector component_ids; // empty = applies to all components. Either cmpd-index or component-id, depending on context. + uint16_t pattern_width = 0; + uint16_t pattern_height = 0; + std::vector polarization_angles; // pattern_width * pattern_height entries + // 0xFFFFFFFF bit-pattern (NaN) = no polarization filter +}; + + +// === Sensor bad pixels map (ISO 23001-17) + +struct SensorBadPixelsMap +{ + struct BadPixelCoordinate + { + uint32_t row, column; + }; + + std::vector component_ids; // empty = applies to all components. Either cmpd-index or component-id, depending on context. + bool correction_applied = false; + std::vector bad_rows; + std::vector bad_columns; + std::vector bad_pixels; +}; + + +// === Sensor non-uniformity correction (ISO 23001-17) + +struct SensorNonUniformityCorrection +{ + std::vector component_ids; // empty = applies to all components. Either cmpd-index or component-id, depending on context. + bool nuc_is_applied = false; + uint32_t image_width = 0; + uint32_t image_height = 0; + std::vector nuc_gains; // image_width * image_height entries + std::vector nuc_offsets; // image_width * image_height entries +}; + + + +// === Image components + +// Given a list of libheif component IDs, map them to a list of cmpd IDs through the given map. +// The output does not contain duplicates and is in no particular order. +std::vector map_component_ids_to_cmpd(const std::vector& component_ids, const std::map& comp_id_to_cmpd); + +// Map a list of unci component indices to libheif components. +// Use to find the libheif components for a metadata box that assigns it through cmpd indices. +// The output does not contain duplicates and is in no particular order. +std::vector map_cmpd_to_component_ids(const std::vector& cmpd_indices, const std::vector>& cmpd_to_comp_ids); + +// Find the closest matching heif_channel for an ISO 23001-17 component type. +// Returns heif_channel_unknown if no good mapping exists. +heif_channel map_uncompressed_component_to_channel(uint16_t component_type); + + +// Per-component description, independent of pixel data. +// Lives on ImageDescription so both ImageItem (handle side, before decoding) +// and HeifPixelImage (decoded side) can carry the same structural view. +struct ComponentDescription +{ + uint32_t component_id = 0; // stable id, matches HeifPixelImage::m_storage ids + + heif_channel channel = heif_channel_unknown; + uint16_t component_type = 0; // heif_cmpd_component_type_* + + // The numeric values of heif_component_datatype are aligned with the + // ISO 23001-17 Table 2 component_format byte (from the uncC box), so on + // the unci read path this field directly holds the file byte and on the + // write path it is emitted as the file byte. For non-unci codecs + // (HEVC/AVIF/JPEG/...) the codec sets this to the appropriate value + // (typically heif_component_datatype_unsigned_integer). + heif_component_datatype datatype = heif_component_datatype_undefined; + + uint16_t bit_depth = 0; // logical bit depth (1..256) + uint32_t width = 0; + uint32_t height = 0; + bool has_data_plane = true; // false for cpat reference colors + + // Empty string means "no content id assigned". + std::string gimi_content_id; +}; + + +class ImageDescription +{ +public: + virtual ~ImageDescription(); + + // TODO: Decide who is responsible for writing the colr boxes. + // Currently it is distributed over various places. + // Either here, in image_item.cc or in grid.cc. + std::vector> generate_property_boxes(bool generate_colr_boxes) const; + + + // --- color profile + + bool has_nclx_color_profile() const; + + virtual void set_color_profile_nclx(const nclx_profile& profile) { m_color_profile_nclx = profile; } + + nclx_profile get_color_profile_nclx() const { return m_color_profile_nclx; } + + // get the stored nclx fallback or return the default nclx if none is stored + nclx_profile get_color_profile_nclx_with_fallback() const; + + virtual void set_color_profile_icc(const std::shared_ptr& profile) { m_color_profile_icc = profile; } + + bool has_icc_color_profile() const { return m_color_profile_icc != nullptr; } + + const std::shared_ptr& get_color_profile_icc() const { return m_color_profile_icc; } + + void set_color_profile(const std::shared_ptr& profile) + { + auto icc = std::dynamic_pointer_cast(profile); + if (icc) { + set_color_profile_icc(icc); + } + + auto nclx = std::dynamic_pointer_cast(profile); + if (nclx) { + set_color_profile_nclx(nclx->get_nclx_color_profile()); + } + } + + + // --- premultiplied alpha + + bool is_premultiplied_alpha() const { return m_premultiplied_alpha; } + + virtual void set_premultiplied_alpha(bool flag) { m_premultiplied_alpha = flag; } + + + // --- pixel aspect ratio + + bool has_nonsquare_pixel_ratio() const { return m_PixelAspectRatio_h != m_PixelAspectRatio_v; } + + void get_pixel_ratio(uint32_t* h, uint32_t* v) const + { + *h = m_PixelAspectRatio_h; + *v = m_PixelAspectRatio_v; + } + + virtual void set_pixel_ratio(uint32_t h, uint32_t v) + { + m_PixelAspectRatio_h = h; + m_PixelAspectRatio_v = v; + } + + // --- clli + + bool has_clli() const { return m_clli.max_content_light_level != 0 || m_clli.max_pic_average_light_level != 0; } + + heif_content_light_level get_clli() const { return m_clli; } + + virtual void set_clli(const heif_content_light_level& clli) { m_clli = clli; } + + // --- mdcv + + bool has_mdcv() const { return m_mdcv.has_value(); } + + heif_mastering_display_colour_volume get_mdcv() const { return *m_mdcv; } + + virtual void set_mdcv(const heif_mastering_display_colour_volume& mdcv) + { + m_mdcv = mdcv; + } + + void unset_mdcv() { m_mdcv.reset(); } + + // --- amve (ambient viewing environment) + + bool has_amve() const { return m_amve.has_value(); } + + heif_ambient_viewing_environment get_amve() const { return *m_amve; } + + virtual void set_amve(const heif_ambient_viewing_environment& amve) + { + m_amve = amve; + } + + void unset_amve() { m_amve.reset(); } + + // --- ndwt (nominal diffuse white) + + // Note: a luminance of 0 is a valid value (it selects the ISO/TS 22028-5 + // default), so presence is tracked separately from the value via std::optional. + + bool has_nominal_diffuse_white() const { return m_nominal_diffuse_white_luminance.has_value(); } + + // Nominal diffuse white luminance in units of 0.0001 cd/m^2. + uint32_t get_nominal_diffuse_white_luminance() const { return m_nominal_diffuse_white_luminance.value_or(0); } + + virtual void set_nominal_diffuse_white_luminance(uint32_t luminance) + { + m_nominal_diffuse_white_luminance = luminance; + } + + void unset_nominal_diffuse_white() { m_nominal_diffuse_white_luminance.reset(); } + + virtual Error set_tai_timestamp(const heif_tai_timestamp_packet* tai) { + delete m_tai_timestamp; + + m_tai_timestamp = heif_tai_timestamp_packet_alloc(); + heif_tai_timestamp_packet_copy(m_tai_timestamp, tai); + return Error::Ok; + } + + [[nodiscard]] const heif_tai_timestamp_packet* get_tai_timestamp() const { + return m_tai_timestamp; + } + + // --- GIMI content ID + + virtual void set_gimi_sample_content_id(std::string id) { m_gimi_sample_content_id = std::move(id); } + + [[nodiscard]] bool has_gimi_sample_content_id() const { return !m_gimi_sample_content_id.empty(); } + + [[nodiscard]] const std::string& get_gimi_sample_content_id() const { return m_gimi_sample_content_id; } + + + [[nodiscard]] bool has_component_content_ids() const + { + return std::any_of(m_components.begin(), m_components.end(), + [](const ComponentDescription& c) { return !c.gimi_content_id.empty(); }); + } + + // Returns a positional vector: entry i is m_components[i].gimi_content_id + // (empty string if unset). Returned by value because it is reconstructed. + [[nodiscard]] std::vector get_component_content_ids() const + { + std::vector ids; + ids.reserve(m_components.size()); + for (const auto& c : m_components) { + ids.push_back(c.gimi_content_id); + } + return ids; + } + + + // --- per-component descriptions (id-based) + // These describe each component independent of pixel storage. Both ImageItem + // (before decoding) and HeifPixelImage (after decoding) carry the same list. + + [[nodiscard]] const std::vector& get_component_descriptions() const { return m_components; } + + // Append a ComponentDescription. The caller is expected to have set + // component_id, either from a fresh mint_component_id() call or by + // matching an id already used by a parallel structure (e.g. + // HeifPixelImage::ComponentStorage::m_component_ids). + void add_component_description(ComponentDescription desc) + { + m_components.push_back(std::move(desc)); + } + + // Bulk-replace the component descriptions and sync the next-id allocator. + // Used by clone/apply paths that rebuild the whole list at once. + void set_component_descriptions(std::vector components, uint32_t next_id) + { + m_components = std::move(components); + m_next_component_id = next_id; + } + + // Erase the description matching this component_id. Returns true if one was + // removed. + bool remove_component_description(uint32_t component_id) + { + auto it = std::find_if(m_components.begin(), m_components.end(), + [component_id](const ComponentDescription& c) { + return c.component_id == component_id; + }); + if (it == m_components.end()) return false; + m_components.erase(it); + return true; + } + + // Mint a fresh component id (monotonically increasing, starting at 1). + [[nodiscard]] uint32_t mint_component_id() { return m_next_component_id++; } + + // Read-only view of the next id that mint_component_id() would return. + // Used by HeifPixelImage::clone_component_descriptions_from() to keep the + // counter aligned across an item-to-image clone. + [[nodiscard]] uint32_t peek_next_component_id() const { return m_next_component_id; } + + [[nodiscard]] ComponentDescription* find_component_description(uint32_t component_id) + { + for (auto& c : m_components) { + if (c.component_id == component_id) return &c; + } + return nullptr; + } + + [[nodiscard]] const ComponentDescription* find_component_description(uint32_t component_id) const + { + for (const auto& c : m_components) { + if (c.component_id == component_id) return &c; + } + return nullptr; + } + + + // --- bayer pattern + + bool has_bayer_pattern(uint32_t component_id) const { return m_bayer_pattern.has_value(); } + + bool has_any_bayer_pattern() const { return m_bayer_pattern.has_value(); } + + const BayerPattern& get_bayer_pattern(uint32_t component_id) const { assert(has_bayer_pattern(component_id)); return *m_bayer_pattern; } + + const BayerPattern& get_any_bayer_pattern() const { assert(has_any_bayer_pattern()); return *m_bayer_pattern; } + + virtual void set_bayer_pattern(const BayerPattern& pattern) { m_bayer_pattern = pattern; } + + + // --- polarization pattern + + bool has_polarization_patterns() const { return !m_polarization_patterns.empty(); } + + const std::vector& get_polarization_patterns() const { return m_polarization_patterns; } + + virtual void set_polarization_patterns(const std::vector& p) { m_polarization_patterns = p; } + + virtual void add_polarization_pattern(const PolarizationPattern& p) { m_polarization_patterns.push_back(p); } + + + // --- sensor bad pixels map + + bool has_sensor_bad_pixels_maps() const { return !m_sensor_bad_pixels_maps.empty(); } + + const std::vector& get_sensor_bad_pixels_maps() const { return m_sensor_bad_pixels_maps; } + + virtual void set_sensor_bad_pixels_maps(const std::vector& m) { m_sensor_bad_pixels_maps = m; } + + virtual void add_sensor_bad_pixels_map(const SensorBadPixelsMap& m) { m_sensor_bad_pixels_maps.push_back(m); } + + + // --- sensor non-uniformity correction + + bool has_sensor_nuc() const { return !m_sensor_nuc.empty(); } + + const std::vector& get_sensor_nuc() const { return m_sensor_nuc; } + + virtual void set_sensor_nuc(const std::vector& n) { m_sensor_nuc = n; } + + virtual void add_sensor_nuc(const SensorNonUniformityCorrection& n) { m_sensor_nuc.push_back(n); } + + + // --- chroma sample location (ISO 23001-17, Section 6.1.4) + + bool has_chroma_location() const { return m_chroma_location.has_value(); } + + uint8_t get_chroma_location() const { return m_chroma_location.value_or(0); } + + virtual void set_chroma_location(uint8_t loc) { m_chroma_location = loc; } + + + // --- sample duration (for images that are frames in a sequence) + // 0 means "no duration assigned" (the default for still images). + + void set_sample_duration(uint32_t d) { m_sample_duration = d; } + + uint32_t get_sample_duration() const { return m_sample_duration; } + + + bool has_omaf_image_projection() const { + return (m_omaf_image_projection != heif_omaf_image_projection_flat); + } + + const heif_omaf_image_projection get_omaf_image_projection() const { + return m_omaf_image_projection; + } + + virtual void set_omaf_image_projection(const heif_omaf_image_projection projection) { + m_omaf_image_projection = projection; + } + + // Copies all per-image metadata from `other` (color profiles, premultiplied + // alpha, pixel aspect ratio, clli, mdcv, tai timestamp, gimi sample content + // id, bayer pattern, polarization patterns, sensor maps, sensor nuc, chroma + // location, omaf projection). Per-component descriptions + // (m_components / m_next_component_id) are intentionally not copied; those + // are managed separately by callers (via add_channel / add_component, or + // set_component_descriptions on the destination). + // + // Bayer / polarization / sensor-map metadata refers to image geometry; + // transforms that change orientation or position copy them verbatim and + // would need separate geometry adjustment to remain semantically correct. + void copy_metadata_from(const ImageDescription& other); + +private: + bool m_premultiplied_alpha = false; + nclx_profile m_color_profile_nclx = nclx_profile::undefined(); + std::shared_ptr m_color_profile_icc; + + uint32_t m_PixelAspectRatio_h = 1; + uint32_t m_PixelAspectRatio_v = 1; + heif_content_light_level m_clli{}; + std::optional m_mdcv; + std::optional m_amve; + std::optional m_nominal_diffuse_white_luminance; + + heif_tai_timestamp_packet* m_tai_timestamp = nullptr; + + // Empty string means "no content id assigned". + std::string m_gimi_sample_content_id; + + // Per-component description vector. Single source of truth for per-component + // metadata (id, channel, type, format, datatype, bit depth, dims, content ID). + std::vector m_components; + + // ID allocator for the per-component descriptions above. Used both by + // HeifPixelImage (decoded side) and ImageItem (handle side) via the + // mint_component_id() / peek_next_component_id() accessors. + uint32_t m_next_component_id = 1; + + std::optional m_bayer_pattern; + + std::vector m_polarization_patterns; + + std::vector m_sensor_bad_pixels_maps; + + std::vector m_sensor_nuc; + + std::optional m_chroma_location; + + uint32_t m_sample_duration = 0; // duration of a sequence frame, 0 for stills + + heif_omaf_image_projection m_omaf_image_projection = heif_omaf_image_projection::heif_omaf_image_projection_flat; + +protected: + std::shared_ptr create_clli_box() const; + + std::shared_ptr create_mdcv_box() const; + + std::shared_ptr create_amve_box() const; + + std::shared_ptr create_ndwt_box() const; + + std::shared_ptr create_pasp_box() const; + + std::shared_ptr create_colr_box_nclx() const; + + std::shared_ptr create_colr_box_icc() const; + + std::shared_ptr create_prfr_box() const; +}; + +#endif diff -Nru libheif-1.19.8/libheif/image/pixelimage.cc libheif-1.23.4/libheif/image/pixelimage.cc --- libheif-1.19.8/libheif/image/pixelimage.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/image/pixelimage.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,2583 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . +*/ + + +#include "pixelimage.h" +#include "common_utils.h" +#include "security_limits.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "codecs/uncompressed/unc_types.h" + + +heif_chroma chroma_from_subsampling(int h, int v) +{ + if (h == 2 && v == 2) { + return heif_chroma_420; + } + else if (h == 2 && v == 1) { + return heif_chroma_422; + } + else if (h == 1 && v == 1) { + return heif_chroma_444; + } + else { + assert(false); + return heif_chroma_undefined; + } +} + + +uint32_t chroma_width(uint32_t w, heif_chroma chroma) +{ + switch (chroma) { + case heif_chroma_420: + case heif_chroma_422: + return w/2 + (w & 1); // note: prevents integer overflow + default: + return w; + } +} + +uint32_t chroma_height(uint32_t h, heif_chroma chroma) +{ + switch (chroma) { + case heif_chroma_420: + return h/2 + (h & 1); // note: prevents integer overflow + default: + return h; + } +} + +uint32_t channel_width(uint32_t w, heif_chroma chroma, heif_channel channel) +{ + if (channel == heif_channel_Cb || channel == heif_channel_Cr) { + return chroma_width(w, chroma); + } + else { + return w; + } +} + +uint32_t channel_height(uint32_t h, heif_chroma chroma, heif_channel channel) +{ + if (channel == heif_channel_Cb || channel == heif_channel_Cr) { + return chroma_height(h, chroma); + } + else { + return h; + } +} + + + +static std::vector map_channel_to_component_type(heif_channel channel, heif_chroma chroma) +{ + switch (channel) { + case heif_channel_Y: + return {heif_cmpd_component_type_Y}; + case heif_channel_Cb: + return {heif_cmpd_component_type_Cb}; + case heif_channel_Cr: + return {heif_cmpd_component_type_Cr}; + case heif_channel_R: + return {heif_cmpd_component_type_red}; + case heif_channel_G: + return {heif_cmpd_component_type_green}; + case heif_channel_B: + return {heif_cmpd_component_type_blue}; + case heif_channel_Alpha: + return {heif_cmpd_component_type_alpha}; + case heif_channel_filter_array: + return {heif_cmpd_component_type_filter_array}; + case heif_channel_depth: + return {heif_cmpd_component_type_depth}; + case heif_channel_disparity: + return {heif_cmpd_component_type_disparity}; + case heif_channel_interleaved: + switch (chroma) { + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBB_LE: + return { + heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue + }; + case heif_chroma_interleaved_RGBA: + case heif_chroma_interleaved_RRGGBBAA_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + return { + heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue, + heif_cmpd_component_type_alpha + }; + default: + assert(false); + return {static_cast(1000 + channel)}; + break; + } + default: + // For other channels without a direct match, + // use an internal custom value. + return {static_cast(1000 + channel)}; + } +} + + + + + +HeifPixelImage::~HeifPixelImage() +{ + for (auto& component : m_storage) { + std::free(component.allocated_mem); + } +} + + +HeifPixelImage::ComponentStorage* HeifPixelImage::find_storage_for_channel(heif_channel channel) +{ + for (auto& component : m_storage) { + if (component.m_channel == channel) { + return &component; + } + } + return nullptr; +} + +const HeifPixelImage::ComponentStorage* HeifPixelImage::find_storage_for_channel(heif_channel channel) const +{ + for (const auto& component : m_storage) { + if (component.m_channel == channel) { + return &component; + } + } + return nullptr; +} + + +int num_interleaved_components_per_plane(heif_chroma chroma) +{ + switch (chroma) { + case heif_chroma_undefined: + case heif_chroma_monochrome: + case heif_chroma_420: + case heif_chroma_422: + case heif_chroma_444: + return 1; + + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBB_LE: + return 3; + + case heif_chroma_interleaved_RGBA: + case heif_chroma_interleaved_RRGGBBAA_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + return 4; + } + + assert(false); + return 0; +} + + +bool is_integer_multiple_of_chroma_size(uint32_t width, + uint32_t height, + heif_chroma chroma) +{ + switch (chroma) { + case heif_chroma_444: + case heif_chroma_monochrome: + return true; + case heif_chroma_422: + return (width & 1) == 0; + case heif_chroma_420: + return (width & 1) == 0 && (height & 1) == 0; + default: + assert(false); + return false; + } +} + + +std::vector get_valid_chroma_values_for_colorspace(heif_colorspace colorspace) +{ + switch (colorspace) { + case heif_colorspace_YCbCr: + return {heif_chroma_420, heif_chroma_422, heif_chroma_444}; + + case heif_colorspace_RGB: + // heif_chroma_planar and heif_chroma_444 are synonyms here. + // HeifPixelImage::create() canonicalizes the stored value to + // heif_chroma_444, so internal state and read-back always see 444; + // listing both here signals to callers that either is accepted, and + // prepares for a possible future switch of the canonical name. + return {heif_chroma_444, + heif_chroma_planar, + heif_chroma_interleaved_RGB, + heif_chroma_interleaved_RGBA, + heif_chroma_interleaved_RRGGBB_BE, + heif_chroma_interleaved_RRGGBBAA_BE, + heif_chroma_interleaved_RRGGBB_LE, + heif_chroma_interleaved_RRGGBBAA_LE}; + + case heif_colorspace_monochrome: + return {heif_chroma_planar}; + + case heif_colorspace_custom: + // Custom-colorspace images may have any number of planar components + // with arbitrary semantics. heif_chroma_planar describes the layout + // (planar, no subsampling); the per-component semantics live in the + // component descriptions. + return {heif_chroma_planar}; + + case heif_colorspace_filter_array: + // Filter-array (CFA / Bayer) images are a single mosaicked plane. + // The spatial pattern encodes color, so the legacy "monochrome" label + // is misleading; heif_chroma_planar describes only the layout. + return {heif_chroma_planar}; + + default: + return {}; + } +} + + +void HeifPixelImage::create(uint32_t width, uint32_t height, heif_colorspace colorspace, heif_chroma chroma) +{ + // Canonicalize (RGB, planar) to (RGB, 444). heif_chroma_planar is accepted + // as a synonym at this layer too (not just at heif_image_create), so any + // internal caller passing it gets the canonical form stored. + if (colorspace == heif_colorspace_RGB && chroma == heif_chroma_planar) { + chroma = heif_chroma_444; + } + + m_width = width; + m_height = height; + m_colorspace = colorspace; + m_chroma = chroma; +} + +static uint32_t rounded_size(uint32_t s) +{ + s = (s + 1U) & ~1U; + + if (s < 64) { + s = 64; + } + + return s; +} + +void HeifPixelImage::register_component_descriptions(ComponentStorage& plane, + const std::vector& component_types) +{ + for (uint16_t type : component_types) { + uint32_t id = mint_component_id(); + plane.m_component_ids.push_back(id); + + ComponentDescription desc; + desc.component_id = id; + desc.channel = plane.m_channel; + desc.component_type = type; + desc.datatype = plane.m_datatype; + desc.bit_depth = plane.m_bit_depth; + desc.width = plane.m_width; + desc.height = plane.m_height; + desc.has_data_plane = true; + add_component_description(std::move(desc)); + } +} + + +void HeifPixelImage::register_component_descriptions(ComponentStorage& plane, + const std::vector& source_descriptions) +{ + for (const ComponentDescription* src : source_descriptions) { + uint32_t id = mint_component_id(); + plane.m_component_ids.push_back(id); + + // Start from the source description so per-component metadata + // (component_type, gimi_content_id, has_data_plane, ...) is preserved. + // If the lookup failed (shouldn't happen on a well-formed source), + // fall back to a default-initialized description. + ComponentDescription desc; + if (src) { + desc = *src; + } + desc.component_id = id; + desc.channel = plane.m_channel; + desc.datatype = plane.m_datatype; + desc.bit_depth = plane.m_bit_depth; + desc.width = plane.m_width; + desc.height = plane.m_height; + add_component_description(std::move(desc)); + } +} + + +Error HeifPixelImage::add_channel(heif_channel channel, uint32_t width, uint32_t height, int bit_depth, + const heif_security_limits* limits, + heif_component_datatype datatype) +{ + // for backwards compatibility, allow for 24/32 bits for RGB/RGBA interleaved chromas + + if (m_chroma == heif_chroma_interleaved_RGB && bit_depth == 24) { + bit_depth = 8; + } + + if (m_chroma == heif_chroma_interleaved_RGBA && bit_depth == 32) { + bit_depth = 8; + } + + // The RRGGBB(AA) interleaved formats store each component as 16 bit. A bit depth + // of <= 8 would be self-inconsistent: the allocated plane would only hold one byte + // per component while readers/writers access the samples as 16-bit values. + if ((m_chroma == heif_chroma_interleaved_RRGGBB_BE || + m_chroma == heif_chroma_interleaved_RRGGBB_LE || + m_chroma == heif_chroma_interleaved_RRGGBBAA_BE || + m_chroma == heif_chroma_interleaved_RRGGBBAA_LE) && + bit_depth <= 8) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Cannot create a 16-bit interleaved channel with a bit depth of 8 or less"}; + } + + int num_interleaved_pixels = num_interleaved_components_per_plane(m_chroma); + + ComponentStorage plane; + plane.m_channel = channel; + + if (auto err = plane.alloc(width, height, datatype, bit_depth, num_interleaved_pixels, limits, m_memory_handle)) { + return err; + } + + register_component_descriptions(plane, map_channel_to_component_type(channel, m_chroma)); + m_storage.push_back(std::move(plane)); + return Error::Ok; +} + + +Error HeifPixelImage::ComponentStorage::alloc(uint32_t width, uint32_t height, heif_component_datatype datatype, int bit_depth, + int num_interleaved_components, + const heif_security_limits* limits, + MemoryHandle& memory_handle) +{ + // bit_depth and num_interleaved_components are exactly as attacker-influenced as width, + // height, and the other inputs checked below (e.g. reachable through the public + // heif_image_add_plane() API), so they get the same real, non-assert validation. + if (bit_depth < 1 || bit_depth > 128) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Invalid bit depth"}; + } + + if (num_interleaved_components < 1 || num_interleaved_components > 255) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Invalid number of interleaved components"}; + } + + if (width == 0 || height == 0) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Invalid image size"}; + } + + if (width == std::numeric_limits::max() || height == std::numeric_limits::max()) { + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Image size too large for memory alignment"}; + } + + // use 16 byte alignment (enough for 128 bit data-types). Every row is an integer number of data-elements. + uint16_t alignment = 16; // must be power of two + + m_width = width; + m_height = height; + + m_mem_width = rounded_size(width); + m_mem_height = rounded_size(height); + + m_bit_depth = static_cast(bit_depth); + m_num_interleaved_components = static_cast(num_interleaved_components); + m_datatype = datatype; + + // Cache bytes-per-pixel for the inner-loop get_bytes_per_pixel(). + int bytes_per_component; + if (bit_depth <= 8) bytes_per_component = 1; + else if (bit_depth <= 16) bytes_per_component = 2; + else if (bit_depth <= 32) bytes_per_component = 4; + else if (bit_depth <= 64) bytes_per_component = 8; + else bytes_per_component = 16; + + // m_bytes_per_pixel is a uint8_t. bytes_per_component * num_interleaved_components can + // exceed 255 even though num_interleaved_components itself is already bounded to <= 255 + // above (e.g. 16 bytes/component * 16 components = 256) -- check before the narrowing + // cast, not after: a silent wrap here wouldn't just misreport bytes-per-pixel, it would + // also make the stride/allocation-size computation below undersize the buffer relative + // to the real width/height/bit-depth the rest of this object claims to have. + if (bytes_per_component * num_interleaved_components > 255) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Number of interleaved components exceeds what can be stored for this bit depth"}; + } + m_bytes_per_pixel = static_cast(bytes_per_component * num_interleaved_components); + + int bytes_per_pixel = m_bytes_per_pixel; + + uint64_t stride_64 = static_cast(m_mem_width) * bytes_per_pixel; + stride_64 = (stride_64 + alignment - 1U) & ~static_cast(alignment - 1U); + if (stride_64 > std::numeric_limits::max()) { + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Image stride overflow"}; + } + stride = static_cast(stride_64); + + assert(alignment>=1); + + if (limits && + limits->max_image_size_pixels && + limits->max_image_size_pixels / height < width) { + + std::stringstream sstr; + sstr << "Allocating an image of size " << width << "x" << height << " exceeds the security limit of " + << limits->max_image_size_pixels << " pixels"; + + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + + // Check for allocation size overflow. + // Test case was an overlay image with size 1x134217727. + // Width 1 gets aligned to 64 and then width * height overflows 32 bit systems. + // + // On 64-bit systems (size_t is 64 bits) the product m_mem_height * stride can + // overflow the uint64_t multiplication itself and wrap to a small value, which + // would then slip past a post-multiply "> SIZE_MAX" comparison and undersize the + // buffer. So bound m_mem_height against stride *before* multiplying, leaving room + // for the trailing "+ alignment - 1". This also subsumes the 32-bit size_t case. + uint64_t max_alloc = std::numeric_limits::max(); + if (stride != 0 && + static_cast(m_mem_height) > (max_alloc - (alignment - 1)) / stride) { + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Image allocation size overflow"}; + } + uint64_t alloc_64 = static_cast(m_mem_height) * stride + alignment - 1; + allocation_size = static_cast(alloc_64); + + if (auto err = memory_handle.alloc(allocation_size, limits, "image data")) { + return err; + } + + // --- allocate memory + + // Must zero-initialize: padding regions (stride, rounded_size(), alignment slack) are not + // written by decoders, so uninitialized contents would leak across decoded images. + allocated_mem = static_cast(std::calloc(1, allocation_size)); + if (allocated_mem == nullptr) { + std::stringstream sstr; + sstr << "Allocating " << allocation_size << " bytes failed"; + + return {heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + sstr.str()}; + } + + uint8_t* mem_8 = allocated_mem; + + // shift beginning of image data to aligned memory position + + auto mem_start_addr = (uint64_t) mem_8; + auto mem_start_offset = (mem_start_addr & (alignment - 1U)); + if (mem_start_offset != 0) { + mem_8 += alignment - mem_start_offset; + } + + mem = mem_8; + + return Error::Ok; +} + + +Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bool adjust_size, + const heif_security_limits* limits) +{ + for (auto& component : m_storage) { + // get_subsampled_size() only adjusts the size for Cb/Cr; for every other + // channel it assumes the component has the full logical image size. We + // cannot compute a correct padded size for a non-Cb/Cr component that does + // not follow that assumption (e.g. multi-component ISO 23001-17 images). + if ((component.m_width != m_width || + component.m_height != m_height) && + (component.m_channel != heif_channel_Cb && + component.m_channel != heif_channel_Cr)) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Cannot extend padding for an image with non-uniform component sizes."}; + } + + uint32_t subsampled_width, subsampled_height; + get_subsampled_size(width, height, component.m_channel, m_chroma, + &subsampled_width, &subsampled_height); + + uint32_t old_width = component.m_width; + uint32_t old_height = component.m_height; + + int bytes_per_pixel = component.get_bytes_per_pixel(); + + if (component.m_mem_width < subsampled_width || + component.m_mem_height < subsampled_height) { + + ComponentStorage newPlane; + newPlane.m_channel = component.m_channel; + newPlane.m_component_ids = component.m_component_ids; + if (auto err = newPlane.alloc(subsampled_width, subsampled_height, component.m_datatype, component.m_bit_depth, + num_interleaved_components_per_plane(m_chroma), + limits, m_memory_handle)) + { + return err; + } + + // This is not needed, but we have to silence the clang-tidy false positive. + if (!newPlane.mem) { + return Error::InternalError; + } + + // copy the visible part of the old plane into the new plane + + for (uint32_t y = 0; y < component.m_height; y++) { + memcpy(static_cast(newPlane.mem) + y * newPlane.stride, + static_cast(component.mem) + y * component.stride, + component.m_width * bytes_per_pixel); + } + + // --- release the old plane before replacing it with the reallocated plane + + m_memory_handle.free(component.allocation_size); + std::free(component.allocated_mem); + + component = newPlane; + } + + // extend plane size + + if (old_width != subsampled_width) { + for (uint32_t y = 0; y < old_height; y++) { + for (uint32_t x = old_width; x < subsampled_width; x++) { + memcpy(static_cast(component.mem) + y * component.stride + x * bytes_per_pixel, + static_cast(component.mem) + y * component.stride + (old_width - 1) * bytes_per_pixel, + bytes_per_pixel); + } + } + } + + for (uint32_t y = old_height; y < subsampled_height; y++) { + memcpy(static_cast(component.mem) + y * component.stride, + static_cast(component.mem) + (old_height - 1) * component.stride, + subsampled_width * bytes_per_pixel); + } + + + if (adjust_size) { + component.m_width = subsampled_width; + component.m_height = subsampled_height; + } + } + + // modify logical image size, if requested + + if (adjust_size) { + m_width = width; + m_height = height; + } + + return Error::Ok; +} + + +Error HeifPixelImage::extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits) +{ + for (auto& component : m_storage) { + // See extend_padding_to_size(): get_subsampled_size() assumes a non-Cb/Cr + // component has the full logical image size, so we cannot compute a correct + // target size for a component that does not follow that assumption. + if ((component.m_width != m_width || + component.m_height != m_height) && + (component.m_channel != heif_channel_Cb && + component.m_channel != heif_channel_Cr)) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Cannot extend an image with non-uniform component sizes."}; + } + + uint32_t subsampled_width, subsampled_height; + get_subsampled_size(width, height, component.m_channel, m_chroma, + &subsampled_width, &subsampled_height); + + uint32_t old_width = component.m_width; + uint32_t old_height = component.m_height; + + int bytes_per_pixel = component.get_bytes_per_pixel(); + + if (component.m_mem_width < subsampled_width || + component.m_mem_height < subsampled_height) { + + ComponentStorage newPlane; + newPlane.m_channel = component.m_channel; + newPlane.m_component_ids = component.m_component_ids; + if (auto err = newPlane.alloc(subsampled_width, subsampled_height, component.m_datatype, component.m_bit_depth, num_interleaved_components_per_plane(m_chroma), limits, m_memory_handle)) { + return err; + } + + // This is not needed, but we have to silence the clang-tidy false positive. + if (!newPlane.mem) { + return Error::InternalError; + } + + // copy the visible part of the old plane into the new plane + + for (uint32_t y = 0; y < component.m_height; y++) { + memcpy(static_cast(newPlane.mem) + y * newPlane.stride, + static_cast(component.mem) + y * component.stride, + component.m_width * bytes_per_pixel); + } + + // --- release the old plane before replacing it with the reallocated plane + + m_memory_handle.free(component.allocation_size); + std::free(component.allocated_mem); + + component = newPlane; + } + + // extend plane size + + uint8_t fill = 0; + if (bytes_per_pixel == 1 && (component.m_channel == heif_channel_Cb || component.m_channel == heif_channel_Cr)) { + fill = 128; + } + + if (old_width != subsampled_width) { + for (uint32_t y = 0; y < old_height; y++) { + memset(static_cast(component.mem) + y * component.stride + old_width * bytes_per_pixel, + fill, + bytes_per_pixel * (subsampled_width - old_width)); + } + } + + for (uint32_t y = old_height; y < subsampled_height; y++) { + memset(static_cast(component.mem) + y * component.stride, + fill, + subsampled_width * bytes_per_pixel); + } + + + component.m_width = subsampled_width; + component.m_height = subsampled_height; + + // Keep ComponentDescriptions in sync with the resized plane so that + // get_component_width/height stays consistent with get_width(channel). + for (uint32_t id : component.m_component_ids) { + if (auto* desc = find_component_description(id)) { + desc->width = subsampled_width; + desc->height = subsampled_height; + } + } + } + + // modify the logical image size + + m_width = width; + m_height = height; + + return Error::Ok; +} + +bool HeifPixelImage::has_channel(heif_channel channel) const +{ + return find_storage_for_channel(channel) != nullptr; +} + + +bool HeifPixelImage::has_alpha() const +{ + return has_channel(heif_channel_Alpha) || + get_chroma_format() == heif_chroma_interleaved_RGBA || + get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE || + get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE; +} + + +uint32_t HeifPixelImage::get_width(heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + return 0; + } + + return comp->m_width; +} + + +uint32_t HeifPixelImage::get_height(heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + return 0; + } + + return comp->m_height; +} + + +uint32_t HeifPixelImage::get_width(uint32_t component_id) const +{ + auto* comp = find_storage_for_component(component_id); + if (!comp) { + return 0; + } + + return comp->m_width; +} + + +uint32_t HeifPixelImage::get_height(uint32_t component_id) const +{ + auto* comp = find_storage_for_component(component_id); + if (!comp) { + return 0; + } + + return comp->m_height; +} + + +bool HeifPixelImage::primary_planes_have_size(uint32_t width, uint32_t height) const +{ + auto channel_has_size = [&](heif_channel channel, uint32_t w, uint32_t h) { + // get_width()/get_height() return 0 for an absent channel -> mismatch. + return get_width(channel) == w && get_height(channel) == h; + }; + + switch (m_colorspace) { + case heif_colorspace_monochrome: + return channel_has_size(heif_channel_Y, width, height); + + case heif_colorspace_YCbCr: { + // Y has the full size; Cb/Cr are subsampled according to the chroma format. + // Downstream color conversion derives chroma plane indices from m_chroma, so + // Cb/Cr must actually match those subsampled dimensions (issue #1796). + if (!channel_has_size(heif_channel_Y, width, height)) { + return false; + } + if (m_chroma == heif_chroma_monochrome) { + return true; + } + uint32_t chroma_w, chroma_h; + get_subsampled_size(width, height, heif_channel_Cb, m_chroma, &chroma_w, &chroma_h); + return channel_has_size(heif_channel_Cb, chroma_w, chroma_h) && + channel_has_size(heif_channel_Cr, chroma_w, chroma_h); + } + + case heif_colorspace_RGB: + if (m_chroma == heif_chroma_444) { + // planar RGB: all three planes must be present and have the full size + return channel_has_size(heif_channel_R, width, height) && + channel_has_size(heif_channel_G, width, height) && + channel_has_size(heif_channel_B, width, height); + } + else { + return channel_has_size(heif_channel_interleaved, width, height); + } + + case heif_colorspace_filter_array: + return channel_has_size(heif_channel_filter_array, width, height); + + case heif_colorspace_undefined: + default: + // Multi-component / custom-colorspace images (CMYK, bayer configs, ...) + // cannot be checked generically; codec-specific overrides handle these. + return true; + } +} + + +bool HeifPixelImage::has_standard_plane_sizes() const +{ + for (const auto& component : m_storage) { + uint32_t expected_w, expected_h; + + switch (component.m_channel) { + case heif_channel_Y: + case heif_channel_Alpha: + case heif_channel_R: + case heif_channel_G: + case heif_channel_B: + case heif_channel_interleaved: + case heif_channel_filter_array: + case heif_channel_depth: + case heif_channel_disparity: + expected_w = m_width; + expected_h = m_height; + break; + + case heif_channel_Cb: + case heif_channel_Cr: + get_subsampled_size(m_width, m_height, component.m_channel, m_chroma, &expected_w, &expected_h); + break; + + default: + // Not one of the standard channels this check knows the geometry of. + // Notably heif_channel_unknown, used by the uncompressed codec for + // padded/unrecognized multi-component data, which has no universal + // size rule (and, unlike the channels above, may legitimately appear + // more than once per image) -- rejected rather than guessed at. + return false; + } + + if (component.m_width != expected_w || component.m_height != expected_h) { + return false; + } + } + + return true; +} + + +std::set HeifPixelImage::get_channel_set() const +{ + std::set channels; + + for (const auto& component : m_storage) { + channels.insert(component.m_channel); + } + + return channels; +} + + +uint16_t HeifPixelImage::get_storage_bits_per_pixel(enum heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + // Channel not present. The return type is unsigned, so the historical + // `return -1` actually yielded 65535; use 0 as an unambiguous + // "not present" value (no real channel has 0 bits per pixel). + return 0; + } + + uint32_t bpp = comp->get_bytes_per_pixel() * 8; + assert(bpp <= 256); + return static_cast(bpp); +} + + +uint16_t HeifPixelImage::get_bits_per_pixel(enum heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + // Channel not present -- see get_storage_bits_per_pixel(). + return 0; + } + + return comp->m_bit_depth; +} + + +uint16_t HeifPixelImage::get_visual_image_bits_per_pixel() const +{ + switch (m_colorspace) { + case heif_colorspace_monochrome: + return get_bits_per_pixel(heif_channel_Y); + break; + case heif_colorspace_YCbCr: + return std::max(get_bits_per_pixel(heif_channel_Y), + std::max(get_bits_per_pixel(heif_channel_Cb), + get_bits_per_pixel(heif_channel_Cr))); + break; + case heif_colorspace_RGB: + if (m_chroma == heif_chroma_444) { + return std::max(get_bits_per_pixel(heif_channel_R), + std::max(get_bits_per_pixel(heif_channel_G), + get_bits_per_pixel(heif_channel_B))); + } + else { + assert(has_channel(heif_channel_interleaved)); + return get_bits_per_pixel(heif_channel_interleaved); + } + break; + case heif_colorspace_custom: + return 0; + break; + case heif_colorspace_filter_array: + assert(has_channel(heif_channel_filter_array)); + return get_bits_per_pixel(heif_channel_filter_array); + default: + assert(false); + return 0; + break; + } +} + + +heif_component_datatype HeifPixelImage::get_datatype(heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + return heif_component_datatype_undefined; + } + + return comp->m_datatype; +} + + +int HeifPixelImage::get_number_of_interleaved_components(heif_channel channel) const +{ + auto* comp = find_storage_for_channel(channel); + if (!comp) { + return 0; + } + + return comp->m_num_interleaved_components; +} + + +Error HeifPixelImage::copy_new_channel_from(const std::shared_ptr& src_image, + heif_channel src_channel, + heif_channel dst_channel, + const heif_security_limits* limits) +{ + assert(src_image->has_channel(src_channel)); + assert(!has_channel(dst_channel)); + + uint32_t width = src_image->get_width(src_channel); + uint32_t height = src_image->get_height(src_channel); + + const auto* src_plane_ptr = src_image->find_storage_for_channel(src_channel); + assert(src_plane_ptr != nullptr); + const auto& src_plane = *src_plane_ptr; + + auto err = add_channel(dst_channel, width, height, + src_image->get_bits_per_pixel(src_channel), limits, + src_plane.m_datatype); + if (err) { + return err; + } + + uint8_t* dst; + size_t dst_stride = 0; + + const uint8_t* src; + size_t src_stride = 0; + + src = src_image->get_channel_memory(src_channel, &src_stride); + dst = get_channel_memory(dst_channel, &dst_stride); + + uint32_t bpl = width * (src_image->get_storage_bits_per_pixel(src_channel) / 8); + + for (uint32_t y = 0; y < height; y++) { + memcpy(dst + y * dst_stride, src + y * src_stride, bpl); + } + + return Error::Ok; +} + + +Error HeifPixelImage::extract_alpha_from_RGBA(const std::shared_ptr& src_image, + const heif_security_limits* limits) +{ + uint32_t width = src_image->get_width(); + uint32_t height = src_image->get_height(); + + // The copy loop below assumes 8-bit interleaved RGBA (4 bytes per pixel, + // alpha at byte offset 3). 16-bit interleaved formats (RRGGBBAA_*) have a + // different layout and would be read/written incorrectly. + if (src_image->get_bits_per_pixel(heif_channel_interleaved) != 8) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "extract_alpha_from_RGBA only supports 8-bit interleaved RGBA"}; + } + + if (Error err = add_channel(heif_channel_Y, width, height, src_image->get_bits_per_pixel(heif_channel_interleaved), limits)) { + return err; + } + + uint8_t* dst; + size_t dst_stride = 0; + + const uint8_t* src; + size_t src_stride = 0; + + src = src_image->get_channel_memory(heif_channel_interleaved, &src_stride); + dst = get_channel_memory(heif_channel_Y, &dst_stride); + + //int bpl = width * (src_image->get_storage_bits_per_pixel(src_channel) / 8); + + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + dst[y * dst_stride + x] = src[y * src_stride + 4 * x + 3]; + } + } + + return Error::Ok; +} + + +Error HeifPixelImage::fill_new_channel(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, + const heif_security_limits* limits) +{ + if (Error err = add_channel(dst_channel, width, height, bpp, limits)) { + return err; + } + + fill_channel(dst_channel, value); + + return Error::Ok; +} + + +void HeifPixelImage::fill_channel(heif_channel dst_channel, uint16_t value) +{ + int num_interleaved = num_interleaved_components_per_plane(m_chroma); + + int bpp = get_bits_per_pixel(dst_channel); + uint32_t width = get_width(dst_channel); + uint32_t height = get_height(dst_channel); + + if (bpp <= 8) { + uint8_t* dst; + size_t dst_stride = 0; + dst = get_channel_memory(dst_channel, &dst_stride); + size_t width_bytes = static_cast(width) * num_interleaved; + + for (uint32_t y = 0; y < height; y++) { + memset(dst + y * dst_stride, value, width_bytes); + } + } + else { + uint16_t* dst; + size_t dst_stride = 0; + dst = get_channel_memory(dst_channel, &dst_stride); + dst_stride /= sizeof(uint16_t); + + size_t row_size = static_cast(width) * num_interleaved; + for (uint32_t y = 0; y < height; y++) { + for (size_t x = 0; x < row_size; x++) { + dst[y * dst_stride + x] = value; + } + } + } +} + + +Error HeifPixelImage::transfer_channel_from_image_as(const std::shared_ptr& source, + heif_channel src_channel, + heif_channel dst_channel) +{ + // A destination image must never end up with two planes for the same channel: + // find_storage_for_channel() and every method built on it (get_bits_per_pixel(), + // get_channel_memory(), get_width()/get_height()) only ever look at the first + // match, so a second, differently-sized/differently-typed plane for the same + // channel would silently be invisible to size queries while still being iterated + // (and written to) by code that walks m_storage directly, e.g. scale_nearest_neighbor(). + if (find_storage_for_channel(dst_channel) != nullptr) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Destination image already has a plane for this channel"}; + } + + // Find and remove the component from source + ComponentStorage plane; + bool source_channel_found = false; + for (auto it = source->m_storage.begin(); it != source->m_storage.end(); ++it) { + if (it->m_channel == src_channel) { + plane = *it; + source->m_storage.erase(it); + source_channel_found = true; + break; + } + } + + if (!source_channel_found) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Channel cannot be transferred because it was not found in the source image."}; + } + + source->m_memory_handle.free(plane.allocation_size); + + // Move the matching ComponentDescription(s) from source to destination. + // The plane's old ids belong to source's m_next_component_id space and may + // collide with destination's ids, so we mint fresh ids on destination and + // rewrite plane.m_component_ids accordingly. Source's descriptions for the + // moved ids are dropped (the buffer is gone). + std::vector new_ids; + new_ids.reserve(plane.m_component_ids.size()); + for (uint32_t old_id : plane.m_component_ids) { + // Take the source description (snapshot; the source entry is removed below). + ComponentDescription desc; + if (auto* src_desc = source->find_component_description(old_id)) { + desc = *src_desc; + } + // Mint a destination id and reset description fields that change on transfer. + desc.component_id = mint_component_id(); + desc.channel = dst_channel; + // Re-derive the cmpd component_type from the new channel so the + // transferred plane is described as e.g. "alpha" rather than carrying + // over the source's "monochrome"/"Y" type. (The most common case is + // moving an alpha aux item's Y plane onto a main image as Alpha.) + auto types = map_channel_to_component_type(dst_channel, heif_chroma_undefined); + if (!types.empty()) { + desc.component_type = types[0]; + } + new_ids.push_back(desc.component_id); + add_component_description(std::move(desc)); + + // Drop the source's description entry for old_id. + source->remove_component_description(old_id); + } + plane.m_component_ids = std::move(new_ids); + plane.m_channel = dst_channel; + m_storage.push_back(plane); + + // Note: we assume that image planes are never transferred between heif_contexts + m_memory_handle.alloc(plane.allocation_size, + source->m_memory_handle.get_security_limits(), + "transferred image data"); + + return Error::Ok; +} + + +bool is_interleaved_with_alpha(heif_chroma chroma) +{ + switch (chroma) { + case heif_chroma_undefined: + case heif_chroma_monochrome: + case heif_chroma_420: + case heif_chroma_422: + case heif_chroma_444: + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBB_LE: + return false; + + case heif_chroma_interleaved_RGBA: + case heif_chroma_interleaved_RRGGBBAA_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + return true; + } + + assert(false); + return false; +} + + +Error HeifPixelImage::copy_image_to(const std::shared_ptr& source, uint32_t x0, uint32_t y0) +{ + std::set channels = source->get_channel_set(); + + uint32_t w = get_width(); + uint32_t h = get_height(); + heif_chroma chroma = get_chroma_format(); + + + for (heif_channel channel : channels) { + + // The source channel set may contain channels that this image does not + // have. get_channel_memory() would return nullptr for those, so skip them + // instead of dereferencing a null pointer. + if (!has_channel(channel)) { + continue; + } + + size_t tile_stride; + const uint8_t* tile_data = source->get_channel_memory(channel, &tile_stride); + + size_t out_stride; + uint8_t* out_data = get_channel_memory(channel, &out_stride); + + if (w <= x0 || h <= y0) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_grid_data}; + } + + if (source->get_bits_per_pixel(channel) != get_bits_per_pixel(channel)) { + return {heif_error_Invalid_input, + heif_suberror_Wrong_tile_image_pixel_depth}; + } + + uint32_t src_width = source->get_width(channel); + uint32_t src_height = source->get_height(channel); + + uint32_t xs = channel_width(x0, chroma, channel); + uint32_t ys = channel_height(y0, chroma, channel); + + // Compute copy size from actual plane bounds to avoid chroma rounding mismatch. + // channel_height(y0) + channel_height(h - y0) can exceed channel_height(h) with 4:2:0 + // due to ceiling division, so we use (plane_size - offset) instead. + uint32_t copy_width = std::min(src_width, channel_width(w, chroma, channel) - xs); + uint32_t copy_height = std::min(src_height, channel_height(h, chroma, channel) - ys); + + copy_width *= source->get_storage_bits_per_pixel(channel) / 8; + xs *= source->get_storage_bits_per_pixel(channel) / 8; + + for (uint32_t py = 0; py < copy_height; py++) { + memcpy(out_data + xs + (ys + py) * out_stride, + tile_data + py * tile_stride, + copy_width); + } + } + + return Error::Ok; +} + + +Result> HeifPixelImage::rotate_ccw(int angle_degrees, const heif_security_limits* limits) +{ + // TODO: Bayer pattern, polarization patterns and sensor maps reference + // image geometry and are currently copied verbatim by + // forward_all_metadata_from(). For 90/270° rotations the layout is + // transposed and for 180° it is flipped, so the copied metadata is no + // longer semantically valid. Either rotate these structures along with + // the pixels, or return an error when such metadata is present and + // rotation would invalidate it. + + // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before rotation + + bool need_conversion = false; + + if (get_chroma_format() == heif_chroma_422) { + if (angle_degrees == 90 || angle_degrees == 270) { + need_conversion = true; + } + else if (angle_degrees == 180 && has_odd_height()) { + need_conversion = true; + } + } + else if (get_chroma_format() == heif_chroma_420) { + if (angle_degrees == 90 && has_odd_width()) { + need_conversion = true; + } + else if (angle_degrees == 180 && (has_odd_width() || has_odd_height())) { + need_conversion = true; + } + else if (angle_degrees == 270 && has_odd_height()) { + need_conversion = true; + } + } + + if (need_conversion) { + heif_color_conversion_options options{}; + heif_color_conversion_options_set_defaults(&options); + + auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile(), // default, undefined + get_bits_per_pixel(heif_channel_Y), options, nullptr, limits); + if (!converted_image_result) { + return converted_image_result.error(); + } + + return (*converted_image_result)->rotate_ccw(angle_degrees, limits); + } + + + // --- create output image + + if (angle_degrees == 0) { + return shared_from_this(); + } + + uint32_t out_width = m_width; + uint32_t out_height = m_height; + + if (angle_degrees == 90 || angle_degrees == 270) { + std::swap(out_width, out_height); + } + + std::shared_ptr out_img = std::make_shared(); + out_img->create(out_width, out_height, m_colorspace, m_chroma); + out_img->copy_metadata_from(*this); + + + // --- rotate all channels + + for (const auto &component: m_storage) { + uint32_t out_plane_width = component.m_width; + uint32_t out_plane_height = component.m_height; + + if (angle_degrees == 90 || angle_degrees == 270) { + std::swap(out_plane_width, out_plane_height); + } + + ComponentStorage out_component; + out_component.m_channel = component.m_channel; + + if (Error err = out_component.alloc(out_plane_width, out_plane_height, + component.m_datatype, component.m_bit_depth, + component.m_num_interleaved_components, + limits, out_img->m_memory_handle)) { + return err; + } + + // Clone per-component metadata (component_type, gimi_content_id, ...) + // from the source descriptions rather than re-deriving from chroma, so + // images built via add_component() preserve their original component + // types and content ids. + std::vector src_descs; + src_descs.reserve(component.m_component_ids.size()); + for (uint32_t cid : component.m_component_ids) { + src_descs.push_back(find_component_description(cid)); + } + out_img->register_component_descriptions(out_component, src_descs); + + if (component.m_bit_depth <= 8) { + component.rotate_ccw(angle_degrees, out_component); + } + else if (component.m_bit_depth <= 16) { + component.rotate_ccw(angle_degrees, out_component); + } + else if (component.m_bit_depth <= 32) { + component.rotate_ccw(angle_degrees, out_component); + } + else if (component.m_bit_depth <= 64) { + component.rotate_ccw(angle_degrees, out_component); + } + else if (component.m_bit_depth <= 128) { + component.rotate_ccw(angle_degrees, out_component); + } + else { + std::stringstream sstr; + sstr << "Cannot rotate images with " << component.m_bit_depth << " bits per pixel"; + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + sstr.str()}; + } + + out_img->m_storage.push_back(std::move(out_component)); + } + + out_img->add_warnings(get_warnings()); + + return out_img; +} + +template +void HeifPixelImage::ComponentStorage::rotate_ccw(int angle_degrees, + ComponentStorage& out_plane) const +{ + uint32_t w = m_width; + uint32_t h = m_height; + + size_t in_stride = stride / sizeof(T); + const T* in_data = static_cast(mem); + + size_t out_stride = out_plane.stride / sizeof(T); + T* out_data = static_cast(out_plane.mem); + + if (angle_degrees == 270) { + for (uint32_t x = 0; x < h; x++) + for (uint32_t y = 0; y < w; y++) { + out_data[y * out_stride + x] = in_data[(h - 1 - x) * in_stride + y]; + } + } else if (angle_degrees == 180) { + for (uint32_t y = 0; y < h; y++) + for (uint32_t x = 0; x < w; x++) { + out_data[y * out_stride + x] = in_data[(h - 1 - y) * in_stride + (w - 1 - x)]; + } + } else if (angle_degrees == 90) { + for (uint32_t x = 0; x < h; x++) + for (uint32_t y = 0; y < w; y++) { + out_data[y * out_stride + x] = in_data[x * in_stride + (w - 1 - y)]; + } + } +} + + +template +void HeifPixelImage::ComponentStorage::mirror_inplace(heif_transform_mirror_direction direction) +{ + uint32_t w = m_width; + uint32_t h = m_height; + + T* data = static_cast(mem); + + if (direction == heif_transform_mirror_direction_horizontal) { + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w / 2; x++) + std::swap(data[y * stride / sizeof(T) + x], data[y * stride / sizeof(T) + w - 1 - x]); + } + } else { + for (uint32_t y = 0; y < h / 2; y++) { + for (uint32_t x = 0; x < w; x++) + std::swap(data[y * stride / sizeof(T) + x], data[(h - 1 - y) * stride / sizeof(T) + x]); + } + } +} + + +Result> HeifPixelImage::mirror_inplace(heif_transform_mirror_direction direction, + const heif_security_limits* limits) +{ + // TODO: Bayer pattern, polarization patterns and sensor maps reference + // image geometry. This function mirrors the pixel data in place but + // leaves those structures untouched, so a horizontal/vertical mirror + // leaves them out of sync with the pixel layout. Either mirror these + // structures along with the pixels, or return an error when such + // metadata is present and the mirror would invalidate it. + + // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before rotation + + bool need_conversion = false; + + if (get_chroma_format() == heif_chroma_422) { + if (direction == heif_transform_mirror_direction_horizontal && has_odd_width()) { + need_conversion = true; + } + } + else if (get_chroma_format() == heif_chroma_420) { + if (has_odd_width() || has_odd_height()) { + need_conversion = true; + } + } + + if (need_conversion) { + heif_color_conversion_options options{}; + heif_color_conversion_options_set_defaults(&options); + + auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile(), // default, undefined + get_bits_per_pixel(heif_channel_Y), options, nullptr, limits); + if (!converted_image_result) { + return converted_image_result.error(); + } + + return (*converted_image_result)->mirror_inplace(direction, limits); + } + + + for (auto& component : m_storage) { + if (component.m_bit_depth <= 8) { + component.mirror_inplace(direction); + } + else if (component.m_bit_depth <= 16) { + component.mirror_inplace(direction); + } + else if (component.m_bit_depth <= 32) { + component.mirror_inplace(direction); + } + else if (component.m_bit_depth <= 64) { + component.mirror_inplace(direction); + } + else if (component.m_bit_depth <= 128) { + component.mirror_inplace(direction); + } + else { + std::stringstream sstr; + sstr << "Cannot mirror images with " << component.m_bit_depth << " bits per pixel"; + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + sstr.str()}; + } + } + + return shared_from_this(); +} + + +int HeifPixelImage::ComponentStorage::get_bytes_per_pixel() const +{ + return m_bytes_per_pixel; +} + + +Result> HeifPixelImage::crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, + const heif_security_limits* limits) const +{ + // TODO: Bayer pattern, polarization patterns and sensor maps reference + // image geometry and are currently copied verbatim by + // forward_all_metadata_from(). A crop shifts the (0,0) origin and + // changes the image dimensions, so the copied metadata may no longer + // match the cropped image (e.g. a 2x2 Bayer pattern with an odd + // left/top offset, or a sensor NUC map sized to the original image). + // Either translate / resample these structures to the crop region, or + // return an error when the crop would invalidate them. + + // (left, right, top, bottom) are coordinate endpoints of the kept region. + // Reject inverted or out-of-bounds rectangles so the unsigned arithmetic + // below cannot underflow into a multi-GB memcpy (issue #1746). + if (right < left || bottom < top || right >= m_width || bottom >= m_height) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Invalid crop region"}; + } + + // ComponentStorage::crop() below maps (left,right,top,bottom) -- already bounded + // against m_width/m_height above -- into per-plane coordinates via + // get_subsampled_size_h/v(), then bulk-memcpy's each row assuming the plane + // actually covers that geometry. Verify that first; a plane smaller than + // m_width/m_height implies (e.g. from Alpha attached without a size check, or + // any other producer that doesn't enforce this) would otherwise be read past + // its end. + if (!has_standard_plane_sizes()) { + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Cropping an image with non-standard plane sizes is not supported"}; + } + + // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before cropping + + bool need_conversion = false; + + if (get_chroma_format() == heif_chroma_422 && (left & 1) == 1) { + need_conversion = true; + } + else if (get_chroma_format() == heif_chroma_420 && + ((left & 1) == 1 || (top & 1) == 1)) { + need_conversion = true; + } + + if (need_conversion) { + heif_color_conversion_options options{}; + heif_color_conversion_options_set_defaults(&options); + + auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile(), // default, undefined + get_bits_per_pixel(heif_channel_Y), options, nullptr, limits); + + if (!converted_image_result) { + return converted_image_result.error(); + } + + return (*converted_image_result)->crop(left, right, top, bottom, limits); + } + + + + auto out_img = std::make_shared(); + out_img->create(right - left + 1, bottom - top + 1, m_colorspace, m_chroma); + out_img->copy_metadata_from(*this); + + + // --- crop all channels + + for (const auto& component : m_storage) { + heif_channel channel = component.m_channel; + + uint32_t plane_left = get_subsampled_size_h(left, channel, m_chroma, scaling_mode::is_divisible); // is always divisible + uint32_t plane_right = get_subsampled_size_h(right, channel, m_chroma, scaling_mode::round_down); // this keeps enough chroma since 'right' is a coordinate and not the width + uint32_t plane_top = get_subsampled_size_v(top, channel, m_chroma, scaling_mode::is_divisible); + uint32_t plane_bottom = get_subsampled_size_v(bottom, channel, m_chroma, scaling_mode::round_down); + + ComponentStorage out_plane; + out_plane.m_channel = channel; + + if (Error err = out_plane.alloc(plane_right - plane_left + 1, + plane_bottom - plane_top + 1, + component.m_datatype, component.m_bit_depth, + component.m_num_interleaved_components, + limits, out_img->m_memory_handle)) { + return err; + } + + // Clone per-component metadata (component_type, gimi_content_id, ...) + // from the source descriptions rather than re-deriving from chroma, so + // images built via add_component() preserve their original component + // types and content ids. + std::vector src_descs; + src_descs.reserve(component.m_component_ids.size()); + for (uint32_t cid : component.m_component_ids) { + src_descs.push_back(find_component_description(cid)); + } + out_img->register_component_descriptions(out_plane, src_descs); + + int bytes_per_pixel = component.get_bytes_per_pixel(); + component.crop(plane_left, plane_right, plane_top, plane_bottom, bytes_per_pixel, out_plane); + + out_img->m_storage.push_back(std::move(out_plane)); + } + + out_img->add_warnings(get_warnings()); + + return out_img; +} + + +void HeifPixelImage::ComponentStorage::crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, + int bytes_per_pixel, ComponentStorage& out_plane) const +{ + size_t in_stride = stride; + auto* in_data = static_cast(mem); + + size_t out_stride = out_plane.stride; + auto* out_data = static_cast(out_plane.mem); + + for (uint32_t y = top; y <= bottom; y++) { + memcpy(&out_data[(y - top) * out_stride], + &in_data[y * in_stride + left * bytes_per_pixel], + (right - left + 1) * bytes_per_pixel); + } +} + + +Error HeifPixelImage::fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_t a) +{ + for (const auto& channel : {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}) { + + auto* comp = find_storage_for_channel(channel); + if (!comp) { + + // alpha channel is optional, R,G,B is required + if (channel == heif_channel_Alpha) { + continue; + } + + return {heif_error_Usage_error, + heif_suberror_Nonexisting_image_channel_referenced}; + + } + + ComponentStorage& plane = *comp; + + if (plane.m_bit_depth != 8) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Can currently only fill images with 8 bits per pixel"}; + } + + size_t h = plane.m_height; + + size_t stride = plane.stride; + auto* data = static_cast(plane.mem); + + uint16_t val16; + switch (channel) { + case heif_channel_R: + val16 = r; + break; + case heif_channel_G: + val16 = g; + break; + case heif_channel_B: + val16 = b; + break; + case heif_channel_Alpha: + val16 = a; + break; + default: + // initialization only to avoid warning of uninitialized variable. + val16 = 0; + // Should already be detected by the check above ("find_storage_for_channel"). + assert(false); + } + + auto val8 = static_cast(val16 >> 8U); + + + // memset() even when h * stride > sizeof(size_t) + + if (std::numeric_limits::max() / stride > h) { + // can fill in one step + memset(data, val8, stride * h); + } + else { + // fill line by line + auto* p = data; + + for (size_t y=0;y(INT32_MAX) + 1; + } + else { + return static_cast(-x); + } +} + + +Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t dx, int32_t dy) +{ + // This function places the overlay using the full-resolution (dx,dy) offset + // directly as a per-plane offset. That is only correct when every plane has + // the full logical image size, i.e. for non-subsampled chroma formats. + // Subsampled chroma (4:2:0 / 4:2:2) would be mis-placed and could even write + // outside of the smaller Cb/Cr planes. + auto has_subsampled_chroma = [](heif_chroma chroma) { + return chroma == heif_chroma_420 || chroma == heif_chroma_422; + }; + + if (has_subsampled_chroma(get_chroma_format()) || + has_subsampled_chroma(overlay->get_chroma_format())) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Overlaying images with subsampled chroma is not supported"}; + } + + std::set channels = overlay->get_channel_set(); + + bool has_alpha = overlay->has_channel(heif_channel_Alpha); + //bool has_alpha_me = has_channel(heif_channel_Alpha); + + // The blend loop below indexes the Alpha plane using the extent of each color + // channel (in_w/in_h, out_w/out_h), not the Alpha plane's own reported extent. + // If the Alpha plane were smaller than the other channels, that would read past + // its allocation, so reject that case up front instead of trusting the sizes + // to agree. + // Note that differently sized Alpha channels are allowed, but we currently do + // not support it here (TODO). + if (has_alpha && + (overlay->get_width(heif_channel_Alpha) != overlay->get_width() || + overlay->get_height(heif_channel_Alpha) != overlay->get_height())) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Overlay image Alpha plane size does not match the other color planes"}; + } + + size_t alpha_stride = 0; + uint8_t* alpha_p; + alpha_p = overlay->get_channel_memory(heif_channel_Alpha, &alpha_stride); + + for (heif_channel channel : channels) { + if (!has_channel(channel)) { + continue; + } + + size_t in_stride = 0; + const uint8_t* in_p; + + size_t out_stride = 0; + uint8_t* out_p; + + in_p = overlay->get_channel_memory(channel, &in_stride); + out_p = get_channel_memory(channel, &out_stride); + + uint32_t in_w = overlay->get_width(channel); + uint32_t in_h = overlay->get_height(channel); + + uint32_t out_w = get_width(channel); + uint32_t out_h = get_height(channel); + + + // --- check whether overlay image overlaps with current image + // Note: all components share the logical image size, so if the overlay + // image lies completely outside for one component it does so for all of + // them -> we can return instead of just skipping the current component. + + if (dx > 0 && static_cast(dx) >= out_w) { + // the overlay image is completely outside the right border -> skip overlaying + return Error::Ok; + } + else if (dx < 0 && in_w <= negate_negative_int32(dx)) { + // the overlay image is completely outside the left border -> skip overlaying + return Error::Ok; + } + + if (dy > 0 && static_cast(dy) >= out_h) { + // the overlay image is completely outside the bottom border -> skip overlaying + return Error::Ok; + } + else if (dy < 0 && in_h <= negate_negative_int32(dy)) { + // the overlay image is completely outside the top border -> skip overlaying + return Error::Ok; + } + + + // --- compute overlapping area + + // top-left points where to start copying in source and destination + uint32_t in_x0; + uint32_t in_y0; + uint32_t out_x0; + uint32_t out_y0; + + // right border + if (dx + static_cast(in_w) > out_w) { + // overlay image extends partially outside of right border + // Notes: + // - (out_w-dx) cannot underflow because dx(static_cast(out_w) - dx); + } + + // bottom border + if (dy + static_cast(in_h) > out_h) { + // overlay image extends partially outside of bottom border + in_h = static_cast(static_cast(out_h) - dy); + } + + // left border + if (dx < 0) { + // overlay image starts partially outside of left border + + in_x0 = negate_negative_int32(dx); + out_x0 = 0; + in_w = in_w - in_x0; // in_x0 < in_w because in_w > -dx = in_x0 + } + else { + in_x0 = 0; + out_x0 = static_cast(dx); + } + + // top border + if (dy < 0) { + // overlay image started partially outside of top border + + in_y0 = negate_negative_int32(dy); + out_y0 = 0; + in_h = in_h - in_y0; // in_y0 < in_h because in_h > -dy = in_y0 + } + else { + in_y0 = 0; + out_y0 = static_cast(dy); + } + + // --- computer overlay in overlapping area + + for (uint32_t y = in_y0; y < in_h; y++) { + if (!has_alpha) { + memcpy(out_p + out_x0 + (out_y0 + y - in_y0) * out_stride, + in_p + in_x0 + y * in_stride, + in_w); + } + else { + for (uint32_t x = in_x0; x < in_w; x++) { + uint8_t* outptr = &out_p[out_x0 + (out_y0 + y - in_y0) * out_stride + x]; + uint8_t in_val = in_p[in_x0 + y * in_stride + x]; + uint8_t alpha_val = alpha_p[in_x0 + y * alpha_stride + x]; + + *outptr = (uint8_t) ((in_val * alpha_val + *outptr * (255 - alpha_val)) / 255); + } + } + } + } + + return Error::Ok; +} + + +// TODO: rewrite this to handle multi-spectral images. +Error HeifPixelImage::scale_nearest_neighbor(std::shared_ptr& out_img, + uint32_t width, uint32_t height, + const heif_security_limits* limits) const +{ + // The per-channel loop below indexes each source plane using coordinates + // derived from m_width/m_height (or, for Cb/Cr, their chroma-subsampled + // size), trusting that the plane actually covers that geometry. Nothing + // else in this class enforces that as an invariant (a plane can be added at + // any size through add_channel()/copy_new_channel_from()/ + // transfer_channel_from_image_as()), so verify it explicitly before doing + // any work. + if (!has_standard_plane_sizes()) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Scaling an image with non-standard plane sizes is not supported"}; + } + + out_img = std::make_shared(); + out_img->create(width, height, m_colorspace, m_chroma); + + + // --- create output image with scaled planes + + if (has_channel(heif_channel_interleaved)) { + if (auto err = out_img->add_channel(heif_channel_interleaved, width, height, get_bits_per_pixel(heif_channel_interleaved), limits)) { + return err; + } + } + else { + if (get_colorspace() == heif_colorspace_RGB) { + if (!has_channel(heif_channel_R) || + !has_channel(heif_channel_G) || + !has_channel(heif_channel_B)) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "RGB input without R,G,B, planes"}; + } + + if (auto err = out_img->add_channel(heif_channel_R, width, height, get_bits_per_pixel(heif_channel_R), limits)) { + return err; + } + if (auto err = out_img->add_channel(heif_channel_G, width, height, get_bits_per_pixel(heif_channel_G), limits)) { + return err; + } + if (auto err = out_img->add_channel(heif_channel_B, width, height, get_bits_per_pixel(heif_channel_B), limits)) { + return err; + } + } + else if (get_colorspace() == heif_colorspace_monochrome) { + if (!has_channel(heif_channel_Y)) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "monochrome input with no Y plane"}; + } + + if (auto err = out_img->add_channel(heif_channel_Y, width, height, get_bits_per_pixel(heif_channel_Y), limits)) { + return err; + } + } + else if (get_colorspace() == heif_colorspace_YCbCr) { + if (!has_channel(heif_channel_Y) || + !has_channel(heif_channel_Cb) || + !has_channel(heif_channel_Cr)) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "YCbCr image without Y,Cb,Cr planes"}; + } + + uint32_t cw, ch; + get_subsampled_size(width, height, heif_channel_Cb, get_chroma_format(), &cw, &ch); + if (auto err = out_img->add_channel(heif_channel_Y, width, height, get_bits_per_pixel(heif_channel_Y), limits)) { + return err; + } + if (auto err = out_img->add_channel(heif_channel_Cb, cw, ch, get_bits_per_pixel(heif_channel_Cb), limits)) { + return err; + } + if (auto err = out_img->add_channel(heif_channel_Cr, cw, ch, get_bits_per_pixel(heif_channel_Cr), limits)) { + return err; + } + } + else { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "unknown color configuration"}; + } + + if (has_channel(heif_channel_Alpha)) { + if (auto err = out_img->add_channel(heif_channel_Alpha, width, height, get_bits_per_pixel(heif_channel_Alpha), limits)) { + return err; + } + } + } + + // The per-channel loop below trusts that every source plane has a matching, + // correctly-sized destination plane, established by has_channel() checks + // per iteration. + // This only works for ordinary images. Images with extra planes cannot + // currently be scaled (TODO). + if (m_storage.size() > out_img->m_storage.size()) { + return {heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Images with extra planes are not supported by scale_nearest_neighbor()."}; + } + + + // --- scale all channels + + int nInterleaved = num_interleaved_components_per_plane(m_chroma); + if (nInterleaved > 1) { + const auto* comp = find_storage_for_channel(heif_channel_interleaved); + // m_chroma alone decides this branch, not which channels m_storage actually holds -- an + // image can have m_chroma set to an interleaved format while carrying separate R/G/B + // planes instead (has_standard_plane_sizes() and the allocation above both accept that: + // neither one cross-checks the chroma format against which channel was actually added). + // Reachable directly through the public API with no decode involved. + if (comp == nullptr) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "Interleaved chroma format without an interleaved plane"}; + } + const ComponentStorage& plane = *comp; + + uint32_t out_w = out_img->get_width(heif_channel_interleaved); + uint32_t out_h = out_img->get_height(heif_channel_interleaved); + + if (plane.m_bit_depth <= 8) { + // SDR interleaved + + size_t in_stride = plane.stride; + const auto* in_data = static_cast(plane.mem); + + size_t out_stride = 0; + auto* out_data = out_img->get_channel_memory(heif_channel_interleaved, &out_stride); + + for (uint32_t y = 0; y < out_h; y++) { + uint32_t iy = static_cast(static_cast(y) * m_height / height); + + for (uint32_t x = 0; x < out_w; x++) { + uint32_t ix = static_cast(static_cast(x) * m_width / width); + + for (int c = 0; c < nInterleaved; c++) { + out_data[y * out_stride + x * nInterleaved + c] = in_data[iy * in_stride + ix * nInterleaved + c]; + } + } + } + } + else { + // HDR interleaved + // TODO: untested + + size_t in_stride = plane.stride; + const uint16_t* in_data = static_cast(plane.mem); + + size_t out_stride = 0; + uint16_t* out_data = out_img->get_channel_memory(heif_channel_interleaved, &out_stride); + + in_stride /= 2; + out_stride /= 2; + + for (uint32_t y = 0; y < out_h; y++) { + uint32_t iy = static_cast(static_cast(y) * m_height / height); + + for (uint32_t x = 0; x < out_w; x++) { + uint32_t ix = static_cast(static_cast(x) * m_width / width); + + for (int c = 0; c < nInterleaved; c++) { + out_data[y * out_stride + x * nInterleaved + c] = in_data[iy * in_stride + ix * nInterleaved + c]; + } + } + } + } + } + else { + for (const auto& component : m_storage) { + heif_channel channel = component.m_channel; + const ComponentStorage& plane = component; + + if (!out_img->has_channel(channel)) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, "scaling input has extra color plane"}; + } + + + uint32_t out_w = out_img->get_width(channel); + uint32_t out_h = out_img->get_height(channel); + + if (plane.m_bit_depth <= 8) { + // SDR planar + + size_t in_stride = plane.stride; + const auto* in_data = static_cast(plane.mem); + + size_t out_stride = 0; + auto* out_data = out_img->get_channel_memory(channel, &out_stride); + + for (uint32_t y = 0; y < out_h; y++) { + uint32_t iy = static_cast(static_cast(y) * m_height / height); + + for (uint32_t x = 0; x < out_w; x++) { + uint32_t ix = static_cast(static_cast(x) * m_width / width); + + out_data[y * out_stride + x] = in_data[iy * in_stride + ix]; + } + } + } + else { + // HDR planar + + size_t in_stride = plane.stride; + const uint16_t* in_data = static_cast(plane.mem); + + size_t out_stride = 0; + uint16_t* out_data = out_img->get_channel_memory(channel, &out_stride); + + in_stride /= 2; + out_stride /= 2; + + for (uint32_t y = 0; y < out_h; y++) { + uint32_t iy = static_cast(static_cast(y) * m_height / height); + + for (uint32_t x = 0; x < out_w; x++) { + uint32_t ix = static_cast(static_cast(x) * m_width / width); + + out_data[y * out_stride + x] = in_data[iy * in_stride + ix]; + } + } + } + } + } + + return Error::Ok; +} + + +void HeifPixelImage::debug_dump() const +{ + auto channels = get_channel_set(); + for (auto c : channels) { + size_t stride = 0; + const uint8_t* p = get_channel_memory(c, &stride); + + // clamp the dump region to the actual plane size to avoid reading past it + uint32_t dump_w = std::min(get_width(c), 8u); + uint32_t dump_h = std::min(get_height(c), 8u); + + for (uint32_t y = 0; y < dump_h; y++) { + for (uint32_t x = 0; x < dump_w; x++) { + printf("%02x ", p[y * stride + x]); + } + printf("\n"); + } + } +} + +Error HeifPixelImage::create_clone_image_at_new_size(const std::shared_ptr& source, uint32_t w, uint32_t h, + const heif_security_limits* limits) +{ + heif_colorspace colorspace = source->get_colorspace(); + heif_chroma chroma = source->get_chroma_format(); + + create(w, h, colorspace, chroma); + + for (const auto& src_plane : source->m_storage) { + // TODO: do we also support images where some planes (e.g. the alpha-plane) have a different size than the main image? + // We could do this by scaling all planes proportionally. This would also handle chroma channels implicitly. + uint32_t plane_w = channel_width(w, chroma, src_plane.m_channel); + uint32_t plane_h = channel_height(h, chroma, src_plane.m_channel); + + ComponentStorage plane; + plane.m_channel = src_plane.m_channel; + plane.m_component_ids = src_plane.m_component_ids; + + if (auto err = plane.alloc(plane_w, plane_h, src_plane.m_datatype, src_plane.m_bit_depth, + src_plane.m_num_interleaved_components, limits, m_memory_handle)) { + return err; + } + + m_storage.push_back(plane); + } + + // The source's descriptions carry the source's geometry; the planes above + // were allocated at the new (w,h) size, so descriptions must be resized to + // match — otherwise get_component_width/height returns stale source dims. + auto descs = source->get_component_descriptions(); + for (auto& desc : descs) { + desc.width = channel_width(w, chroma, desc.channel); + desc.height = channel_height(h, chroma, desc.channel); + } + set_component_descriptions(std::move(descs), source->peek_next_component_id()); + + copy_metadata_from(*source); + + return Error::Ok; +} + + +Result> +HeifPixelImage::extract_image_area(uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, + const heif_security_limits* limits) const +{ + // The top-left corner must lie inside the image. Without this check, + // get_width() - x0 (and the per-channel offsets derived from x0/y0) would + // underflow and the copy loop below would read far outside the source planes. + if (x0 >= get_width() || y0 >= get_height()) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "extract_image_area: top-left position is outside the image"}; + } + + // The per-channel copy below clamps against the chroma-mapped *declared* + // image extent, not each plane's own actual extent, and xs/ys are also + // derived from the declared geometry -- so a plane smaller than that implies + // (same enabling condition as crop()/scale_nearest_neighbor()) both reads + // past its end and, since xs/ys can then exceed the plane's real bounds, + // underflows the unsigned subtraction feeding the memcpy length. Reject the + // same inconsistent state crop() now does. + if (!has_standard_plane_sizes()) { + return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, + "Extracting an area from an image with non-standard plane sizes is not supported"}; + } + + uint32_t minW = std::min(w, get_width() - x0); + uint32_t minH = std::min(h, get_height() - y0); + + auto areaImg = std::make_shared(); + Error err = areaImg->create_clone_image_at_new_size(shared_from_this(), minW, minH, limits); + if (err) { + return err; + } + + std::set channels = get_channel_set(); + heif_chroma chroma = get_chroma_format(); + + for (heif_channel channel : channels) { + + size_t src_stride; + const uint8_t* src_data = get_channel_memory(channel, &src_stride); + + size_t out_stride; + uint8_t* out_data = areaImg->get_channel_memory(channel, &out_stride); + + if (areaImg->get_bits_per_pixel(channel) != get_bits_per_pixel(channel)) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Wrong_tile_image_pixel_depth + }; + } + + uint32_t xs = channel_width(x0, chroma, channel); + uint32_t ys = channel_height(y0, chroma, channel); + + // Clamp copy size to source plane bounds to avoid chroma rounding mismatch OOB read. + uint32_t src_plane_h = channel_height(get_height(), chroma, channel); + uint32_t src_plane_w = channel_width(get_width(), chroma, channel); + uint32_t copy_width = std::min(channel_width(minW, chroma, channel), src_plane_w - xs); + uint32_t copy_height = std::min(channel_height(minH, chroma, channel), src_plane_h - ys); + + copy_width *= get_storage_bits_per_pixel(channel) / 8; + xs *= get_storage_bits_per_pixel(channel) / 8; + + for (uint32_t py = 0; py < copy_height; py++) { + memcpy(out_data + py * out_stride, + src_data + xs + (ys + py) * src_stride, + copy_width); + } + } + + err = areaImg->extend_to_size_with_zero(w,h,limits); + if (err) { + return err; + } + + return areaImg; +} + + +// --- index-based component access methods + +HeifPixelImage::ComponentStorage* HeifPixelImage::find_storage_for_component(uint32_t component_id) +{ + for (auto& plane : m_storage) { + // we search through all indices in case we have an interleaved plane + if (std::find(plane.m_component_ids.begin(), + plane.m_component_ids.end(), + component_id) != plane.m_component_ids.end()) { + return &plane; + } + } + return nullptr; +} + + +const HeifPixelImage::ComponentStorage* HeifPixelImage::find_storage_for_component(uint32_t component_id) const +{ + return const_cast(this)->find_storage_for_component(component_id); +} + + +heif_channel HeifPixelImage::get_component_channel(uint32_t component_id) const +{ + auto* desc = find_component_description(component_id); + assert(desc); + return desc->channel; +} + + +uint32_t HeifPixelImage::get_component_width(uint32_t component_id) const +{ + auto* desc = find_component_description(component_id); + assert(desc); + return desc->width; +} + + +uint32_t HeifPixelImage::get_component_height(uint32_t component_id) const +{ + auto* desc = find_component_description(component_id); + assert(desc); + return desc->height; +} + + +uint16_t HeifPixelImage::get_component_bits_per_pixel(uint32_t component_id) const +{ + auto* desc = find_component_description(component_id); + assert(desc); + return desc->bit_depth; +} + + +uint16_t HeifPixelImage::get_component_storage_bits_per_pixel(uint32_t component_id) const +{ + // Storage is a buffer-layout concern (alignment / padding), so this stays + // routed through ComponentStorage rather than the description. + auto* comp = find_storage_for_component(component_id); + assert(comp); + uint32_t bpp = comp->get_bytes_per_pixel() * 8; + assert(bpp); + return static_cast(bpp); +} + + +heif_component_datatype HeifPixelImage::get_component_datatype(uint32_t component_id) const +{ + auto* desc = find_component_description(component_id); + assert(desc); + return desc->datatype; +} + + +uint16_t HeifPixelImage::get_component_type(uint32_t component_id) const +{ + if (const auto* desc = find_component_description(component_id)) { + return desc->component_type; + } + return heif_cmpd_component_type_UNDEFINED; +} + + +std::vector HeifPixelImage::get_component_ids_interleaved() const +{ + const ComponentStorage* comp = find_storage_for_channel(heif_channel_interleaved); + assert(comp); + return comp->m_component_ids; +} + + +Result HeifPixelImage::add_component(uint32_t width, uint32_t height, + uint16_t component_type, + heif_component_datatype datatype, int bit_depth, + const heif_security_limits* limits) +{ + heif_channel channel = map_uncompressed_component_to_channel(component_type); + + ComponentStorage plane; + plane.m_channel = channel; + + if (Error err = plane.alloc(width, height, datatype, bit_depth, 1, limits, m_memory_handle)) { + return {err}; + } + + register_component_descriptions(plane, std::vector{component_type}); + uint32_t component_id = plane.m_component_ids.front(); + m_storage.push_back(std::move(plane)); + return component_id; +} + + +uint32_t HeifPixelImage::add_component_without_data(uint16_t component_type) +{ + uint32_t new_component_id = mint_component_id(); + + ComponentDescription desc; + desc.component_id = new_component_id; + desc.channel = map_uncompressed_component_to_channel(component_type); + desc.component_type = component_type; + desc.has_data_plane = false; + add_component_description(std::move(desc)); + + return new_component_id; +} + + +void HeifPixelImage::clone_component_descriptions_from(const ImageDescription& src) +{ + set_component_descriptions(src.get_component_descriptions(), + src.peek_next_component_id()); +} + + +void HeifPixelImage::apply_descriptions_from(const ImageDescription& src) +{ + const auto& src_descs = src.get_component_descriptions(); + if (src_descs.empty()) { + return; // nothing to apply (e.g. grid/iden ImageItem with no description) + } + + // Skip when this image's descriptions already match src's exactly. This + // is the unci decode path: the decoder used clone_component_descriptions_from + // (item) so the full description list (including any cpat reference-only + // entries with has_data_plane=false) was copied verbatim. Comparing the + // full lists also handles multiple planes that share a channel + // (e.g. unci multi-component-of-same-type), which the channel-keyed remap + // below can't represent. + const auto& my_descs = get_component_descriptions(); + if (my_descs.size() == src_descs.size()) { + bool already_aligned = true; + for (size_t i = 0; i < src_descs.size(); i++) { + if (my_descs[i].component_id != src_descs[i].component_id || + my_descs[i].channel != src_descs[i].channel) { + already_aligned = false; + break; + } + } + if (already_aligned) { + return; + } + } + + // Snapshot pre-remap descriptions keyed by channel (for any "extra" + // channels not in src that we need to keep, like alpha-from-aux). + std::map auto_minted_by_channel; + for (const auto& d : my_descs) { + auto_minted_by_channel[d.channel] = d; + } + + // Build a channel -> actual-plane-dimensions map. For tile decodes the + // src description carries full-image dims, but the decoded plane was + // allocated at tile size; the description we publish should match what + // the buffer actually contains. + std::map> plane_dims_by_channel; + for (const auto& plane : m_storage) { + plane_dims_by_channel[plane.m_channel] = {plane.m_width, plane.m_height}; + } + + // Build the new component list from src's data-plane descriptions and a + // channel -> src-id map. + std::vector new_components; + std::map src_id_by_channel; + for (const auto& d : src_descs) { + if (d.has_data_plane) { + ComponentDescription copy = d; + auto it = plane_dims_by_channel.find(d.channel); + if (it != plane_dims_by_channel.end()) { + copy.width = it->second.first; + copy.height = it->second.second; + } + new_components.push_back(copy); + src_id_by_channel[d.channel] = d.component_id; + } + } + + // Compute a starting id for any extras (above src's high-water mark). + uint32_t next_id = src.peek_next_component_id(); + for (const auto& d : new_components) { + if (d.component_id >= next_id) next_id = d.component_id + 1; + } + + // Remap each plane's m_component_ids by channel match against src; for + // channels not in src, re-add the auto-minted description with a fresh id. + for (auto& plane : m_storage) { + if (plane.m_component_ids.empty()) continue; + + heif_channel ch = plane.m_channel; + auto src_it = src_id_by_channel.find(ch); + if (src_it != src_id_by_channel.end()) { + plane.m_component_ids.assign(1, src_it->second); + } else { + auto auto_it = auto_minted_by_channel.find(ch); + if (auto_it != auto_minted_by_channel.end()) { + ComponentDescription extra = auto_it->second; + extra.component_id = next_id++; + new_components.push_back(extra); + plane.m_component_ids.assign(1, extra.component_id); + } + } + } + + set_component_descriptions(std::move(new_components), next_id); +} + + +Error HeifPixelImage::allocate_buffer_for_component(uint32_t component_id, + const heif_security_limits* limits) +{ + auto* desc = find_component_description(component_id); + if (!desc) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "allocate_buffer_for_component: unknown component id"}; + } + if (!desc->has_data_plane) { + return Error::Ok; // reference component (e.g. cpat); no buffer needed + } + + ComponentStorage plane; + plane.m_channel = desc->channel; + plane.m_component_ids = std::vector{component_id}; + if (Error err = plane.alloc(desc->width, desc->height, + desc->datatype, desc->bit_depth, + 1, limits, m_memory_handle)) { + return err; + } + m_storage.push_back(plane); + return Error::Ok; +} + + +#if 0 +Result HeifPixelImage::add_component_for_index(uint32_t component_index, + uint32_t width, uint32_t height, + heif_component_datatype datatype, int bit_depth, + const heif_security_limits* limits) +{ + if (component_index >= m_cmpd_component_types.size()) { + return Error{heif_error_Usage_error, heif_suberror_Invalid_parameter_value, + "component_index out of range of cmpd table"}; + } + + uint16_t component_type = m_cmpd_component_types[component_index]; + + ComponentStorage plane; + plane.m_channel = map_uncompressed_component_to_channel(component_type); + plane.m_component_index = std::vector{component_index}; + if (Error err = plane.alloc(width, height, datatype, bit_depth, 1, limits, m_memory_handle)) { + return err; + } + + m_storage.push_back(plane); + return component_index; +} +#endif + + +std::vector HeifPixelImage::get_used_component_ids() const +{ + const auto& descs = get_component_descriptions(); + std::vector indices; + indices.reserve(descs.size()); + + for (const auto& desc : descs) { + indices.push_back(desc.component_id); + } + + return indices; +} + + +std::vector HeifPixelImage::get_used_planar_component_ids() const +{ + std::vector indices; + + for (const auto& plane : m_storage) { + if (plane.m_component_ids.size() == 1) { + indices.push_back(plane.m_component_ids[0]); + } + } + + return indices; +} + + +uint8_t* HeifPixelImage::get_component(uint32_t component_id, size_t* out_stride) +{ + return get_component_memory(component_id, out_stride); +} + + +const uint8_t* HeifPixelImage::get_component(uint32_t component_id, size_t* out_stride) const +{ + return get_component_memory(component_id, out_stride); +} diff -Nru libheif-1.19.8/libheif/image/pixelimage.h libheif-1.23.4/libheif/image/pixelimage.h --- libheif-1.19.8/libheif/image/pixelimage.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/image/pixelimage.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,409 @@ +/* + * HEIF codec. + * Copyright (c) 2017 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . +*/ + + +#ifndef LIBHEIF_IMAGE_H +#define LIBHEIF_IMAGE_H + +#include "image_description.h" +#include "security_limits.h" + +#include +#include +#include +#include +#include + +constexpr uint16_t heif_cmpd_component_type_UNDEFINED = 0x7FFF; + + +heif_chroma chroma_from_subsampling(int h, int v); + +uint32_t chroma_width(uint32_t w, heif_chroma chroma); + +uint32_t chroma_height(uint32_t h, heif_chroma chroma); + +uint32_t channel_width(uint32_t w, heif_chroma chroma, heif_channel channel); + +uint32_t channel_height(uint32_t h, heif_chroma chroma, heif_channel channel); + +bool is_interleaved_with_alpha(heif_chroma chroma); + +int num_interleaved_components_per_plane(heif_chroma chroma); + +bool is_integer_multiple_of_chroma_size(uint32_t width, + uint32_t height, + heif_chroma chroma); + +// Returns the list of valid heif_chroma values for a given colorspace. +std::vector get_valid_chroma_values_for_colorspace(heif_colorspace colorspace); + + + +class HeifPixelImage : public std::enable_shared_from_this, + public ImageDescription, + public ErrorBuffer +{ +public: + explicit HeifPixelImage() = default; + + ~HeifPixelImage() override; + + void create(uint32_t width, uint32_t height, heif_colorspace colorspace, heif_chroma chroma); + + Error create_clone_image_at_new_size(const std::shared_ptr& source, uint32_t w, uint32_t h, + const heif_security_limits* limits); + + Error add_channel(heif_channel channel, uint32_t width, uint32_t height, int bit_depth, + const heif_security_limits* limits, + heif_component_datatype datatype = heif_component_datatype_unsigned_integer); + + bool has_channel(heif_channel channel) const; + + // Has alpha information either as a separate channel or in the interleaved format. + bool has_alpha() const; + + // Get logical image size. Individual components may vary. + uint32_t get_width() const { return m_width; } + + // Get logical image size. Individual components may vary. + uint32_t get_height() const { return m_height; } + + uint32_t get_width(heif_channel channel) const; + + uint32_t get_height(heif_channel channel) const; + + uint32_t get_width(uint32_t component_id) const; + + uint32_t get_height(uint32_t component_id) const; + + bool has_odd_width() const { return !!(m_width & 1); } + + bool has_odd_height() const { return !!(m_height & 1); } + + // Returns true if the "primary" pixel plane(s) have exactly the given size. + // Which plane is "primary" depends on the colorspace: + // YCbCr / monochrome -> the Y plane (Cb/Cr may be legitimately subsampled) + // RGB planar -> R, G and B planes must all be present and all equal + // RGB interleaved -> the interleaved plane + // filter_array -> the filter_array plane + // undefined / custom -> not checked here; returns true + bool primary_planes_have_size(uint32_t width, uint32_t height) const; + + // Returns true if every plane actually stored in m_storage -- not just the + // "primary" ones for the current colorspace -- has the size that plane's + // channel is expected to have: m_width x m_height for Y/Alpha/R/G/B/ + // interleaved, and the chroma-subsampled size (via get_subsampled_size()) + // for Cb/Cr. Any other channel type fails the check. Unlike + // primary_planes_have_size(), this checks each stored component directly + // rather than going through get_width(channel)/get_height(channel) (which + // only ever sees the first plane for a channel), so it also catches a + // same-channel duplicate whose size doesn't match, not just a missing or + // undersized single plane. + bool has_standard_plane_sizes() const; + + heif_chroma get_chroma_format() const { return m_chroma; } + + heif_colorspace get_colorspace() const { return m_colorspace; } + + std::set get_channel_set() const; + + uint16_t get_storage_bits_per_pixel(heif_channel channel) const; + + uint16_t get_bits_per_pixel(heif_channel channel) const; + + // Get the maximum bit depth of a visual channel (YCbCr or RGB). + uint16_t get_visual_image_bits_per_pixel() const; + + heif_component_datatype get_datatype(heif_channel channel) const; + + int get_number_of_interleaved_components(heif_channel channel) const; + + // Note: we are using size_t as stride type since the stride is usually involved in a multiplication with the line number. + // For very large images (e.g. >2 GB), this can result in an integer overflow and corresponding illegal memory access. + // (see https://github.com/strukturag/libheif/issues/1419) + uint8_t* get_channel_memory(heif_channel channel, size_t* out_stride) { return get_channel_memory(channel, out_stride); } + + const uint8_t* get_channel_memory(heif_channel channel, size_t* out_stride) const { return get_channel_memory(channel, out_stride); } + + template + T* get_channel_memory(heif_channel channel, size_t* out_stride) + { + auto* comp = find_storage_for_channel(channel); + if (!comp) { + if (out_stride) + *out_stride = 0; + + return nullptr; + } + + if (out_stride) { + *out_stride = static_cast(comp->stride); + } + + return static_cast(comp->mem); + } + + template + const T* get_channel_memory(heif_channel channel, size_t* out_stride) const + { + return const_cast(this)->get_channel_memory(channel, out_stride); + } + + + // --- id-based component access (for ISO 23001-17 multi-component images) + + uint32_t get_number_of_used_components() const { return static_cast(get_component_descriptions().size()); } + + //uint32_t get_total_number_of_cmpd_components() const { return static_cast(m_cmpd_component_types.size()); } + + heif_channel get_component_channel(uint32_t component_id) const; + + uint32_t get_component_width(uint32_t component_id) const; + uint32_t get_component_height(uint32_t component_id) const; + uint16_t get_component_bits_per_pixel(uint32_t component_id) const; + uint16_t get_component_storage_bits_per_pixel(uint32_t component_id) const; + heif_component_datatype get_component_datatype(uint32_t component_id) const; + + // Look up the component type from the cmpd table. Works for any cmpd index, + // even those that have no image plane (e.g. bayer reference components). + uint16_t get_component_type(uint32_t component_id) const; + + //std::vector get_cmpd_component_types() const { return m_cmpd_component_types; } + + std::vector get_component_ids_interleaved() const; + + //uint32_t get_component_cmpd_index() const { assert(m_cmpd_component_types.size()==1); return m_cmpd_component_types[0]; } + + // Encoder path: auto-generates component_index by appending to cmpd table. + Result add_component(uint32_t width, uint32_t height, + uint16_t component_type, + heif_component_datatype datatype, int bit_depth, + const heif_security_limits* limits); + + // TODO: replace uint16_t component_type with class that also handled the std::string type + uint32_t add_component_without_data(uint16_t component_type); + + // Decoder path: copy the per-component description from a source + // ImageDescription (typically the ImageItem the decoder is about to populate). + // After this, allocate_buffer_for_component() can be called with each id + // already present in m_components to allocate its pixel buffer. + void clone_component_descriptions_from(const ImageDescription& src); + + // Post-decode reconciliation: rebind this image's component descriptions + // and the m_component_ids on each plane so they match `src` (typically the + // ImageItem). Used after a plugin-based codec decode returns: the plugin + // auto-minted ids via the public C API (heif_image_add_plane_safe), but + // the handle has a canonical description list we want to expose. + // Channels in `src` overwrite this image's matching descriptions. + // Channels present in this image but not in `src` (e.g. alpha-from-aux + // attached after main decode) are kept under fresh ids that won't collide + // with `src`. No-op if `src` has no descriptions. + void apply_descriptions_from(const ImageDescription& src); + + // Decoder path: allocate a pixel buffer for a component whose description + // (channel, datatype, bit_depth, width, height) is already in m_components. + // No new id is minted. Used after clone_component_descriptions_from(). + Error allocate_buffer_for_component(uint32_t component_id, + const heif_security_limits* limits); + + // Decoder path: uses a pre-populated cmpd table to look up the component type. +#if 0 + Result add_component_for_index(uint32_t component_index, + uint32_t width, uint32_t height, + heif_component_datatype datatype, int bit_depth, + const heif_security_limits* limits); +#endif + + // Populate the cmpd component types table (decoder path). + //void set_cmpd_component_types(std::vector types) { m_cmpd_component_types = std::move(types); } + + //const std::vector& get_cmpd_component_types() { return m_cmpd_component_types; } + + // Returns the component ids of all components, in component-description + // order. This includes reference components that have no pixel plane + // (has_data_plane == false); the result size always equals + // get_number_of_used_components(). + std::vector get_used_component_ids() const; + + std::vector get_used_planar_component_ids() const; + + uint8_t* get_component(uint32_t component_id, size_t* out_stride); + const uint8_t* get_component(uint32_t component_id, size_t* out_stride) const; + + template + T* get_component_memory(uint32_t component_id, size_t* out_stride) + { + auto* comp = find_storage_for_component(component_id); + if (!comp) { + if (out_stride) *out_stride = 0; + return nullptr; + } + + if (out_stride) { + *out_stride = comp->stride; + } + return static_cast(comp->mem); + } + + template + const T* get_component_memory(uint32_t component_id, size_t* out_stride) const + { + return const_cast(this)->get_component_memory(component_id, out_stride); + } + + Error copy_new_channel_from(const std::shared_ptr& src_image, + heif_channel src_channel, + heif_channel dst_channel, + const heif_security_limits* limits); + + Error extract_alpha_from_RGBA(const std::shared_ptr& srcimage, const heif_security_limits* limits); + + void fill_channel(heif_channel dst_channel, uint16_t value); + + Error fill_new_channel(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, const heif_security_limits* limits); + + Error transfer_channel_from_image_as(const std::shared_ptr& source, + heif_channel src_channel, + heif_channel dst_channel); + + Error copy_image_to(const std::shared_ptr& source, uint32_t x0, uint32_t y0); + + Result> rotate_ccw(int angle_degrees, const heif_security_limits* limits); + + Result> mirror_inplace(heif_transform_mirror_direction, const heif_security_limits* limits); + + Result> crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, + const heif_security_limits* limits) const; + + Error fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_t a); + + Error overlay(std::shared_ptr& overlay, int32_t dx, int32_t dy); + + Error scale_nearest_neighbor(std::shared_ptr& output, uint32_t width, uint32_t height, + const heif_security_limits* limits) const; + + + void debug_dump() const; + + Error extend_padding_to_size(uint32_t width, uint32_t height, bool adjust_size, + const heif_security_limits* limits); + + Error extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits); + + Result> extract_image_area(uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, + const heif_security_limits* limits) const; + + + // --- warnings + + void add_warning(Error warning) { m_warnings.emplace_back(std::move(warning)); } + + void add_warnings(const std::vector& warning) { for (const auto& err : warning) m_warnings.emplace_back(err); } + + const std::vector& get_warnings() const { return m_warnings; } + +private: + struct ComponentStorage + { + heif_channel m_channel = heif_channel_Y; + + // index into the cmpd component definition table + // Interleaved channels will have a list of indices in the order R,G,B,A + std::vector m_component_ids; + + // limits=nullptr disables the limits + Error alloc(uint32_t width, uint32_t height, heif_component_datatype datatype, int bit_depth, + int num_interleaved_components, + const heif_security_limits* limits, + MemoryHandle& memory_handle); + + heif_component_datatype m_datatype = heif_component_datatype_unsigned_integer; + + // logical bit depth per component + // For interleaved formats, it is the number of bits for one component. + // It is not the storage width. + uint16_t m_bit_depth = 0; // 1-256 + uint8_t m_num_interleaved_components = 1; + + // Cached at alloc() time so get_bytes_per_pixel() doesn't have to do + // the bit-depth → bytes ladder on every call. Equal to + // bytes_per_component * m_num_interleaved_components. + uint8_t m_bytes_per_pixel = 0; + + // the "visible" area of the component + uint32_t m_width = 0; + uint32_t m_height = 0; + + // the allocated memory size + uint32_t m_mem_width = 0; + uint32_t m_mem_height = 0; + + void* mem = nullptr; // aligned memory start + uint8_t* allocated_mem = nullptr; // unaligned memory we allocated + size_t allocation_size = 0; + size_t stride = 0; // bytes per line + + int get_bytes_per_pixel() const; + + template void mirror_inplace(heif_transform_mirror_direction); + + template + void rotate_ccw(int angle_degrees, ComponentStorage& out_plane) const; + + void crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, int bytes_per_pixel, ComponentStorage& out_plane) const; + }; + + ComponentStorage* find_storage_for_channel(heif_channel channel); + const ComponentStorage* find_storage_for_channel(heif_channel channel) const; + + ComponentStorage* find_storage_for_component(uint32_t component_id); + const ComponentStorage* find_storage_for_component(uint32_t component_id) const; + + // After plane.alloc() has succeeded, mints fresh component ids, appends + // them to plane.m_component_ids, and pushes fully-populated + // ComponentDescription entries for each component_type. Channel, datatype, + // bit_depth, width and height are read from `plane`. Must only be called + // once allocation has succeeded so that no descriptions are registered for + // a plane that failed to materialize. + void register_component_descriptions(ComponentStorage& plane, + const std::vector& component_types); + + // Overload that clones existing ComponentDescriptions (preserving + // component_type, gimi_content_id, has_data_plane, and any other per-id + // metadata). Geometry/datatype/bit_depth/channel fields are overwritten + // from `plane`; component_ids are freshly minted on this image. Use this + // when allocating a new plane that mirrors an existing one (e.g. + // geometry-preserving transforms like rotation and crop). + void register_component_descriptions(ComponentStorage& plane, + const std::vector& source_descriptions); + + uint32_t m_width = 0; + uint32_t m_height = 0; + heif_colorspace m_colorspace = heif_colorspace_undefined; + heif_chroma m_chroma = heif_chroma_undefined; + + std::vector m_storage; + MemoryHandle m_memory_handle; + + std::vector m_warnings; +}; + +#endif diff -Nru libheif-1.19.8/libheif/image-items/avc.cc libheif-1.23.4/libheif/image-items/avc.cc --- libheif-1.19.8/libheif/image-items/avc.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/avc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,7 @@ /* * HEIF AVC codec. * Copyright (c) 2023 Brad Hards + * Copyright (c) 2025 Dirk Farin * * This file is part of libheif. * @@ -19,104 +20,25 @@ */ #include "avc.h" -#include -#include -#include -#include -#include "file.h" #include "context.h" #include "codecs/avc_dec.h" #include "codecs/avc_boxes.h" #include +#include "codecs/avc_enc.h" -Result ImageItem_AVC::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) -{ -#if 0 - CodedImageData codedImage; - - auto hvcC = std::make_shared(); - - heif_image c_api_image; - c_api_image.image = image; - - struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - if (err.code) { - return Error(err.code, - err.subcode, - err.message); - } - - int encoded_width = 0; - int encoded_height = 0; - - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); - - if (data == nullptr) { - break; - } - - - const uint8_t NAL_SPS = 33; - - if ((data[0] >> 1) == NAL_SPS) { - Box_hvcC::configuration config; - - parse_sps_for_hvcC_configuration(data, size, &config, &encoded_width, &encoded_height); - - hvcC->set_configuration(config); - - codedImage.encoded_image_width = encoded_width; - codedImage.encoded_image_height = encoded_height; - } - - switch (data[0] >> 1) { - case 0x20: - case 0x21: - case 0x22: - hvcC->append_nal_data(data, size); - break; - - default: - codedImage.append_with_4bytes_size(data, size); - // m_heif_file->append_iloc_data_with_4byte_size(image_id, data, size); - } - } - - if (!encoded_width || !encoded_height) { - return Error(heif_error_Encoder_plugin_error, - heif_suberror_Invalid_image_size); - } - - codedImage.properties.push_back(hvcC); +ImageItem_AVC::ImageItem_AVC(HeifContext* ctx, heif_item_id id) + : ImageItem(ctx, id) +{ + m_encoder = std::make_shared(); +} - // Make sure that the encoder plugin works correctly and the encoded image has the correct size. - - if (encoder->plugin->plugin_api_version >= 3 && - encoder->plugin->query_encoded_size != nullptr) { - uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); - - encoder->plugin->query_encoded_size(encoder->encoder, - image->get_width(), image->get_height(), - &check_encoded_width, - &check_encoded_height); - assert((int)check_encoded_width == encoded_width); - assert((int)check_encoded_height == encoded_height); - } - - return codedImage; -#endif - assert(false); // TODO - return {}; +ImageItem_AVC::ImageItem_AVC(HeifContext* ctx) + : ImageItem(ctx) +{ + m_encoder = std::make_shared(); } @@ -126,7 +48,13 @@ } -Error ImageItem_AVC::on_load_file() +std::shared_ptr ImageItem_AVC::get_encoder() const +{ + return m_encoder; +} + + +Error ImageItem_AVC::initialize_decoder() { auto avcC_box = get_property(); if (!avcC_box) { @@ -136,10 +64,13 @@ m_decoder = std::make_shared(avcC_box); + return Error::Ok; +} + +void ImageItem_AVC::set_decoder_input_data() +{ DataExtent extent; extent.set_from_image_item(get_context()->get_heif_file(), get_id()); m_decoder->set_data_extent(std::move(extent)); - - return Error::Ok; } diff -Nru libheif-1.19.8/libheif/image-items/avc.h libheif-1.23.4/libheif/image-items/avc.h --- libheif-1.19.8/libheif/image-items/avc.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/avc.h 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,7 @@ /* * HEIF AVC codec. * Copyright (c) 2023 Brad Hards + * Copyright (c) 2025 Dirk Farin * * This file is part of libheif. * @@ -33,30 +34,30 @@ class ImageItem_AVC : public ImageItem { public: - ImageItem_AVC(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_AVC(HeifContext* ctx, heif_item_id id); - ImageItem_AVC(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_AVC(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("avc1"); } const char* get_auxC_alpha_channel_type() const override { return "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; } - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } - heif_compression_format get_compression_format() const override { return heif_compression_AVC; } - Error on_load_file() override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; + + heif_brand2 get_compatible_brand() const override { return heif_brand2_avci; } protected: Result> get_decoder() const override; -public: - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + std::shared_ptr get_encoder() const override; +public: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; }; #endif diff -Nru libheif-1.19.8/libheif/image-items/avif.cc libheif-1.23.4/libheif/image-items/avif.cc --- libheif-1.19.8/libheif/image-items/avif.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/avif.cc 2026-09-06 14:45:17.000000000 +0000 @@ -18,13 +18,14 @@ * along with libheif. If not, see . */ -#include "pixelimage.h" +#include "image/pixelimage.h" #include "avif.h" #include "codecs/avif_dec.h" +#include "codecs/avif_enc.h" #include "codecs/avif_boxes.h" #include "bitstream.h" #include "common_utils.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "file.h" #include #include @@ -35,8 +36,18 @@ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf +ImageItem_AVIF::ImageItem_AVIF(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) +{ + m_encoder = std::make_shared(); +} -Error ImageItem_AVIF::on_load_file() +ImageItem_AVIF::ImageItem_AVIF(HeifContext* ctx) : ImageItem(ctx) +{ + m_encoder = std::make_shared(); +} + + +Error ImageItem_AVIF::initialize_decoder() { auto av1C_box = get_property(); if (!av1C_box) { @@ -46,59 +57,15 @@ m_decoder = std::make_shared(av1C_box); - DataExtent extent; - extent.set_from_image_item(get_context()->get_heif_file(), get_id()); - - m_decoder->set_data_extent(std::move(extent)); - return Error::Ok; } +void ImageItem_AVIF::set_decoder_input_data() +{ + DataExtent extent; + extent.set_from_image_item(get_context()->get_heif_file(), get_id()); -Result ImageItem_AVIF::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) -{ - CodedImageData codedImage; - - Box_av1C::configuration config; - - // Fill preliminary av1C in case we cannot parse the sequence_header() correctly in the code below. - // TODO: maybe we can remove this later. - fill_av1C_configuration(&config, image); - - heif_image c_api_image; - c_api_image.image = image; - - struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - if (err.code) { - return Error(err.code, - err.subcode, - err.message); - } - - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); - - bool found_config = fill_av1C_configuration_from_stream(&config, data, size); - (void) found_config; - - if (data == nullptr) { - break; - } - - codedImage.append(data, size); - } - - auto av1C = std::make_shared(); - av1C->set_configuration(config); - codedImage.properties.push_back(av1C); - - return codedImage; + m_decoder->set_data_extent(std::move(extent)); } @@ -108,7 +75,13 @@ } -Result> ImageItem_AVIF::get_decoder() const +Result> ImageItem_AVIF::get_decoder() const { return {m_decoder}; } + + +std::shared_ptr ImageItem_AVIF::get_encoder() const +{ + return m_encoder; +} diff -Nru libheif-1.19.8/libheif/image-items/avif.h libheif-1.23.4/libheif/image-items/avif.h --- libheif-1.19.8/libheif/image-items/avif.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/avif.h 2026-09-06 14:45:17.000000000 +0000 @@ -36,33 +36,33 @@ class ImageItem_AVIF : public ImageItem { public: - ImageItem_AVIF(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_AVIF(HeifContext* ctx, heif_item_id id); - ImageItem_AVIF(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_AVIF(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("av01"); } const char* get_auxC_alpha_channel_type() const override { return "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; } - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } - heif_compression_format get_compression_format() const override { return heif_compression_AV1; } - Error on_load_file() override; + heif_brand2 get_compatible_brand() const override { return heif_brand2_avif; } -public: - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; protected: Result> read_bitstream_configuration_data() const override; - Result> get_decoder() const override; + Result> get_decoder() const override; + + std::shared_ptr get_encoder() const override; private: std::shared_ptr m_decoder; + + std::shared_ptr m_encoder; }; #endif diff -Nru libheif-1.19.8/libheif/image-items/grid.cc libheif-1.23.4/libheif/image-items/grid.cc --- libheif-1.19.8/libheif/image-items/grid.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/grid.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,9 +24,11 @@ #include #include #include +#include #include #include -#include +#include +#include "api_structs.h" #include "security_limits.h" @@ -60,22 +62,12 @@ "Grid image data incomplete"}; } - m_output_width = ((data[4] << 24) | - (data[5] << 16) | - (data[6] << 8) | - (data[7])); - - m_output_height = ((data[8] << 24) | - (data[9] << 16) | - (data[10] << 8) | - (data[11])); + m_output_width = four_bytes_to_uint32(data[4], data[5], data[6], data[7]); + m_output_height = four_bytes_to_uint32(data[8], data[9], data[10], data[11]); } else { - m_output_width = ((data[4] << 8) | - (data[5])); - - m_output_height = ((data[6] << 8) | - (data[7])); + m_output_width = two_bytes_to_uint16(data[4], data[5]); + m_output_height = two_bytes_to_uint16(data[6], data[7]); } return Error::Ok; @@ -143,24 +135,27 @@ } -extern void set_default_encoding_options(heif_encoding_options& options); - - ImageItem_Grid::ImageItem_Grid(HeifContext* ctx) : ImageItem(ctx) { - set_default_encoding_options(m_encoding_options); + m_tile_encoding_options = heif_encoding_options_alloc(); } ImageItem_Grid::ImageItem_Grid(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) { - set_default_encoding_options(m_encoding_options); + m_tile_encoding_options = heif_encoding_options_alloc(); +} + + +ImageItem_Grid::~ImageItem_Grid() +{ + heif_encoding_options_free(m_tile_encoding_options); } -Error ImageItem_Grid::on_load_file() +Error ImageItem_Grid::initialize_decoder() { Error err = read_grid_spec(); if (err) { @@ -175,13 +170,12 @@ { auto heif_file = get_context()->get_heif_file(); - std::vector grid_data; - Error err = heif_file->get_uncompressed_item_data(get_id(), &grid_data); - if (err) { - return err; + auto gridDataResult = heif_file->get_uncompressed_item_data(get_id()); + if (!gridDataResult) { + return gridDataResult.error(); } - err = m_grid_spec.parse(grid_data); + Error err = m_grid_spec.parse(*gridDataResult); if (err) { return err; } @@ -214,19 +208,47 @@ } -Result> ImageItem_Grid::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +Result> ImageItem_Grid::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { + if (decode_state.processed_ids.contains(get_id())) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iref' has cyclic references"}; + } + + decode_state.processed_ids.insert(get_id()); + + if (decode_tile_only) { - return decode_grid_tile(options, tile_x0, tile_y0); + return decode_grid_tile(options, tile_x0, tile_y0, decode_state); } else { - return decode_full_grid_image(options); + return decode_full_grid_image(options, decode_state); } } +// Note: ImageItem_Grid does not override check_decoded_image_size(). The composed +// grid image is built to the grid-header size by construction (decode_and_paste_tile_image +// creates the canvas at get_grid_spec() size), so checking it against that same size +// would be tautological. The base default checks the composed image against 'ispe', +// which is the meaningful cross-check (grid-header size vs signaled size). -Result> ImageItem_Grid::decode_full_grid_image(const heif_decoding_options& options) const +#if ENABLE_PARALLEL_TILE_DECODING +static void wait_for_jobs(std::deque >* jobs) { + if (jobs->empty()) { + return; + } + + while (!jobs->empty()) { + jobs->front().get(); + jobs->pop_front(); + } +} +#endif + +Result> ImageItem_Grid::decode_full_grid_image(const heif_decoding_options& options, const DecodeTraversalState& decode_state) const { std::shared_ptr img; // the decoded image @@ -288,6 +310,7 @@ int progress_counter = 0; bool cancelled = false; + std::shared_ptr > warnings(new std::vector()); for (uint32_t y = 0; y < grid.get_rows() && !cancelled; y++) { uint32_t x0 = 0; @@ -298,11 +321,30 @@ std::shared_ptr tileImg = get_context()->get_image(tileID, true); if (!tileImg) { + if (!options.strict_decoding && reference_idx != 0) { + // Skip missing tiles (unless it's the first one). + warnings->push_back(Error{ + heif_error_Invalid_input, + heif_suberror_Missing_grid_images, + }); + reference_idx++; + x0 += tile_width; + continue; + } + return Error{heif_error_Invalid_input, heif_suberror_Missing_grid_images, "Nonexistent grid image referenced"}; } if (auto error = tileImg->get_item_error()) { + if (!options.strict_decoding && reference_idx != 0) { + // Skip missing tiles (unless it's the first one). + warnings->push_back(error); + reference_idx++; + x0 += tile_width; + continue; + } + return error; } @@ -313,8 +355,10 @@ return err; } - if (src_width < grid.get_width() / grid.get_columns() || - src_height < grid.get_height() / grid.get_rows()) { + // Integer division would let e.g. 9 tiles of 11px each "cover" a 107px canvas + // (107/9 == 11), leaving an 8-pixel gap inside the visible image area. + if (static_cast(src_width) * grid.get_columns() < grid.get_width() || + static_cast(src_height) * grid.get_rows() < grid.get_height()) { return Error{heif_error_Invalid_input, heif_suberror_Invalid_grid_data, "Grid tiles do not cover whole image"}; @@ -345,7 +389,7 @@ } } - err = decode_and_paste_tile_image(tileID, x0, y0, img, options, progress_counter); + err = decode_and_paste_tile_image(tileID, x0, y0, img, options, progress_counter, warnings, decode_state); if (err) { return err; } @@ -370,11 +414,11 @@ if (errs.size() >= (size_t) get_context()->get_max_decoding_threads()) { Error e = errs.front().get(); + errs.pop_front(); if (e) { + wait_for_jobs(&errs); return e; } - - errs.pop_front(); } @@ -393,18 +437,18 @@ errs.push_back(std::async(std::launch::async, &ImageItem_Grid::decode_and_paste_tile_image, this, data.tileID, data.x_origin, data.y_origin, std::ref(img), options, - std::ref(progress_counter))); + std::ref(progress_counter), warnings, decode_state)); } // check for decoding errors in remaining tiles while (!errs.empty()) { Error e = errs.front().get(); + errs.pop_front(); if (e) { + wait_for_jobs(&errs); return e; } - - errs.pop_front(); } } #endif @@ -417,36 +461,94 @@ return Error{heif_error_Canceled, heif_suberror_Unspecified, "Decoding the image was canceled"}; } + if (!img) { + // No tile could be decoded and the output canvas was never created. + // Returning the null image would only produce a meaningless generic error + // further up. Return the first per-tile error instead, which names the + // actual cause, for example that no decoder plugin is installed (#1876). + if (!warnings->empty()) { + return warnings->front(); + } + + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_grid_data, + "Grid image without tiles"}; + } + + img->add_warnings(*warnings.get()); + return img; } +static Error progress_and_return_ok(const heif_decoding_options& options, int& progress_counter) { + if (options.on_progress) { +#if ENABLE_PARALLEL_TILE_DECODING + static std::mutex progressMutex; + std::lock_guard lock(progressMutex); +#endif + + options.on_progress(heif_progress_step_total, ++progress_counter, options.progress_user_data); + } + return Error::Ok; +} + Error ImageItem_Grid::decode_and_paste_tile_image(heif_item_id tileID, uint32_t x0, uint32_t y0, std::shared_ptr& inout_image, const heif_decoding_options& options, - int& progress_counter) const + int& progress_counter, + const std::shared_ptr >& warnings, + DecodeTraversalState decode_state) const { std::shared_ptr tile_img; +#if ENABLE_PARALLEL_TILE_DECODING + static std::mutex warningsMutex; +#endif auto tileItem = get_context()->get_image(tileID, true); + if (!tileItem && !options.strict_decoding) { + // We ignore missing images. The un-pasted canvas region stays zero from calloc(). +#if ENABLE_PARALLEL_TILE_DECODING + std::lock_guard lock(warningsMutex); +#endif + warnings->emplace_back( + heif_error_Invalid_input, + heif_suberror_Missing_grid_images, + "Missing grid image" + ); + return progress_and_return_ok(options, progress_counter); + } + assert(tileItem); if (auto error = tileItem->get_item_error()) { return error; } - auto decodeResult = tileItem->decode_image(options, false, 0, 0); - if (decodeResult.error) { - return decodeResult.error; + auto decodeResult = tileItem->decode_image(options, false, 0, 0, std::move(decode_state)); + if (!decodeResult) { + if (!options.strict_decoding) { + // We ignore broken tiles. The un-pasted canvas region stays zero from calloc(). +#if ENABLE_PARALLEL_TILE_DECODING + std::lock_guard lock(warningsMutex); +#endif + warnings->push_back(decodeResult.error()); + return progress_and_return_ok(options, progress_counter); + } + + return decodeResult.error(); } - tile_img = decodeResult.value; + tile_img = *decodeResult; uint32_t w = get_grid_spec().get_width(); uint32_t h = get_grid_spec().get_height(); // --- generate the image canvas for combining all the tiles - if (!inout_image) { // this avoids that we normally have to lock a mutex + { #if ENABLE_PARALLEL_TILE_DECODING + // All threads have to take this mutex, even those that will only read `inout_image`. + // Otherwise, the image initialization would not be synchronized to them (they could, + // for example, see the image pointer before the image content initialization is visible). static std::mutex createImageMutex; std::lock_guard lock(createImageMutex); #endif @@ -465,12 +567,12 @@ assert(alpha_bpp <= 16); auto alpha_default_value = static_cast((1UL << alpha_bpp) - 1UL); - grid_image->fill_plane(heif_channel_Alpha, alpha_default_value); + grid_image->fill_channel(heif_channel_Alpha, alpha_default_value); } - grid_image->forward_all_metadata_from(tile_img); + grid_image->copy_metadata_from(*tile_img); - inout_image = grid_image; // We have to set this at the very end because of the unlocked check to `inout_image` above. + inout_image = grid_image; } } @@ -487,32 +589,33 @@ inout_image->copy_image_to(tile_img, x0, y0); - if (options.on_progress) { -#if ENABLE_PARALLEL_TILE_DECODING - static std::mutex progressMutex; - std::lock_guard lock(progressMutex); -#endif - - options.on_progress(heif_progress_step_total, ++progress_counter, options.progress_user_data); - } - - return Error::Ok; + return progress_and_return_ok(options, progress_counter); } -Result> ImageItem_Grid::decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty) const +Result> ImageItem_Grid::decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty, + DecodeTraversalState decode_state) const { uint32_t idx = ty * m_grid_spec.get_columns() + tx; - assert(idx < m_grid_tile_ids.size()); + if (idx >= m_grid_tile_ids.size()) { + return Error{heif_error_Invalid_input, + heif_suberror_Missing_grid_images, + "Grid tile coordinate out of range"}; + } heif_item_id tile_id = m_grid_tile_ids[idx]; std::shared_ptr tile_item = get_context()->get_image(tile_id, true); + if (!tile_item) { + return Error{heif_error_Invalid_input, + heif_suberror_Missing_grid_images, + "Grid tile references a non-existent item"}; + } if (auto error = tile_item->get_item_error()) { return error; } - return tile_item->decode_compressed_image(options, true, tx, ty); + return tile_item->decode_compressed_image(options, false, 0, 0, std::move(decode_state)); } @@ -557,10 +660,16 @@ void ImageItem_Grid::get_tile_size(uint32_t& w, uint32_t& h) const { - heif_item_id first_tile_id = get_grid_tiles()[0]; - auto tile = get_context()->get_image(first_tile_id, true); - if (tile->get_item_error()) { + const auto& tile_ids = get_grid_tiles(); + if (tile_ids.empty() || tile_ids[0] == 0) { + w = h = 0; + return; + } + + auto tile = get_context()->get_image(tile_ids[0], true); + if (tile == nullptr || tile->get_item_error()) { w = h = 0; + return; } w = tile->get_width(); @@ -571,13 +680,12 @@ int ImageItem_Grid::get_luma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); if (!image) { return -1; } @@ -588,25 +696,23 @@ int ImageItem_Grid::get_chroma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); return image->get_chroma_bits_per_pixel(); } Result> ImageItem_Grid::get_decoder() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { - return {err}; + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { + return child_result.error(); } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); if (!image) { return Error{heif_error_Invalid_input, heif_suberror_Nonexisting_item_referenced}; @@ -619,12 +725,40 @@ } +void ImageItem_Grid::populate_component_descriptions() +{ + if (!get_component_descriptions().empty()) { + return; + } + + if (m_grid_tile_ids.empty()) { + ImageItem::populate_component_descriptions(); + return; + } + + auto child = get_context()->get_image(m_grid_tile_ids[0], true); + if (!child) { + ImageItem::populate_component_descriptions(); + return; + } + + // Try child-delegation first (correct for unci children with float/signed/ + // complex datatypes). If the child has no descriptions yet (e.g. it's a + // visual codec without an initialized decoder), fall back to the base + // populate which queries this item's own colorspace/bpp accessors (which + // already delegate to the child). + if (!populate_descriptions_from_child(*child, child->get_width(), child->get_height())) { + ImageItem::populate_component_descriptions(); + } +} + + Result> ImageItem_Grid::add_new_grid_item(HeifContext* ctx, uint32_t output_width, uint32_t output_height, uint16_t tile_rows, uint16_t tile_columns, - const struct heif_encoding_options* encoding_options) + const heif_encoding_options* encoding_options) { std::shared_ptr grid_image; if (tile_rows > 0xFFFF / tile_columns) { @@ -643,11 +777,16 @@ // Create Grid Item std::shared_ptr file = ctx->get_heif_file(); - heif_item_id grid_id = file->add_new_image(fourcc("grid")); + auto grid_id_result = file->add_new_image(fourcc("grid")); + if (!grid_id_result) { + return grid_id_result.error(); + } + heif_item_id grid_id = *grid_id_result; grid_image = std::make_shared(ctx, grid_id); - grid_image->set_encoding_options(encoding_options); + grid_image->set_tile_encoding_options(encoding_options); grid_image->set_grid_spec(grid); grid_image->set_resolution(output_width, output_height); + grid_image->m_grid_orientation = encoding_options->image_orientation; ctx->insert_image_item(grid_id, grid_image); const int construction_method = 1; // 0=mdat 1=idat @@ -661,7 +800,9 @@ file->add_iref_reference(grid_id, fourcc("dimg"), tile_ids); // Add ISPE property - file->add_ispe_property(grid_id, output_width, output_height, false); + if (Error err = file->add_ispe_property(grid_id, output_width, output_height, false)) { + return err; + } // PIXI property will be added when the first tile is set @@ -672,19 +813,25 @@ return grid_image; } +void ImageItem_Grid::set_tile_encoding_options(const heif_encoding_options* options) +{ + heif_encoding_options_copy(m_tile_encoding_options, options); + + // do not propagate image transformation to tiles + m_tile_encoding_options->image_orientation = heif_orientation_normal; +} + Error ImageItem_Grid::add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, - struct heif_encoder* encoder) + heif_encoder* encoder) { - auto encoding_options = get_encoding_options(); - auto encodingResult = get_context()->encode_image(image, encoder, - *encoding_options, + *m_tile_encoding_options, heif_image_input_class_normal); - if (encodingResult.error != Error::Ok) { - return encodingResult.error; + if (!encodingResult) { + return encodingResult.error(); } std::shared_ptr encoded_image = *encodingResult; @@ -702,6 +849,30 @@ auto pixi = encoded_image->get_property(); add_property(pixi, true); + // copy over extra properties to grid item + + if (tile_x == 0 && tile_y == 0) { + auto property_boxes = encoded_image->generate_property_boxes(false); + for (auto& property : property_boxes) { + add_property(property, is_property_essential(property)); + } + + // add color profile similar to first tile image + // TODO: this shouldn't be necessary. The colr profiles should be in the ImageDescription above. + auto colr_boxes = add_color_profile(image, *m_tile_encoding_options, + heif_image_input_class_normal, + m_tile_encoding_options->output_nclx_profile); + for (auto& property : colr_boxes) { + add_property(property, is_property_essential(property)); + } + + // Add transformative properties + + if (Error err = get_context()->get_heif_file()->add_orientation_properties(get_id(), m_grid_orientation)) { + return err; + } + } + return Error::Ok; } @@ -710,8 +881,8 @@ const std::vector>& tiles, uint16_t rows, uint16_t columns, - struct heif_encoder* encoder, - const struct heif_encoding_options& options) + heif_encoder* encoder, + const heif_encoding_options& options) { std::shared_ptr griditem; @@ -719,8 +890,8 @@ ImageGrid grid; grid.set_num_tiles(columns, rows); - uint32_t tile_width = tiles[0]->get_width(heif_channel_interleaved); - uint32_t tile_height = tiles[0]->get_height(heif_channel_interleaved); + uint32_t tile_width = tiles[0]->get_width(); + uint32_t tile_height = tiles[0]->get_height(); grid.set_output_size(tile_width * columns, tile_height * rows); std::vector grid_data = grid.write(); @@ -738,8 +909,8 @@ encoder, options, heif_image_input_class_normal); - if (encodingResult.error) { - return encodingResult.error; + if (!encodingResult) { + return encodingResult.error(); } else { out_tile = *encodingResult; @@ -756,7 +927,11 @@ // Create Grid Item - heif_item_id grid_id = file->add_new_image(fourcc("grid")); + auto grid_id_result = file->add_new_image(fourcc("grid")); + if (!grid_id_result) { + return grid_id_result.error(); + } + heif_item_id grid_id = *grid_id_result; griditem = std::make_shared(ctx, grid_id); ctx->insert_image_item(grid_id, griditem); const int construction_method = 1; // 0=mdat 1=idat @@ -779,10 +954,28 @@ griditem->add_property(pixi_property, true); + // copy over extra properties to grid item + + auto property_boxes = tiles[0]->generate_property_boxes(true); + for (auto& property : property_boxes) { + griditem->add_property(property, griditem->is_property_essential(property)); + } + // Set Brands - file->set_brand(encoder->plugin->compression_format, - griditem->is_miaf_compatible()); + //file->set_brand(encoder->plugin->compression_format, + // griditem->is_miaf_compatible()); return griditem; } + +heif_brand2 ImageItem_Grid::get_compatible_brand() const +{ + if (m_grid_tile_ids.empty()) { return 0; } + + heif_item_id child_id = m_grid_tile_ids[0]; + auto child = get_context()->get_image(child_id, false); + if (!child) { return 0; } + + return child->get_compatible_brand(); +} diff -Nru libheif-1.19.8/libheif/image-items/grid.h libheif-1.23.4/libheif/image-items/grid.h --- libheif-1.19.8/libheif/image-items/grid.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/grid.h 2026-09-06 14:45:17.000000000 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include class ImageGrid @@ -80,6 +81,8 @@ ImageItem_Grid(HeifContext* ctx); + ~ImageItem_Grid() override; + uint32_t get_infe_type() const override { return fourcc("grid"); } static Result> add_new_grid_item(HeifContext* ctx, @@ -87,18 +90,18 @@ uint32_t output_height, uint16_t tile_rows, uint16_t tile_columns, - const struct heif_encoding_options* encoding_options); + const heif_encoding_options* encoding_options); Error add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, - struct heif_encoder* encoder); + heif_encoder* encoder); static Result> add_and_encode_full_grid(HeifContext* ctx, const std::vector>& tiles, uint16_t rows, uint16_t columns, - struct heif_encoder* encoder, - const struct heif_encoding_options& options); + heif_encoder* encoder, + const heif_encoding_options& options); // TODO: nclx depends on contained format @@ -106,28 +109,34 @@ // heif_compression_format get_compression_format() const override { return heif_compression_HEVC; } - Error on_load_file() override; + Error initialize_decoder() override; + + // Delegates to the first grid tile's already-populated descriptions and + // rescales per-component dims to the grid's full ispe size, so the handle + // exposes the correct datatype/bit-depth even for unci float tiles + // (which the base populate would mis-tag as unsigned_integer). + void populate_component_descriptions() override; int get_luma_bits_per_pixel() const override; int get_chroma_bits_per_pixel() const override; - void set_encoding_options(const heif_encoding_options* options) { - m_encoding_options = *options; - } - - const heif_encoding_options* get_encoding_options() const { return &m_encoding_options; } + void set_tile_encoding_options(const heif_encoding_options* options); - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override { + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override + { return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Cannot encode image to 'grid'"}; } - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; + + heif_brand2 get_compatible_brand() const override; protected: Result> get_decoder() const override; @@ -152,17 +161,20 @@ ImageGrid m_grid_spec; std::vector m_grid_tile_ids; - heif_encoding_options m_encoding_options; + heif_orientation m_grid_orientation = heif_orientation_normal; + heif_encoding_options* m_tile_encoding_options = nullptr; Error read_grid_spec(); - Result> decode_full_grid_image(const heif_decoding_options& options) const; + Result> decode_full_grid_image(const heif_decoding_options& options, const DecodeTraversalState& decode_state) const; - Result> decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty) const; + Result> decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty, DecodeTraversalState decode_state) const; Error decode_and_paste_tile_image(heif_item_id tileID, uint32_t x0, uint32_t y0, std::shared_ptr& inout_image, - const heif_decoding_options& options, int& progress_counter) const; + const heif_decoding_options& options, int& progress_counter, + const std::shared_ptr >& warnings, + DecodeTraversalState decode_state) const; }; diff -Nru libheif-1.19.8/libheif/image-items/hevc.cc libheif-1.23.4/libheif/image-items/hevc.cc --- libheif-1.19.8/libheif/image-items/hevc.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/hevc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -31,10 +31,25 @@ #include #include #include -#include "api/libheif/api_structs.h" +#include "api_structs.h" +#include "codecs/hevc_enc.h" -Error ImageItem_HEVC::on_load_file() +ImageItem_HEVC::ImageItem_HEVC(HeifContext* ctx, heif_item_id id) + : ImageItem(ctx, id) +{ + m_encoder = std::make_shared(); +} + + +ImageItem_HEVC::ImageItem_HEVC(HeifContext* ctx) + : ImageItem(ctx) +{ + m_encoder = std::make_shared(); +} + + +Error ImageItem_HEVC::initialize_decoder() { auto hvcC_box = get_property(); if (!hvcC_box) { @@ -44,98 +59,44 @@ m_decoder = std::make_shared(hvcC_box); - DataExtent extent; - extent.set_from_image_item(get_context()->get_heif_file(), get_id()); - - m_decoder->set_data_extent(std::move(extent)); - return Error::Ok; } -Result ImageItem_HEVC::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) -{ - CodedImageData codedImage; - - auto hvcC = std::make_shared(); - - heif_image c_api_image; - c_api_image.image = image; - - struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - if (err.code) { - return Error(err.code, - err.subcode, - err.message); - } - - int encoded_width = 0; - int encoded_height = 0; - - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); - - if (data == nullptr) { - break; - } - - - const uint8_t NAL_SPS = 33; - - if ((data[0] >> 1) == NAL_SPS) { - Box_hvcC::configuration config; - - parse_sps_for_hvcC_configuration(data, size, &config, &encoded_width, &encoded_height); +void ImageItem_HEVC::set_decoder_input_data() +{ + DataExtent extent; + extent.set_from_image_item(get_context()->get_heif_file(), get_id()); - hvcC->set_configuration(config); + m_decoder->set_data_extent(std::move(extent)); +} - codedImage.encoded_image_width = encoded_width; - codedImage.encoded_image_height = encoded_height; - } - switch (data[0] >> 1) { - case 0x20: - case 0x21: - case 0x22: - hvcC->append_nal_data(data, size); - break; +heif_brand2 ImageItem_HEVC::get_compatible_brand() const +{ + auto hvcC = get_property(); - default: - codedImage.append_with_4bytes_size(data, size); - // m_heif_file->append_iloc_data_with_4byte_size(image_id, data, size); - } + if (has_essential_property_other_than(std::set{fourcc("hvcC"), + fourcc("irot"), + fourcc("imir"), + fourcc("clap")})) { + return 0; } - if (!encoded_width || !encoded_height) { - return Error(heif_error_Encoder_plugin_error, - heif_suberror_Invalid_image_size); + const auto& config = hvcC->get_configuration(); + if (config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_Main) || + config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_MainStillPicture)) { + return heif_brand2_heic; } - codedImage.properties.push_back(hvcC); - - - // Make sure that the encoder plugin works correctly and the encoded image has the correct size. - - if (encoder->plugin->plugin_api_version >= 3 && - encoder->plugin->query_encoded_size != nullptr) { - uint32_t check_encoded_width = image->get_width(), check_encoded_height = image->get_height(); - - encoder->plugin->query_encoded_size(encoder->encoder, - image->get_width(), image->get_height(), - &check_encoded_width, - &check_encoded_height); - - assert((int)check_encoded_width == encoded_width); - assert((int)check_encoded_height == encoded_height); + if (config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_Main10) || + config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_RExt)) { + return heif_brand2_heix; } - return codedImage; + // TODO: what brand should we use for this case? + + return heif_brand2_heix; } @@ -145,12 +106,18 @@ } -Result> ImageItem_HEVC::get_decoder() const +Result> ImageItem_HEVC::get_decoder() const { return {m_decoder}; } +std::shared_ptr ImageItem_HEVC::get_encoder() const +{ + return m_encoder; +} + + void ImageItem_HEVC::set_preencoded_hevc_image(const std::vector& data) { auto hvcC = std::make_shared(); diff -Nru libheif-1.19.8/libheif/image-items/hevc.h libheif-1.23.4/libheif/image-items/hevc.h --- libheif-1.19.8/libheif/image-items/hevc.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/hevc.h 2026-09-06 14:45:17.000000000 +0000 @@ -34,25 +34,23 @@ class ImageItem_HEVC : public ImageItem { public: - ImageItem_HEVC(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_HEVC(HeifContext* ctx, heif_item_id id); - ImageItem_HEVC(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_HEVC(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("hvc1"); } // TODO: MIAF says that the *:hevc:* urn is deprecated and we should use "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha" const char* get_auxC_alpha_channel_type() const override { return "urn:mpeg:hevc:2015:auxid:1"; } - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } - heif_compression_format get_compression_format() const override { return heif_compression_HEVC; } - Error on_load_file() override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + + heif_brand2 get_compatible_brand() const override; // currently not used void set_preencoded_hevc_image(const std::vector& data); @@ -60,10 +58,13 @@ protected: Result> read_bitstream_configuration_data() const override; - Result> get_decoder() const override; + Result> get_decoder() const override; + + std::shared_ptr get_encoder() const override; private: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; }; #endif diff -Nru libheif-1.19.8/libheif/image-items/iden.cc libheif-1.23.4/libheif/image-items/iden.cc --- libheif-1.19.8/libheif/image-items/iden.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/iden.cc 2026-09-06 14:45:17.000000000 +0000 @@ -35,9 +35,19 @@ } -Result> ImageItem_iden::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +Result> ImageItem_iden::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { + if (decode_state.processed_ids.contains(get_id())) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iref' has cyclic references"}; + } + + decode_state.processed_ids.insert(get_id()); + + std::shared_ptr img; // find the ID of the image this image is derived from @@ -52,12 +62,18 @@ std::vector image_references = iref_box->get_references(get_id(), fourcc("dimg")); - if ((int) image_references.size() != 1) { + if (image_references.size() > 1) { return Error(heif_error_Invalid_input, heif_suberror_Unspecified, "'iden' image with more than one reference image"); } + if (image_references.empty()) { + return Error(heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iden' image without 'dimg' reference"); + } + heif_item_id reference_image_id = image_references[0]; @@ -77,19 +93,18 @@ return error; } - return imgitem->decode_image(options, decode_tile_only, tile_x0, tile_y0); + return imgitem->decode_image(options, decode_tile_only, tile_x0, tile_y0, decode_state); } Error ImageItem_iden::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { - return err; + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { + return child_result.error(); } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); if (!image) { return Error{heif_error_Invalid_input, heif_suberror_Nonexisting_item_referenced}; @@ -101,13 +116,12 @@ int ImageItem_iden::get_luma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); if (!image) { return -1; } @@ -118,16 +132,41 @@ int ImageItem_iden::get_chroma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); if (!image) { return -1; } return image->get_chroma_bits_per_pixel(); } + +heif_brand2 ImageItem_iden::get_compatible_brand() const +{ + assert(false); + return 0; // TODO (we never write 'iden' images) +} + + +void ImageItem_iden::populate_component_descriptions() +{ + if (!get_component_descriptions().empty()) { + return; + } + + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { + return; + } + + auto child = get_context()->get_image(*child_result, true); + if (!child) { + return; + } + + populate_descriptions_from_child(*child, child->get_width(), child->get_height()); +} diff -Nru libheif-1.19.8/libheif/image-items/iden.h libheif-1.23.4/libheif/image-items/iden.h --- libheif-1.19.8/libheif/image-items/iden.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/iden.h 2026-09-06 14:45:17.000000000 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include class ImageItem_iden : public ImageItem @@ -45,23 +46,31 @@ Error get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const override; + // Delegates to the referenced image's component descriptions, rescaled to + // this iden's ispe. Without this override the base populate would bail + // (iden has no get_decoder()) and the handle would report 0 components. + void populate_component_descriptions() override; + int get_luma_bits_per_pixel() const override; int get_chroma_bits_per_pixel() const override; - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override { return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Cannot encode image to 'iden'"}; } - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; + + heif_brand2 get_compatible_brand() const override; -private: + // No check_decoded_image_size() override: the base class implementation is correct here too. }; diff -Nru libheif-1.19.8/libheif/image-items/image_item.cc libheif-1.23.4/libheif/image-items/image_item.cc --- libheif-1.19.8/libheif/image-items/image_item.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/image_item.cc 2026-09-06 14:45:17.000000000 +0000 @@ -33,13 +33,15 @@ #include "tiled.h" #include "codecs/decoder.h" #include "color-conversion/colorconversion.h" -#include "api/libheif/api_structs.h" +#include "api_structs.h" #include "plugin_registry.h" #include "security_limits.h" +#include #include #include #include +#include //#include #if WITH_UNCOMPRESSED_CODEC @@ -61,33 +63,48 @@ } -std::shared_ptr ImageItem::get_file() const +bool ImageItem::is_property_essential(const std::shared_ptr& property) const { - return m_heif_context->get_heif_file(); + if (property->get_short_type() == fourcc("ispe")) { + return is_ispe_essential(); + } + else { + return property->is_essential(); + } } -heif_property_id ImageItem::add_property(std::shared_ptr property, bool essential) +std::shared_ptr ImageItem::get_file() const { - // TODO: is this correct? What happens when add_property does deduplicate the property? - m_properties.push_back(property); - return get_file()->add_property(get_id(), property, essential); + return m_heif_context->get_heif_file(); } -heif_property_id ImageItem::add_property_without_deduplication(std::shared_ptr property, bool essential) +heif_property_id ImageItem::add_property(const std::shared_ptr& property, bool essential) { - m_properties.push_back(property); - return get_file()->add_property_without_deduplication(get_id(), property, essential); + if (!property) { + return 0; + } + + // HeifFile::add_property() deduplicates the property box, so only remember it here if the + // item does not hold it yet. The returned id is the position in the item's property list and + // is correct in both cases. + if (std::find(m_properties.begin(), m_properties.end(), property) == m_properties.end()) { + m_properties.push_back(property); + } + + return get_file()->add_property(get_id(), property, essential); } -Error ImageItem::init_decoder_from_item(heif_item_id id) +heif_property_id ImageItem::add_property_without_deduplication(const std::shared_ptr& property, bool essential) { - m_id = id; + if (!property) { + return 0; + } - Error err = on_load_file(); - return err; + m_properties.push_back(property); + return get_file()->add_property_without_deduplication(get_id(), property, essential); } @@ -166,14 +183,14 @@ std::stringstream sstr; sstr << "Image item of type '" << fourcc_to_string(item_type) << "' is not supported."; Error err{ heif_error_Unsupported_feature, heif_suberror_Unsupported_image_type, sstr.str() }; - return std::make_shared(item_type, id, err); + return std::make_shared(ctx, item_type, id, err); #endif } else if (item_type == fourcc("j2k1")) { return std::make_shared(ctx, id); } else if (item_type == fourcc("lhv1")) { - return std::make_shared(item_type, id, + return std::make_shared(ctx, item_type, id, Error{heif_error_Unsupported_feature, heif_suberror_Unsupported_image_type, "Layered HEVC images (lhv1) are not supported yet"}); @@ -190,9 +207,11 @@ else if (item_type == fourcc("iden")) { return std::make_shared(ctx, id); } +#if HEIF_ENABLE_EXPERIMENTAL_FEATURES else if (item_type == fourcc("tili")) { return std::make_shared(ctx, id); } +#endif else { // This item has an unknown type. It could be an image or anything else. // Do not process the item. @@ -213,6 +232,8 @@ return std::make_shared(ctx); case heif_compression_VVC: return std::make_shared(ctx); + case heif_compression_AVC: + return std::make_shared(ctx); #if WITH_UNCOMPRESSED_CODEC case heif_compression_uncompressed: return std::make_shared(ctx); @@ -229,25 +250,28 @@ } -Result ImageItem::encode_to_bitstream_and_boxes(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +Result ImageItem::encode_to_bitstream_and_boxes(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { // === generate compressed image bitstream - Result encodeResult = encode(image, encoder, options, input_class); - if (encodeResult.error) { + Result encodeResult = encode(image, encoder, options, input_class); + if (!encodeResult) { return encodeResult; } - CodedImageData& codedImage = encodeResult.value; + Encoder::CodedImageData& codedImage = *encodeResult; // === generate properties // --- choose which color profile to put into 'colr' box - add_color_profile(image, options, input_class, options.output_nclx_profile, codedImage); + auto colr_boxes = add_color_profile(image, options, input_class, options.output_nclx_profile); + codedImage.properties.insert(codedImage.properties.end(), + colr_boxes.begin(), + colr_boxes.end()); // --- ispe @@ -291,7 +315,9 @@ input_height != encoded_height) { auto clap = std::make_shared(); - clap->set(input_width, input_height, encoded_width, encoded_height); + if (Error err = clap->set(input_width, input_height, encoded_width, encoded_height)) { + return err; + } codedImage.properties.push_back(clap); } @@ -306,19 +332,24 @@ // --- write PIXI property std::shared_ptr pixi = std::make_shared(); - if (colorspace == heif_colorspace_monochrome) { - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y)); + bool valid_pixi = false; + + if (colorspace == heif_colorspace_filter_array) { + // Skip pixi for filter array images — bit depth info is in uncC + } + else if (colorspace == heif_colorspace_monochrome) { + valid_pixi = pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y)); } else if (colorspace == heif_colorspace_YCbCr) { - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y)); - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cb)); - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cr)); + valid_pixi = (pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y)) && + pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cb)) && + pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cr))); } else if (colorspace == heif_colorspace_RGB) { if (chroma == heif_chroma_444) { - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_R)); - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_G)); - pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_B)); + valid_pixi = (pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_R)) && + pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_G)) && + pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_B))); } else if (chroma == heif_chroma_interleaved_RGB || chroma == heif_chroma_interleaved_RGBA || @@ -326,43 +357,26 @@ chroma == heif_chroma_interleaved_RRGGBB_BE || chroma == heif_chroma_interleaved_RRGGBBAA_LE || chroma == heif_chroma_interleaved_RRGGBBAA_BE) { - uint8_t bpp = image->get_bits_per_pixel(heif_channel_interleaved); - pixi->add_channel_bits(bpp); - pixi->add_channel_bits(bpp); - pixi->add_channel_bits(bpp); + uint16_t bpp = image->get_bits_per_pixel(heif_channel_interleaved); + valid_pixi = (pixi->add_channel_bits(bpp) && + pixi->add_channel_bits(bpp) && + pixi->add_channel_bits(bpp)); } } - codedImage.properties.push_back(pixi); - - - // --- write PASP property - - if (image->has_nonsquare_pixel_ratio()) { - auto pasp = std::make_shared(); - image->get_pixel_ratio(&pasp->hSpacing, &pasp->vSpacing); - - codedImage.properties.push_back(pasp); - } - - // --- write CLLI property - - if (image->has_clli()) { - auto clli = std::make_shared(); - clli->clli = image->get_clli(); - - codedImage.properties.push_back(clli); + if (valid_pixi) { + codedImage.properties.push_back(pixi); } + // --- generate properties for image extra data - // --- write MDCV property + // copy over ImageDescription into image item + *static_cast(this) = static_cast(*image); - if (image->has_mdcv()) { - auto mdcv = std::make_shared(); - mdcv->mdcv = image->get_mdcv(); - - codedImage.properties.push_back(mdcv); - } + auto extra_data_properties = image->generate_property_boxes(false); + codedImage.properties.insert(codedImage.properties.end(), + extra_data_properties.begin(), + extra_data_properties.end()); return encodeResult; } @@ -370,9 +384,9 @@ Error ImageItem::encode_to_item(HeifContext* ctx, const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { uint32_t input_width = image->get_width(); uint32_t input_height = image->get_height(); @@ -382,14 +396,18 @@ // compress image and assign data to item - Result codingResult = encode_to_bitstream_and_boxes(image, encoder, options, input_class); - if (codingResult.error) { - return codingResult.error; + Result codingResult = encode_to_bitstream_and_boxes(image, encoder, options, input_class); + if (!codingResult) { + return codingResult.error(); } - CodedImageData& codedImage = codingResult.value; + Encoder::CodedImageData& codedImage = *codingResult; - auto infe_box = ctx->get_heif_file()->add_new_infe_box(get_infe_type()); + auto infe_result = ctx->get_heif_file()->add_new_infe_box(get_infe_type()); + if (!infe_result) { + return infe_result.error(); + } + auto infe_box = *infe_result; heif_item_id image_id = infe_box->get_item_ID(); set_id(image_id); @@ -398,9 +416,12 @@ // set item properties - for (auto& propertyBox : codingResult.value.properties) { + for (auto& propertyBox : codingResult->properties) { + bool essential = is_property_essential(propertyBox); + + // TODO: can we simply use add_property() ? int index = ctx->get_heif_file()->get_ipco_box()->find_or_append_child_box(propertyBox); - ctx->get_heif_file()->get_ipma_box()->add_property_for_item_ID(image_id, Box_ipma::PropertyAssociation{propertyBox->is_essential(), + ctx->get_heif_file()->get_ipma_box()->add_property_for_item_ID(image_id, Box_ipma::PropertyAssociation{essential, uint16_t(index + 1)}); } @@ -421,7 +442,9 @@ } // TODO: move this into encode_to_bistream_and_boxes() - ctx->get_heif_file()->add_orientation_properties(image_id, options.image_orientation); + if (Error err = ctx->get_heif_file()->add_orientation_properties(image_id, options.image_orientation)) { + return err; + } return Error::Ok; } @@ -474,7 +497,7 @@ if (*inout_colorspace == heif_colorspace_YCbCr) { auto nclx = get_color_profile_nclx(); - if (nclx && nclx->get_matrix_coefficients() == 0) { + if (nclx.get_matrix_coefficients() == 0) { *inout_colorspace = heif_colorspace_RGB; *inout_chroma = heif_chroma_444; // TODO: this or keep the original chroma? } @@ -487,11 +510,11 @@ Error ImageItem::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { auto decoderResult = get_decoder(); - if (decoderResult.error) { - return decoderResult.error; + if (!decoderResult) { + return decoderResult.error(); } - auto decoder = decoderResult.value; + auto decoder = *decoderResult; Error err = decoder->get_coded_image_colorspace(out_colorspace, out_chroma); if (err) { @@ -507,11 +530,11 @@ int ImageItem::get_luma_bits_per_pixel() const { auto decoderResult = get_decoder(); - if (decoderResult.error) { - return decoderResult.error; + if (!decoderResult) { + return -1; } - auto decoder = decoderResult.value; + auto decoder = *decoderResult; return decoder->get_luma_bits_per_pixel(); } @@ -520,144 +543,183 @@ int ImageItem::get_chroma_bits_per_pixel() const { auto decoderResult = get_decoder(); - if (decoderResult.error) { - return decoderResult.error; + if (!decoderResult) { + return -1; } - auto decoder = decoderResult.value; + auto decoder = *decoderResult; return decoder->get_chroma_bits_per_pixel(); } -static std::shared_ptr compute_target_nclx_profile(const std::shared_ptr& image, const heif_color_profile_nclx* output_nclx_profile) +Result ImageItem::encode(const std::shared_ptr& image, + heif_encoder* h_encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { - auto target_nclx_profile = std::make_shared(); - - // If there is an output NCLX specified, use that. - if (output_nclx_profile) { - target_nclx_profile->set_from_heif_color_profile_nclx(output_nclx_profile); - } - // Otherwise, if there is an input NCLX, keep that. - else if (auto input_nclx = image->get_color_profile_nclx()) { - *target_nclx_profile = *input_nclx; - } - // Otherwise, just use the defaults (set below) - else { - target_nclx_profile->set_undefined(); - } - - target_nclx_profile->replace_undefined_values_with_sRGB_defaults(); - - return target_nclx_profile; + auto encoder = get_encoder(); + return encoder->encode(image, h_encoder, options, input_class); } -static bool nclx_profile_matches_spec(heif_colorspace colorspace, - std::shared_ptr image_nclx, - const struct heif_color_profile_nclx* spec_nclx) +void ImageItem::set_alpha_channel(std::shared_ptr img) { - if (colorspace != heif_colorspace_YCbCr) { - return true; - } - - // No target specification -> always matches - if (!spec_nclx) { - return true; - } - - if (!image_nclx) { - // if no input nclx is specified, compare against default one - image_nclx = std::make_shared(); - } - - if (image_nclx->get_full_range_flag() != (spec_nclx->full_range_flag == 0 ? false : true)) { - return false; + m_alpha_channel = std::move(img); + if (!m_alpha_channel) { + return; } - if (image_nclx->get_matrix_coefficients() != spec_nclx->matrix_coefficients) { - return false; + // Avoid emitting a duplicate Alpha description if set_alpha_channel was + // called more than once. + for (const auto& d : get_component_descriptions()) { + if (d.channel == heif_channel_Alpha) { + return; + } } - // TODO: are the colour primaries relevant for matrix-coefficients != 12,13 ? - // If not, we should skip this test for anything else than matrix-coefficients != 12,13. - if (image_nclx->get_colour_primaries() != spec_nclx->color_primaries) { - return false; + // Bit depth of the alpha plane comes from the alpha aux item's coded + // image (typically a monochrome HEVC/AVIF channel). Fall back to 8 bpp + // if the decoder cannot tell us. + int alpha_bpp = m_alpha_channel->get_luma_bits_per_pixel(); + if (alpha_bpp <= 0) { + alpha_bpp = 8; } - return true; + ComponentDescription desc; + desc.component_id = mint_component_id(); + desc.channel = heif_channel_Alpha; + desc.component_type = heif_cmpd_component_type_alpha; + desc.datatype = heif_component_datatype_unsigned_integer; + desc.bit_depth = static_cast(alpha_bpp); + desc.width = get_ispe_width(); + desc.height = get_ispe_height(); + desc.has_data_plane = true; + add_component_description(std::move(desc)); } -Result> ImageItem::convert_colorspace_for_encoding(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options) +void ImageItem::populate_component_descriptions() { - const heif_color_profile_nclx* output_nclx_profile; - - if (const auto* nclx = get_forced_output_nclx()) { - output_nclx_profile = nclx; - } - else { - output_nclx_profile = options.output_nclx_profile; + // Idempotent: a subclass override (e.g. unci) may already have populated. + if (!get_component_descriptions().empty()) { + return; } - - heif_colorspace colorspace = image->get_colorspace(); - heif_chroma chroma = image->get_chroma_format(); - - if (encoder->plugin->plugin_api_version >= 2) { - encoder->plugin->query_input_colorspace2(encoder->encoder, &colorspace, &chroma); - } - else { - encoder->plugin->query_input_colorspace(&colorspace, &chroma); + // Visual codecs (HEVC/AVC/AVIF/JPEG/JPEG2000/VVC). Requires the decoder to + // be initialized so we can read colorspace / chroma / bit depths from the + // codec config. If the decoder isn't ready (e.g. on the encoder-output + // path that doesn't call initialize_decoder, or for items whose codec is + // not supported), bail out and leave m_components empty. + auto decoderResult = get_decoder(); + if (!decoderResult || !*decoderResult) { + return; } + heif_colorspace colorspace = heif_colorspace_undefined; + heif_chroma chroma = heif_chroma_undefined; + if (Error err = get_coded_image_colorspace(&colorspace, &chroma); err) { + return; + } + + uint32_t img_w = get_ispe_width(); + uint32_t img_h = get_ispe_height(); + int luma_bpp = get_luma_bits_per_pixel(); + int chroma_bpp = get_chroma_bits_per_pixel(); + if (luma_bpp <= 0) luma_bpp = 8; + if (chroma_bpp <= 0) chroma_bpp = luma_bpp; + + auto emit = [this](heif_channel ch, uint16_t type, int bpp, + uint32_t w, uint32_t h) { + ComponentDescription desc; + desc.component_id = mint_component_id(); + desc.channel = ch; + desc.component_type = type; + desc.datatype = heif_component_datatype_unsigned_integer; + desc.bit_depth = static_cast(bpp); + desc.width = w; + desc.height = h; + desc.has_data_plane = true; + add_component_description(std::move(desc)); + }; + + switch (colorspace) { + case heif_colorspace_monochrome: + emit(heif_channel_Y, heif_cmpd_component_type_monochrome, luma_bpp, img_w, img_h); + break; + + case heif_colorspace_YCbCr: { + uint32_t cw = channel_width(img_w, chroma, heif_channel_Cb); + uint32_t ch_ = channel_height(img_h, chroma, heif_channel_Cb); + emit(heif_channel_Y, heif_cmpd_component_type_Y, luma_bpp, img_w, img_h); + emit(heif_channel_Cb, heif_cmpd_component_type_Cb, chroma_bpp, cw, ch_); + emit(heif_channel_Cr, heif_cmpd_component_type_Cr, chroma_bpp, cw, ch_); + break; + } + + case heif_colorspace_RGB: + emit(heif_channel_R, heif_cmpd_component_type_red, luma_bpp, img_w, img_h); + emit(heif_channel_G, heif_cmpd_component_type_green, luma_bpp, img_w, img_h); + emit(heif_channel_B, heif_cmpd_component_type_blue, luma_bpp, img_w, img_h); + break; - - // If output format forces an NCLX, use that. Otherwise use user selected NCLX. - - std::shared_ptr target_nclx_profile = compute_target_nclx_profile(image, output_nclx_profile); - - // --- convert colorspace - - std::shared_ptr output_image; - - if (colorspace == image->get_colorspace() && - chroma == image->get_chroma_format() && - nclx_profile_matches_spec(colorspace, image->get_color_profile_nclx(), output_nclx_profile)) { - return image; + default: + // Other colorspaces (filter_array, nonvisual) are unci-only and are + // populated by the unci override. + break; } +} - // @TODO: use color profile when converting - int output_bpp = 0; // same as input - - //auto target_nclx = std::make_shared(); - //target_nclx->set_from_heif_color_profile_nclx(target_heif_nclx); +bool ImageItem::populate_descriptions_from_child(const ImageItem& child, + uint32_t child_w, uint32_t child_h) +{ + const auto& child_descs = child.get_component_descriptions(); + if (child_descs.empty()) { + return false; + } - auto output_image_result = convert_colorspace(image, colorspace, chroma, target_nclx_profile, - output_bpp, options.color_conversion_options, - get_context()->get_security_limits()); - if (output_image_result.error) { - return output_image_result.error; + uint32_t img_w = get_ispe_width(); + uint32_t img_h = get_ispe_height(); + if (img_w == 0 || img_h == 0 || child_w == 0 || child_h == 0) { + return false; } - return *output_image_result; + for (const auto& src : child_descs) { + ComponentDescription d = src; + d.component_id = mint_component_id(); + if (src.has_data_plane) { + // Preserve subsampling ratio: a half-size chroma plane in the child + // becomes half of img_w/h in the wrapper. + uint64_t w64 = static_cast(img_w) * src.width / child_w; + uint64_t h64 = static_cast(img_h) * src.height / child_h; + d.width = static_cast(w64); + d.height = static_cast(h64); + } + add_component_description(std::move(d)); + } + return true; } -void ImageItem::add_color_profile(const std::shared_ptr& image, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class, - const heif_color_profile_nclx* target_heif_nclx, - ImageItem::CodedImageData& inout_codedImage) +std::vector > +ImageItem::add_color_profile(const std::shared_ptr& image, + const heif_encoding_options& options, + heif_image_input_class input_class, + const heif_color_profile_nclx* target_heif_nclx) { + std::vector > colr_boxes; + if (input_class == heif_image_input_class_normal || input_class == heif_image_input_class_thumbnail) { + // No color profile for non-visual images (e.g. elevation data) + if (image->get_colorspace() == heif_colorspace_custom) { + return colr_boxes; + } + auto icc_profile = image->get_color_profile_icc(); if (icc_profile) { auto colr = std::make_shared(); colr->set_color_profile(icc_profile); - inout_codedImage.properties.push_back(colr); + colr_boxes.push_back(colr); } @@ -682,63 +744,67 @@ auto colr = std::make_shared(); colr->set_color_profile(target_nclx_profile); - inout_codedImage.properties.push_back(colr); + colr_boxes.push_back(colr); } } -} - - -void ImageItem::CodedImageData::append(const uint8_t* data, size_t size) -{ - bitstream.insert(bitstream.end(), data, data + size); -} - - -void ImageItem::CodedImageData::append_with_4bytes_size(const uint8_t* data, size_t size) -{ - assert(size <= 0xFFFFFFFF); - - uint8_t size_field[4]; - size_field[0] = (uint8_t) ((size >> 24) & 0xFF); - size_field[1] = (uint8_t) ((size >> 16) & 0xFF); - size_field[2] = (uint8_t) ((size >> 8) & 0xFF); - size_field[3] = (uint8_t) ((size >> 0) & 0xFF); - bitstream.insert(bitstream.end(), size_field, size_field + 4); - bitstream.insert(bitstream.end(), data, data + size); + return colr_boxes; } Error ImageItem::transform_requested_tile_position_to_original_tile_position(uint32_t& tile_x, uint32_t& tile_y) const { Result>> propertiesResult = get_properties(); - if (propertiesResult.error) { - return propertiesResult.error; + if (!propertiesResult) { + return propertiesResult.error(); } + // The caller's (tile_x, tile_y) are in the *displayed* tile grid, so they + // must be validated against the displayed dimensions, not the in-file ones. + // For rotations of 90°/270° the displayed grid has its columns and rows + // swapped relative to the file. Using the file dims (as before) both let + // out-of-range coordinates through and produced unsigned underflows inside + // the inverse-rotation formulas (e.g. `num_rows - 1 - tile_x` with + // `tile_x >= num_rows`). heif_image_tiling tiling = get_heif_image_tiling(); + if (Error err = process_image_transformations_on_tiling(tiling)) { + return err; + } - //for (auto& prop : std::ranges::reverse_view(propertiesResult.value)) { - for (auto propIter = propertiesResult.value.rbegin(); propIter != propertiesResult.value.rend(); propIter++) { + if (tile_x >= tiling.num_columns || tile_y >= tiling.num_rows) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Tile coordinate out of range for displayed image"}; + } + + // Walk the property chain in reverse, undoing each transformation as we go. + // Track the current (intermediate) tile-grid dimensions so each inverse uses + // the right extent and so 90°/270° rotations swap dims for subsequent steps. + uint32_t cur_cols = tiling.num_columns; + uint32_t cur_rows = tiling.num_rows; + + for (auto propIter = propertiesResult->rbegin(); propIter != propertiesResult->rend(); propIter++) { if (auto irot = std::dynamic_pointer_cast(*propIter)) { switch (irot->get_rotation_ccw()) { case 90: { - uint32_t tx0 = tiling.num_columns - 1 - tile_y; + uint32_t tx0 = cur_rows - 1 - tile_y; uint32_t ty0 = tile_x; - tile_y = ty0; tile_x = tx0; + tile_y = ty0; + std::swap(cur_cols, cur_rows); break; } case 270: { uint32_t tx0 = tile_y; - uint32_t ty0 = tiling.num_rows - 1 - tile_x; - tile_y = ty0; + uint32_t ty0 = cur_cols - 1 - tile_x; tile_x = tx0; + tile_y = ty0; + std::swap(cur_cols, cur_rows); break; } case 180: { - tile_x = tiling.num_columns - 1 - tile_x; - tile_y = tiling.num_rows - 1 - tile_y; + tile_x = cur_cols - 1 - tile_x; + tile_y = cur_rows - 1 - tile_y; break; } case 0: @@ -752,10 +818,10 @@ if (auto imir = std::dynamic_pointer_cast(*propIter)) { switch (imir->get_mirror_direction()) { case heif_transform_mirror_direction_horizontal: - tile_x = tiling.num_columns - 1 - tile_x; + tile_x = cur_cols - 1 - tile_x; break; case heif_transform_mirror_direction_vertical: - tile_y = tiling.num_rows - 1 - tile_y; + tile_y = cur_rows - 1 - tile_y; break; default: assert(false); @@ -768,9 +834,333 @@ } -Result> ImageItem::decode_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +void ImageItem::set_clli(const heif_content_light_level& clli) +{ + ImageDescription::set_clli(clli); + add_property(create_clli_box(), false); +} + + +void ImageItem::set_mdcv(const heif_mastering_display_colour_volume& mdcv) +{ + ImageDescription::set_mdcv(mdcv); + add_property(create_mdcv_box(), false); +} + + +void ImageItem::set_amve(const heif_ambient_viewing_environment& amve) +{ + ImageDescription::set_amve(amve); + add_property(create_amve_box(), false); +} + + +void ImageItem::set_nominal_diffuse_white_luminance(uint32_t luminance) +{ + ImageDescription::set_nominal_diffuse_white_luminance(luminance); + add_property(create_ndwt_box(), false); +} + + +void ImageItem::set_pixel_ratio(uint32_t h, uint32_t v) +{ + ImageDescription::set_pixel_ratio(h, v); + add_property(create_pasp_box(), false); +} + + +void ImageItem::set_color_profile_nclx(const nclx_profile& profile) +{ + ImageDescription::set_color_profile_nclx(profile); + add_property(create_colr_box_nclx(), false); +} + + +void ImageItem::set_color_profile_icc(const std::shared_ptr& profile) +{ + ImageDescription::set_color_profile_icc(profile); + add_property(create_colr_box_icc(), false); +} + +void ImageItem::set_omaf_image_projection(heif_omaf_image_projection projection) +{ + ImageDescription::set_omaf_image_projection(projection); + add_property(create_prfr_box(), true); +} + + +namespace { + +// Detect cycles in the decode reference graph reached from `root`, following the +// same edges the decode recursion follows. This uses an explicit heap worklist +// rather than recursion on purpose: the graph depth is influenced by the input +// (a chain of derived items, and unbounded when the item-count limit is +// disabled), so a recursive walk could exhaust the native stack and crash the +// process before any decode, on the read path of an untrusted file. The worklist +// grows on the heap instead, so depth is bounded only by available memory. +// +// It is a depth-first walk. `on_path` holds the items on the current +// root-to-node path; reaching one that is already on the path is a cycle. +// `verified` memoizes items whose subtree is already proven acyclic, so a shared +// sub-image reached through several paths is visited once and the walk stays +// linear rather than exponential in the number of root-to-item paths +// (cf. the decode amplification bound, GHSA-x8xm-cm2c-cfc8). +Error check_decode_reference_cycles(const ImageItem* root) +{ + std::set on_path; // items on the current DFS path + std::set verified; // items whose subtree is proven acyclic + + // Collect the decode-input children of an item, in the exact order the decode + // recursion follows them: the derived-image ('dimg') inputs (grid tiles, + // overlay inputs, the 'iden' base) first, then the alpha ('auxl') auxiliary. + // The returned shared_ptrs keep the child ImageItems alive for as long as the + // frame that holds them stays on the worklist. + auto collect_children = [](const ImageItem* item) { + std::vector> children; + auto file = item->get_file(); + auto iref = file ? file->get_iref_box() : nullptr; + if (iref) { + for (heif_item_id child_id : iref->get_references(item->get_id(), fourcc("dimg"))) { + if (auto child = item->get_context()->get_image(child_id, true)) { + children.push_back(std::move(child)); + } + } + } + if (const auto& alpha = item->get_alpha_channel()) { + children.push_back(alpha); + } + return children; + }; + + // One worklist frame per item currently on the DFS path. `next` is the index + // of the child to descend into next; when it reaches the end, the item's whole + // subtree has been proven acyclic and the item leaves the path. + struct Frame { + const ImageItem* item; + std::vector> children; + size_t next = 0; + }; + + std::vector stack; + on_path.insert(root->get_id()); + stack.push_back(Frame{root, collect_children(root), 0}); + + while (!stack.empty()) { + Frame& top = stack.back(); + + if (top.next >= top.children.size()) { + // All children proven acyclic: leave the DFS path and memoize the subtree. + heif_item_id done_id = top.item->get_id(); + on_path.erase(done_id); + verified.insert(done_id); + stack.pop_back(); + continue; + } + + const ImageItem* child = top.children[top.next++].get(); + // From here on `top` must not be used: the push_back below may reallocate + // `stack` and invalidate the reference. + + heif_item_id child_id = child->get_id(); + if (on_path.find(child_id) != on_path.end()) { + return {heif_error_Invalid_input, + heif_suberror_Item_reference_cycle, + "Image reference cycle"}; + } + if (verified.find(child_id) != verified.end()) { + continue; // subtree already proven acyclic; do not descend into it again + } + + on_path.insert(child_id); + auto grandchildren = collect_children(child); + stack.push_back(Frame{child, std::move(grandchildren), 0}); + } + + return Error::Ok; +} + + +// --- MIAF derived-image dependency constraints (ISO/IEC 23000-22, clause 7.3.11) +// +// MIAF restricts the derivation chain to a fixed order. From base to top it is: +// coded image(s) -> [iden] -> grid -> [iden] -> overlay -> [iden] +// (7.3.11.1), plus: an 'iden' shall not be derived directly from another 'iden' +// (7.3.11.2), and a grid tile that is an 'iden' must refer directly to a coded +// image (7.3.11.4.1). So, ignoring 'iden', a chain may apply overlay above grid +// above the coded base, each at most once. We model that with a "structural +// rank": coded=0, grid=1, overlay=2. Walking from the top down, each derived +// item must have rank <= the rank its position allows, and it lowers the rank +// allowed for its own inputs (grid inputs must be coded; overlay inputs may be +// grid or below). 'iden' is transparent to the rank but must not sit directly +// on another 'iden'. Only the 'dimg' derivation is constrained here; auxiliary +// images are checked as their own fresh chains. +enum { MIAF_RANK_CODED = 0, MIAF_RANK_GRID = 1, MIAF_RANK_OVERLAY = 2 }; + +int miaf_structural_rank(const ImageItem* item, bool& is_iden) +{ + uint32_t type = item->get_infe_type(); + is_iden = (type == fourcc("iden")); + if (type == fourcc("iovl")) { return MIAF_RANK_OVERLAY; } + if (type == fourcc("grid")) { return MIAF_RANK_GRID; } + return MIAF_RANK_CODED; // coded image, or 'iden' (rank unused when is_iden) +} + +// `max_rank` is the highest structural rank allowed at this item's position; +// `parent_is_iden` is true when the immediate parent on the derivation path is +// an 'iden'. `verified` memoizes (item, max_rank, parent_is_iden) triples that +// already passed, keeping a shared sub-image from being re-walked per path. +Error check_miaf_derivation_constraints(const ImageItem* item, + int max_rank, bool parent_is_iden, + std::set& verified) +{ + heif_item_id id = item->get_id(); + + bool is_iden = false; + int rank = miaf_structural_rank(item, is_iden); + + if (is_iden) { + if (parent_is_iden) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "MIAF: an 'iden' image is derived directly from another 'iden' image"}; + } + } + else if (rank > max_rank) { + return {heif_error_Invalid_input, heif_suberror_Unspecified, + "MIAF: derived-image dependencies are not in the order allowed by ISO/IEC 23000-22"}; + } + + uint64_t key = (static_cast(id) << 4) | + (static_cast(max_rank & 0x3) << 2) | + (parent_is_iden ? 2u : 0u) | (is_iden ? 1u : 0u); + if (!verified.insert(key).second) { + return Error::Ok; // already verified in this context + } + + // Rank budget passed to this item's own 'dimg' inputs. + int child_max_rank; + bool child_parent_is_iden; + if (is_iden) { + child_max_rank = max_rank; // transparent: inputs keep this position + child_parent_is_iden = true; + } + else if (rank == MIAF_RANK_OVERLAY) { + child_max_rank = MIAF_RANK_GRID; // overlay inputs: grid or below + child_parent_is_iden = false; + } + else if (rank == MIAF_RANK_GRID) { + child_max_rank = MIAF_RANK_CODED; // grid inputs: coded (or iden -> coded) + child_parent_is_iden = false; + } + else { + return Error::Ok; // coded image: leaf of the derivation chain + } + + auto file = item->get_file(); + auto iref = file ? file->get_iref_box() : nullptr; + if (iref) { + for (heif_item_id child_id : iref->get_references(id, fourcc("dimg"))) { + auto child = item->get_context()->get_image(child_id, true); + if (child) { + if (Error err = check_miaf_derivation_constraints(child.get(), child_max_rank, + child_parent_is_iden, verified)) { + return err; + } + } + } + } + + // An auxiliary (e.g. alpha) image is a separate image whose own derivation + // chain must independently satisfy MIAF, so check it as a fresh chain. + if (auto alpha = item->get_alpha_channel()) { + if (Error err = check_miaf_derivation_constraints(alpha.get(), MIAF_RANK_OVERLAY, + /*parent_is_iden=*/false, verified)) { + return err; + } + } + + return Error::Ok; +} + +} // namespace + + +Error ImageItem::verify_decodable() const { + // Always: reject a cyclic decode reference graph (both 'dimg' and 'auxl' + // edges) before decoding. See the declaration in image_item.h. + if (Error err = check_decode_reference_cycles(this)) { + return err; + } + + // Optionally: enforce MIAF's restricted derived-image dependencies + // (ISO/IEC 23000-22, clause 7.3.11). Applied when the file declares the 'miaf' + // brand. + // + // TODO(v1.24.x): also apply this when a security-limits flag + // (always_apply_MIAF_derivation_constraints) is set, so that a malicious file + // cannot bypass the check simply by omitting the 'miaf' brand. That flag is a + // heif_security_limits API addition and therefore has to wait for v1.24.x. + bool apply_miaf = false; + if (auto file = get_file()) { + if (auto ftyp = file->get_ftyp_box()) { + apply_miaf = ftyp->has_compatible_brand(heif_brand2_miaf); + } + } + + if (apply_miaf) { + std::set verified; + if (Error err = check_miaf_derivation_constraints(this, MIAF_RANK_OVERLAY, + /*parent_is_iden=*/false, verified)) { + return err; + } + } + + return Error::Ok; +} + + +Result> ImageItem::decode_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const +{ + // Check for cycles before taking m_decode_mutex: a derived item that + // (transitively) references itself would otherwise re-enter decode_image() + // on the same ImageItem and self-deadlock on the non-recursive mutex. + // The matching insert lives inside decode_compressed_image() of derived + // items (grid/overlay/iden), so the current item is in decode_state only + // when called from one of its own descendants. + // + // Second-layer hardening, not required for correctness: the top-level decode + // already ran ImageItem::verify_decodable() (HeifContext::decode_image), which + // proves the whole reachable decode graph is acyclic before any recursion, so + // this per-path check can never fire on a graph that reached here. It is kept + // as a cheap in-decode backstop, and the same applies to the equivalent checks + // in decode_compressed_image() and in grid/overlay/iden. We may remove them in + // the future once verify_decodable() is the sole cycle guard. + if (decode_state.processed_ids.contains(m_id)) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iref' has cyclic references"}; + } + + // Bound the total number of sub-image decodes for this top-level decode. + // Derived images (grid/iovl/iden) can reference the same base image through + // indirection, and because the cycle-detection set is per-path, a shared + // subtree is otherwise re-decoded once per path that reaches it, which grows + // as branch^depth for nested references. This is the single choke point that + // every item decode passes through. (GHSA-x8xm-cm2c-cfc8) + if (!decode_state.count_decode()) { + return Error{heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "Too many derived-image decode operations (possible reference amplification)"}; + } + + if (m_item_error) { + return m_item_error; + } + + std::lock_guard lock(m_decode_mutex); + // --- check whether image size (according to 'ispe') exceeds maximum if (!decode_tile_only) { @@ -794,24 +1184,33 @@ // --- decode image - Result> decodingResult = decode_compressed_image(options, decode_tile_only, tile_x0, tile_y0); - if (decodingResult.error) { - return decodingResult.error; + Result> decodingResult = decode_compressed_image(options, decode_tile_only, tile_x0, tile_y0, decode_state); + if (!decodingResult) { + return decodingResult.error(); + } + + auto img = *decodingResult; + if (!img) { + // Safety net: no known decoding path returns a null image without an error anymore. + return Error(heif_error_Decoder_plugin_error, heif_suberror_Unspecified, + "Decoding returned no image"); } - auto img = decodingResult.value; + // --- validate the decoded image against the signaled size (pre-transform) + + if (Error err = check_decoded_image_size(*img, decode_tile_only, tile_x0, tile_y0)) { + return err; + } std::shared_ptr file = m_heif_context->get_heif_file(); // --- apply image transformations - Error error; - if (options.ignore_transformations == false) { Result>> propertiesResult = get_properties(); - if (propertiesResult.error) { - return propertiesResult.error; + if (!propertiesResult) { + return propertiesResult.error(); } const std::vector>& properties = *propertiesResult; @@ -819,21 +1218,21 @@ for (const auto& property : properties) { if (auto rot = std::dynamic_pointer_cast(property)) { auto rotateResult = img->rotate_ccw(rot->get_rotation_ccw(), m_heif_context->get_security_limits()); - if (rotateResult.error) { - return error; + if (!rotateResult) { + return rotateResult.error(); } - img = rotateResult.value; + img = *rotateResult; } if (auto mirror = std::dynamic_pointer_cast(property)) { auto mirrorResult = img->mirror_inplace(mirror->get_mirror_direction(), get_context()->get_security_limits()); - if (mirrorResult.error) { - return error; + if (!mirrorResult) { + return mirrorResult.error(); } - img = mirrorResult.value; + img = *mirrorResult; } @@ -846,10 +1245,15 @@ uint32_t img_width = img->get_width(); uint32_t img_height = img->get_height(); - int left = clap->left_rounded(img_width); - int right = clap->right_rounded(img_width); - int top = clap->top_rounded(img_height); - int bottom = clap->bottom_rounded(img_height); + auto clapCrop = clap->get_crop(img_width, img_height); + if (!clapCrop) { + return clapCrop.error(); + } + + int left = clapCrop->left; + int right = clapCrop->right; + int top = clapCrop->top; + int bottom = clapCrop->bottom; if (left < 0) { left = 0; } if (top < 0) { top = 0; } @@ -864,13 +1268,20 @@ } auto cropResult = img->crop(left, right, top, bottom, m_heif_context->get_security_limits()); - if (cropResult.error) { - return cropResult.error; + if (!cropResult) { + return cropResult.error(); } - img = cropResult.value; + img = *cropResult; } } + + + if (auto iscl = std::dynamic_pointer_cast(property)) { + return Error(heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Image scaling (iscl) transformative property is not yet supported"); + } } } @@ -884,12 +1295,27 @@ std::shared_ptr alpha_image = get_alpha_channel(); if (alpha_image) { - auto alphaDecodingResult = alpha_image->decode_image(options, decode_tile_only, tile_x0, tile_y0); - if (alphaDecodingResult.error) { - return alphaDecodingResult.error; + if (alpha_image->get_item_error()) { + return alpha_image->get_item_error(); + } + + // Record this item on the current decode path before following the alpha + // ('auxl') edge. Unlike the derived-image ('dimg') edges, whose cycle-guard + // insert happens inside decode_compressed_image(), the alpha edge is + // followed here in the base decode_image() using this frame's own + // decode_state. decode_compressed_image() only received a *copy* of it, so + // its insert of m_id is invisible here. Without adding m_id ourselves, a + // cycle of alpha references (auxl A->B, B->A) would re-enter decode_image() + // on an item whose non-recursive m_decode_mutex is still held one frame up, + // deadlocking the decode thread. (GHSA-8fmq-r4pf-7m57) + decode_state.processed_ids.insert(m_id); + + auto alphaDecodingResult = alpha_image->decode_image(options, decode_tile_only, tile_x0, tile_y0, decode_state); + if (!alphaDecodingResult) { + return alphaDecodingResult.error(); } - std::shared_ptr alpha = alphaDecodingResult.value; + std::shared_ptr alpha = *alphaDecodingResult; // TODO: check that sizes are the same and that we have an Y channel // BUT: is there any indication in the standard that the alpha channel should have the same size? @@ -916,7 +1342,7 @@ // It might also be that a specific output format implies that alpha is scaled (RGBA32). That would favor an enum for the scaling filter option + a bool to switch auto-filtering on. // But we can only do this when libheif itself doesn't assume anymore that the alpha channel has the same resolution. - if ((alpha_image->get_width() != img->get_width()) || (alpha_image->get_height() != img->get_height())) { + if ((alpha->get_width() != img->get_width()) || (alpha->get_height() != img->get_height())) { std::shared_ptr scaled_alpha; Error err = alpha->scale_nearest_neighbor(scaled_alpha, img->get_width(), img->get_height(), m_heif_context->get_security_limits()); if (err) { @@ -924,7 +1350,9 @@ } alpha = std::move(scaled_alpha); } - img->transfer_plane_from_image_as(alpha, channel, heif_channel_Alpha); + if (Error err = img->transfer_channel_from_image_as(alpha, channel, heif_channel_Alpha)) { + return err; + } if (is_premultiplied_alpha()) { img->set_premultiplied_alpha(true); @@ -937,9 +1365,71 @@ // If there is an NCLX profile in the HEIF/AVIF metadata, use this for the color conversion. // Otherwise, use the profile that is stored in the image stream itself and then set the // (non-NCLX) profile later. - auto nclx = get_color_profile_nclx(); - if (nclx) { - img->set_color_profile_nclx(nclx); + const auto heif_nclx = get_color_profile_nclx(); + if (heif_nclx.is_defined()) { + + // Since we have a HEIF colr box, we overwrite the bitstream's CICP parameter + // with that parameter from the colr box. + nclx_profile consolidated_nclx = heif_nclx; + + // If the decoder plugin populated an NCLX profile from the bitstream's + // color signalling (e.g. HEVC SPS VUI, AV1 sequence header), compare it + // against the colr box. Per ISO/IEC 14496-12 and ISO/IEC 23000-22 (MIAF) + // the colr box overrides the bitstream, but a mismatch is a strong + // indication of a muxer bug. + const auto bitstream_nclx = img->get_color_profile_nclx(); + if (bitstream_nclx.is_defined()) { + + // Check whether there is a CICP mismatch between the HEIF colr box and the compressed bitstream + // If yes, output a warning. + + auto cicp_mismatch = [](uint16_t bs, uint16_t cr) { + return bs != 2 /*unspecified*/ && cr != 2 && bs != cr; + }; + + if (cicp_mismatch(bitstream_nclx.m_colour_primaries, heif_nclx.m_colour_primaries) || + cicp_mismatch(bitstream_nclx.m_transfer_characteristics, heif_nclx.m_transfer_characteristics) || + cicp_mismatch(bitstream_nclx.m_matrix_coefficients, heif_nclx.m_matrix_coefficients) || + bitstream_nclx.m_full_range_flag != heif_nclx.m_full_range_flag) { + std::stringstream msg; + msg << "colr box NCLX (" + << heif_nclx.m_colour_primaries << "/" + << heif_nclx.m_transfer_characteristics << "/" + << heif_nclx.m_matrix_coefficients << "/" + << (heif_nclx.m_full_range_flag ? "full" : "limited") + << ") disagrees with bitstream signalling (" + << bitstream_nclx.m_colour_primaries << "/" + << bitstream_nclx.m_transfer_characteristics << "/" + << bitstream_nclx.m_matrix_coefficients << "/" + << (bitstream_nclx.m_full_range_flag ? "full" : "limited") + << "); colr takes precedence per ISO/IEC 14496-12 and ISO/IEC 23000-22 (MIAF)"; + add_decoding_warning({heif_error_Invalid_input, + heif_suberror_NCLX_colr_VUI_mismatch, + msg.str()}); + } + + // Fix full-range flag in images that are probably broken. + + if (options.version >= 9 && options.autocorrect_broken_input) { + + // Some Sony cameras mis-tag full_range_flag=0 in colr while the bitstream VUI is correct (full_range_flag=1), see issue #1770. + // Rationale for the fix: if the bitstream explicitly says full_range=1, it probably does with for a reason. + // Thus, we keep the full-range flag. + + if (bitstream_nclx.get_full_range_flag() == true && + heif_nclx.get_full_range_flag() == false) { + add_decoding_warning({ + heif_error_Invalid_input, + heif_suberror_NCLX_colr_VUI_mismatch, + "Autocorrecting full-range flag to ON (colr=limited, bitstream=full)" + }); + + consolidated_nclx.set_full_range_flag(true); + } + } + } + + img->set_color_profile_nclx(consolidated_nclx); } auto icc = get_color_profile_icc(); @@ -968,14 +1458,49 @@ img->set_mdcv(mdcv->mdcv); } + // AMVE + + auto amve = get_property(); + if (amve) { + img->set_amve(amve->amve); + } + + // NDWT + + auto ndwt = get_property(); + if (ndwt) { + img->set_nominal_diffuse_white_luminance(ndwt->get_diffuse_white_luminance()); + } + // PASP auto pasp = get_property(); if (pasp) { img->set_pixel_ratio(pasp->hSpacing, pasp->vSpacing); } + + // TAI + + auto itai = get_property(); + if (itai) { + img->set_tai_timestamp(itai->get_tai_timestamp_packet()); + } + + // GIMI content ID + + auto gimi_content_id = get_property(); + if (gimi_content_id) { + img->set_gimi_sample_content_id(gimi_content_id->get_content_id()); + } + + // Image projection (OMAF) + auto prfr = get_property(); + if (prfr) { + img->set_omaf_image_projection(prfr->get_omaf_image_projection()); + } } + return img; } @@ -994,48 +1519,72 @@ } #endif -Result> ImageItem::get_compressed_image_data() const +Result> ImageItem::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { - // TODO: Remove this later when decoding is done through Decoder. + if (decode_state.processed_ids.contains(m_id)) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iref' has cyclic references"}; + } + + decode_state.processed_ids.insert(m_id); - // --- get the compressed image data - // data from configuration blocks + DataExtent extent; + extent.set_from_image_item(get_file(), get_id()); - Result> confData = read_bitstream_configuration_data(); - if (confData.error) { - return confData.error; + auto decoderResult = get_decoder(); + if (!decoderResult) { + return decoderResult.error(); } - std::vector data = confData.value; + auto decoder = *decoderResult; - // image data, usually from 'mdat' + decoder->set_data_extent(std::move(extent)); - Error error = m_heif_context->get_heif_file()->append_data_from_iloc(m_id, data); - if (error) { - return error; - } + // Tighten max_image_size_pixels for this decode so a decoder plugin (e.g. + // dav1d) cannot allocate buffers far larger than the ispe-declared size + // when the codec bitstream lies about its dimensions. + heif_security_limits tightened = tighten_image_size_limit_for_ispe( + get_context()->get_security_limits(), + get_ispe_width(), get_ispe_height(), + max_coding_unit_size_for_codec(get_compression_format())); - return data; + return decoder->decode_single_frame_from_compressed_data(options, &tightened); } -Result> ImageItem::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +Error ImageItem::check_decoded_image_size(const HeifPixelImage& img, + bool decode_tile_only, + uint32_t tile_x0, uint32_t tile_y0) const { - DataExtent extent; - extent.set_from_image_item(get_file(), get_id()); + uint32_t expected_w, expected_h; - auto decoderResult = get_decoder(); - if (decoderResult.error) { - return decoderResult.error; + if (decode_tile_only) { + // The decoded buffer is a single tile, sized to the signaled tile size. + get_tile_size(expected_w, expected_h); + } + else { + // Pre-transform coded size from the 'ispe' property. + expected_w = get_ispe_width(); + expected_h = get_ispe_height(); } - auto decoder = decoderResult.value; + // No 'ispe' / no tile size known -> cannot validate (a missing-'ispe' warning is + // already emitted upstream). Skip rather than reject. + if (expected_w == 0 || expected_h == 0) { + return Error::Ok; + } - decoder->set_data_extent(std::move(extent)); + if (!img.primary_planes_have_size(expected_w, expected_h)) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_image_size, + "Decoded image does not have the size signaled in the file."}; + } - return decoder->decode_single_frame_from_compressed_data(options); + return Error::Ok; } @@ -1049,10 +1598,25 @@ tiling.num_columns = 1; tiling.num_rows = 1; - tiling.tile_width = m_width; - tiling.tile_height = m_height; - tiling.image_width = m_width; - tiling.image_height = m_height; + // Report the coded (pre-transformation) dimensions here. The caller applies + // the transformative properties (irot, imir, clap) via + // process_image_transformations_on_tiling(), so handing it the already + // transformed m_width/m_height would apply them a second time. For a clap + // that shrinks the image to zero this double application underflowed inside + // Box_clap::get_crop() (GHSA-jc8f-p23p-5hjg); for irot/imir it silently + // produced wrong dimensions. The grid/unc/tiled overrides likewise report + // coded dimensions. + uint32_t coded_width = m_width; + uint32_t coded_height = m_height; + if (has_ispe_resolution()) { + coded_width = get_ispe_width(); + coded_height = get_ispe_height(); + } + + tiling.tile_width = coded_width; + tiling.tile_height = coded_height; + tiling.image_width = coded_width; + tiling.image_height = coded_height; tiling.top_offset = 0; tiling.left_offset = 0; @@ -1080,11 +1644,29 @@ } +bool ImageItem::has_essential_property_other_than(const std::set& props) const +{ + Result>> propertiesResult = get_properties(); + if (!propertiesResult) { + return false; + } + + for (const auto& property : *propertiesResult) { + if (is_property_essential(property) && + props.find(property->get_short_type()) == props.end()) { + return true; + } + } + + return false; +} + + Error ImageItem::process_image_transformations_on_tiling(heif_image_tiling& tiling) const { Result>> propertiesResult = get_properties(); - if (propertiesResult.error) { - return propertiesResult.error; + if (!propertiesResult) { + return propertiesResult.error(); } const std::vector>& properties = *propertiesResult; @@ -1168,10 +1750,15 @@ if (auto clap = std::dynamic_pointer_cast(property)) { std::shared_ptr clap_img; - int left = clap->left_rounded(tiling.image_width); - int right = clap->right_rounded(tiling.image_width); - int top = clap->top_rounded(tiling.image_height); - int bottom = clap->bottom_rounded(tiling.image_height); + auto cropResult = clap->get_crop(tiling.image_width, tiling.image_height); + if (!cropResult) { + return cropResult.error(); + } + + int left = cropResult->left; + int right = cropResult->right; + int top = cropResult->top; + int bottom = cropResult->bottom; if (left < 0) { left = 0; } if (top < 0) { top = 0; } @@ -1190,6 +1777,14 @@ top_excess += top; bottom_excess += bottom; } + + // --- scaling (not supported yet) + + if (auto iscl = std::dynamic_pointer_cast(property)) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Image scaling (iscl) transformative property is not yet supported"}; + } } tiling.left_offset = left_excess; diff -Nru libheif-1.19.8/libheif/image-items/image_item.h libheif-1.23.4/libheif/image-items/image_item.h --- libheif-1.19.8/libheif/image-items/image_item.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/image_item.h 2026-09-06 14:45:17.000000000 +0000 @@ -23,12 +23,20 @@ #include "api/libheif/heif.h" #include "error.h" +#include "security_limits.h" #include "nclx.h" #include +#include #include #include +#include #include +#include + +#include "image/pixelimage.h" #include "api/libheif/heif_plugin.h" +#include "codecs/encoder.h" + class HeifContext; @@ -43,17 +51,24 @@ std::string content_type; std::string item_uri_type; std::vector m_data; + + // Accounts m_data against the context's total-memory budget for the lifetime of + // this metadata object. Metadata items may be (brotli/zlib) compressed and are + // held until the context is destroyed, so up to max_items of them accumulate. + // Without persistent accounting this cumulative decompressed memory bypasses the + // security limits (GHSA-24wx-9w62-c96w). MemoryHandle is move-only, which makes + // ImageMetadata move-only; it is only ever held via shared_ptr, so that is fine. + MemoryHandle m_memory_handle; }; -class ImageItem : public ErrorBuffer +class ImageItem : public ImageDescription, + public ErrorBuffer { public: - ImageItem(HeifContext* file); + ImageItem(HeifContext* ctx); - ImageItem(HeifContext* file, heif_item_id id); - - virtual ~ImageItem() = default; + ImageItem(HeifContext* ctx, heif_item_id id); static std::shared_ptr alloc_for_infe_box(HeifContext*, const std::shared_ptr&); @@ -63,20 +78,20 @@ static uint32_t compression_format_to_fourcc_infe_type(heif_compression_format); - Result> convert_colorspace_for_encoding(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options); - virtual uint32_t get_infe_type() const { return 0; } virtual const char* get_auxC_alpha_channel_type() const { return "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; } virtual bool is_ispe_essential() const { return false; } - virtual Error get_item_error() const { return Error::Ok; } + bool is_property_essential(const std::shared_ptr& property) const; + + virtual Error get_item_error() const { return m_item_error; } - // If the output format requires a specific nclx (like JPEG), return this. Otherwise, return NULL. - virtual const heif_color_profile_nclx* get_forced_output_nclx() const { return nullptr; } + // Mark this item as undecodable. The file still loads and other items remain + // usable, but decoding this item (or listing it as a non-error image) will + // surface this error. + void set_item_error(const Error& err) { m_item_error = err; } virtual heif_compression_format get_compression_format() const { return heif_compression_undefined; } @@ -98,7 +113,35 @@ void set_properties(std::vector> properties) { m_properties = std::move(properties); - } + // Codec-config-independent populate (e.g. unci, which reads cmpd/uncC + // directly off the just-set property boxes). Visual codecs leave + // m_components empty here; their populate runs after initialize_decoder() + // because they need codec config (colorspace, bit depth) which only the + // decoder can read. + populate_component_descriptions(); + } + + // Populate the inherited ImageDescription::m_components from the just-set + // property boxes / codec config. The unci subclass overrides this and + // reads cmpd/uncC directly. The base implementation handles visual codecs + // (HEVC/AVC/AVIF/JPEG/JPEG2000/VVC) by querying get_coded_image_colorspace() + // + get_luma_bits_per_pixel() + get_chroma_bits_per_pixel() and emitting + // Y/Cb/Cr or R/G/B / monochrome descriptions in canonical order. + // Idempotent: skips work if m_components is already populated. + // Alpha-from-aux (separate alpha item linked via auxl) is *not* emitted + // here; the alpha plane is attached at decode time via + // transfer_channel_from_image_as, which mints its own component on the + // destination. + virtual void populate_component_descriptions(); + + // Populate this item's m_components by cloning descriptions from a child + // item (e.g. tili's tile item, grid/iden/iovl's first child) and rescaling + // per-component dims from the child's logical size to this item's full + // image (ispe) size. Returns true on success. Returns false (and leaves + // m_components untouched) if the child has no descriptions or sizes are + // unusable, so the caller can fall back to the base populate. + bool populate_descriptions_from_child(const class ImageItem& child, + uint32_t child_w, uint32_t child_h); template std::shared_ptr get_property() const @@ -112,9 +155,21 @@ return nullptr; } - heif_property_id add_property(std::shared_ptr property, bool essential); + template + std::vector> get_all_properties() const + { + std::vector> result; + for (auto& property : m_properties) { + if (auto box = std::dynamic_pointer_cast(property)) { + result.push_back(box); + } + } + return result; + } + + heif_property_id add_property(const std::shared_ptr& property, bool essential); - heif_property_id add_property_without_deduplication(std::shared_ptr property, bool essential); + heif_property_id add_property_without_deduplication(const std::shared_ptr& property, bool essential); void set_resolution(uint32_t w, uint32_t h) { @@ -159,7 +214,7 @@ Error postprocess_coded_image_colorspace(heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) const; - virtual void process_before_write() { } + virtual Error process_before_write() { return {}; } // -- thumbnails @@ -182,7 +237,9 @@ m_is_alpha_channel = true; } - void set_alpha_channel(std::shared_ptr img) { m_alpha_channel = std::move(img); } + // Attach an alpha aux ImageItem and append a corresponding Alpha + // ComponentDescription to this item's m_components. Idempotent. + void set_alpha_channel(std::shared_ptr img); bool is_alpha_channel() const { return m_is_alpha_channel; } @@ -209,7 +266,7 @@ const std::shared_ptr& get_depth_channel() const { return m_depth_channel; } - void set_depth_representation_info(struct heif_depth_representation_info& info) + void set_depth_representation_info(heif_depth_representation_info& info) { m_has_depth_representation_info = true; m_depth_representation_info = info; @@ -220,7 +277,7 @@ return m_has_depth_representation_info; } - const struct heif_depth_representation_info& get_depth_representation_info() const + const heif_depth_representation_info& get_depth_representation_info() const { return m_depth_representation_info; } @@ -275,73 +332,91 @@ const std::vector>& get_metadata() const { return m_metadata; } - // --- miaf - // TODO: we should have a function that challs all MIAF constraints and sets the compatibility flag. - void mark_not_miaf_compatible() { m_miaf_compatible = false; } + // --- ImageDescription - bool is_miaf_compatible() const { return m_miaf_compatible; } + void set_clli(const heif_content_light_level& clli) override; + void set_mdcv(const heif_mastering_display_colour_volume& mdcv) override; - // === decoding === + void set_amve(const heif_ambient_viewing_environment& amve) override; - virtual Error on_load_file() { return Error::Ok; } + void set_nominal_diffuse_white_luminance(uint32_t luminance) override; - Error init_decoder_from_item(heif_item_id id); + void set_pixel_ratio(uint32_t h, uint32_t v) override; - Result> decode_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const; + void set_color_profile_nclx(const nclx_profile& profile) override; - virtual Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const; + void set_color_profile_icc(const std::shared_ptr& profile) override; - virtual Result> get_compressed_image_data() const; + void set_omaf_image_projection(heif_omaf_image_projection image_projection) override; - Result>> get_properties() const; + // --- miaf - // === encoding === + // TODO: we should have a function that checks all MIAF constraints and sets the compatibility flag. + void mark_not_miaf_compatible() { m_miaf_compatible = false; } - struct CodedImageData - { - std::vector> properties; - std::vector bitstream; + bool is_miaf_compatible() const { return m_miaf_compatible; } - // If 0, the encoded size is equal to the input size. - uint32_t encoded_image_width = 0; - uint32_t encoded_image_height = 0; + // return 0 if we don't know the brand + virtual heif_brand2 get_compatible_brand() const { return 0; } - void append(const uint8_t* data, size_t size); + // === decoding === - void append_with_4bytes_size(const uint8_t* data, size_t size); - }; + virtual Error initialize_decoder() { return Error::Ok; } - Result encode_to_bitstream_and_boxes(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class); + virtual void set_decoder_input_data() { } - Error encode_to_item(HeifContext* ctx, - const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class); + virtual Result> decode_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, + uint32_t tile_y0, + DecodeTraversalState decode_state) const; + + // Validate, before any decoding starts, that this item can be safely decoded: + // the graph of items reached by the decode recursion (derived-image 'dimg' + // inputs and the alpha 'auxl' auxiliary) must be acyclic. A reference cycle + // would otherwise let two parallel grid-tile workers take two item mutexes in + // opposite order and deadlock (GHSA-prgh-72vc-3xmc). Cycles are not rejected + // at file load, so that a file's independently valid items stay decodable; + // this per-item decode-time check is what makes a cyclic item safe. Called + // once per top-level decode in HeifContext::decode_image(). This is the single + // place to add further + // decodability constraints (e.g. no alpha auxiliary on an alpha image, MIAF + // derivation-chain limits). + Error verify_decodable() const; + + virtual Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, + uint32_t tile_y0, + DecodeTraversalState decode_state) const; + + // Validate the just-decoded pixel image against the size signaled for this item. + // Called by decode_image() right after decode_compressed_image(), BEFORE transforms, + // so the reference is the pre-transform coded size (ispe), or the signaled tile size + // for a tile decode -- NOT get_width()/get_height() (which are post-transform). + // Default impl handles plain coded codecs (HEVC/AVC/VVC/AVIF/JPEG). Subclasses + // override where the size source or component layout differs. + virtual Error check_decoded_image_size(const HeifPixelImage& img, + bool decode_tile_only, + uint32_t tile_x0, uint32_t tile_y0) const; - const std::shared_ptr& get_color_profile_nclx() const { return m_color_profile_nclx; } + Result>> get_properties() const; - const std::shared_ptr& get_color_profile_icc() const { return m_color_profile_icc; } + bool has_essential_property_other_than(const std::set&) const; - void set_color_profile(const std::shared_ptr& profile) - { - auto icc = std::dynamic_pointer_cast(profile); - if (icc) { - m_color_profile_icc = std::move(icc); - } + // === encoding === - auto nclx = std::dynamic_pointer_cast(profile); - if (nclx) { - m_color_profile_nclx = std::move(nclx); - } - }; + Result encode_to_bitstream_and_boxes(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class); + + // Entry point for encoding pixel images. + Error encode_to_item(HeifContext* ctx, + const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class); void set_intrinsic_matrix(const Box_cmin::RelativeIntrinsicMatrix& cmin) { m_has_intrinsic_matrix = true; @@ -372,7 +447,7 @@ const std::vector& get_region_item_ids() const { return m_region_item_ids; } - void add_decoding_warning(Error err) { m_decoding_warnings.emplace_back(std::move(err)); } + void add_decoding_warning(Error err) const { m_decoding_warnings.emplace_back(std::move(err)); } const std::vector& get_decoding_warnings() const { return m_decoding_warnings; } @@ -391,10 +466,20 @@ }; } + virtual std::shared_ptr get_encoder() const { return nullptr; } + + void add_text_item_id(heif_item_id id) { + m_text_item_ids.push_back(id); + } + + const std::vector& get_text_item_ids() const { return m_text_item_ids; } + private: HeifContext* m_heif_context; std::vector> m_properties; + Error m_item_error; // if set, this item cannot be decoded (e.g. unsupported required reference types) + heif_item_id m_id = 0; uint32_t m_width = 0, m_height = 0; // after all transformations have been applied bool m_is_primary = false; @@ -411,7 +496,7 @@ std::shared_ptr m_depth_channel; bool m_has_depth_representation_info = false; - struct heif_depth_representation_info m_depth_representation_info; + heif_depth_representation_info m_depth_representation_info; bool m_is_aux_image = false; std::string m_aux_image_type; @@ -419,9 +504,6 @@ std::vector> m_metadata; - std::shared_ptr m_color_profile_nclx; - std::shared_ptr m_color_profile_icc; - bool m_miaf_compatible = true; std::vector m_region_item_ids; @@ -432,23 +514,29 @@ bool m_has_extrinsic_matrix = false; Box_cmex::ExtrinsicMatrix m_extrinsic_matrix{}; - std::vector m_decoding_warnings; + mutable std::vector m_decoding_warnings; + + mutable std::mutex m_decode_mutex; + + std::vector m_text_item_ids; + + void generate_property_boxes_for_ImageDescription(); protected: // Result> read_bitstream_configuration_data_override(heif_item_id itemId, heif_compression_format format) const; - virtual Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) { return {}; } + virtual Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class); // --- encoding utility functions - static void add_color_profile(const std::shared_ptr& image, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class, - const heif_color_profile_nclx* target_heif_nclx, - ImageItem::CodedImageData& inout_codedImage); + static std::vector > + add_color_profile(const std::shared_ptr& image, + const heif_encoding_options& options, + heif_image_input_class input_class, + const heif_color_profile_nclx* target_heif_nclx); }; @@ -457,8 +545,12 @@ public: // dummy ImageItem class that is a placeholder for unsupported item types - ImageItem_Error(uint32_t item_type, heif_item_id id, Error err) - : ImageItem(nullptr, id), m_item_type(item_type), m_item_error(err) {} + // Carry the real context, like every other ImageItem. Error items used to be + // constructed with a null context, which made get_context()/get_file() a + // null-deref hazard for any code that reaches an error item (e.g. an error + // item attached as a depth/aux image, then handed to verify_decodable()). + ImageItem_Error(HeifContext* context, uint32_t item_type, heif_item_id id, Error err) + : ImageItem(context, id), m_item_type(item_type), m_item_error(std::move(err)) {} uint32_t get_infe_type() const override { @@ -467,6 +559,21 @@ Error get_item_error() const override { return m_item_error; } + Result> decode_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, + uint32_t tile_y0, + DecodeTraversalState decode_state) const override + { + return m_item_error; + } + + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, + uint32_t tile_y0, DecodeTraversalState decode_state) const override + { + return m_item_error; + } + [[nodiscard]] int get_luma_bits_per_pixel() const override { return -1; } [[nodiscard]] int get_chroma_bits_per_pixel() const override { return -1; } diff -Nru libheif-1.19.8/libheif/image-items/jpeg.cc libheif-1.23.4/libheif/image-items/jpeg.cc --- libheif-1.19.8/libheif/image-items/jpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/jpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -20,95 +20,25 @@ #include "jpeg.h" #include "codecs/jpeg_dec.h" +#include "codecs/jpeg_enc.h" #include "codecs/jpeg_boxes.h" #include "security_limits.h" -#include "pixelimage.h" -#include "api/libheif/api_structs.h" +#include "image/pixelimage.h" +#include "api_structs.h" #include #include -static uint8_t JPEG_SOS = 0xDA; - - -const heif_color_profile_nclx* ImageItem_JPEG::get_forced_output_nclx() const +ImageItem_JPEG::ImageItem_JPEG(HeifContext* ctx, heif_item_id id) + : ImageItem(ctx, id) { - // JPEG always uses CCIR-601 - - static heif_color_profile_nclx target_heif_nclx; - target_heif_nclx.version = 1; - target_heif_nclx.matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; - target_heif_nclx.color_primaries = heif_color_primaries_ITU_R_BT_601_6; - target_heif_nclx.transfer_characteristics = heif_transfer_characteristic_ITU_R_BT_601_6; - target_heif_nclx.full_range_flag = true; - - return &target_heif_nclx; + m_encoder = std::make_shared(); } - -Result ImageItem_JPEG::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +ImageItem_JPEG::ImageItem_JPEG(HeifContext* ctx) + : ImageItem(ctx) { - CodedImageData codedImage; - - - heif_image c_api_image; - c_api_image.image = image; - - struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - if (err.code) { - return Error(err.code, - err.subcode, - err.message); - } - - std::vector vec; - - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); - - if (data == nullptr) { - break; - } - - size_t oldsize = vec.size(); - vec.resize(oldsize + size); - memcpy(vec.data() + oldsize, data, size); - } - -#if 0 - // Optional: split the JPEG data into a jpgC box and the actual image data. - // Currently disabled because not supported yet in other decoders. - if (false) { - size_t pos = find_jpeg_marker_start(vec, JPEG_SOS); - if (pos > 0) { - std::vector jpgC_data(vec.begin(), vec.begin() + pos); - auto jpgC = std::make_shared(); - jpgC->set_data(jpgC_data); - - auto ipma_box = m_heif_file->get_ipma_box(); - int index = m_heif_file->get_ipco_box()->find_or_append_child_box(jpgC); - ipma_box->add_property_for_item_ID(image_id, Box_ipma::PropertyAssociation{true, uint16_t(index + 1)}); - - std::vector image_data(vec.begin() + pos, vec.end()); - vec = std::mo ve(image_data); - } - } -#endif - (void) JPEG_SOS; - - codedImage.bitstream = std::move(vec); - -#if 0 - // TODO: extract 'jpgC' header data -#endif - - return {codedImage}; + m_encoder = std::make_shared(); } @@ -123,17 +53,34 @@ return {m_decoder}; } -Error ImageItem_JPEG::on_load_file() + +std::shared_ptr ImageItem_JPEG::get_encoder() const +{ + return m_encoder; +} + + +Error ImageItem_JPEG::initialize_decoder() { // Note: jpgC box is optional. NULL is a valid value. auto jpgC_box = get_property(); m_decoder = std::make_shared(jpgC_box); + return Error::Ok; +} + + +void ImageItem_JPEG::set_decoder_input_data() +{ DataExtent extent; extent.set_from_image_item(get_context()->get_heif_file(), get_id()); m_decoder->set_data_extent(std::move(extent)); +} - return Error::Ok; + +heif_brand2 ImageItem_JPEG::get_compatible_brand() const +{ + return heif_brand2_jpeg; } diff -Nru libheif-1.19.8/libheif/image-items/jpeg.h libheif-1.23.4/libheif/image-items/jpeg.h --- libheif-1.19.8/libheif/image-items/jpeg.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/jpeg.h 2026-09-06 14:45:17.000000000 +0000 @@ -31,33 +31,30 @@ class ImageItem_JPEG : public ImageItem { public: - ImageItem_JPEG(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) { } + ImageItem_JPEG(HeifContext* ctx, heif_item_id id); - ImageItem_JPEG(HeifContext* ctx) : ImageItem(ctx) { } + ImageItem_JPEG(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("jpeg"); } - const heif_color_profile_nclx* get_forced_output_nclx() const override; - heif_compression_format get_compression_format() const override { return heif_compression_JPEG; } + Error initialize_decoder() override; - Error on_load_file() override; - -public: + void set_decoder_input_data() override; - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + heif_brand2 get_compatible_brand() const override; protected: Result> get_decoder() const override; + std::shared_ptr get_encoder() const override; + Result> read_bitstream_configuration_data() const override; private: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; }; #endif // LIBHEIF_JPEG_H diff -Nru libheif-1.19.8/libheif/image-items/jpeg2000.cc libheif-1.23.4/libheif/image-items/jpeg2000.cc --- libheif-1.19.8/libheif/image-items/jpeg2000.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/jpeg2000.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,53 +19,25 @@ */ #include "jpeg2000.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "codecs/jpeg2000_dec.h" +#include "codecs/jpeg2000_enc.h" #include "codecs/jpeg2000_boxes.h" #include -#include -#include #include - -Result ImageItem_JPEG2000::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +ImageItem_JPEG2000::ImageItem_JPEG2000(HeifContext* ctx, heif_item_id id) + : ImageItem(ctx, id) { - CodedImageData codedImageData; - - heif_image c_api_image; - c_api_image.image = image; - - encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - - // get compressed data - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, nullptr); - - if (data == nullptr) { - break; - } - - codedImageData.append(data, size); - } - - // add 'j2kH' property - auto j2kH = std::make_shared(); - - // add 'cdef' to 'j2kH' - auto cdef = std::make_shared(); - cdef->set_channels(image->get_colorspace()); - j2kH->append_child_box(cdef); + m_encoder = std::make_shared(); +} - codedImageData.properties.push_back(j2kH); - return codedImageData; +ImageItem_JPEG2000::ImageItem_JPEG2000(HeifContext* ctx) + : ImageItem(ctx) +{ + m_encoder = std::make_shared(); } @@ -88,12 +60,20 @@ return std::vector{}; } + Result> ImageItem_JPEG2000::get_decoder() const { return {m_decoder}; } -Error ImageItem_JPEG2000::on_load_file() + +std::shared_ptr ImageItem_JPEG2000::get_encoder() const +{ + return m_encoder; +} + + +Error ImageItem_JPEG2000::initialize_decoder() { auto j2kH = get_property(); if (!j2kH) { @@ -104,10 +84,47 @@ m_decoder = std::make_shared(j2kH); + return Error::Ok; +} + + +void ImageItem_JPEG2000::set_decoder_input_data() +{ DataExtent extent; extent.set_from_image_item(get_context()->get_heif_file(), get_id()); m_decoder->set_data_extent(std::move(extent)); +} + +Error ImageItem_JPEG2000::check_decoded_image_size(const HeifPixelImage& img, + bool decode_tile_only, + uint32_t tile_x0, uint32_t tile_y0) const +{ + uint32_t expected_w, expected_h; + + if (decode_tile_only) { + get_tile_size(expected_w, expected_h); + } + else { + expected_w = get_ispe_width(); + expected_h = get_ispe_height(); + } + + if (expected_w == 0 || expected_h == 0) { + return Error::Ok; + } + + // Only the logical image size is checked; the component layout may be arbitrary. + if (img.get_width() != expected_w || img.get_height() != expected_h) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_image_size, + "Decoded image does not have the size signaled in the file."}; + } return Error::Ok; } + +heif_brand2 ImageItem_JPEG2000::get_compatible_brand() const +{ + return heif_brand2_j2ki; +} diff -Nru libheif-1.19.8/libheif/image-items/jpeg2000.h libheif-1.23.4/libheif/image-items/jpeg2000.h --- libheif-1.19.8/libheif/image-items/jpeg2000.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/jpeg2000.h 2026-09-06 14:45:17.000000000 +0000 @@ -33,29 +33,38 @@ class ImageItem_JPEG2000 : public ImageItem { public: - ImageItem_JPEG2000(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_JPEG2000(HeifContext* ctx, heif_item_id id); - ImageItem_JPEG2000(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_JPEG2000(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("j2k1"); } heif_compression_format get_compression_format() const override { return heif_compression_JPEG2000; } - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + heif_brand2 get_compatible_brand() const override; protected: Result> read_bitstream_configuration_data() const override; Result> get_decoder() const override; + std::shared_ptr get_encoder() const override; + public: - Error on_load_file() override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; + + // JPEG2000 codestreams (SIZ marker) can carry arbitrary component grids and + // subsampling, so the generic per-plane check is not appropriate. Validate only + // the logical image size (set by the plugin from SIZ Xsiz/Ysiz) against 'ispe'. + Error check_decoded_image_size(const HeifPixelImage& img, + bool decode_tile_only, + uint32_t tile_x0, uint32_t tile_y0) const override; private: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; }; #endif // LIBHEIF_JPEG2000_H diff -Nru libheif-1.19.8/libheif/image-items/mask_image.cc libheif-1.23.4/libheif/image-items/mask_image.cc --- libheif-1.19.8/libheif/image-items/mask_image.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/mask_image.cc 2026-09-06 14:45:17.000000000 +0000 @@ -71,25 +71,20 @@ std::shared_ptr ispe = image->get_property(); std::shared_ptr mskC = image->get_property(); - uint32_t width = 0; - uint32_t height = 0; - - if (ispe) { - width = ispe->get_width(); - height = ispe->get_height(); - - Error error = check_for_valid_image_size(context->get_security_limits(), width, height); - if (error) { - return error; - } - } - if (!ispe || !mskC) { return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Missing required box for mask codec"); } + uint32_t width = ispe->get_width(); + uint32_t height = ispe->get_height(); + + Error error = check_for_valid_image_size(context->get_security_limits(), width, height); + if (error) { + return error; + } + if ((mskC->get_bits_per_pixel() != 8) && (mskC->get_bits_per_pixel() != 16)) { return Error(heif_error_Unsupported_feature, @@ -97,7 +92,9 @@ "Unsupported bit depth for mask item"); } - if (data.size() < width * height) { + uint32_t bytes_per_pixel = (mskC->get_bits_per_pixel() + 7) / 8; + + if (data.size() / (static_cast(width) * bytes_per_pixel) < height) { return {heif_error_Invalid_input, heif_suberror_Unspecified, "Mask image data is too short"}; @@ -105,30 +102,31 @@ img = std::make_shared(); img->create(width, height, heif_colorspace_monochrome, heif_chroma_monochrome); - auto err = img->add_plane(heif_channel_Y, width, height, mskC->get_bits_per_pixel(), + auto err = img->add_channel(heif_channel_Y, width, height, mskC->get_bits_per_pixel(), context->get_security_limits()); if (err) { return err; } size_t stride; - uint8_t* dst = img->get_plane(heif_channel_Y, &stride); - if (((uint32_t)stride) == width) { - memcpy(dst, data.data(), data.size()); + uint8_t* dst = img->get_channel_memory(heif_channel_Y, &stride); + if (stride == static_cast(width) * bytes_per_pixel) { + memcpy(dst, data.data(), static_cast(width) * bytes_per_pixel * height); } else { - for (uint32_t i = 0; i < height; i++) + for (size_t i = 0; i < height; i++) { - memcpy(dst + i * stride, data.data() + i * width, width); + memcpy(dst + i * stride, data.data() + i * width * bytes_per_pixel, width * bytes_per_pixel); } } return Error::Ok; } -Result> ImageItem_mask::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +Result> ImageItem_mask::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { std::shared_ptr img; @@ -154,12 +152,12 @@ } -Result ImageItem_mask::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +Result ImageItem_mask::encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { - CodedImageData codedImageData; + Encoder::CodedImageData codedImageData; if (image->get_colorspace() != heif_colorspace_monochrome) { @@ -178,7 +176,7 @@ // TODO: we could add an option to lossless-compress this data std::vector data; size_t src_stride; - uint8_t* src_data = image->get_plane(heif_channel_Y, &src_stride); + uint8_t* src_data = image->get_channel_memory(heif_channel_Y, &src_stride); uint32_t w = image->get_width(); uint32_t h = image->get_height(); @@ -195,7 +193,15 @@ } std::shared_ptr mskC = std::make_shared(); - mskC->set_bits_per_pixel(image->get_bits_per_pixel(heif_channel_Y)); + uint16_t bpp = image->get_bits_per_pixel(heif_channel_Y); + if (bpp > 255) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Too many bits per pixel for mask image." + }; + } + mskC->set_bits_per_pixel(static_cast(bpp)); codedImageData.properties.push_back(mskC); return codedImageData; diff -Nru libheif-1.19.8/libheif/image-items/mask_image.h libheif-1.23.4/libheif/image-items/mask_image.h --- libheif-1.19.8/libheif/image-items/mask_image.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/mask_image.h 2026-09-06 14:45:17.000000000 +0000 @@ -26,13 +26,14 @@ #include "box.h" #include "bitstream.h" -#include "pixelimage.h" +#include "image/pixelimage.h" #include "file.h" #include "context.h" #include #include #include +#include /** * Mask Configuration Property (mskC). @@ -89,8 +90,6 @@ uint32_t get_infe_type() const override { return fourcc("mski"); } - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } - heif_compression_format get_compression_format() const override { return heif_compression_mask; } bool is_ispe_essential() const override { return true; } @@ -99,13 +98,14 @@ int get_chroma_bits_per_pixel() const override { return 0; } - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; - - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; + + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; }; #endif //LIBHEIF_MASK_IMAGE_H diff -Nru libheif-1.19.8/libheif/image-items/overlay.cc libheif-1.23.4/libheif/image-items/overlay.cc --- libheif-1.19.8/libheif/image-items/overlay.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/overlay.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,6 +24,8 @@ #include "color-conversion/colorconversion.h" #include "security_limits.h" +#include + template void writevec(uint8_t* data, size_t& idx, I value, int len) @@ -38,7 +40,7 @@ static int32_t readvec_signed(const std::vector& data, int& ptr, int len) { - const uint32_t high_bit = 0x80 << ((len - 1) * 8); + const uint32_t high_bit = UINT32_C(0x80) << ((len - 1) * 8); uint32_t val = 0; while (len--) { @@ -216,7 +218,7 @@ } -Error ImageItem_Overlay::on_load_file() +Error ImageItem_Overlay::initialize_decoder() { Error err = read_overlay_spec(); if (err) { @@ -242,23 +244,32 @@ m_overlay_image_ids = iref_box->get_references(get_id(), fourcc("dimg")); - /* TODO: probably, it is valid that an iovl image has no references ? - - if (image_references.empty()) { + // An overlay with no input images is degenerate: ISO/IEC 23008-12 image-overlay + // derivation places "one or more" input images onto the canvas. + if (m_overlay_image_ids.empty()) { return Error(heif_error_Invalid_input, heif_suberror_Missing_grid_images, - "'iovl' image with more than one reference image"); + "'iovl' image has no referenced input images"); + } + + // Limit the number of images composited into a single overlay. This is a + // hardcoded stopgap (see MAX_OVERLAY_IMAGES) until a configurable limit can be + // added to heif_security_limits in the next major release. Skipped when the + // security limits are disabled. (GHSA-x8xm-cm2c-cfc8) + if (get_context()->get_security_limits()->max_items != 0 && + m_overlay_image_ids.size() > MAX_OVERLAY_IMAGES) { + return Error(heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "'iovl' image composites more input images than allowed"); } - */ - std::vector overlay_data; - Error err = heif_file->get_uncompressed_item_data(get_id(), &overlay_data); - if (err) { - return err; + auto overlayDataResult = heif_file->get_uncompressed_item_data(get_id()); + if (!overlayDataResult) { + return overlayDataResult.error(); } - err = m_overlay_spec.parse(m_overlay_image_ids.size(), overlay_data); + Error err = m_overlay_spec.parse(m_overlay_image_ids.size(), *overlayDataResult); if (err) { return err; } @@ -273,15 +284,45 @@ } -Result> ImageItem_Overlay::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +Result> ImageItem_Overlay::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { - return decode_overlay_image(options); + return decode_overlay_image(options, decode_state); } +// Note: ImageItem_Overlay does not override check_decoded_image_size(). The overlay +// canvas is built to the overlay-header size by construction (decode_overlay_image +// creates it at m_overlay_spec canvas size), so checking it against that same size +// would be tautological. The base default checks the canvas against 'ispe', which is +// the meaningful cross-check (overlay-header size vs signaled size). + -Result> ImageItem_Overlay::decode_overlay_image(const heif_decoding_options& options) const +Result> ImageItem_Overlay::decode_overlay_image(const heif_decoding_options& options, + DecodeTraversalState decode_state) const { + if (decode_state.processed_ids.contains(get_id())) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "'iref' has cyclic references"}; + } + + decode_state.processed_ids.insert(get_id()); + + // Bound the depth of overlays nested inside one another. Real files place at + // most one overlay in a decode chain; deep nesting is a fast-rejection path + // for the reference-amplification gadget. decode_state (and thus this counter) + // is copied by value at each hop, so overlay_nesting measures depth along the + // current path only. (GHSA-x8xm-cm2c-cfc8) + decode_state.overlay_nesting++; + if (decode_state.max_overlay_nesting != 0 && + decode_state.overlay_nesting > decode_state.max_overlay_nesting) { + return Error{heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "'iovl' overlay images nested too deeply"}; + } + + std::shared_ptr img; uint32_t w = m_overlay_spec.get_canvas_width(); @@ -297,13 +338,13 @@ img->create(w, h, heif_colorspace_RGB, heif_chroma_444); - if (auto error = img->add_plane(heif_channel_R, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths + if (auto error = img->add_channel(heif_channel_R, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths return error; } - if (auto error = img->add_plane(heif_channel_G, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths + if (auto error = img->add_channel(heif_channel_G, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths return error; } - if (auto error = img->add_plane(heif_channel_B, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths + if (auto error = img->add_channel(heif_channel_B, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths return error; } @@ -333,22 +374,24 @@ return error; } - auto decodeResult = imgItem->decode_image(options, false, 0,0); - if (decodeResult.error) { - return decodeResult.error; + auto decodeResult = imgItem->decode_image(options, false, 0,0, decode_state); + if (!decodeResult) { + return decodeResult.error(); } - std::shared_ptr overlay_img = decodeResult.value; + std::shared_ptr overlay_img = *decodeResult; // process overlay in RGB space if (overlay_img->get_colorspace() != heif_colorspace_RGB || overlay_img->get_chroma_format() != heif_chroma_444) { - auto overlay_img_result = convert_colorspace(overlay_img, heif_colorspace_RGB, heif_chroma_444, nullptr, 0, options.color_conversion_options, + auto overlay_img_result = convert_colorspace(overlay_img, heif_colorspace_RGB, heif_chroma_444, + nclx_profile::undefined(), + 0, options.color_conversion_options, options.color_conversion_options_ext, get_context()->get_security_limits()); - if (overlay_img_result.error) { - return overlay_img_result.error; + if (!overlay_img_result) { + return overlay_img_result.error(); } else { overlay_img = *overlay_img_result; @@ -376,26 +419,24 @@ int ImageItem_Overlay::get_luma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); return image->get_luma_bits_per_pixel(); } int ImageItem_Overlay::get_chroma_bits_per_pixel() const { - heif_item_id child; - Error err = get_context()->get_id_of_non_virtual_child_image(get_id(), child); - if (err) { + auto child_result = get_context()->find_first_coded_image_id(get_id()); + if (child_result.is_error()) { return -1; } - auto image = get_context()->get_image(child, true); + auto image = get_context()->get_image(*child_result, true); return image->get_chroma_bits_per_pixel(); } @@ -409,6 +450,41 @@ } +void ImageItem_Overlay::populate_component_descriptions() +{ + if (!get_component_descriptions().empty()) { + return; + } + + // The overlay is always composed in RGB 8-bit 4:4:4 onto the canvas + // (decode_overlay_image converts each input child to RGB and uses an RGB + // background color). So the description we publish reflects that fixed + // output format, not the children's formats. + uint32_t w = get_ispe_width(); + uint32_t h = get_ispe_height(); + if (w == 0 || h == 0) { + return; + } + + auto emit = [this, w, h](heif_channel ch, uint16_t type) { + ComponentDescription d; + d.component_id = mint_component_id(); + d.channel = ch; + d.component_type = type; + d.datatype = heif_component_datatype_unsigned_integer; + d.bit_depth = 8; + d.width = w; + d.height = h; + d.has_data_plane = true; + add_component_description(std::move(d)); + }; + + emit(heif_channel_R, heif_cmpd_component_type_red); + emit(heif_channel_G, heif_cmpd_component_type_green); + emit(heif_channel_B, heif_cmpd_component_type_blue); +} + + Result> ImageItem_Overlay::add_new_overlay_item(HeifContext* ctx, const ImageOverlay& overlayspec) { if (overlayspec.get_num_offsets() > 0xFFFF) { @@ -433,7 +509,11 @@ // Create IOVL Item - heif_item_id iovl_id = file->add_new_image(fourcc("iovl")); + auto iovl_id_result = file->add_new_image(fourcc("iovl")); + if (!iovl_id_result) { + return iovl_id_result.error(); + } + heif_item_id iovl_id = *iovl_id_result; std::shared_ptr iovl_image = std::make_shared(ctx, iovl_id); ctx->insert_image_item(iovl_id, iovl_image); const int construction_method = 1; // 0=mdat 1=idat @@ -457,3 +537,14 @@ return iovl_image; } + +heif_brand2 ImageItem_Overlay::get_compatible_brand() const +{ + if (m_overlay_image_ids.empty()) { return 0; } + + heif_item_id child_id = m_overlay_image_ids[0]; + auto child = get_context()->get_image(child_id, false); + if (!child) { return 0; } + + return child->get_compatible_brand(); +} diff -Nru libheif-1.19.8/libheif/image-items/overlay.h libheif-1.23.4/libheif/image-items/overlay.h --- libheif-1.19.8/libheif/image-items/overlay.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/overlay.h 2026-09-06 14:45:17.000000000 +0000 @@ -25,6 +25,7 @@ #include #include #include +#include class ImageOverlay @@ -97,7 +98,7 @@ // heif_compression_format get_compression_format() const override { return heif_compression_HEVC; } - Error on_load_file() override; + Error initialize_decoder() override; int get_luma_bits_per_pixel() const override; @@ -105,17 +106,26 @@ Error get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const override; - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override + // Delegates to the first overlay-input image's component descriptions, + // rescaled to this overlay's ispe (the canvas size). Without this override + // the base populate would bail (iovl has no get_decoder()) and the handle + // would report 0 components. + void populate_component_descriptions() override; + + heif_brand2 get_compatible_brand() const override; + + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override { return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Cannot encode image to 'iovl'"}; } - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; // --- iovl specific @@ -128,7 +138,8 @@ Error read_overlay_spec(); - Result> decode_overlay_image(const heif_decoding_options& options) const; + Result> decode_overlay_image(const heif_decoding_options& options, + DecodeTraversalState decode_state) const; }; diff -Nru libheif-1.19.8/libheif/image-items/tiled.cc libheif-1.23.4/libheif/image-items/tiled.cc --- libheif-1.19.8/libheif/image-items/tiled.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/tiled.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,9 +22,13 @@ #include "context.h" #include "file.h" #include +#include #include "security_limits.h" #include "codecs/hevc_dec.h" -#include "libheif/api_structs.h" +#if WITH_UNCOMPRESSED_CODEC +#include "codecs/uncompressed/unc_boxes.h" +#endif +#include "api_structs.h" static uint64_t readvec(const std::vector& data, size_t& ptr, int len) @@ -39,17 +43,51 @@ } -uint64_t number_of_tiles(const heif_tiled_image_parameters& params) +Result number_of_tiles(const heif_tiled_image_parameters& params, const heif_security_limits* limits) { uint64_t nTiles = nTiles_h(params) * static_cast(nTiles_v(params)); + // Enforce the limit before the extra-dimensions loop so it is checked + // even when number_of_extra_dimensions == 0. + if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) { + return Error{ + heif_error_Unsupported_filetype, + heif_suberror_Security_limit_exceeded, + "Number of tiles exceeds security limit" + }; + } + for (int i = 0; i < params.number_of_extra_dimensions; i++) { // We only support up to 8 extra dimensions if (i == 8) { break; } + if (params.extra_dimensions[i] == 0) { + return Error{ + heif_error_Unsupported_filetype, + heif_suberror_Unspecified, + "Zero extra dimension size." + }; + } + + if (nTiles > UINT64_MAX / params.extra_dimensions[i]) { + return Error{ + heif_error_Unsupported_filetype, + heif_suberror_Unspecified, + "Number of tiles exceeds uint64 maximum." + }; + } + nTiles *= params.extra_dimensions[i]; + + if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) { + return Error{ + heif_error_Unsupported_filetype, + heif_suberror_Security_limit_exceeded, + "Number of tiles exceeds security limit" + }; + } } return nTiles; @@ -58,13 +96,18 @@ uint32_t nTiles_h(const heif_tiled_image_parameters& params) { - return (params.image_width + params.tile_width - 1) / params.tile_width; + // 64-bit arithmetic prevents wrap-around when image_width + tile_width - 1 + // exceeds UINT32_MAX. The quotient is bounded by image_width, so the + // narrowing cast is safe. Callers are responsible for ensuring tile_width > 0. + return static_cast( + (static_cast(params.image_width) + params.tile_width - 1) / params.tile_width); } uint32_t nTiles_v(const heif_tiled_image_parameters& params) { - return (params.image_height + params.tile_height - 1) / params.tile_height; + return static_cast( + (static_cast(params.image_height) + params.tile_height - 1) / params.tile_height); } @@ -302,15 +345,19 @@ { m_parameters = params; - auto max_tiles = heif_get_global_security_limits()->max_number_of_tiles; - - if (max_tiles && number_of_tiles(params) > max_tiles) { - return {heif_error_Unsupported_filetype, - heif_suberror_Security_limit_exceeded, - "Number of tiles exceeds security limit"}; + Result num_tiles_result = number_of_tiles(params, heif_get_global_security_limits()); + if (auto err = num_tiles_result.error()) { + return err; } - m_offsets.resize(number_of_tiles(params)); + // TODO(security): this offset table is bounded only by max_number_of_tiles + // (default 4096*4096 => ~256 MB for the TileOffset vector), and the allocation + // is neither counted against max_total_memory via MemoryHandle nor deferred to + // decode time. It also uses the global security limits instead of the context + // limits. Route this allocation through MemoryHandle and use the context limits + // so a small 'tili' file cannot pre-allocate hundreds of MB at file-open time. + // (Reported in GHSA-x8xm-cm2c-cfc8, variant V3. 'tili' is experimental.) + m_offsets.resize(*num_tiles_result); for (auto& tile: m_offsets) { tile.offset = TILD_OFFSET_NOT_LOADED; @@ -322,23 +369,19 @@ Error TiledHeader::read_full_offset_table(const std::shared_ptr& file, heif_item_id tild_id, const heif_security_limits* limits) { - auto max_tiles = heif_get_global_security_limits()->max_number_of_tiles; - - uint64_t nTiles = number_of_tiles(m_parameters); - if (max_tiles && nTiles > max_tiles) { - return {heif_error_Invalid_input, - heif_suberror_Security_limit_exceeded, - "Number of tiles exceeds security limit."}; + Result nTiles_result = number_of_tiles(m_parameters, limits); + if (auto err = nTiles_result.error()) { + return err; } - return read_offset_table_range(file, tild_id, 0, nTiles); + return read_offset_table_range(file, tild_id, 0, *nTiles_result); } Error TiledHeader::read_offset_table_range(const std::shared_ptr& file, heif_item_id tild_id, uint64_t start, uint64_t end) { - const Error eofError(heif_error_Invalid_input, + Error eofError(heif_error_Invalid_input, heif_suberror_Unspecified, "Tili header data incomplete"); @@ -361,6 +404,13 @@ return err; } + // Make sure we actually received as much data as we are about to parse. The + // returned buffer may be shorter than requested (e.g. truncated iloc/idat + // extents), and readvec() does not bounds-check its input. + if (data.size() < size_to_read) { + return eofError; + } + size_t idx = 0; for (uint64_t i = start; i < end; i++) { m_offsets[i].offset = readvec(data, idx, m_parameters.offset_field_length / 8); @@ -392,6 +442,13 @@ std::pair TiledHeader::get_tile_offset_table_range_to_read(uint32_t idx, uint32_t nEntries) const { + // Defense in depth: callers are expected to validate idx, but if they don't, + // returning an empty range prevents the subsequent read_offset_table_range + // from writing past m_offsets. + if (idx >= m_offsets.size()) { + return {0, 0}; + } + uint32_t start = idx; uint32_t end = idx+1; @@ -418,11 +475,33 @@ } -void TiledHeader::set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size) +Error TiledHeader::set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size) { + // Offset and size are written into bit-fields of the configured widths; + // silently truncating here produces files where the offset table points to + // garbage. Reject the value so the caller knows to widen the fields. + uint8_t off_bits = m_parameters.offset_field_length; + uint8_t sz_bits = m_parameters.size_field_length; + + if (off_bits < 64 && offset >> off_bits) { + std::stringstream sstr; + sstr << "Tile offset " << offset << " does not fit in the configured " + << static_cast(off_bits) << "-bit offset field. Use a wider " + "offset_field_length (40/48/64) when encoding the tili image."; + return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()}; + } + + if (sz_bits != 0 && sz_bits < 32 && size >> sz_bits) { + std::stringstream sstr; + sstr << "Tile size " << size << " does not fit in the configured " + << static_cast(sz_bits) << "-bit size field."; + return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()}; + } + uint64_t idx = uint64_t{tile_y} * nTiles_h(m_parameters) + tile_x; m_offsets[idx].offset = offset; m_offsets[idx].size = size; + return Error::Ok; } @@ -437,19 +516,39 @@ } -std::vector TiledHeader::write_offset_table() +Result> TiledHeader::write_offset_table() { - uint64_t nTiles = number_of_tiles(m_parameters); + Result nTiles_result = number_of_tiles(m_parameters, nullptr); + if (auto err = nTiles_result.error()) { + return err; + } + int offset_entry_size = (m_parameters.offset_field_length + m_parameters.size_field_length) / 8; - uint64_t size = nTiles * offset_entry_size; + uint64_t size = *nTiles_result * offset_entry_size; std::vector data; data.resize(size); size_t idx = 0; + uint8_t off_bits = m_parameters.offset_field_length; + uint8_t sz_bits = m_parameters.size_field_length; + for (const auto& offset: m_offsets) { + if (off_bits < 64 && offset.offset >> off_bits) { + std::stringstream sstr; + sstr << "Tile offset " << offset.offset << " does not fit in the " + "configured " << static_cast(off_bits) << "-bit offset field."; + return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()}; + } + if (sz_bits != 0 && sz_bits < 32 && offset.size >> sz_bits) { + std::stringstream sstr; + sstr << "Tile size " << offset.size << " does not fit in the " + "configured " << static_cast(sz_bits) << "-bit size field."; + return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()}; + } + writevec(data.data(), idx, offset.offset, m_parameters.offset_field_length / 8); if (m_parameters.size_field_length != 0) { @@ -484,12 +583,20 @@ ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx) : ImageItem(ctx) { + m_tile_encoding_options = heif_encoding_options_alloc(); } ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) { + m_tile_encoding_options = heif_encoding_options_alloc(); +} + + +ImageItem_Tiled::~ImageItem_Tiled() +{ + heif_encoding_options_free(m_tile_encoding_options); } @@ -499,7 +606,7 @@ } -Error ImageItem_Tiled::on_load_file() +Error ImageItem_Tiled::initialize_decoder() { auto heif_file = get_context()->get_heif_file(); @@ -541,11 +648,30 @@ // TODO: remove when spec is final and old test images have been converted if (tilC_box->get_version() == 1) { auto propertiesResult = get_properties(); - if (propertiesResult.error) { - return propertiesResult.error; + if (!propertiesResult) { + return propertiesResult.error(); } - m_tile_item->set_properties(*propertiesResult); + // Filter out per-tile boxes incompatible with tili's shared template + auto props = *propertiesResult; +#if WITH_UNCOMPRESSED_CODEC + for (const auto& box : props) { + if (box->get_short_type() == fourcc("icef")) { + auto icef = std::dynamic_pointer_cast(box); + if (icef && icef->get_units().size() > 1) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "icef box with multiple units is incompatible with tili shared tile template."}; + } + } + } + std::erase_if(props, [](const std::shared_ptr& box) { + uint32_t type = box->get_short_type(); + return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc"); + }); +#endif + + m_tile_item->set_properties(props); } else { // --- This is the new method @@ -554,6 +680,24 @@ auto tile_properties = tilC_box->get_all_child_boxes(); + // Filter out per-tile boxes incompatible with tili's shared template +#if WITH_UNCOMPRESSED_CODEC + for (const auto& box : tile_properties) { + if (box->get_short_type() == fourcc("icef")) { + auto icef = std::dynamic_pointer_cast(box); + if (icef && icef->get_units().size() > 1) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "icef box with multiple units is incompatible with tili shared tile template."}; + } + } + } + std::erase_if(tile_properties, [](const std::shared_ptr& box) { + uint32_t type = box->get_short_type(); + return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc"); + }); +#endif + bool have_ispe = false; for (const auto& property : tile_properties) { if (property->get_short_type() == fourcc("ispe")) { @@ -589,27 +733,58 @@ } +void ImageItem_Tiled::populate_component_descriptions() +{ + // Idempotent: skip if already populated. + if (!get_component_descriptions().empty()) { + return; + } + + // initialize_decoder() must have run; m_tile_item carries the per-tile + // properties (cmpd/uncC for unci tiles, codec config for visual tiles) + // and its own populate has filled tile-sized component descriptions. + if (!m_tile_item) { + return; + } + + uint32_t tile_w = m_tild_header.get_parameters().tile_width; + uint32_t tile_h = m_tild_header.get_parameters().tile_height; + populate_descriptions_from_child(*m_tile_item, tile_w, tile_h); +} + + Result> ImageItem_Tiled::add_new_tiled_item(HeifContext* ctx, const heif_tiled_image_parameters* parameters, - const heif_encoder* encoder) + const heif_encoder* encoder, + const heif_encoding_options* encoding_options) { - auto max_tild_tiles = ctx->get_security_limits()->max_number_of_tiles; - if (max_tild_tiles && number_of_tiles(*parameters) > max_tild_tiles) { - return Error{heif_error_Usage_error, - heif_suberror_Security_limit_exceeded, - "Number of tiles exceeds security limit."}; + Result num_tiles_result = number_of_tiles(*parameters, ctx->get_security_limits()); + if (const auto& err = num_tiles_result.error()) { + return err; } - // Create 'tili' Item auto file = ctx->get_heif_file(); - heif_item_id tild_id = ctx->get_heif_file()->add_new_image(fourcc("tili")); + auto tild_id_result = ctx->get_heif_file()->add_new_image(fourcc("tili")); + if (!tild_id_result) { + return tild_id_result.error(); + } + heif_item_id tild_id = *tild_id_result; auto tild_image = std::make_shared(ctx, tild_id); tild_image->set_resolution(parameters->image_width, parameters->image_height); ctx->insert_image_item(tild_id, tild_image); + if (encoding_options) { + // encoding options for the tiles, but do not apply transformative properties + heif_encoding_options_copy(tild_image->m_tile_encoding_options, encoding_options); + tild_image->m_tile_encoding_options->image_orientation = heif_orientation_normal; + + // orientation of the main image + tild_image->m_image_orientation = encoding_options->image_orientation; + } + // Create tilC box auto tilC_box = std::make_shared(); @@ -623,10 +798,13 @@ tild_header.set_parameters(*parameters); tild_header.set_compression_format(encoder->plugin->compression_format); - std::vector header_data = tild_header.write_offset_table(); + Result> header_data_result = tild_header.write_offset_table(); + if (auto err = header_data_result.error()) { + return err; + } const int construction_method = 0; // 0=mdat 1=idat - file->append_iloc_data(tild_id, header_data, construction_method); + file->append_iloc_data(tild_id, *header_data_result, construction_method); if (parameters->image_width > 0xFFFFFFFF || parameters->image_height > 0xFFFFFFFF) { @@ -649,7 +827,7 @@ #endif tild_image->set_tild_header(tild_header); - tild_image->set_next_tild_position(header_data.size()); + tild_image->set_next_tild_position(header_data_result->size()); // Set Brands //m_heif_file->set_brand(encoder->plugin->compression_format, @@ -661,29 +839,29 @@ Error ImageItem_Tiled::add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, - struct heif_encoder* encoder) + heif_encoder* encoder) { auto item = ImageItem::alloc_for_compression_format(get_context(), encoder->plugin->compression_format); - heif_encoding_options* options = heif_encoding_options_alloc(); // TODO: should this be taken from heif_context_add_tiled_image() ? - - Result> colorConversionResult = item->convert_colorspace_for_encoding(image, encoder, *options); - if (colorConversionResult.error) { - heif_encoding_options_free(options); - return colorConversionResult.error; + Result> colorConversionResult; + colorConversionResult = item->get_encoder()->convert_colorspace_for_encoding(image, encoder, + m_tile_encoding_options->output_nclx_profile, + &m_tile_encoding_options->color_conversion_options, + get_context()->get_security_limits()); + if (!colorConversionResult) { + return colorConversionResult.error(); } - std::shared_ptr colorConvertedImage = colorConversionResult.value; + std::shared_ptr colorConvertedImage = *colorConversionResult; - Result encodeResult = item->encode_to_bitstream_and_boxes(colorConvertedImage, encoder, *options, heif_image_input_class_normal); // TODO (other than JPEG) - heif_encoding_options_free(options); + Result encodeResult = item->encode_to_bitstream_and_boxes(colorConvertedImage, encoder, *m_tile_encoding_options, heif_image_input_class_normal); - if (encodeResult.error) { - return encodeResult.error; + if (!encodeResult) { + return encodeResult.error(); } const int construction_method = 0; // 0=mdat 1=idat - get_file()->append_iloc_data(get_id(), encodeResult.value.bitstream, construction_method); + get_file()->append_iloc_data(get_id(), encodeResult->bitstream, construction_method); auto& header = m_tild_header; @@ -695,19 +873,21 @@ } uint64_t offset = get_next_tild_position(); - size_t dataSize = encodeResult.value.bitstream.size(); + size_t dataSize = encodeResult->bitstream.size(); if (dataSize > 0xFFFFFFFF) { return {heif_error_Encoding_error, heif_suberror_Unspecified, "Compressed tile size exceeds maximum tile size."}; } - header.set_tild_tile_range(tile_x, tile_y, offset, static_cast(dataSize)); - set_next_tild_position(offset + encodeResult.value.bitstream.size()); + if (Error err = header.set_tild_tile_range(tile_x, tile_y, offset, static_cast(dataSize))) { + return err; + } + set_next_tild_position(offset + encodeResult->bitstream.size()); auto tilC = get_property(); assert(tilC); std::vector>& tile_properties = tilC->get_tile_properties(); - for (auto& propertyBox : encodeResult.value.properties) { + for (auto& propertyBox : encodeResult->properties) { // we do not have to save ispe boxes in the tile properties as this is automatically synthesized @@ -715,6 +895,28 @@ continue; } +#if WITH_UNCOMPRESSED_CODEC + // icef/sbpm/snuc contain per-tile data incompatible with tili's shared tile template + uint32_t ptype = propertyBox->get_short_type(); + + if (ptype == fourcc("icef")) { + auto icef = std::dynamic_pointer_cast(propertyBox); + if (icef && icef->get_units().size() > 1) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "icef box with multiple units is incompatible with tili shared tile template."}; + } + // Single-unit icef can be safely skipped + continue; + } + + if (ptype == fourcc("sbpm") || ptype == fourcc("snuc")) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Cannot store per-tile property (" + fourcc_to_string(ptype) + ") in tili shared tile template."}; + } +#endif + // skip properties that exist already bool exists = std::any_of(tile_properties.begin(), @@ -733,43 +935,59 @@ get_file()->add_property(get_id(), propertyBox, propertyBox->is_essential()); break; } + + if (Error err = get_file()->add_orientation_properties(get_id(), m_image_orientation)) { + return err; + } } - get_file()->set_brand(encoder->plugin->compression_format, - true); // TODO: out_grid_image->is_miaf_compatible()); + //get_file()->set_brand(encoder->plugin->compression_format, + // true); // TODO: out_grid_image->is_miaf_compatible()); return Error::Ok; } -void ImageItem_Tiled::process_before_write() +Error ImageItem_Tiled::process_before_write() { // overwrite offsets const int construction_method = 0; // 0=mdat 1=idat - std::vector header_data = m_tild_header.write_offset_table(); - get_file()->replace_iloc_data(get_id(), 0, header_data, construction_method); + Result> header_data_result = m_tild_header.write_offset_table(); + if (auto err = header_data_result.error()) { + return err; + } + + get_file()->replace_iloc_data(get_id(), 0, *header_data_result, construction_method); + return {}; } Result> -ImageItem_Tiled::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +ImageItem_Tiled::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { if (decode_tile_only) { return decode_grid_tile(options, tile_x0, tile_y0); } else { return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, - "'tili' images can only be access per tile"}; + "'tili' images can only be accessed per tile"}; } } Error ImageItem_Tiled::append_compressed_tile_data(std::vector& data, uint32_t tx, uint32_t ty) const { - uint32_t idx = (uint32_t) (ty * nTiles_h(m_tild_header.get_parameters()) + tx); + uint64_t idx64 = static_cast(ty) * nTiles_h(m_tild_header.get_parameters()) + tx; + if (idx64 >= m_tild_header.get_num_tiles()) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Tile index out of range."}; + } + auto idx = static_cast(idx64); if (!m_tild_header.is_tile_offset_known(idx)) { Error err = const_cast(this)->load_tile_offset_entry(idx); @@ -795,17 +1013,17 @@ { // --- get compressed data - Error err = m_tile_item->init_decoder_from_item(0); + Error err = m_tile_item->initialize_decoder(); if (err) { return err; } Result> dataResult = m_tile_item->read_bitstream_configuration_data(); - if (dataResult.error) { - return dataResult.error; + if (!dataResult) { + return dataResult.error(); } - std::vector& data = dataResult.value; + std::vector data = std::move(*dataResult); err = append_compressed_tile_data(data, tx, ty); if (err) { return err; @@ -814,9 +1032,9 @@ // --- decode DataExtent extent; - extent.m_raw = data; + extent.m_raw = std::move(data); - return extent; + return std::move(extent); } @@ -824,13 +1042,19 @@ ImageItem_Tiled::decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty) const { Result extentResult = get_compressed_data_for_tile(tx, ty); - if (extentResult.error) { - return extentResult.error; + if (!extentResult) { + return extentResult.error(); } m_tile_decoder->set_data_extent(std::move(*extentResult)); - return m_tile_decoder->decode_single_frame_from_compressed_data(options); + uint32_t tw = 0, th = 0; + get_tile_size(tw, th); + heif_security_limits tightened = tighten_image_size_limit_for_ispe( + get_context()->get_security_limits(), tw, th, + max_coding_unit_size_for_codec(m_tile_decoder->get_compression_format())); + + return m_tile_decoder->decode_single_frame_from_compressed_data(options, &tightened); } @@ -876,8 +1100,8 @@ uint32_t tx=0, ty=0; // TODO: find a tile that is defined. Result extentResult = get_compressed_data_for_tile(tx, ty); - if (extentResult.error) { - return extentResult.error; + if (!extentResult) { + return extentResult.error(); } m_tile_decoder->set_data_extent(std::move(*extentResult)); @@ -909,4 +1133,18 @@ m_tile_decoder->set_data_extent(std::move(any_tile_extent)); return m_tile_decoder->get_chroma_bits_per_pixel(); -} \ No newline at end of file +} + +heif_brand2 ImageItem_Tiled::get_compatible_brand() const +{ + return 0; + + // TODO: it is not clear to me what brand to use here. + + /* + switch (m_tild_header.get_parameters().compression_format_fourcc) { + case heif_compression_HEVC: + return heif_brand2_heic; + } + */ +} diff -Nru libheif-1.19.8/libheif/image-items/tiled.h libheif-1.23.4/libheif/image-items/tiled.h --- libheif-1.19.8/libheif/image-items/tiled.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/tiled.h 2026-09-06 14:45:17.000000000 +0000 @@ -21,7 +21,6 @@ #ifndef LIBHEIF_TILED_H #define LIBHEIF_TILED_H - #include "image_item.h" #include "codecs/decoder.h" #include "box.h" @@ -30,9 +29,11 @@ #include #include #include "libheif/heif_experimental.h" +#include "libheif/heif_encoding.h" +#include -uint64_t number_of_tiles(const heif_tiled_image_parameters& params); +Result number_of_tiles(const heif_tiled_image_parameters& params, const heif_security_limits* limits); uint32_t nTiles_h(const heif_tiled_image_parameters& params); @@ -102,14 +103,19 @@ Error read_offset_table_range(const std::shared_ptr& file, heif_item_id tild_id, uint64_t start, uint64_t end); - std::vector write_offset_table(); + Result> write_offset_table(); std::string dump() const; - void set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size); + // Returns an error if `offset` does not fit in offset_field_length or + // `size` does not fit in size_field_length. Catches the encoder-side + // overflow that would otherwise silently truncate the field at write time. + Error set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size); size_t get_header_size() const; + size_t get_num_tiles() const { return m_offsets.size(); } + uint64_t get_tile_offset(uint32_t idx) const { return m_offsets[idx].offset; } uint32_t get_tile_size(uint32_t idx) const { return m_offsets[idx].size; } @@ -146,6 +152,8 @@ ImageItem_Tiled(HeifContext* ctx); + ~ImageItem_Tiled() override; + uint32_t get_infe_type() const override { return fourcc("tili"); } // TODO: nclx depends on contained format @@ -154,16 +162,23 @@ heif_compression_format get_compression_format() const override; static Result> add_new_tiled_item(HeifContext* ctx, const heif_tiled_image_parameters* parameters, - const heif_encoder* encoder); + const heif_encoder* encoder, + const heif_encoding_options* encoding_options); Error add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, struct heif_encoder* encoder); - Error on_load_file() override; + Error initialize_decoder() override; + + // Copies per-component descriptions from the embedded tile item (which has + // tile-sized dims) and rescales them to the full image's ispe dimensions, + // so the handle exposes the same per-component metadata (datatype, + // bit-depth, type) that the decoded image will report after a tile decode. + void populate_component_descriptions() override; - void process_before_write() override; + Error process_before_write() override; Error get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const override; @@ -171,16 +186,20 @@ int get_chroma_bits_per_pixel() const override; - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override { + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override + { return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified, "Cannot encode image to 'tild'"}; } - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; + + heif_brand2 get_compatible_brand() const override; // --- tild @@ -200,11 +219,14 @@ TiledHeader m_tild_header; uint64_t m_next_tild_position = 0; + heif_orientation m_image_orientation = heif_orientation_normal; + heif_encoding_options* m_tile_encoding_options = nullptr; + uint32_t mReadChunkSize_bytes = 64*1024; // 64 kiB bool m_preload_offset_table = false; std::shared_ptr m_tile_item; - std::shared_ptr m_tile_decoder; + std::shared_ptr m_tile_decoder; Result get_compressed_data_for_tile(uint32_t tx, uint32_t ty) const; diff -Nru libheif-1.19.8/libheif/image-items/unc_image.cc libheif-1.23.4/libheif/image-items/unc_image.cc --- libheif-1.19.8/libheif/image-items/unc_image.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/unc_image.cc 2026-09-06 14:45:17.000000000 +0000 @@ -35,34 +35,115 @@ #include "codecs/uncompressed/unc_boxes.h" #include "unc_image.h" #include "codecs/uncompressed/unc_dec.h" +#include "codecs/uncompressed/unc_enc.h" #include "codecs/uncompressed/unc_codec.h" #include "image_item.h" +#include "codecs/uncompressed/unc_encoder.h" +struct unciHeaders; -static void maybe_make_minimised_uncC(std::shared_ptr& uncC, const std::shared_ptr& image) +ImageItem_uncompressed::ImageItem_uncompressed(HeifContext* ctx, heif_item_id id) + : ImageItem(ctx, id) { - uncC->set_version(0); - if (image->get_colorspace() != heif_colorspace_RGB) { + m_encoder = std::make_shared(); +} + +void ImageItem_uncompressed::populate_component_descriptions() +{ + // Idempotent: this method is called from both set_properties() (where + // unci populates from cmpd/uncC, no decoder needed) and from + // context.cc after initialize_decoder() (where visual codecs populate). + // For unci items the first call wins; the second is a no-op. + if (!get_component_descriptions().empty()) { return; } - if (!((image->get_chroma_format() == heif_chroma_interleaved_RGB) || (image->get_chroma_format() == heif_chroma_interleaved_RGBA))) { + + auto uncC = get_property(); + if (!uncC) { return; } - if (image->get_bits_per_pixel(heif_channel_interleaved) != 8) { + auto cmpd = get_property(); + + // For minimized (version-1) uncC, expand the profile fourcc into the + // components vector and synthesize cmpd if missing. + fill_uncC_and_cmpd_from_profile(uncC, cmpd); + if (!cmpd) { return; } - if (image->get_chroma_format() == heif_chroma_interleaved_RGBA) { - uncC->set_profile(fourcc("rgba")); - } else { - uncC->set_profile(fourcc("rgb3")); + + const auto& cmpd_components = cmpd->get_components(); + uint32_t img_width = get_ispe_width(); + uint32_t img_height = get_ispe_height(); + + // Determine chroma format so we can apply the correct subsampling for Cb/Cr. + heif_chroma chroma = heif_chroma_undefined; + heif_colorspace colourspace = heif_colorspace_undefined; + (void) UncompressedImageCodec::get_heif_chroma_uncompressed(uncC, cmpd, &chroma, &colourspace, nullptr); + + // Track which cmpd indices already got a description from the uncC walk, + // so we don't duplicate them when handling cpat. + std::vector cmpd_index_has_description(cmpd_components.size(), false); + + // 1. uncC components — these get real planes after decode. + for (const auto& uc : uncC->get_components()) { + if (uc.component_index >= cmpd_components.size()) { + continue; // malformed; skip + } + uint16_t component_type = cmpd_components[uc.component_index].component_type; + + heif_channel channel = map_uncompressed_component_to_channel(component_type); + uint32_t plane_w = channel_width(img_width, chroma, channel); + uint32_t plane_h = channel_height(img_height, chroma, channel); + + ComponentDescription desc; + desc.component_id = mint_component_id(); + desc.channel = channel; + desc.component_type = component_type; + desc.datatype = unc_component_format_to_datatype(uc.component_format); + desc.bit_depth = uc.component_bit_depth; + desc.width = plane_w; + desc.height = plane_h; + desc.has_data_plane = true; + add_component_description(std::move(desc)); + + cmpd_index_has_description[uc.component_index] = true; + } + + // 2. cpat reference components — one per UNIQUE cmpd_index that's + // referenced by the pattern but doesn't have an uncC plane. Two pattern + // pixels that share a cmpd_index share the same component id; the + // BayerPattern only carries per-pixel gain, not per-pixel identity. + if (auto cpat = get_property()) { + for (const auto& pixel : cpat->get_pattern().pixels) { + if (pixel.cmpd_index >= cmpd_components.size()) continue; + if (cmpd_index_has_description[pixel.cmpd_index]) continue; + + uint16_t component_type = cmpd_components[pixel.cmpd_index].component_type; + + ComponentDescription desc; + desc.component_id = mint_component_id(); + desc.channel = map_uncompressed_component_to_channel(component_type); + desc.component_type = component_type; + desc.has_data_plane = false; + add_component_description(std::move(desc)); + + cmpd_index_has_description[pixel.cmpd_index] = true; + } } - uncC->set_version(1); } -Result> ImageItem_uncompressed::decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const +ImageItem_uncompressed::ImageItem_uncompressed(HeifContext* ctx) + : ImageItem(ctx) +{ + m_encoder = std::make_shared(); +} + + +Result> ImageItem_uncompressed::decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const { std::shared_ptr img; @@ -91,256 +172,94 @@ } -struct unciHeaders +Result ImageItem_uncompressed::encode(const std::shared_ptr& src_image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) { - std::shared_ptr uncC; - std::shared_ptr cmpd; -}; - - -static Result generate_headers(const std::shared_ptr& src_image, - const heif_unci_image_parameters* parameters, - const struct heif_encoding_options* options) -{ - unciHeaders headers; - - bool uses_tiles = (parameters->tile_width != parameters->image_width || - parameters->tile_height != parameters->image_height); - - std::shared_ptr uncC = std::make_shared(); - if (options && options->prefer_uncC_short_form && !uses_tiles) { - maybe_make_minimised_uncC(uncC, src_image); + Result> uncEncoder = unc_encoder_factory::get_unc_encoder(src_image, options); + if (!uncEncoder) { + return {uncEncoder.error()}; } - if (uncC->get_version() == 1) { - headers.uncC = std::move(uncC); - } else { - std::shared_ptr cmpd = std::make_shared(); - - Error error = fill_cmpd_and_uncC(cmpd, uncC, src_image, parameters); - if (error) { - return error; - } - - headers.cmpd = std::move(cmpd); - headers.uncC = std::move(uncC); - } - - return headers; + return (*uncEncoder)->encode(src_image, options); } -Result> encode_image_tile(const std::shared_ptr& src_image) -{ - std::vector data; - - if (src_image->get_colorspace() == heif_colorspace_YCbCr) - { - uint64_t offset = 0; - for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) - { - size_t src_stride; - uint32_t src_width = src_image->get_width(channel); - uint32_t src_height = src_image->get_height(channel); - const uint8_t* src_data = src_image->get_plane(channel, &src_stride); - uint64_t out_size = src_width * uint64_t{src_height}; - data.resize(data.size() + out_size); - for (uint32_t y = 0; y < src_height; y++) { - memcpy(data.data() + offset + y * src_width, src_data + src_stride * y, src_width); - } - offset += out_size; - } - - return data; - } - else if (src_image->get_colorspace() == heif_colorspace_RGB) - { - if (src_image->get_chroma_format() == heif_chroma_444) - { - uint64_t offset = 0; - std::vector channels = {heif_channel_R, heif_channel_G, heif_channel_B}; - if (src_image->has_channel(heif_channel_Alpha)) - { - channels.push_back(heif_channel_Alpha); - } - for (heif_channel channel : channels) - { - size_t src_stride; - const uint8_t* src_data = src_image->get_plane(channel, &src_stride); - uint64_t out_size = static_cast(src_image->get_height()) * src_image->get_width(); - - data.resize(data.size() + out_size); - for (uint32_t y = 0; y < src_image->get_height(); y++) { - memcpy(data.data() + offset + y * src_image->get_width(), src_data + y * src_stride, src_image->get_width()); - } - - offset += out_size; - } - - return data; - } - else if ((src_image->get_chroma_format() == heif_chroma_interleaved_RGB) || - (src_image->get_chroma_format() == heif_chroma_interleaved_RGBA) || - (src_image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_BE) || - (src_image->get_chroma_format() == heif_chroma_interleaved_RRGGBB_LE) || - (src_image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE) || - (src_image->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE)) - { - int bytes_per_pixel = 0; - switch (src_image->get_chroma_format()) { - case heif_chroma_interleaved_RGB: - bytes_per_pixel=3; - break; - case heif_chroma_interleaved_RGBA: - bytes_per_pixel=4; - break; - case heif_chroma_interleaved_RRGGBB_BE: - case heif_chroma_interleaved_RRGGBB_LE: - bytes_per_pixel=6; - break; - case heif_chroma_interleaved_RRGGBBAA_BE: - case heif_chroma_interleaved_RRGGBBAA_LE: - bytes_per_pixel=8; - break; - default: - assert(false); - } - - size_t src_stride; - const uint8_t* src_data = src_image->get_plane(heif_channel_interleaved, &src_stride); - uint64_t out_size = static_cast(src_image->get_height()) * src_image->get_width() * bytes_per_pixel; - data.resize(out_size); - for (uint32_t y = 0; y < src_image->get_height(); y++) { - memcpy(data.data() + y * src_image->get_width() * bytes_per_pixel, src_data + src_stride * y, src_image->get_width() * bytes_per_pixel); - } - - return data; - } - else - { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Unsupported RGB chroma"); - } - } - else if (src_image->get_colorspace() == heif_colorspace_monochrome) - { - uint64_t offset = 0; - std::vector channels; - if (src_image->has_channel(heif_channel_Alpha)) - { - channels = {heif_channel_Y, heif_channel_Alpha}; - } - else - { - channels = {heif_channel_Y}; - } - for (heif_channel channel : channels) - { - size_t src_stride; - const uint8_t* src_data = src_image->get_plane(channel, &src_stride); - uint64_t out_size = static_cast(src_image->get_height()) * src_stride; - data.resize(data.size() + out_size); - memcpy(data.data() + offset, src_data, out_size); - offset += out_size; - } - - return data; - } - else - { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unsupported_data_version, - "Unsupported colourspace"); - } - -} - - -Result ImageItem_uncompressed::encode(const std::shared_ptr& src_image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +Result> ImageItem_uncompressed::add_unci_item(HeifContext* ctx, + const heif_unci_image_parameters* parameters, + const heif_encoding_options* encoding_options, + const std::shared_ptr& prototype) { - heif_unci_image_parameters parameters{}; - parameters.image_width = src_image->get_width(); - parameters.image_height = src_image->get_height(); - parameters.tile_width = parameters.image_width; - parameters.tile_height = parameters.image_height; - - - // --- generate configuration property boxes - - Result genHeadersResult = generate_headers(src_image, ¶meters, &options); - if (genHeadersResult.error) { - return genHeadersResult.error; - } + assert(encoding_options != nullptr); - const unciHeaders& headers = *genHeadersResult; + // Resolve effective unci parameters: the direct argument takes precedence; otherwise + // fall back to encoding_options->unci_parameters. At least one must be non-null. - CodedImageData codedImageData; - if (headers.uncC) { - codedImageData.properties.push_back(headers.uncC); - } - if (headers.cmpd) { - codedImageData.properties.push_back(headers.cmpd); + if (parameters == nullptr && + (encoding_options->version < 8 || encoding_options->unci_parameters == nullptr)) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "heif_context_add_empty_unci_image: either the 'parameters' argument or " + "heif_encoding_options::unci_parameters must be non-null."}; } - - // --- encode image - - Result> codedBitstreamResult = encode_image_tile(src_image); - if (codedBitstreamResult.error) { - return codedBitstreamResult.error; + if (parameters == nullptr) { + parameters = encoding_options->unci_parameters; } - codedImageData.bitstream = *codedBitstreamResult; - - return codedImageData; -} - - -Result> ImageItem_uncompressed::add_unci_item(HeifContext* ctx, - const heif_unci_image_parameters* parameters, - const struct heif_encoding_options* encoding_options, - const std::shared_ptr& prototype) -{ // Check input parameters if (parameters->image_width % parameters->tile_width != 0 || parameters->image_height % parameters->tile_height != 0) { - return Error{heif_error_Invalid_input, + return Error{heif_error_Usage_error, heif_suberror_Invalid_parameter_value, "ISO 23001-17 image size must be an integer multiple of the tile size."}; } + + Result> uncEncoder = unc_encoder_factory::get_unc_encoder(prototype, *encoding_options); + if (!uncEncoder) { + return {uncEncoder.error()}; + } + + // Create 'unci' Item auto file = ctx->get_heif_file(); - heif_item_id unci_id = ctx->get_heif_file()->add_new_image(fourcc("unci")); + auto unci_id_result = ctx->get_heif_file()->add_new_image(fourcc("unci")); + if (!unci_id_result) { + return unci_id_result.error(); + } + heif_item_id unci_id = *unci_id_result; auto unci_image = std::make_shared(ctx, unci_id); unci_image->set_resolution(parameters->image_width, parameters->image_height); + unci_image->m_unc_encoder = std::move(*uncEncoder); + unci_image->m_tile_encoding_options = *encoding_options; + unci_image->m_tile_encoding_options.image_orientation = heif_orientation_normal; + ctx->insert_image_item(unci_id, unci_image); // Generate headers - Result genHeadersResult = generate_headers(prototype, parameters, encoding_options); - if (genHeadersResult.error) { - return genHeadersResult.error; - } + // --- generate configuration property boxes - const unciHeaders& headers = *genHeadersResult; + auto uncC = unci_image->m_unc_encoder->get_uncC(); - assert(headers.uncC); + uncC->set_number_of_tile_columns(parameters->image_width / parameters->tile_width); + uncC->set_number_of_tile_rows(parameters->image_height / parameters->tile_height); - if (headers.uncC) { - unci_image->add_property(headers.uncC, true); + unci_image->add_property(uncC, true); + if (!uncC->is_minimized()) { + unci_image->add_property(unci_image->m_unc_encoder->get_cmpd(), true); } - if (headers.cmpd) { - unci_image->add_property(headers.cmpd, true); + + // Add cpat property if Bayer pattern is set + if (unci_image->m_unc_encoder->get_cpat()) { + unci_image->add_property(unci_image->m_unc_encoder->get_cpat(), true); } // Add `ispe` property @@ -351,38 +270,28 @@ unci_image->add_property(ispe, true); if (parameters->compression != heif_unci_compression_off) { - auto icef = std::make_shared(); auto cmpC = std::make_shared(); + cmpC->set_compression_type(unci_compression_to_fourcc(parameters->compression)); cmpC->set_compressed_unit_type(heif_cmpC_compressed_unit_type_image_tile); - if (false) { - } -#if HAVE_ZLIB - else if (parameters->compression == heif_unci_compression_deflate) { - cmpC->set_compression_type(fourcc("defl")); - } - else if (parameters->compression == heif_unci_compression_zlib) { - cmpC->set_compression_type(fourcc("zlib")); - } -#endif -#if HAVE_BROTLI - else if (parameters->compression == heif_unci_compression_brotli) { - cmpC->set_compression_type(fourcc("brot")); - } -#endif - else { - assert(false); - } - unci_image->add_property(cmpC, true); + + auto icef = std::make_shared(); unci_image->add_property_without_deduplication(icef, true); // icef is empty. A normal add_property() would lead to a wrong deduplication. } + // Add transformative properties + + if (Error err = ctx->get_heif_file()->add_orientation_properties(unci_id, encoding_options->image_orientation)) { + return err; + } + + // Create empty image. If we use compression, we append the data piece by piece. if (parameters->compression == heif_unci_compression_off) { - uint64_t tile_size = headers.uncC->compute_tile_data_size_bytes(parameters->image_width / headers.uncC->get_number_of_tile_columns(), - parameters->image_height / headers.uncC->get_number_of_tile_rows()); + uint64_t tile_size = unci_image->m_unc_encoder->compute_tile_data_size_bytes(parameters->image_width / uncC->get_number_of_tile_columns(), + parameters->image_height / uncC->get_number_of_tile_rows()); std::vector dummydata; dummydata.resize(tile_size); @@ -396,13 +305,13 @@ } // Set Brands - ctx->get_heif_file()->set_brand(heif_compression_uncompressed, unci_image->is_miaf_compatible()); + //ctx->get_heif_file()->set_brand(heif_compression_uncompressed, unci_image->is_miaf_compatible()); return {unci_image}; } -Error ImageItem_uncompressed::add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image) +Error ImageItem_uncompressed::add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, bool save_alpha) { std::shared_ptr uncC = get_property(); assert(uncC); @@ -412,9 +321,35 @@ uint32_t tile_idx = tile_y * uncC->get_number_of_tile_columns() + tile_x; - Result> codedBitstreamResult = encode_image_tile(image); - if (codedBitstreamResult.error) { - return codedBitstreamResult.error; + if (tile_y >= uncC->get_number_of_tile_rows() || + tile_x >= uncC->get_number_of_tile_columns()) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "tile_x and/or tile_y are out of range."}; + } + + // All tiles have the same size (add_unci_item() enforces that the image size is an integer + // multiple of the tile size). We compute the position at which the tile data is written from the + // size of the passed image, so a differently sized tile would be written to a wrong offset. + + uint32_t expected_tile_width, expected_tile_height; + get_tile_size(expected_tile_width, expected_tile_height); + + if (tile_width != expected_tile_width || + tile_height != expected_tile_height) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Tile image size does not match the tile size of the uncompressed image."}; + } + + + if (image->has_alpha() && !save_alpha) { + // TODO: drop alpha + } + + Result> codedBitstreamResult = m_unc_encoder->encode_tile(image); + if (!codedBitstreamResult) { + return codedBitstreamResult.error(); } std::shared_ptr cmpC = get_property(); @@ -426,43 +361,27 @@ // uncompressed - uint64_t tile_data_size = uncC->compute_tile_data_size_bytes(tile_width, tile_height); + uint64_t tile_data_size = m_unc_encoder->compute_tile_data_size_bytes(tile_width, tile_height); get_file()->replace_iloc_data(get_id(), tile_idx * tile_data_size, *codedBitstreamResult, 0); } else { - std::vector compressed_data; - const std::vector& raw_data = codedBitstreamResult.value; - (void)raw_data; - - uint32_t compr = cmpC->get_compression_type(); - switch (compr) { -#if HAVE_ZLIB - case fourcc("defl"): - compressed_data = compress_deflate(raw_data.data(), raw_data.size()); - break; - case fourcc("zlib"): - compressed_data = compress_zlib(raw_data.data(), raw_data.size()); - break; -#endif -#if HAVE_BROTLI - case fourcc("brot"): - compressed_data = compress_brotli(raw_data.data(), raw_data.size()); - break; -#endif - default: - assert(false); - break; + const std::vector& raw_data = *codedBitstreamResult; + + auto compressed = compress_unci_fourcc(cmpC->get_compression_type(), + raw_data.data(), raw_data.size()); + if (!compressed) { + return compressed.error(); } - get_file()->append_iloc_data(get_id(), compressed_data, 0); + get_file()->append_iloc_data(get_id(), *compressed, 0); Box_icef::CompressedUnitInfo unit_info; unit_info.unit_offset = m_next_tile_write_pos; - unit_info.unit_size = compressed_data.size(); + unit_info.unit_size = compressed->size(); icef->set_component(tile_idx, unit_info); - m_next_tile_write_pos += compressed_data.size(); + m_next_tile_write_pos += compressed->size(); } return Error::Ok; @@ -510,10 +429,16 @@ return {m_decoder}; } -Error ImageItem_uncompressed::on_load_file() +std::shared_ptr ImageItem_uncompressed::get_encoder() const +{ + return m_encoder; +} + +Error ImageItem_uncompressed::initialize_decoder() { std::shared_ptr cmpd = get_property(); std::shared_ptr uncC = get_property(); + std::shared_ptr ispe = get_property(); if (!uncC) { return Error{heif_error_Invalid_input, @@ -521,14 +446,38 @@ "No 'uncC' box found."}; } - m_decoder = std::make_shared(uncC, cmpd); + if (!ispe) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "No 'ispe' box found for uncompressed image item."}; + } + + // libheif's pixel allocation and processing (rotate/mirror) only support + // bit depths up to 128 per component. Reject larger values up front so they + // surface as a clean Unsupported_feature error rather than failing inside + // HeifPixelImage::ComponentStorage::alloc(). + for (const auto& component : uncC->get_components()) { + if (component.component_bit_depth > 128) { + std::stringstream sstr; + sstr << "Uncompressed image with " << component.component_bit_depth + << " bits per component is not supported."; + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + sstr.str()}; + } + } + + m_decoder = std::make_shared(uncC, cmpd, ispe); + + return Error::Ok; +} +void ImageItem_uncompressed::set_decoder_input_data() +{ DataExtent extent; extent.set_from_image_item(get_context()->get_heif_file(), get_id()); m_decoder->set_data_extent(std::move(extent)); - - return Error::Ok; } @@ -536,3 +485,8 @@ { return m_decoder->has_alpha_component(); } + +heif_brand2 ImageItem_uncompressed::get_compatible_brand() const +{ + return 0; // TODO: not clear to me what to use +} diff -Nru libheif-1.19.8/libheif/image-items/unc_image.h libheif-1.23.4/libheif/image-items/unc_image.h --- libheif-1.19.8/libheif/image-items/unc_image.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/unc_image.h 2026-09-06 14:45:17.000000000 +0000 @@ -22,24 +22,28 @@ #ifndef LIBHEIF_UNC_IMAGE_H #define LIBHEIF_UNC_IMAGE_H -#include "pixelimage.h" +#include "image/pixelimage.h" #include "file.h" #include "context.h" +#include "libheif/heif_uncompressed.h" #include #include #include #include +#include +#include "codecs/uncompressed/unc_encoder.h" +class unc_encoder; class HeifContext; class ImageItem_uncompressed : public ImageItem { public: - ImageItem_uncompressed(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_uncompressed(HeifContext* ctx, heif_item_id id); - ImageItem_uncompressed(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_uncompressed(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("unci"); } @@ -50,42 +54,60 @@ bool has_coded_alpha_channel() const override; - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } + // const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } bool is_ispe_essential() const override { return true; } void get_tile_size(uint32_t& w, uint32_t& h) const override; + // Walk the cmpd / uncC / cpat property boxes and populate + // ImageDescription::m_components with one ComponentDescription per uncC + // component plus one (has_data_plane=false) per extra cpat reference + // component. Called once from ImageItem::set_properties(). + void populate_component_descriptions() override; + // Code from encode_uncompressed_image() has been moved to here. - Result> decode_compressed_image(const struct heif_decoding_options& options, - bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const override; + Result> decode_compressed_image(const heif_decoding_options& options, + bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0, + DecodeTraversalState decode_state) const override; heif_image_tiling get_heif_image_tiling() const override; - Error on_load_file() override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; + + heif_brand2 get_compatible_brand() const override; public: // --- encoding - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; - - static Result> add_unci_item(HeifContext* ctx, - const heif_unci_image_parameters* parameters, - const struct heif_encoding_options* encoding_options, - const std::shared_ptr& prototype); + Result encode(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_encoding_options& options, + heif_image_input_class input_class) override; + + static Result > add_unci_item(HeifContext* ctx, + const heif_unci_image_parameters* parameters, + const heif_encoding_options* encoding_options, + const std::shared_ptr& prototype); - Error add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image); + Error add_image_tile(uint32_t tile_x, uint32_t tile_y, const std::shared_ptr& image, bool save_alpha); protected: Result> get_decoder() const override; + std::shared_ptr get_encoder() const override; + private: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; + + std::unique_ptr m_unc_encoder; + heif_encoding_options m_tile_encoding_options; + /* Result generate_headers(const std::shared_ptr& src_image, const heif_unci_image_parameters* parameters, diff -Nru libheif-1.19.8/libheif/image-items/vvc.cc libheif-1.23.4/libheif/image-items/vvc.cc --- libheif-1.19.8/libheif/image-items/vvc.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/vvc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -20,77 +20,23 @@ #include "vvc.h" #include "codecs/vvc_dec.h" +#include "codecs/vvc_enc.h" #include "codecs/vvc_boxes.h" #include #include #include -#include "api/libheif/api_structs.h" +#include "api_structs.h" #include -Result ImageItem_VVC::encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) +ImageItem_VVC::ImageItem_VVC(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) { - CodedImageData codedImage; - - auto vvcC = std::make_shared(); - codedImage.properties.push_back(vvcC); - - - heif_image c_api_image; - c_api_image.image = image; - - struct heif_error err = encoder->plugin->encode_image(encoder->encoder, &c_api_image, input_class); - if (err.code) { - return Error(err.code, - err.subcode, - err.message); - } - - int encoded_width = 0; - int encoded_height = 0; - - for (;;) { - uint8_t* data; - int size; - - encoder->plugin->get_compressed_data(encoder->encoder, &data, &size, NULL); - - if (data == NULL) { - break; - } - - - const uint8_t NAL_SPS = 15; - - uint8_t nal_type = 0; - if (size>=2) { - nal_type = (data[1] >> 3) & 0x1F; - } - - if (nal_type == NAL_SPS) { - Box_vvcC::configuration config; - - parse_sps_for_vvcC_configuration(data, size, &config, &encoded_width, &encoded_height); - - vvcC->set_configuration(config); - } - - switch (nal_type) { - case 14: // VPS - case 15: // SPS - case 16: // PPS - vvcC->append_nal_data(data, size); - break; - - default: - codedImage.append_with_4bytes_size(data, size); - } - } + m_encoder = std::make_shared(); +} - return codedImage; +ImageItem_VVC::ImageItem_VVC(HeifContext* ctx) : ImageItem(ctx) +{ + m_encoder = std::make_shared(); } @@ -122,7 +68,14 @@ return {m_decoder}; } -Error ImageItem_VVC::on_load_file() + +std::shared_ptr ImageItem_VVC::get_encoder() const +{ + return m_encoder; +} + + +Error ImageItem_VVC::initialize_decoder() { auto vvcC_box = get_property(); if (!vvcC_box) { @@ -132,10 +85,18 @@ m_decoder = std::make_shared(vvcC_box); + return Error::Ok; +} + +void ImageItem_VVC::set_decoder_input_data() +{ DataExtent extent; extent.set_from_image_item(get_context()->get_heif_file(), get_id()); m_decoder->set_data_extent(std::move(extent)); +} - return Error::Ok; +heif_brand2 ImageItem_VVC::get_compatible_brand() const +{ + return heif_brand2_vvic; } diff -Nru libheif-1.19.8/libheif/image-items/vvc.h libheif-1.23.4/libheif/image-items/vvc.h --- libheif-1.19.8/libheif/image-items/vvc.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/image-items/vvc.h 2026-09-06 14:45:17.000000000 +0000 @@ -31,24 +31,23 @@ class ImageItem_VVC : public ImageItem { public: - ImageItem_VVC(HeifContext* ctx, heif_item_id id) : ImageItem(ctx, id) {} + ImageItem_VVC(HeifContext* ctx, heif_item_id id); - ImageItem_VVC(HeifContext* ctx) : ImageItem(ctx) {} + ImageItem_VVC(HeifContext* ctx); uint32_t get_infe_type() const override { return fourcc("vvc1"); } const char* get_auxC_alpha_channel_type() const override { return "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; } - const heif_color_profile_nclx* get_forced_output_nclx() const override { return nullptr; } - heif_compression_format get_compression_format() const override { return heif_compression_VVC; } - Result encode(const std::shared_ptr& image, - struct heif_encoder* encoder, - const struct heif_encoding_options& options, - enum heif_image_input_class input_class) override; + Error initialize_decoder() override; + + void set_decoder_input_data() override; + + std::shared_ptr get_encoder() const override; - Error on_load_file() override; + heif_brand2 get_compatible_brand() const override; protected: Result> get_decoder() const override; @@ -57,6 +56,7 @@ private: std::shared_ptr m_decoder; + std::shared_ptr m_encoder; }; #endif // LIBHEIF_VVC_H diff -Nru libheif-1.19.8/libheif/init.cc libheif-1.23.4/libheif/init.cc --- libheif-1.19.8/libheif/init.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/init.cc 2026-09-06 14:45:17.000000000 +0000 @@ -39,6 +39,9 @@ #endif +#define STRINGIFY2(x) #x +#define STRINGIFY(x) STRINGIFY2(x) + void heif_unload_all_plugins(); #if ENABLE_PLUGIN_LOADING @@ -102,7 +105,7 @@ } -struct heif_error heif_init(struct heif_init_params*) +heif_error heif_init(heif_init_params*) { #if ENABLE_MULTITHREADING_SUPPORT std::lock_guard lock(heif_init_mutex()); @@ -246,6 +249,22 @@ switch (loadedPlugin.info->type) { case heif_plugin_type_encoder: { auto* encoder_plugin = static_cast(plugin_info->plugin); + if (encoder_plugin->plugin_api_version < heif_encoder_plugin_minimum_version) { + return {heif_error_Plugin_loading_error, + heif_suberror_Unsupported_plugin_version, + "Encoder plugin needs to be at least version " STRINGIFY(heif_encoder_plugin_latest_version) + }; + } + + if (encoder_plugin->plugin_api_version >= 4 && + encoder_plugin->minimum_required_libheif_version > heif_get_version_number()) { + return { + heif_error_Plugin_loading_error, + heif_suberror_Unsupported_plugin_version, + "Encoder plugin requires at least libheif version " LIBHEIF_VERSION + }; + } + struct heif_error err = heif_register_encoder_plugin(encoder_plugin); if (err.code) { return err; @@ -255,6 +274,22 @@ case heif_plugin_type_decoder: { auto* decoder_plugin = static_cast(plugin_info->plugin); + if (decoder_plugin->plugin_api_version < heif_decoder_plugin_minimum_version) { + return {heif_error_Plugin_loading_error, + heif_suberror_Unsupported_plugin_version, + "Decoder plugin needs to be at least version " STRINGIFY(heif_decoder_plugin_latest_version) + }; + } + + if (decoder_plugin->plugin_api_version >= 4 && + decoder_plugin->minimum_required_libheif_version > heif_get_version_number()) { + return { + heif_error_Plugin_loading_error, + heif_suberror_Unsupported_plugin_version, + "Decoder plugin requires at least libheif version " LIBHEIF_VERSION + }; + } + struct heif_error err = heif_register_decoder_plugin(decoder_plugin); if (err.code) { return err; @@ -320,6 +355,10 @@ int nPlugins = 0; + // Loading the plugins may return several errors, but we can only return one of them, + // which is remembered here. + heif_error err_result = heif_error_ok; + for (const auto& filename : libraryFiles) { const struct heif_plugin_info* info = nullptr; auto err = heif_load_plugin(filename.c_str(), &info); @@ -334,6 +373,10 @@ nPlugins++; } + else { + // remember error + err_result = err; + } } if (nPlugins < output_array_size && out_plugins) { @@ -344,7 +387,7 @@ *out_nPluginsLoaded = nPlugins; } - return heif_error_ok; + return err_result; } #else diff -Nru libheif-1.19.8/libheif/init.h libheif-1.23.4/libheif/init.h --- libheif-1.19.8/libheif/init.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/init.h 2026-09-06 14:45:17.000000000 +0000 @@ -38,11 +38,11 @@ class PluginLibrary { public: - virtual struct heif_error load_from_file(const char*) = 0; + virtual heif_error load_from_file(const char*) = 0; virtual void release() = 0; - virtual struct heif_plugin_info* get_plugin_info() = 0; + virtual heif_plugin_info* get_plugin_info() = 0; }; diff -Nru libheif-1.19.8/libheif/mdat_data.h libheif-1.23.4/libheif/mdat_data.h --- libheif-1.19.8/libheif/mdat_data.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/mdat_data.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,65 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_MDAT_DATA_H +#define LIBHEIF_MDAT_DATA_H + +#include +#include +#include +#include + + +class MdatData +{ +public: + virtual ~MdatData() = default; + + // returns the start position of the appended data + virtual size_t append_data(const std::vector& data) = 0; + + virtual size_t get_data_size() const = 0; + + virtual Error write(StreamWriter&) = 0; +}; + + +class MdatData_Memory : public MdatData +{ +public: + size_t append_data(const std::vector& data) override { + size_t startPos = m_data.size(); + m_data.insert(m_data.end(), data.begin(), data.end()); + return startPos; + } + + size_t get_data_size() const override { return m_data.size(); } + + Error write(StreamWriter& writer) override + { + writer.write(m_data); + return Error::Ok; + } + +private: + std::vector m_data; +}; + +#endif //LIBHEIF_MDAT_DATA_H diff -Nru libheif-1.19.8/libheif/mini.cc libheif-1.23.4/libheif/mini.cc --- libheif-1.19.8/libheif/mini.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/mini.cc 2026-09-06 14:45:17.000000000 +0000 @@ -1,6 +1,7 @@ /* * HEIF codec. * Copyright (c) 2024 Brad Hards + * Copyright (c) 2026 Dirk Farin * * This file is part of libheif. * @@ -20,12 +21,18 @@ #include "mini.h" #include "file.h" +#include "nclx.h" +#include "security_limits.h" #include "codecs/avif_boxes.h" #include "codecs/hevc_boxes.h" +#include +#include #include #include +#include #include +#include #include #include #include @@ -35,9 +42,22 @@ { uint64_t start_offset = range.get_istream()->get_position(); std::size_t length = range.get_remaining_bytes(); + + // Register the payload allocation with the total-memory tracker (also + // checks against max_memory_block_size). The buffer is local to parse(), + // so use a scoped handle that releases the budget when parse() returns. + MemoryHandle mini_data_handle; + if (auto err = mini_data_handle.alloc(length, limits, "MinimizedImageBox payload")) { + return err; + } + std::vector mini_data(length); - range.read(mini_data.data(), mini_data.size()); - BitReader bits(mini_data.data(), (int)(mini_data.size())); + if (!range.read(mini_data.data(), mini_data.size())) { + return range.get_error(); + } + + BitReader bits(mini_data.data(), mini_data.size()); + m_version = bits.get_bits8(2); m_explicit_codec_types_flag = bits.get_flag(); m_float_flag = bits.get_flag(); @@ -50,38 +70,38 @@ m_xmp_flag = bits.get_flag(); m_chroma_subsampling = bits.get_bits8(2); m_orientation = bits.get_bits8(3) + 1; - bool small_dimensions_flag = bits.get_flag(); - if (small_dimensions_flag) - { - m_width = 1 + bits.get_bits32(7); - m_height = 1 + bits.get_bits32(7); - } - else - { - m_width = 1 + bits.get_bits32(15); - m_height = 1 + bits.get_bits32(15); - } + + bool large_dimensions_flag = bits.get_flag(); + // large_dimensions_flag = !large_dimensions_flag; // HACK to get old behavior + m_width = 1 + bits.get_bits32(large_dimensions_flag ? 15 : 7); + m_height = 1 + bits.get_bits32(large_dimensions_flag ? 15 : 7); + if ((m_chroma_subsampling == 1) || (m_chroma_subsampling == 2)) { - m_chroma_is_horizontally_centred = bits.get_flag(); + m_chroma_is_horizontally_centered = bits.get_flag(); } if (m_chroma_subsampling == 1) { - m_chroma_is_vertically_centred = bits.get_flag(); + m_chroma_is_vertically_centered = bits.get_flag(); } bool high_bit_depth_flag = false; if (m_float_flag) { - uint8_t bit_depth_log2_minus4 = bits.get_bits8(2); - m_bit_depth = (uint8_t)powl(2, (bit_depth_log2_minus4 + 4)); + uint8_t bit_depth_log2 = bits.get_bits8(2) + 4; // [4;6] (7 is reserved) + if (bit_depth_log2 > 6) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Reserved float bit_depth_log2 value 7 in MinimizedImageBox"}; + } + m_bit_depth = (uint8_t)powl(2, (bit_depth_log2)); // [16,32,64] } else { high_bit_depth_flag = bits.get_flag(); if (high_bit_depth_flag) { - m_bit_depth = 9 + bits.get_bits8(3); + m_bit_depth = bits.get_bits8(3) + 9; // [9;16] } } @@ -92,16 +112,13 @@ if (m_explicit_cicp_flag) { + // Per ISO/IEC 23008-12 Annex O.3.2, matrix_coefficients is always + // present (8 bits) when explicit_cicp_flag is set, irrespective of + // chroma_subsampling. The chroma_subsampling==0 ⇒ MC=2 default is + // only used in the non-explicit branch (Table O.3, row 1). m_colour_primaries = bits.get_bits8(8); m_transfer_characteristics = bits.get_bits8(8); - if (m_chroma_subsampling != 0) - { - m_matrix_coefficients = bits.get_bits8(8); - } - else - { - m_matrix_coefficients = 2; - } + m_matrix_coefficients = bits.get_bits8(8); } else { @@ -115,15 +132,22 @@ m_infe_type = bits.get_bits32(32); m_codec_config_type = bits.get_bits32(32); } + if (m_hdr_flag) { m_gainmap_flag = bits.get_flag(); if (m_gainmap_flag) { - uint32_t gainmap_width_minus1 = bits.get_bits32(small_dimensions_flag ? 7 : 15); - m_gainmap_width = gainmap_width_minus1 + 1; - uint32_t gainmap_height_minus1 = bits.get_bits32(small_dimensions_flag ? 7 : 15); - m_gainmap_height = gainmap_height_minus1 + 1; + bool gainmap_dimension_same_as_main_item_flag = bits.get_flag(); + if (gainmap_dimension_same_as_main_item_flag) { + m_gainmap_width = m_width; + m_gainmap_height = m_height; + } + else { + m_gainmap_width = bits.get_bits32(large_dimensions_flag ? 15 : 7) + 1; + m_gainmap_height = bits.get_bits32(large_dimensions_flag ? 15 : 7) + 1; + } + m_gainmap_matrix_coefficients = bits.get_bits8(8); m_gainmap_full_range_flag = bits.get_flag(); m_gainmap_chroma_subsampling = bits.get_bits8(2); @@ -135,13 +159,19 @@ { m_gainmap_chroma_is_vertically_centred = bits.get_flag(); } + m_gainmap_float_flag = bits.get_flag(); bool gainmap_high_bit_depth_flag = false; if (m_gainmap_float_flag) { - uint8_t bit_depth_log2_minus4 = bits.get_bits8(2); - m_gainmap_bit_depth = (uint8_t)powl(2, (bit_depth_log2_minus4 + 4)); + uint8_t bit_depth_log2 = bits.get_bits8(2) + 4; + if (bit_depth_log2 > 6) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Reserved float gainmap bit_depth_log2 value 7 in MinimizedImageBox"}; + } + m_gainmap_bit_depth = (uint8_t)powl(2, (bit_depth_log2)); } else { @@ -151,6 +181,7 @@ m_gainmap_bit_depth = 9 + bits.get_bits8(3); } } + m_tmap_icc_flag = bits.get_flag(); m_tmap_explicit_cicp_flag = bits.get_flag(); if (m_tmap_explicit_cicp_flag) @@ -174,7 +205,7 @@ bool cclv_flag = bits.get_flag(); bool amve_flag = bits.get_flag(); m_reve_flag = bits.get_flag(); - m_ndwt_flag = bits.get_flag(); + bool ndwt_flag = bits.get_flag(); if (clli_flag) { @@ -250,10 +281,10 @@ bits.skip_bits(16); } - if (m_ndwt_flag) + if (ndwt_flag) { - // TODO: NominalDiffuseWhite isn't published yet - bits.skip_bits(32); + m_ndwt = std::make_shared(); + m_ndwt->set_diffuse_white_luminance(bits.get_bits32(32)); } if (m_gainmap_flag) @@ -263,7 +294,7 @@ bool tmap_cclv_flag = bits.get_flag(); bool tmap_amve_flag = bits.get_flag(); m_tmap_reve_flag = bits.get_flag(); - m_tmap_ndwt_flag = bits.get_flag(); + bool tmap_ndwt_flag = bits.get_flag(); if (tmap_clli_flag) { @@ -339,108 +370,178 @@ bits.skip_bits(16); } - if (m_tmap_ndwt_flag) + if (tmap_ndwt_flag) { - // TODO: NominalDiffuseWhite isn't published yet - bits.skip_bits(32); + m_tmap_ndwt = std::make_shared(); + m_tmap_ndwt->set_diffuse_white_luminance(bits.get_bits32(32)); } } } // Chunk sizes - bool few_metadata_bytes_flag = false; + bool large_metadata_flag = false; if (m_icc_flag || m_exif_flag || m_xmp_flag || (m_hdr_flag && m_gainmap_flag)) { - few_metadata_bytes_flag = bits.get_flag(); + large_metadata_flag = bits.get_flag(); } - bool few_codec_config_bytes_flag = bits.get_flag(); - bool few_item_data_bytes_flag = bits.get_flag(); + + bool large_codec_config_flag = bits.get_flag(); + bool large_item_data_flag = bits.get_flag(); uint32_t icc_data_size = 0; if (m_icc_flag) { - icc_data_size = bits.get_bits32(few_metadata_bytes_flag ? 10 : 20) + 1; + icc_data_size = bits.get_bits32(large_metadata_flag ? 20 : 10) + 1; } + uint32_t tmap_icc_data_size = 0; if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) { - tmap_icc_data_size = bits.get_bits32(few_metadata_bytes_flag ? 10 : 20) + 1; + tmap_icc_data_size = bits.get_bits32(large_metadata_flag ? 20 : 10) + 1; } + uint32_t gainmap_metadata_size = 0; if (m_hdr_flag && m_gainmap_flag) { - gainmap_metadata_size = bits.get_bits32(few_metadata_bytes_flag ? 10 : 20); + gainmap_metadata_size = bits.get_bits32(large_metadata_flag ? 20 : 10); } + if (m_hdr_flag && m_gainmap_flag) { - m_gainmap_item_data_size = bits.get_bits32(few_item_data_bytes_flag ? 15 : 28); + m_gainmap_item_data_size = bits.get_bits32(large_item_data_flag ? 28 : 15); } + uint32_t gainmap_item_codec_config_size = 0; - if (m_hdr_flag && m_gainmap_flag && (m_gainmap_item_data_size > 0)) + if (m_hdr_flag && m_gainmap_flag && m_gainmap_item_data_size > 0) { - gainmap_item_codec_config_size = bits.get_bits32(few_codec_config_bytes_flag ? 3 : 12); + gainmap_item_codec_config_size = bits.get_bits32(large_codec_config_flag ? 12 : 3); } - uint32_t main_item_codec_config_size = bits.get_bits32(few_codec_config_bytes_flag ? 3 : 12); - m_main_item_data_size = bits.get_bits32(few_item_data_bytes_flag ? 15 : 28) + 1; + uint32_t main_item_codec_config_size = bits.get_bits32(large_codec_config_flag ? 12 : 3); + m_main_item_data_size = bits.get_bits32(large_item_data_flag ? 28 : 15) + 1; if (m_alpha_flag) { - m_alpha_item_data_size = bits.get_bits32(few_item_data_bytes_flag ? 15 : 28); + m_alpha_item_data_size = bits.get_bits32(large_item_data_flag ? 28 : 15); } + uint32_t alpha_item_codec_config_size = 0; - if (m_alpha_flag && (m_alpha_item_data_size > 0)) + if (m_alpha_flag && m_alpha_item_data_size > 0) { - alpha_item_codec_config_size = bits.get_bits32(few_codec_config_bytes_flag ? 3 : 12); + alpha_item_codec_config_size = bits.get_bits32(large_codec_config_flag ? 12 : 3); + } + + if (m_exif_flag || m_xmp_flag) + { + m_exif_xmp_compressed_flag = bits.get_flag(); } if (m_exif_flag) { - m_exif_item_data_size = bits.get_bits32(few_metadata_bytes_flag ? 10 : 20) + 1; + m_exif_data_size = bits.get_bits32(large_metadata_flag ? 20 : 10) + 1; } if (m_xmp_flag) { - m_xmp_item_data_size = bits.get_bits32(few_metadata_bytes_flag ? 10 : 20) + 1; + m_xmp_data_size = bits.get_bits32(large_metadata_flag ? 20 : 10) + 1; } bits.skip_to_byte_boundary(); - // Chunks - if (m_alpha_flag && (m_alpha_item_data_size > 0) && (alpha_item_codec_config_size > 0)) - { - m_alpha_item_codec_config = bits.read_bytes(alpha_item_codec_config_size); + // Validate that the declared chunk sizes don't exceed the remaining payload. + // Without this, a malformed mini box with 28-bit *_item_data_size fields set + // near 2^28 makes skip_bytes/read_bytes loop hundreds of millions of times + // past EOF, eventually triggering signed-int overflow in BitReader::refill(). + { + uint64_t required_bytes = (uint64_t) main_item_codec_config_size + m_main_item_data_size; + if (m_alpha_flag && m_alpha_item_data_size > 0) { + required_bytes += alpha_item_codec_config_size; + required_bytes += m_alpha_item_data_size; + } + if (m_hdr_flag && m_gainmap_flag && m_gainmap_item_data_size > 0) { + required_bytes += gainmap_item_codec_config_size; + required_bytes += m_gainmap_item_data_size; + } + if (m_icc_flag) required_bytes += icc_data_size; + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) required_bytes += tmap_icc_data_size; + if (m_hdr_flag && m_gainmap_flag) required_bytes += gainmap_metadata_size; + if (m_exif_flag) required_bytes += m_exif_data_size; + if (m_xmp_flag) required_bytes += m_xmp_data_size; + + int64_t remaining_bits = bits.get_bits_remaining(); + if (remaining_bits < 0 || required_bytes > (uint64_t) remaining_bits / 8) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Declared chunk sizes in MinimizedImageBox exceed available payload."}; + } } - if (m_hdr_flag && m_gainmap_flag && (gainmap_item_codec_config_size > 0)) - { - m_gainmap_item_codec_config = bits.read_bytes(gainmap_item_codec_config_size); + + // Enforce the color-profile size limit on embedded ICC blobs, matching + // the check applied to regular 'colr' boxes in nclx.cc. + if (limits && limits->max_color_profile_size) { + if (m_icc_flag && icc_data_size > limits->max_color_profile_size) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "ICC color profile in MinimizedImageBox exceeds maximum supported size"}; + } + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag && + tmap_icc_data_size > limits->max_color_profile_size) { + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + "Tone-map ICC color profile in MinimizedImageBox exceeds maximum supported size"}; + } } + if (main_item_codec_config_size > 0) { m_main_item_codec_config = bits.read_bytes(main_item_codec_config_size); } + // Chunks + if (m_alpha_flag && m_alpha_item_data_size > 0) { + if (alpha_item_codec_config_size == 0) { + m_alpha_item_codec_config = m_main_item_codec_config; + } + else { + // TODO: should we have a flag indicating that we have explicit config for alpha? + m_alpha_item_codec_config = bits.read_bytes(alpha_item_codec_config_size); + } + } + + if (m_hdr_flag && m_gainmap_flag && m_gainmap_item_data_size > 0) + { + if (gainmap_item_codec_config_size == 0) { + m_gainmap_item_codec_config = m_main_item_codec_config; + } + else { + // TODO: should we have a flag indicating that we have explicit config for the gain map? + m_gainmap_item_codec_config = bits.read_bytes(gainmap_item_codec_config_size); + } + } + if (m_icc_flag) { m_icc_data = bits.read_bytes(icc_data_size); } + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) { m_tmap_icc_data = bits.read_bytes(tmap_icc_data_size); } - if (m_hdr_flag && m_gainmap_flag && (gainmap_metadata_size > 0)) + + if (m_hdr_flag && m_gainmap_flag && gainmap_metadata_size > 0) { m_gainmap_metadata = bits.read_bytes(gainmap_metadata_size); } - if (m_alpha_flag && (m_alpha_item_data_size > 0)) + if (m_alpha_flag && m_alpha_item_data_size > 0) { m_alpha_item_data_offset = bits.get_current_byte_index() + start_offset; bits.skip_bytes(m_alpha_item_data_size); } - if (m_alpha_flag && m_gainmap_flag && (m_gainmap_item_data_size > 0)) + if (m_hdr_flag && m_gainmap_flag && m_gainmap_item_data_size > 0) { m_gainmap_item_data_offset = bits.get_current_byte_index() + start_offset; - bits.skip_bits(m_gainmap_item_data_size); + bits.skip_bytes(m_gainmap_item_data_size); } m_main_item_data_offset = bits.get_current_byte_index() + start_offset; @@ -449,13 +550,14 @@ if (m_exif_flag) { m_exif_item_data_offset = bits.get_current_byte_index() + start_offset; - bits.skip_bytes(m_exif_item_data_size); + bits.skip_bytes(m_exif_data_size); } if (m_xmp_flag) { m_xmp_item_data_offset = bits.get_current_byte_index() + start_offset; - bits.skip_bytes(m_xmp_item_data_size); + bits.skip_bytes(m_xmp_data_size); } + return range.get_error(); } @@ -483,11 +585,11 @@ if ((m_chroma_subsampling == 1) || (m_chroma_subsampling == 2)) { - sstr << indent << "chroma_is_horizontally_centered: " << m_chroma_is_horizontally_centred << "\n"; + sstr << indent << "chroma_is_horizontally_centered: " << m_chroma_is_horizontally_centered << "\n"; } if (m_chroma_subsampling == 1) { - sstr << indent << "chroma_is_vertically_centered: " << m_chroma_is_vertically_centred << "\n"; + sstr << indent << "chroma_is_vertically_centered: " << m_chroma_is_vertically_centered << "\n"; } sstr << "bit_depth: " << (int)m_bit_depth << "\n"; @@ -603,15 +705,17 @@ } sstr << indent << "reve_flag: " << m_reve_flag << "\n"; - sstr << indent << "ndwt_flag: " << m_ndwt_flag << "\n"; if (m_reve_flag) { // TODO - this isn't published yet } - if (m_ndwt_flag) + if (m_ndwt) { - // TODO - this isn't published yet + sstr << indent << "ndwt.diffuse_white_luminance: " << m_ndwt->get_diffuse_white_luminance() << "\n"; + } + else { + sstr << indent << "ndwt: ---\n"; } if (m_gainmap_flag) @@ -681,15 +785,17 @@ } sstr << indent << "tmap_reve_flag: " << m_tmap_reve_flag << "\n"; - sstr << indent << "tmap_ndwt_flag: " << m_tmap_ndwt_flag << "\n"; if (m_tmap_reve_flag) { // TODO - this isn't published yet } - if (m_tmap_ndwt_flag) + if (m_tmap_ndwt) { - // TODO - this isn't published yet + sstr << indent << "tmap_ndwt.diffuse_white_luminance: " << m_tmap_ndwt->get_diffuse_white_luminance() << "\n"; + } + else { + sstr << indent << "tmap_ndwt: ---\n"; } } } @@ -733,16 +839,442 @@ if (m_exif_flag) { - sstr << "exif_data offset: " << m_exif_item_data_offset << ", size: " << m_exif_item_data_size << "\n"; + sstr << "exif_data offset: " << m_exif_item_data_offset << ", size: " << m_exif_data_size << "\n"; } if (m_xmp_flag) { - sstr << "xmp_data offset: " << m_xmp_item_data_offset << ", size: " << m_xmp_item_data_size << "\n"; + sstr << "xmp_data offset: " << m_xmp_item_data_offset << ", size: " << m_xmp_data_size << "\n"; } return sstr.str(); } +static void write_cclv_to_bits(BitWriter& bits, const Box_cclv& cclv) +{ + bool primaries_present = cclv.ccv_primaries_are_valid(); + bool min_lum_present = cclv.min_luminance_is_valid(); + bool max_lum_present = cclv.max_luminance_is_valid(); + bool avg_lum_present = cclv.avg_luminance_is_valid(); + + bits.write_bits(0, 2); // ccv_cancel_flag, ccv_persistence_flag + bits.write_flag(primaries_present); + bits.write_flag(min_lum_present); + bits.write_flag(max_lum_present); + bits.write_flag(avg_lum_present); + bits.write_bits(0, 2); // reserved + + if (primaries_present) { + bits.write_bits32s(cclv.get_ccv_primary_x0()); + bits.write_bits32s(cclv.get_ccv_primary_y0()); + bits.write_bits32s(cclv.get_ccv_primary_x1()); + bits.write_bits32s(cclv.get_ccv_primary_y1()); + bits.write_bits32s(cclv.get_ccv_primary_x2()); + bits.write_bits32s(cclv.get_ccv_primary_y2()); + } + if (min_lum_present) { + bits.write_bits32(cclv.get_min_luminance(), 32); + } + if (max_lum_present) { + bits.write_bits32(cclv.get_max_luminance(), 32); + } + if (avg_lum_present) { + bits.write_bits32(cclv.get_avg_luminance(), 32); + } +} + + +Error Box_mini::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + BitWriter bits; + + // --- Bit-packed header --- + + // First byte: version(2) + 6 flags + bits.write_bits8(m_version, 2); + bits.write_flag(m_explicit_codec_types_flag); + bits.write_flag(m_float_flag); + bits.write_flag(m_full_range_flag); + bits.write_flag(m_alpha_flag); + bits.write_flag(m_explicit_cicp_flag); + bits.write_flag(m_hdr_flag); + + // Second byte area: icc(1), exif(1), xmp(1), chroma_subsampling(2), orientation(3) + bits.write_flag(m_icc_flag); + bits.write_flag(m_exif_flag); + bits.write_flag(m_xmp_flag); + bits.write_bits8(m_chroma_subsampling, 2); + assert(m_orientation >= 1 && m_orientation <= 8); + bits.write_bits8(m_orientation - 1, 3); + + // Dimensions + bool large_dimensions_flag = (m_width > 128) || (m_height > 128); + bits.write_flag(large_dimensions_flag); + bits.write_bits32(m_width - 1, large_dimensions_flag ? 15 : 7); + bits.write_bits32(m_height - 1, large_dimensions_flag ? 15 : 7); + + // Chroma centering + if (m_chroma_subsampling == 1 || m_chroma_subsampling == 2) { + bits.write_flag(m_chroma_is_horizontally_centered); + } + if (m_chroma_subsampling == 1) { + bits.write_flag(m_chroma_is_vertically_centered); + } + + // Bit depth + if (m_float_flag) { + // bit_depth_log2 = log2(m_bit_depth), stored as (log2 - 4) + uint8_t bit_depth_log2; + switch (m_bit_depth) { + case 16: bit_depth_log2 = 4; break; + case 32: bit_depth_log2 = 5; break; + case 64: bit_depth_log2 = 6; break; + case 128: bit_depth_log2 = 7; break; + default: bit_depth_log2 = 4; break; // fallback + } + bits.write_bits8(bit_depth_log2 - 4, 2); + } + else { + bool high_bit_depth_flag = (m_bit_depth > 8); + bits.write_flag(high_bit_depth_flag); + if (high_bit_depth_flag) { + bits.write_bits8(m_bit_depth - 9, 3); + } + } + + // Alpha premultiplied + if (m_alpha_flag) { + bits.write_flag(m_alpha_is_premultiplied); + } + + // CICP + if (m_explicit_cicp_flag) { + // matrix_coefficients is always 8 bits in the explicit branch + // (ISO/IEC 23008-12 Annex O.3.2). + bits.write_bits8(static_cast(m_colour_primaries), 8); + bits.write_bits8(static_cast(m_transfer_characteristics), 8); + bits.write_bits8(static_cast(m_matrix_coefficients), 8); + } + + // Explicit codec types + if (m_explicit_codec_types_flag) { + bits.write_bits32(m_infe_type, 32); + bits.write_bits32(m_codec_config_type, 32); + } + + // --- HDR block --- + if (m_hdr_flag) { + bits.write_flag(m_gainmap_flag); + + if (m_gainmap_flag) { + bool gainmap_dimension_same_as_main_item_flag = + (m_gainmap_width == m_width && m_gainmap_height == m_height); + bits.write_flag(gainmap_dimension_same_as_main_item_flag); + if (!gainmap_dimension_same_as_main_item_flag) { + bits.write_bits32(m_gainmap_width - 1, large_dimensions_flag ? 15 : 7); + bits.write_bits32(m_gainmap_height - 1, large_dimensions_flag ? 15 : 7); + } + bits.write_bits8(m_gainmap_matrix_coefficients, 8); + bits.write_flag(m_gainmap_full_range_flag); + bits.write_bits8(m_gainmap_chroma_subsampling, 2); + + if (m_gainmap_chroma_subsampling == 1 || m_gainmap_chroma_subsampling == 2) { + bits.write_flag(m_gainmap_chroma_is_horizontally_centred); + } + if (m_gainmap_chroma_subsampling == 1) { + bits.write_flag(m_gainmap_chroma_is_vertically_centred); + } + + bits.write_flag(m_gainmap_float_flag); + + if (m_gainmap_float_flag) { + uint8_t gm_bit_depth_log2; + switch (m_gainmap_bit_depth) { + case 16: gm_bit_depth_log2 = 4; break; + case 32: gm_bit_depth_log2 = 5; break; + case 64: gm_bit_depth_log2 = 6; break; + case 128: gm_bit_depth_log2 = 7; break; + default: gm_bit_depth_log2 = 4; break; + } + bits.write_bits8(gm_bit_depth_log2 - 4, 2); + } + else { + bool gainmap_high_bit_depth_flag = (m_gainmap_bit_depth > 8); + bits.write_flag(gainmap_high_bit_depth_flag); + if (gainmap_high_bit_depth_flag) { + bits.write_bits8(m_gainmap_bit_depth - 9, 3); + } + } + + bits.write_flag(m_tmap_icc_flag); + bits.write_flag(m_tmap_explicit_cicp_flag); + if (m_tmap_explicit_cicp_flag) { + bits.write_bits8(static_cast(m_tmap_colour_primaries), 8); + bits.write_bits8(static_cast(m_tmap_transfer_characteristics), 8); + bits.write_bits8(static_cast(m_tmap_matrix_coefficients), 8); + bits.write_flag(m_tmap_full_range_flag); + } + } + + // HDR metadata flags + bits.write_flag(m_clli != nullptr); + bits.write_flag(m_mdcv != nullptr); + bits.write_flag(m_cclv != nullptr); + bits.write_flag(m_amve != nullptr); + bits.write_flag(m_reve_flag); + bits.write_flag(m_ndwt != nullptr); + + if (m_clli) { + bits.write_bits16(m_clli->clli.max_content_light_level, 16); + bits.write_bits16(m_clli->clli.max_pic_average_light_level, 16); + } + + if (m_mdcv) { + for (int c = 0; c < 3; c++) { + bits.write_bits16(m_mdcv->mdcv.display_primaries_x[c], 16); + bits.write_bits16(m_mdcv->mdcv.display_primaries_y[c], 16); + } + bits.write_bits16(m_mdcv->mdcv.white_point_x, 16); + bits.write_bits16(m_mdcv->mdcv.white_point_y, 16); + bits.write_bits32(m_mdcv->mdcv.max_display_mastering_luminance, 32); + bits.write_bits32(m_mdcv->mdcv.min_display_mastering_luminance, 32); + } + + if (m_cclv) { + write_cclv_to_bits(bits, *m_cclv); + } + + if (m_amve) { + bits.write_bits32(m_amve->amve.ambient_illumination, 32); + bits.write_bits16(m_amve->amve.ambient_light_x, 16); + bits.write_bits16(m_amve->amve.ambient_light_y, 16); + } + + if (m_reve_flag) { + // TODO: ReferenceViewingEnvironment isn't published yet — write zeros + bits.write_bits32(0, 32); + bits.write_bits16(0, 16); + bits.write_bits16(0, 16); + bits.write_bits32(0, 32); + bits.write_bits16(0, 16); + bits.write_bits16(0, 16); + } + + if (m_ndwt) { + bits.write_bits32(m_ndwt->get_diffuse_white_luminance(), 32); + } + + // Tmap HDR metadata (if gainmap) + if (m_gainmap_flag) { + bits.write_flag(m_tmap_clli != nullptr); + bits.write_flag(m_tmap_mdcv != nullptr); + bits.write_flag(m_tmap_cclv != nullptr); + bits.write_flag(m_tmap_amve != nullptr); + bits.write_flag(m_tmap_reve_flag); + bits.write_flag(m_tmap_ndwt != nullptr); + + if (m_tmap_clli) { + bits.write_bits16(m_tmap_clli->clli.max_content_light_level, 16); + bits.write_bits16(m_tmap_clli->clli.max_pic_average_light_level, 16); + } + + if (m_tmap_mdcv) { + for (int c = 0; c < 3; c++) { + bits.write_bits16(m_tmap_mdcv->mdcv.display_primaries_x[c], 16); + bits.write_bits16(m_tmap_mdcv->mdcv.display_primaries_y[c], 16); + } + bits.write_bits16(m_tmap_mdcv->mdcv.white_point_x, 16); + bits.write_bits16(m_tmap_mdcv->mdcv.white_point_y, 16); + bits.write_bits32(m_tmap_mdcv->mdcv.max_display_mastering_luminance, 32); + bits.write_bits32(m_tmap_mdcv->mdcv.min_display_mastering_luminance, 32); + } + + if (m_tmap_cclv) { + write_cclv_to_bits(bits, *m_tmap_cclv); + } + + if (m_tmap_amve) { + bits.write_bits32(m_tmap_amve->amve.ambient_illumination, 32); + bits.write_bits16(m_tmap_amve->amve.ambient_light_x, 16); + bits.write_bits16(m_tmap_amve->amve.ambient_light_y, 16); + } + + if (m_tmap_reve_flag) { + bits.write_bits32(0, 32); + bits.write_bits16(0, 16); + bits.write_bits16(0, 16); + bits.write_bits32(0, 32); + bits.write_bits16(0, 16); + bits.write_bits16(0, 16); + } + + if (m_tmap_ndwt) { + bits.write_bits32(m_tmap_ndwt->get_diffuse_white_luminance(), 32); + } + } + } + + // --- Size fields --- + + // Determine actual data sizes for write path + uint32_t icc_data_size = static_cast(m_icc_data.size()); + uint32_t tmap_icc_data_size = static_cast(m_tmap_icc_data.size()); + uint32_t gainmap_metadata_size = static_cast(m_gainmap_metadata.size()); + uint32_t main_item_data_size = static_cast(m_main_item_data.size()); + uint32_t alpha_item_data_size = static_cast(m_alpha_item_data.size()); + uint32_t gainmap_item_data_size = static_cast(m_gainmap_item_data.size()); + uint32_t exif_data_size = static_cast(m_exif_data_bytes.size()); + uint32_t xmp_data_size = static_cast(m_xmp_data_bytes.size()); + + uint32_t main_item_codec_config_size = static_cast(m_main_item_codec_config.size()); + uint32_t alpha_item_codec_config_size = 0; + if (m_alpha_flag && alpha_item_data_size > 0) { + // If alpha codec config differs from main, we need to write it separately + if (m_alpha_item_codec_config != m_main_item_codec_config) { + alpha_item_codec_config_size = static_cast(m_alpha_item_codec_config.size()); + } + // else size stays 0, meaning "reuse main config" + } + uint32_t gainmap_item_codec_config_size = 0; + if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) { + if (m_gainmap_item_codec_config != m_main_item_codec_config) { + gainmap_item_codec_config_size = static_cast(m_gainmap_item_codec_config.size()); + } + } + + // Compute "large" flags based on actual sizes + bool large_metadata_flag = false; + if (m_icc_flag || m_exif_flag || m_xmp_flag || (m_hdr_flag && m_gainmap_flag)) { + // Check if any metadata size exceeds 10-bit capacity + // ICC/exif/xmp store (size-1), max representable size with 10 bits = 1024 + // gainmap_metadata/tmap_icc store raw or (size-1), same limit + uint32_t max_meta = 0; + if (m_icc_flag) max_meta = std::max(max_meta, icc_data_size); + if (m_exif_flag) max_meta = std::max(max_meta, exif_data_size); + if (m_xmp_flag) max_meta = std::max(max_meta, xmp_data_size); + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) max_meta = std::max(max_meta, tmap_icc_data_size); + if (m_hdr_flag && m_gainmap_flag) max_meta = std::max(max_meta, gainmap_metadata_size + 1); // gainmap_metadata is raw, others are size-1 + large_metadata_flag = (max_meta > 1024); + + bits.write_flag(large_metadata_flag); + } + + bool large_codec_config_flag = (main_item_codec_config_size > 7 || + alpha_item_codec_config_size > 7 || + gainmap_item_codec_config_size > 7); + bits.write_flag(large_codec_config_flag); + + bool large_item_data_flag = (main_item_data_size > 32768 || // main stores size-1, max representable = 32768 + alpha_item_data_size > 32767 || + gainmap_item_data_size > 32767); + bits.write_flag(large_item_data_flag); + + // Write size fields in parse order + if (m_icc_flag) { + bits.write_bits32(icc_data_size - 1, large_metadata_flag ? 20 : 10); + } + + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) { + bits.write_bits32(tmap_icc_data_size - 1, large_metadata_flag ? 20 : 10); + } + + if (m_hdr_flag && m_gainmap_flag) { + bits.write_bits32(gainmap_metadata_size, large_metadata_flag ? 20 : 10); + } + + if (m_hdr_flag && m_gainmap_flag) { + bits.write_bits32(gainmap_item_data_size, large_item_data_flag ? 28 : 15); + } + + if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) { + bits.write_bits32(gainmap_item_codec_config_size, large_codec_config_flag ? 12 : 3); + } + + bits.write_bits32(main_item_codec_config_size, large_codec_config_flag ? 12 : 3); + bits.write_bits32(main_item_data_size - 1, large_item_data_flag ? 28 : 15); + + if (m_alpha_flag) { + bits.write_bits32(alpha_item_data_size, large_item_data_flag ? 28 : 15); + } + + if (m_alpha_flag && alpha_item_data_size > 0) { + bits.write_bits32(alpha_item_codec_config_size, large_codec_config_flag ? 12 : 3); + } + + if (m_exif_flag || m_xmp_flag) { + bits.write_flag(m_exif_xmp_compressed_flag); + } + + if (m_exif_flag) { + bits.write_bits32(exif_data_size - 1, large_metadata_flag ? 20 : 10); + } + if (m_xmp_flag) { + bits.write_bits32(xmp_data_size - 1, large_metadata_flag ? 20 : 10); + } + + // --- Byte alignment --- + bits.skip_to_byte_boundary(); + + // --- Byte-aligned data blocks --- + + // Codec configs + if (main_item_codec_config_size > 0) { + bits.write_bytes(m_main_item_codec_config); + } + + if (m_alpha_flag && alpha_item_data_size > 0) { + if (alpha_item_codec_config_size > 0) { + bits.write_bytes(m_alpha_item_codec_config); + } + } + + if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) { + if (gainmap_item_codec_config_size > 0) { + bits.write_bytes(m_gainmap_item_codec_config); + } + } + + // ICC and metadata + if (m_icc_flag) { + bits.write_bytes(m_icc_data); + } + + if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) { + bits.write_bytes(m_tmap_icc_data); + } + + if (m_hdr_flag && m_gainmap_flag && gainmap_metadata_size > 0) { + bits.write_bytes(m_gainmap_metadata); + } + + // Image data (order: alpha, gainmap, main, exif, xmp) + if (m_alpha_flag && alpha_item_data_size > 0) { + bits.write_bytes(m_alpha_item_data); + } + + if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) { + bits.write_bytes(m_gainmap_item_data); + } + + bits.write_bytes(m_main_item_data); + + if (m_exif_flag) { + bits.write_bytes(m_exif_data_bytes); + } + + if (m_xmp_flag) { + bits.write_bytes(m_xmp_data_bytes); + } + + // Flush to StreamWriter + writer.write(bits.get_data()); + + prepend_header(writer, box_start); + return Error::Ok; +} + + static uint32_t get_item_type_for_brand(const heif_brand2 brand) { switch(brand) { @@ -756,6 +1288,44 @@ } +// Parse a codec config blob (raw av1C/hvcC payload, no box header) by wrapping +// it in a synthetic box header so Box::read can dispatch and m_size is set +// properly (av1C/hvcC parse refuses to run with unspecified box size). +static Error parse_codec_config_box(const std::vector& config_bytes, + uint32_t type_4cc, + std::shared_ptr* out_box) +{ + const size_t header_size = 8; + if (config_bytes.size() > std::numeric_limits::max() - header_size) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Codec config in MinimizedImageBox is too large"}; + } + + const size_t total_size = header_size + config_bytes.size(); + if (total_size > 0x7FFFFFFFu) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_mini_box, + "Codec config in MinimizedImageBox is too large"}; + } + + std::vector framed(total_size); + framed[0] = (uint8_t)((total_size >> 24) & 0xff); + framed[1] = (uint8_t)((total_size >> 16) & 0xff); + framed[2] = (uint8_t)((total_size >> 8) & 0xff); + framed[3] = (uint8_t)(total_size & 0xff); + framed[4] = (uint8_t)((type_4cc >> 24) & 0xff); + framed[5] = (uint8_t)((type_4cc >> 16) & 0xff); + framed[6] = (uint8_t)((type_4cc >> 8) & 0xff); + framed[7] = (uint8_t)(type_4cc & 0xff); + std::copy(config_bytes.begin(), config_bytes.end(), framed.begin() + header_size); + + auto istr = std::make_shared(framed.data(), framed.size(), false); + BitstreamRange range(istr, framed.size(), nullptr); + return Box::read(range, out_box, heif_get_global_security_limits()); +} + + Error Box_mini::create_expanded_boxes(class HeifFile* file) { file->init_meta_box(); @@ -771,7 +1341,13 @@ primary_infe_box->set_item_ID(1); // TODO: check explicit codec flag - uint32_t minor_version = file->get_ftyp_box()->get_minor_version(); + auto ftyp_box = file->get_ftyp_box(); + if (!ftyp_box) { + return {heif_error_Invalid_input, + heif_suberror_No_ftyp_box, + "MinimizedImageBox requires an ftyp box to identify the codec brand"}; + } + uint32_t minor_version = ftyp_box->get_minor_version(); heif_brand2 mini_brand = minor_version; uint32_t infe_type = get_item_type_for_brand(mini_brand); if (infe_type == 0) { @@ -800,6 +1376,9 @@ exif_infe_box->set_flags(1); exif_infe_box->set_item_ID(6); exif_infe_box->set_item_type_4cc(fourcc("Exif")); + if (m_exif_xmp_compressed_flag) { + exif_infe_box->set_content_encoding("deflate"); + } file->add_infe_box(6, exif_infe_box); } @@ -810,6 +1389,9 @@ xmp_infe_box->set_item_ID(7); xmp_infe_box->set_item_type_4cc(fourcc("mime")); xmp_infe_box->set_content_type("application/rdf+xml"); + if (m_exif_xmp_compressed_flag) { + xmp_infe_box->set_content_encoding("deflate"); + } file->add_infe_box(7, xmp_infe_box); } @@ -817,30 +1399,22 @@ file->set_ipco_box(ipco_box); if (get_main_item_codec_config().size() != 0) { - std::shared_ptr istr = std::make_shared( - get_main_item_codec_config().data(), - get_main_item_codec_config().size(), - false - ); - BitstreamRange codec_range(istr, get_main_item_codec_config().size(), nullptr); - - std::shared_ptr main_item_codec_prop; + uint32_t config_type; if (infe_type == fourcc("av01")) { - std::shared_ptr codec_prop = std::make_shared(); - codec_prop->parse(codec_range, heif_get_global_security_limits()); - main_item_codec_prop = std::move(codec_prop); + config_type = fourcc("av1C"); } else if (infe_type == fourcc("hvc1")) { - std::shared_ptr codec_prop = std::make_shared(); - codec_prop->parse(codec_range, heif_get_global_security_limits()); - main_item_codec_prop = std::move(codec_prop); + config_type = fourcc("hvcC"); } else { - // not found std::stringstream sstr; sstr << "Minimised file requires infe support for " << fourcc_to_string(infe_type) << " but this is not yet supported."; return Error(heif_error_Unsupported_filetype, heif_suberror_Unspecified, sstr.str()); } + std::shared_ptr main_item_codec_prop; + if (auto err = parse_codec_config_box(get_main_item_codec_config(), config_type, &main_item_codec_prop)) { + return err; + } ipco_box->append_child_box(main_item_codec_prop); // entry 1 } else { ipco_box->append_child_box(std::make_shared()); // placeholder for entry 1 @@ -860,11 +1434,12 @@ ipco_box->append_child_box(pixi); // entry 3 std::shared_ptr colr = std::make_shared(); - std::shared_ptr nclx = std::make_shared(); - nclx->set_colour_primaries(get_colour_primaries()); - nclx->set_transfer_characteristics(get_transfer_characteristics()); - nclx->set_matrix_coefficients(get_matrix_coefficients()); - nclx->set_full_range_flag(get_full_range_flag()); + nclx_profile colorProfile; + colorProfile.set_colour_primaries(get_colour_primaries()); + colorProfile.set_transfer_characteristics(get_transfer_characteristics()); + colorProfile.set_matrix_coefficients(get_matrix_coefficients()); + colorProfile.set_full_range_flag(get_full_range_flag()); + std::shared_ptr nclx = std::make_shared(colorProfile); colr->set_color_profile(nclx); ipco_box->append_child_box(colr); // entry 4 @@ -878,29 +1453,22 @@ } if (get_alpha_item_codec_config().size() != 0) { - std::shared_ptr istr = std::make_shared( - get_alpha_item_codec_config().data(), - get_alpha_item_codec_config().size(), - false - ); - BitstreamRange alpha_codec_range(istr, get_alpha_item_codec_config().size(), nullptr); - std::shared_ptr alpha_item_codec_prop; + uint32_t config_type; if (infe_type == fourcc("av01")) { - std::shared_ptr codec_prop = std::make_shared(); - codec_prop->parse(alpha_codec_range, heif_get_global_security_limits()); - alpha_item_codec_prop = std::move(codec_prop); + config_type = fourcc("av1C"); } else if (infe_type == fourcc("hvc1")) { - std::shared_ptr codec_prop = std::make_shared(); - codec_prop->parse(alpha_codec_range, heif_get_global_security_limits()); - alpha_item_codec_prop = std::move(codec_prop); + config_type = fourcc("hvcC"); } else { - // not found std::stringstream sstr; sstr << "Minimised file requires infe support for " << fourcc_to_string(infe_type) << " but this is not yet supported."; return Error(heif_error_Unsupported_filetype, heif_suberror_Unspecified, sstr.str()); } + std::shared_ptr alpha_item_codec_prop; + if (auto err = parse_codec_config_box(get_alpha_item_codec_config(), config_type, &alpha_item_codec_prop)) { + return err; + } ipco_box->append_child_box(alpha_item_codec_prop); // entry 6 } else { ipco_box->append_child_box(std::make_shared()); // placeholder for entry 6 @@ -917,32 +1485,31 @@ // TODO: replace this placeholder with pixi box version 1 once that is supported ipco_box->append_child_box(std::make_shared()); // placeholder for entry 8 - if (get_orientation() == 2) { - std::shared_ptr irot = std::make_shared(); - irot->set_rotation_ccw(2 * 90); - ipco_box->append_child_box(irot); // entry 9 - } else if ((get_orientation() == 4) || (get_orientation() == 6) || (get_orientation() == 7)) { - std::shared_ptr irot = std::make_shared(); - irot->set_rotation_ccw(1 * 90); - ipco_box->append_child_box(irot); // entry 9 - } else if (get_orientation() == 5) { - std::shared_ptr irot = std::make_shared(); - irot->set_rotation_ccw(3 * 90); - ipco_box->append_child_box(irot); // entry 9 - } else { - ipco_box->append_child_box(std::make_shared()); // placeholder for entry 9 + // HDR metadata: expand the parsed mini-bitstream fields into separate ipco + // property boxes so the standard property-walk machinery (and the public + // heif_image*_has/get_content_light_level / mastering_display_colour_volume + // / ambient_viewing_environment / nominal_diffuse_white APIs) sees them. + uint16_t hdr_clli_prop_index = 0; + uint16_t hdr_mdcv_prop_index = 0; + uint16_t hdr_cclv_prop_index = 0; + uint16_t hdr_amve_prop_index = 0; + uint16_t hdr_ndwt_prop_index = 0; + // append_child_box() returns the 0-based child index; ipma uses 1-based + // property indices. + if (m_clli) { + hdr_clli_prop_index = static_cast(ipco_box->append_child_box(m_clli) + 1); + } + if (m_mdcv) { + hdr_mdcv_prop_index = static_cast(ipco_box->append_child_box(m_mdcv) + 1); } - - if ((get_orientation() == 1) || (get_orientation() == 6)) { - std::shared_ptr imir = std::make_shared(); - imir->set_mirror_direction(heif_transform_mirror_direction_horizontal); - ipco_box->append_child_box(imir); // entry 10 - } else if ((get_orientation() == 3) || (get_orientation() == 4)) { - std::shared_ptr imir = std::make_shared(); - imir->set_mirror_direction(heif_transform_mirror_direction_vertical); - ipco_box->append_child_box(imir); // entry 10 - } else { - ipco_box->append_child_box(std::make_shared()); // placeholder for entry 10 + if (m_cclv) { + hdr_cclv_prop_index = static_cast(ipco_box->append_child_box(m_cclv) + 1); + } + if (m_amve) { + hdr_amve_prop_index = static_cast(ipco_box->append_child_box(m_amve) + 1); + } + if (m_ndwt) { + hdr_ndwt_prop_index = static_cast(ipco_box->append_child_box(m_ndwt) + 1); } auto ipma_box = std::make_shared(); @@ -952,18 +1519,46 @@ ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, uint16_t(3)}); ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{true, uint16_t(4)}); ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{true, uint16_t(5)}); - ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{true, uint16_t(9)}); - ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{true, uint16_t(10)}); if (get_alpha_item_data_size() != 0) { ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{true, uint16_t(6)}); ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{false, uint16_t(2)}); ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{true, uint16_t(7)}); ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{false, uint16_t(8)}); - ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{true, uint16_t(9)}); - ipma_box->add_property_for_item_ID(2, Box_ipma::PropertyAssociation{true, uint16_t(10)}); } - // TODO: will need more once we support HDR / gainmap representation + + // Associate HDR-metadata properties with the primary item (non-essential — + // matches what ImageItem::set_clli/set_mdcv/set_amve/set_ndwt emit on + // the write path). + if (hdr_clli_prop_index) { + ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, hdr_clli_prop_index}); + } + if (hdr_mdcv_prop_index) { + ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, hdr_mdcv_prop_index}); + } + if (hdr_cclv_prop_index) { + ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, hdr_cclv_prop_index}); + } + if (hdr_amve_prop_index) { + ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, hdr_amve_prop_index}); + } + if (hdr_ndwt_prop_index) { + ipma_box->add_property_for_item_ID(1, Box_ipma::PropertyAssociation{false, hdr_ndwt_prop_index}); + } + // TODO: gainmap (tmap) HDR metadata expansion is still missing. + + // Append irot/imir (only as needed) and associate them with the items. + // mini's orientation field uses standard EXIF orientation values 1..8, + // matching the heif_orientation enum. + auto orientation = static_cast(get_orientation()); + if (Error err = file->add_orientation_properties(1, orientation)) { + return err; + } + if (get_alpha_item_data_size() != 0) { + if (Error err = file->add_orientation_properties(2, orientation)) { + return err; + } + } auto iloc_box = std::make_shared(); file->set_iloc_box(iloc_box); @@ -1030,3 +1625,506 @@ return Error::Ok; } + + +// --- Map a single irot/imir box to its equivalent heif_orientation --- +// +// Used together with heif_orientation_concat() to compose the cumulative +// orientation of a sequence of transform boxes. Box order in ipma is not +// strictly fixed across files, so accumulating via concat lets us recover +// the right orientation regardless of whether irot or imir appears first. + +static heif_orientation transform_box_to_orientation(const std::shared_ptr& box) +{ + if (auto irot = std::dynamic_pointer_cast(box)) { + // irot stores the rotation in counter-clockwise degrees. + switch (irot->get_rotation_ccw()) { + case 0: return heif_orientation_normal; + case 90: return heif_orientation_rotate_270_cw; // 90 ccw == 270 cw + case 180: return heif_orientation_rotate_180; + case 270: return heif_orientation_rotate_90_cw; // 270 ccw == 90 cw + default: return heif_orientation_normal; + } + } + if (auto imir = std::dynamic_pointer_cast(box)) { + switch (imir->get_mirror_direction()) { + case heif_transform_mirror_direction_horizontal: + return heif_orientation_flip_horizontally; + case heif_transform_mirror_direction_vertical: + return heif_orientation_flip_vertically; + case heif_transform_mirror_direction_invalid: + return heif_orientation_normal; + } + } + return heif_orientation_normal; +} + + +heif_orientation Box_mini::compute_orientation_from_properties( + const std::vector>& properties) +{ + heif_orientation orientation = heif_orientation_normal; + for (auto& prop : properties) { + if (std::dynamic_pointer_cast(prop) || + std::dynamic_pointer_cast(prop)) { + orientation = heif_orientation_concat(orientation, transform_box_to_orientation(prop)); + } + } + return orientation; +} + + +// --- Extract codec config as raw bytes (without box header) --- + +static std::vector extract_codec_config_bytes(const std::shared_ptr& codec_config_box) +{ + if (!codec_config_box) { + return {}; + } + + StreamWriter temp_writer; + codec_config_box->write(temp_writer); + auto full_data = temp_writer.get_data(); + + // Strip the 8-byte box header (size + fourcc) + if (full_data.size() <= 8) { + return {}; + } + return std::vector(full_data.begin() + 8, full_data.end()); +} + + +// --- Eligibility check --- + +bool Box_mini::can_convert_to_mini(const HeifFile* file, std::string& out_reason) +{ + // Must have a primary item + heif_item_id primary_id = file->get_primary_image_ID(); + if (primary_id == 0) { + out_reason = "no primary item"; + return false; + } + + // Check primary item type + uint32_t item_type = file->get_item_type_4cc(primary_id); + if (item_type != fourcc("av01") && item_type != fourcc("hvc1")) { + out_reason = "primary item type not supported for mini (need av01 or hvc1)"; + return false; + } + + // Check dimensions + std::vector> properties; + file->get_properties(primary_id, properties); + + for (auto& prop : properties) { + if (auto ispe = std::dynamic_pointer_cast(prop)) { + if (ispe->get_width() > 32768 || ispe->get_height() > 32768) { + out_reason = "dimensions exceed mini box limits"; + return false; + } + } + } + + // Check that we don't have unsupported derived image types + auto item_ids = file->get_item_IDs(); + heif_item_id alpha_id = 0; + heif_item_id exif_id = 0; + heif_item_id xmp_id = 0; + + for (auto id : item_ids) { + if (id == primary_id) continue; + + uint32_t type = file->get_item_type_4cc(id); + if (type == fourcc("grid") || type == fourcc("iovl") || type == fourcc("iden")) { + out_reason = "derived image items (grid/overlay/identity) not supported in mini"; + return false; + } + + // Check for alpha auxiliary + auto iref = file->get_iref_box(); + if (iref) { + auto refs = iref->get_references(id, fourcc("auxl")); + if (!refs.empty() && refs[0] == primary_id) { + if (alpha_id != 0) { + out_reason = "multiple alpha items not supported in mini"; + return false; + } + alpha_id = id; + continue; + } + auto cdsc_refs = iref->get_references(id, fourcc("cdsc")); + if (!cdsc_refs.empty() && cdsc_refs[0] == primary_id) { + if (type == fourcc("Exif")) { + if (exif_id != 0) { + out_reason = "multiple EXIF items not supported in mini"; + return false; + } + exif_id = id; + continue; + } + if (type == fourcc("mime")) { + auto infe = file->get_infe_box(id); + if (infe && infe->get_content_type() == "application/rdf+xml") { + if (xmp_id != 0) { + out_reason = "multiple XMP items not supported in mini"; + return false; + } + xmp_id = id; + continue; + } + else { + out_reason = "unsupported mime item type for mini: " + (infe ? infe->get_content_type() : "unknown"); + return false; + } + } + } + } + + // If it's a hidden item or an item type we know about, skip it. + // Otherwise, it's unsupported for mini. + auto infe = file->get_infe_box(id); + if (infe && !infe->is_hidden_item() && type != item_type) { + out_reason = "unsupported additional item type for mini: " + fourcc_to_string(type); + return false; + } + } + + // The mini box has a single compressed flag for both EXIF and XMP. + // If both are present, they must use the same compression method. + if (exif_id != 0 && xmp_id != 0) { + auto exif_infe = file->get_infe_box(exif_id); + auto xmp_infe = file->get_infe_box(xmp_id); + bool exif_compressed = (exif_infe && exif_infe->get_content_encoding() == "deflate"); + bool xmp_compressed = (xmp_infe && xmp_infe->get_content_encoding() == "deflate"); + if (exif_compressed != xmp_compressed) { + out_reason = "EXIF and XMP have different compression methods"; + return false; + } + } + + return true; +} + + +// --- Meta-to-Mini conversion --- + +std::shared_ptr Box_mini::create_from_heif_file(HeifFile* file) +{ + std::string reason; + if (!can_convert_to_mini(file, reason)) { + return nullptr; + } + + auto mini = std::make_shared(); + mini->set_version(0); + + heif_item_id primary_id = file->get_primary_image_ID(); + uint32_t item_type = file->get_item_type_4cc(primary_id); + + bool is_avif = (item_type == fourcc("av01")); + + // For av01/hvc1 the codec is identified via the ftyp minor version brand, + // so we don't need explicit codec type fields in the mini bitstream. + mini->set_explicit_codec_types_flag(false); + + // Get properties for primary item + std::vector> properties; + file->get_properties(primary_id, properties); + + // Extract properties + std::shared_ptr ispe; + std::shared_ptr pixi; + std::shared_ptr colr_nclx; + std::shared_ptr colr_icc; + std::shared_ptr codec_config; + std::shared_ptr clli_box; + std::shared_ptr mdcv_box; + std::shared_ptr cclv_box; + std::shared_ptr amve_box; + std::shared_ptr ndwt_box; + + for (auto& prop : properties) { + if (auto p = std::dynamic_pointer_cast(prop)) { + ispe = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + pixi = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + if (p->get_color_profile_type() == fourcc("nclx")) { + colr_nclx = p; + } + else { + colr_icc = p; + } + } + else if (std::dynamic_pointer_cast(prop) || std::dynamic_pointer_cast(prop)) { + codec_config = prop; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + clli_box = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + mdcv_box = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + cclv_box = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + amve_box = p; + } + else if (auto p = std::dynamic_pointer_cast(prop)) { + ndwt_box = p; + } + } + + heif_orientation orientation = compute_orientation_from_properties(properties); + + // Dimensions + if (ispe) { + mini->set_width(ispe->get_width()); + mini->set_height(ispe->get_height()); + } + + // Bit depth + if (pixi && pixi->get_num_channels() > 0) { + mini->set_bit_depth(static_cast(pixi->get_bits_per_channel(0))); + } + mini->set_float_flag(false); // TODO: detect float from codec config + + // CICP / color + bool has_icc = (colr_icc != nullptr); + mini->set_icc_flag(has_icc); + + if (has_icc) { + auto raw_profile = std::dynamic_pointer_cast(colr_icc->get_color_profile()); + if (raw_profile) { + mini->set_icc_data(raw_profile->get_data()); + } + } + + if (colr_nclx) { + auto nclx = std::dynamic_pointer_cast(colr_nclx->get_color_profile()); + if (nclx) { + auto profile = nclx->get_nclx_color_profile(); + mini->set_colour_primaries(profile.m_colour_primaries); + mini->set_transfer_characteristics(profile.m_transfer_characteristics); + mini->set_matrix_coefficients(profile.m_matrix_coefficients); + mini->set_full_range_flag(profile.m_full_range_flag); + } + } + + // Determine chroma subsampling from codec config + // mini chroma_subsampling values: 0=monochrome, 1=4:2:0, 2=4:2:2, 3=4:4:4 + uint8_t chroma_sub = 0; + if (is_avif) { + auto av1c = std::dynamic_pointer_cast(codec_config); + if (av1c) { + auto& config = av1c->get_configuration(); + if (config.chroma_subsampling_x == 1 && config.chroma_subsampling_y == 1) { + chroma_sub = 1; // 4:2:0 + } + else if (config.chroma_subsampling_x == 1 && config.chroma_subsampling_y == 0) { + chroma_sub = 2; // 4:2:2 + } + else if (config.chroma_subsampling_x == 0 && config.chroma_subsampling_y == 0) { + if (config.monochrome) { + chroma_sub = 0; + } + else { + chroma_sub = 3; // 4:4:4 + } + } + } + } + else if (item_type == fourcc("hvc1")) { + auto hvcc = std::dynamic_pointer_cast(codec_config); + if (hvcc) { + // HEVC chroma_format uses the same values as mini chroma_subsampling + chroma_sub = hvcc->get_configuration().chroma_format; + } + } + mini->set_chroma_subsampling(chroma_sub); + + // Determine if explicit CICP is needed (vs implicit defaults) + bool need_explicit_cicp = true; + uint16_t default_primaries = has_icc ? 2 : 1; + uint16_t default_transfer = has_icc ? 2 : 13; + uint16_t default_matrix = (chroma_sub == 0) ? 2 : 6; + + if (colr_nclx) { + auto nclx = std::dynamic_pointer_cast(colr_nclx->get_color_profile()); + if (nclx) { + auto profile = nclx->get_nclx_color_profile(); + if (profile.m_colour_primaries == default_primaries && + profile.m_transfer_characteristics == default_transfer && + profile.m_matrix_coefficients == default_matrix) { + need_explicit_cicp = false; + } + } + } + else { + // No NCLX profile, use defaults + mini->set_colour_primaries(default_primaries); + mini->set_transfer_characteristics(default_transfer); + mini->set_matrix_coefficients(default_matrix); + need_explicit_cicp = false; + } + mini->set_explicit_cicp_flag(need_explicit_cicp); + + // Orientation (accumulated via heif_orientation_concat during the property walk above) + mini->set_orientation(static_cast(orientation)); + + // Codec config + auto config_bytes = extract_codec_config_bytes(codec_config); + mini->set_main_item_codec_config(config_bytes); + + // Find alpha, exif, xmp items + heif_item_id alpha_id = 0; + heif_item_id exif_id = 0; + heif_item_id xmp_id = 0; + + auto item_ids = file->get_item_IDs(); + auto iref = file->get_iref_box(); + + for (auto id : item_ids) { + if (id == primary_id) continue; + if (!iref) continue; + + auto auxl_refs = iref->get_references(id, fourcc("auxl")); + if (!auxl_refs.empty() && auxl_refs[0] == primary_id) { + alpha_id = id; + continue; + } + + auto cdsc_refs = iref->get_references(id, fourcc("cdsc")); + if (!cdsc_refs.empty() && cdsc_refs[0] == primary_id) { + uint32_t type = file->get_item_type_4cc(id); + if (type == fourcc("Exif")) { + exif_id = id; + } + else if (type == fourcc("mime")) { + auto infe = file->get_infe_box(id); + if (infe && infe->get_content_type() == "application/rdf+xml") { + xmp_id = id; + } + } + } + } + + // Alpha + mini->set_alpha_flag(alpha_id != 0); + if (alpha_id != 0) { + mini->set_alpha_is_premultiplied(false); // TODO: detect from auxC + + // Alpha codec config + std::vector> alpha_props; + file->get_properties(alpha_id, alpha_props); + for (auto& prop : alpha_props) { + if (std::dynamic_pointer_cast(prop) || std::dynamic_pointer_cast(prop)) { + auto alpha_config_bytes = extract_codec_config_bytes(prop); + mini->set_alpha_item_codec_config(alpha_config_bytes); + break; + } + } + + // Alpha item data from iloc + auto iloc = file->get_iloc_box(); + if (iloc) { + for (auto& item : iloc->get_items()) { + if (item.item_ID == alpha_id) { + std::vector data; + for (auto& extent : item.extents) { + data.insert(data.end(), extent.data.begin(), extent.data.end()); + } + mini->set_alpha_item_data(std::move(data)); + break; + } + } + } + } + + // EXIF and XMP share a single compressed flag in the mini box. + // Determine it from whichever item is present (can_convert_to_mini already + // verified they agree when both exist). + mini->set_exif_flag(exif_id != 0); + mini->set_xmp_flag(xmp_id != 0); + + bool metadata_compressed = false; + if (exif_id != 0) { + auto infe = file->get_infe_box(exif_id); + if (infe && infe->get_content_encoding() == "deflate") { + metadata_compressed = true; + } + } + if (xmp_id != 0) { + auto infe = file->get_infe_box(xmp_id); + if (infe && infe->get_content_encoding() == "deflate") { + metadata_compressed = true; + } + } + mini->set_exif_xmp_compressed_flag(metadata_compressed); + + if (exif_id != 0) { + auto iloc = file->get_iloc_box(); + if (iloc) { + for (auto& item : iloc->get_items()) { + if (item.item_ID == exif_id) { + std::vector data; + for (auto& extent : item.extents) { + data.insert(data.end(), extent.data.begin(), extent.data.end()); + } + mini->set_exif_data(std::move(data)); + break; + } + } + } + } + + if (xmp_id != 0) { + auto iloc = file->get_iloc_box(); + if (iloc) { + for (auto& item : iloc->get_items()) { + if (item.item_ID == xmp_id) { + std::vector data; + for (auto& extent : item.extents) { + data.insert(data.end(), extent.data.begin(), extent.data.end()); + } + mini->set_xmp_data(std::move(data)); + break; + } + } + } + } + + // Main item data from iloc + { + auto iloc = file->get_iloc_box(); + if (iloc) { + for (auto& item : iloc->get_items()) { + if (item.item_ID == primary_id) { + std::vector data; + for (auto& extent : item.extents) { + data.insert(data.end(), extent.data.begin(), extent.data.end()); + } + mini->set_main_item_data(std::move(data)); + break; + } + } + } + } + + // HDR metadata on the primary item (clli/mdcv/cclv/amve/ndwt). The mini + // bitstream gates these via hdr_flag — set it whenever at least one of + // those property boxes is present. + // Gainmap (tmap) conversion is still TODO; m_gainmap_flag stays false here. + bool has_hdr_metadata = clli_box || mdcv_box || cclv_box || amve_box || ndwt_box; + mini->set_hdr_flag(has_hdr_metadata); + if (clli_box) mini->set_clli(clli_box); + if (mdcv_box) mini->set_mdcv(mdcv_box); + if (cclv_box) mini->set_cclv(cclv_box); + if (amve_box) mini->set_amve(amve_box); + if (ndwt_box) mini->set_ndwt(ndwt_box); + + return mini; +} diff -Nru libheif-1.19.8/libheif/mini.h libheif-1.23.4/libheif/mini.h --- libheif-1.19.8/libheif/mini.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/mini.h 2026-09-06 14:45:17.000000000 +0000 @@ -27,6 +27,7 @@ #include #include #include +#include class Box_mini : public Box { @@ -38,6 +39,8 @@ Error create_expanded_boxes(class HeifFile* file); + // --- Getters --- + bool get_icc_flag() const { return m_icc_flag; } bool get_exif_flag() const { return m_exif_flag; } bool get_xmp_flag() const { return m_xmp_flag; } @@ -51,6 +54,8 @@ std::vector get_main_item_codec_config() const { return m_main_item_codec_config; } std::vector get_alpha_item_codec_config() const { return m_alpha_item_codec_config; } + std::vector get_gainmap_item_codec_config() const { return m_gainmap_item_codec_config; } + std::vector get_icc_data() const { return m_icc_data; } uint64_t get_main_item_data_offset() const { return m_main_item_data_offset; } @@ -58,17 +63,103 @@ uint64_t get_alpha_item_data_offset() const { return m_alpha_item_data_offset; } uint32_t get_alpha_item_data_size() const { return m_alpha_item_data_size; } uint64_t get_exif_item_data_offset() const { return m_exif_item_data_offset; } - uint32_t get_exif_item_data_size() const { return m_exif_item_data_size; } + uint32_t get_exif_item_data_size() const { return m_exif_data_size; } uint64_t get_xmp_item_data_offset() const { return m_xmp_item_data_offset; } - uint32_t get_xmp_item_data_size() const { return m_xmp_item_data_size; } + uint32_t get_xmp_item_data_size() const { return m_xmp_data_size; } uint16_t get_colour_primaries() const { return m_colour_primaries; } uint16_t get_transfer_characteristics() const { return m_transfer_characteristics; } uint16_t get_matrix_coefficients() const { return m_matrix_coefficients; } bool get_full_range_flag() const { return m_full_range_flag; } + // --- Setters (for write path) --- + + void set_version(uint8_t v) { m_version = v; } + void set_explicit_codec_types_flag(bool f) { m_explicit_codec_types_flag = f; } + void set_float_flag(bool f) { m_float_flag = f; } + void set_full_range_flag(bool f) { m_full_range_flag = f; } + void set_alpha_flag(bool f) { m_alpha_flag = f; } + void set_explicit_cicp_flag(bool f) { m_explicit_cicp_flag = f; } + void set_hdr_flag(bool f) { m_hdr_flag = f; } + void set_icc_flag(bool f) { m_icc_flag = f; } + void set_exif_flag(bool f) { m_exif_flag = f; } + void set_xmp_flag(bool f) { m_xmp_flag = f; } + void set_chroma_subsampling(uint8_t cs) { m_chroma_subsampling = cs; } + void set_orientation(uint8_t o) { m_orientation = o; } + void set_width(uint32_t w) { m_width = w; } + void set_height(uint32_t h) { m_height = h; } + void set_bit_depth(uint8_t bd) { m_bit_depth = bd; } + void set_chroma_is_horizontally_centered(bool f) { m_chroma_is_horizontally_centered = f; } + void set_chroma_is_vertically_centered(bool f) { m_chroma_is_vertically_centered = f; } + void set_alpha_is_premultiplied(bool f) { m_alpha_is_premultiplied = f; } + void set_colour_primaries(uint16_t cp) { m_colour_primaries = cp; } + void set_transfer_characteristics(uint16_t tc) { m_transfer_characteristics = tc; } + void set_matrix_coefficients(uint16_t mc) { m_matrix_coefficients = mc; } + void set_infe_type(uint32_t t) { m_infe_type = t; } + void set_codec_config_type(uint32_t t) { m_codec_config_type = t; } + void set_exif_xmp_compressed_flag(bool f) { m_exif_xmp_compressed_flag = f; } + + void set_main_item_codec_config(std::vector data) { m_main_item_codec_config = std::move(data); } + void set_alpha_item_codec_config(std::vector data) { m_alpha_item_codec_config = std::move(data); } + void set_gainmap_item_codec_config(std::vector data) { m_gainmap_item_codec_config = std::move(data); } + void set_icc_data(std::vector data) { m_icc_data = std::move(data); } + + void set_main_item_data(std::vector data) { m_main_item_data = std::move(data); } + void set_alpha_item_data(std::vector data) { m_alpha_item_data = std::move(data); } + void set_gainmap_item_data(std::vector data) { m_gainmap_item_data = std::move(data); } + void set_exif_data(std::vector data) { m_exif_data_bytes = std::move(data); } + void set_xmp_data(std::vector data) { m_xmp_data_bytes = std::move(data); } + + // Gainmap setters + void set_gainmap_flag(bool f) { m_gainmap_flag = f; } + void set_gainmap_width(uint32_t w) { m_gainmap_width = w; } + void set_gainmap_height(uint32_t h) { m_gainmap_height = h; } + void set_gainmap_matrix_coefficients(uint8_t mc) { m_gainmap_matrix_coefficients = mc; } + void set_gainmap_full_range_flag(bool f) { m_gainmap_full_range_flag = f; } + void set_gainmap_chroma_subsampling(uint8_t cs) { m_gainmap_chroma_subsampling = cs; } + void set_gainmap_float_flag(bool f) { m_gainmap_float_flag = f; } + void set_gainmap_bit_depth(uint8_t bd) { m_gainmap_bit_depth = bd; } + void set_tmap_icc_flag(bool f) { m_tmap_icc_flag = f; } + void set_tmap_explicit_cicp_flag(bool f) { m_tmap_explicit_cicp_flag = f; } + void set_tmap_colour_primaries(uint16_t cp) { m_tmap_colour_primaries = cp; } + void set_tmap_transfer_characteristics(uint16_t tc) { m_tmap_transfer_characteristics = tc; } + void set_tmap_matrix_coefficients(uint16_t mc) { m_tmap_matrix_coefficients = mc; } + void set_tmap_full_range_flag(bool f) { m_tmap_full_range_flag = f; } + void set_tmap_icc_data(std::vector data) { m_tmap_icc_data = std::move(data); } + void set_gainmap_metadata(std::vector data) { m_gainmap_metadata = std::move(data); } + + // HDR metadata setters + void set_clli(std::shared_ptr box) { m_clli = std::move(box); } + void set_mdcv(std::shared_ptr box) { m_mdcv = std::move(box); } + void set_cclv(std::shared_ptr box) { m_cclv = std::move(box); } + void set_amve(std::shared_ptr box) { m_amve = std::move(box); } + void set_ndwt(std::shared_ptr box) { m_ndwt = std::move(box); } + void set_tmap_clli(std::shared_ptr box) { m_tmap_clli = std::move(box); } + void set_tmap_mdcv(std::shared_ptr box) { m_tmap_mdcv = std::move(box); } + void set_tmap_cclv(std::shared_ptr box) { m_tmap_cclv = std::move(box); } + void set_tmap_amve(std::shared_ptr box) { m_tmap_amve = std::move(box); } + void set_tmap_ndwt(std::shared_ptr box) { m_tmap_ndwt = std::move(box); } + std::string dump(Indent &) const override; + Error write(StreamWriter& writer) const override; + + // Check if a HeifFile can be represented as a mini box. + // Returns true if conversion is possible. If false, out_reason explains why not. + static bool can_convert_to_mini(const class HeifFile* file, std::string& out_reason); + + // Create a Box_mini from a HeifFile's meta box structure. + // This is the inverse of create_expanded_boxes(). + // Returns nullptr if conversion is not possible. + static std::shared_ptr create_from_heif_file(class HeifFile* file); + + // Compose the cumulative EXIF-style orientation of a sequence of property + // boxes by composing irot/imir transforms in list order via + // heif_orientation_concat(). Non-transform properties are ignored. + // Returns heif_orientation_normal (1) for an empty list. + static heif_orientation compute_orientation_from_properties( + const std::vector>& properties); + protected: Error parse(BitstreamRange &range, const heif_security_limits *limits) override; @@ -83,14 +174,15 @@ bool m_icc_flag = false; bool m_exif_flag = false; bool m_xmp_flag = false; + bool m_exif_xmp_compressed_flag = false; // when enabled, data is compressed with 'deflate' uint8_t m_chroma_subsampling = 0; uint8_t m_orientation = 0; uint32_t m_width = 0; uint32_t m_height = 0; uint8_t m_bit_depth = 8; - bool m_chroma_is_horizontally_centred = false; - bool m_chroma_is_vertically_centred = false; + bool m_chroma_is_horizontally_centered = false; + bool m_chroma_is_vertically_centered = false; bool m_alpha_is_premultiplied = false; uint16_t m_colour_primaries = 0; uint16_t m_transfer_characteristics = 0; @@ -118,22 +210,20 @@ bool m_tmap_full_range_flag = false; bool m_reve_flag = false; - bool m_ndwt_flag = false; std::shared_ptr m_clli; std::shared_ptr m_mdcv; std::shared_ptr m_cclv; std::shared_ptr m_amve; // std::shared_ptr m_reve; - // std::shared_ptr m_ndwt; + std::shared_ptr m_ndwt; bool m_tmap_reve_flag = false; - bool m_tmap_ndwt_flag = false; std::shared_ptr m_tmap_clli; std::shared_ptr m_tmap_mdcv; std::shared_ptr m_tmap_cclv; std::shared_ptr m_tmap_amve; // std::shared_ptr m_tmap_reve; - // std::shared_ptr m_tmap_ndwt; + std::shared_ptr m_tmap_ndwt; std::vector m_alpha_item_codec_config; std::vector m_gainmap_item_codec_config; @@ -149,9 +239,16 @@ uint64_t m_gainmap_item_data_offset = 0; uint32_t m_gainmap_item_data_size = 0; uint64_t m_exif_item_data_offset = 0; - uint32_t m_exif_item_data_size = 0; + uint32_t m_exif_data_size = 0; uint64_t m_xmp_item_data_offset = 0; - uint32_t m_xmp_item_data_size = 0; + uint32_t m_xmp_data_size = 0; + + // Image data for write path (not populated during parse) + std::vector m_main_item_data; + std::vector m_alpha_item_data; + std::vector m_gainmap_item_data; + std::vector m_exif_data_bytes; + std::vector m_xmp_data_bytes; }; #endif diff -Nru libheif-1.19.8/libheif/nclx.cc libheif-1.23.4/libheif/nclx.cc --- libheif-1.19.8/libheif/nclx.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/nclx.cc 2026-09-06 14:45:17.000000000 +0000 @@ -127,6 +127,9 @@ break; case 9: case 10: + // TODO: case 10 is BT.2020 constant-luminance, which is not a linear matrix conversion + // (Y' is derived from gamma-corrected linear luminance; Cb/Cr need EOTF inversion). + // We currently fall through to the NCL coefficients, which is wrong for CL content. result.Kr = 0.2627f; result.Kb = 0.0593f; break; @@ -226,44 +229,64 @@ heif_suberror_End_of_data); } - m_colour_primaries = range.read16(); - m_transfer_characteristics = range.read16(); - m_matrix_coefficients = range.read16(); - m_full_range_flag = (range.read8() & 0x80 ? true : false); + m_profile.m_colour_primaries = range.read16(); + m_profile.m_transfer_characteristics = range.read16(); + m_profile.m_matrix_coefficients = range.read16(); + m_profile.m_full_range_flag = (range.read8() & 0x80 ? true : false); return Error::Ok; } -Error color_profile_nclx::get_nclx_color_profile(struct heif_color_profile_nclx** out_data) const +Error color_profile_nclx::parse_nclc(BitstreamRange& range) +{ + StreamReader::grow_status status; + status = range.wait_for_available_bytes(6); + if (status != StreamReader::grow_status::size_reached) { + // TODO: return recoverable error at timeout + return Error(heif_error_Invalid_input, + heif_suberror_End_of_data); + } + + m_profile.m_colour_primaries = range.read16(); + m_profile.m_transfer_characteristics = range.read16(); + m_profile.m_matrix_coefficients = range.read16(); + + // use full range for RGB, limited range otherwise + m_profile.m_full_range_flag = (m_profile.m_matrix_coefficients == 0); + + return Error::Ok; +} + +Error nclx_profile::get_nclx_color_profile(heif_color_profile_nclx** out_data) const { *out_data = nullptr; - struct heif_color_profile_nclx* nclx = alloc_nclx_color_profile(); + heif_color_profile_nclx* nclx = heif_nclx_color_profile_alloc(); if (nclx == nullptr) { return Error(heif_error_Memory_allocation_error, heif_suberror_Unspecified); } - struct heif_error err; + heif_error err; nclx->version = 1; err = heif_nclx_color_profile_set_color_primaries(nclx, get_colour_primaries()); if (err.code) { - free_nclx_color_profile(nclx); + heif_nclx_color_profile_free(nclx); return {err.code, err.subcode}; } err = heif_nclx_color_profile_set_transfer_characteristics(nclx, get_transfer_characteristics()); if (err.code) { - free_nclx_color_profile(nclx); + heif_nclx_color_profile_free(nclx); return {err.code, err.subcode}; } err = heif_nclx_color_profile_set_matrix_coefficients(nclx, get_matrix_coefficients()); if (err.code) { - free_nclx_color_profile(nclx); + heif_nclx_color_profile_free(nclx); return {err.code, err.subcode}; } @@ -288,31 +311,7 @@ } -struct heif_color_profile_nclx* color_profile_nclx::alloc_nclx_color_profile() -{ - auto profile = (heif_color_profile_nclx*) malloc(sizeof(struct heif_color_profile_nclx)); - - if (profile) { - profile->version = 1; - - // sRGB defaults - profile->color_primaries = heif_color_primaries_ITU_R_BT_709_5; // 1 - profile->transfer_characteristics = heif_transfer_characteristic_IEC_61966_2_1; // 13 - profile->matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; // 6 - profile->full_range_flag = true; - } - - return profile; -} - - -void color_profile_nclx::free_nclx_color_profile(struct heif_color_profile_nclx* profile) -{ - free(profile); -} - - -void color_profile_nclx::set_sRGB_defaults() +void nclx_profile::set_sRGB_defaults() { // sRGB defaults m_colour_primaries = 1; @@ -322,7 +321,7 @@ } -void color_profile_nclx::set_undefined() +void nclx_profile::set_undefined() { m_colour_primaries = 2; m_transfer_characteristics = 2; @@ -331,7 +330,13 @@ } -void color_profile_nclx::set_from_heif_color_profile_nclx(const struct heif_color_profile_nclx* nclx) +bool nclx_profile::is_undefined() const +{ + return *this == nclx_profile::undefined(); +} + + +void nclx_profile::set_from_heif_color_profile_nclx(const heif_color_profile_nclx* nclx) { if (nclx) { m_colour_primaries = nclx->color_primaries; @@ -342,7 +347,17 @@ } -void color_profile_nclx::replace_undefined_values_with_sRGB_defaults() +void nclx_profile::copy_to_heif_color_profile_nclx(heif_color_profile_nclx* nclx) +{ + assert(nclx); + nclx->color_primaries = (enum heif_color_primaries)m_colour_primaries; + nclx->transfer_characteristics = (enum heif_transfer_characteristics)m_transfer_characteristics; + nclx->matrix_coefficients = (enum heif_matrix_coefficients)m_matrix_coefficients; + nclx->full_range_flag = m_full_range_flag; +} + + +void nclx_profile::replace_undefined_values_with_sRGB_defaults() { if (m_matrix_coefficients == heif_matrix_coefficients_unspecified) { m_matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; @@ -358,6 +373,14 @@ } +bool nclx_profile::equal_except_transfer_curve(const nclx_profile& b) const +{ + return (m_matrix_coefficients == b.m_matrix_coefficients && + m_colour_primaries == b.m_colour_primaries && + m_full_range_flag == b.m_full_range_flag); +} + + Error Box_colr::parse(BitstreamRange& range, const heif_security_limits* limits) { StreamReader::grow_status status; @@ -371,6 +394,14 @@ return err; } } + else if (colour_type == fourcc("nclc")) { + auto color_profile = std::make_shared(); + m_color_profile = color_profile; + Error err = color_profile->parse_nclc(range); + if (err) { + return err; + } + } else if (colour_type == fourcc("prof") || colour_type == fourcc("rICC")) { if (!has_fixed_box_size()) { @@ -436,20 +467,20 @@ std::string color_profile_nclx::dump(Indent& indent) const { std::ostringstream sstr; - sstr << indent << "colour_primaries: " << m_colour_primaries << "\n" - << indent << "transfer_characteristics: " << m_transfer_characteristics << "\n" - << indent << "matrix_coefficients: " << m_matrix_coefficients << "\n" - << indent << "full_range_flag: " << m_full_range_flag << "\n"; + sstr << indent << "colour_primaries: " << m_profile.m_colour_primaries << "\n" + << indent << "transfer_characteristics: " << m_profile.m_transfer_characteristics << "\n" + << indent << "matrix_coefficients: " << m_profile.m_matrix_coefficients << "\n" + << indent << "full_range_flag: " << m_profile.m_full_range_flag << "\n"; return sstr.str(); } Error color_profile_nclx::write(StreamWriter& writer) const { - writer.write16(m_colour_primaries); - writer.write16(m_transfer_characteristics); - writer.write16(m_matrix_coefficients); - writer.write8(m_full_range_flag ? 0x80 : 0x00); + writer.write16(m_profile.m_colour_primaries); + writer.write16(m_profile.m_transfer_characteristics); + writer.write16(m_profile.m_matrix_coefficients); + writer.write8(m_profile.m_full_range_flag ? 0x80 : 0x00); return Error::Ok; } diff -Nru libheif-1.19.8/libheif/nclx.h libheif-1.23.4/libheif/nclx.h --- libheif-1.19.8/libheif/nclx.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/nclx.h 2026-09-06 14:45:17.000000000 +0000 @@ -118,24 +118,26 @@ }; -class color_profile_nclx : public color_profile +struct nclx_profile { -public: - color_profile_nclx() { set_sRGB_defaults(); } + uint16_t m_colour_primaries = heif_color_primaries_unspecified; + uint16_t m_transfer_characteristics = heif_transfer_characteristic_unspecified; + uint16_t m_matrix_coefficients = heif_matrix_coefficients_unspecified; + bool m_full_range_flag = true; - uint32_t get_type() const override { return fourcc("nclx"); } + bool operator==(const nclx_profile& b) const = default; - std::string dump(Indent&) const override; + bool operator!=(const nclx_profile& b) const = default; - Error parse(BitstreamRange& range); + static nclx_profile undefined() { return {}; } - Error write(StreamWriter& writer) const override; + static nclx_profile defaults() { nclx_profile profile; profile.set_sRGB_defaults(); return profile; } - uint16_t get_colour_primaries() const { return m_colour_primaries; } + heif_color_primaries get_colour_primaries() const { return static_cast(m_colour_primaries); } - uint16_t get_transfer_characteristics() const { return m_transfer_characteristics; } + heif_transfer_characteristics get_transfer_characteristics() const { return static_cast(m_transfer_characteristics); } - uint16_t get_matrix_coefficients() const { return m_matrix_coefficients; } + heif_matrix_coefficients get_matrix_coefficients() const { return static_cast(m_matrix_coefficients); } bool get_full_range_flag() const { return m_full_range_flag; } @@ -151,21 +153,48 @@ void set_undefined(); - Error get_nclx_color_profile(struct heif_color_profile_nclx** out_data) const; + bool is_undefined() const; - static struct heif_color_profile_nclx* alloc_nclx_color_profile(); + bool is_defined() const { return !is_undefined(); } - static void free_nclx_color_profile(struct heif_color_profile_nclx* profile); + void replace_undefined_values_with_sRGB_defaults(); - void set_from_heif_color_profile_nclx(const struct heif_color_profile_nclx* nclx); + bool equal_except_transfer_curve(const nclx_profile& b) const; - void replace_undefined_values_with_sRGB_defaults(); + Error get_nclx_color_profile(heif_color_profile_nclx** out_data) const; + + void set_from_heif_color_profile_nclx(const heif_color_profile_nclx* nclx); + + void copy_to_heif_color_profile_nclx(heif_color_profile_nclx* nclx); +}; + + +class color_profile_nclx : public color_profile +{ +public: + color_profile_nclx() { m_profile.set_sRGB_defaults(); } + + color_profile_nclx(const nclx_profile& profile) : m_profile(profile) { } + + void set_from_heif_color_profile_nclx(const heif_color_profile_nclx* nclx) + { + m_profile.set_from_heif_color_profile_nclx(nclx); + } + + uint32_t get_type() const override { return fourcc("nclx"); } + + std::string dump(Indent&) const override; + + Error parse(BitstreamRange& range); + + Error parse_nclc(BitstreamRange& range); + + Error write(StreamWriter& writer) const override; + + nclx_profile get_nclx_color_profile() const { return m_profile; } private: - uint16_t m_colour_primaries = heif_color_primaries_unspecified; - uint16_t m_transfer_characteristics = heif_transfer_characteristic_unspecified; - uint16_t m_matrix_coefficients = heif_matrix_coefficients_unspecified; - bool m_full_range_flag = true; + nclx_profile m_profile; }; diff -Nru libheif-1.19.8/libheif/omaf_boxes.cc libheif-1.23.4/libheif/omaf_boxes.cc --- libheif-1.19.8/libheif/omaf_boxes.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/omaf_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,88 @@ +/* + * libheif OMAF (ISO/IEC 23090-2) + * + * Copyright (c) 2026 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . +*/ + +#include "omaf_boxes.h" + +#include +#include + + +Error Box_prfr::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("prfr"); + } + + uint8_t projection_type = (range.read8() & 0x1F); + m_projection = static_cast(projection_type); + return range.get_error(); +} + +std::string Box_prfr::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + sstr << indent << "projection_type: "; + switch (m_projection) { + case heif_omaf_image_projection_equirectangular: + sstr << "equirectangular\n"; + break; + case heif_omaf_image_projection_cube_map: + sstr << "cube-map\n"; + break; + default: + sstr << "unknown (" << m_projection << ")\n"; + break; + } + return sstr.str(); +} + +Error Box_prfr::write(StreamWriter& writer) const +{ + if (static_cast(m_projection) >= 32) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image projection value out of the 5-bit prfr range." + }; + } + + size_t box_start = reserve_box_header_space(writer); + writer.write8(static_cast(m_projection) & 0x1F); + prepend_header(writer, box_start); + return Error::Ok; +} + +Error Box_prfr::set_image_projection(heif_omaf_image_projection projection) +{ + if (static_cast(projection) >= 32) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image projection value out of the 5-bit prfr range." + }; + } + + m_projection = projection; + return Error::Ok; +} diff -Nru libheif-1.19.8/libheif/omaf_boxes.h libheif-1.23.4/libheif/omaf_boxes.h --- libheif-1.19.8/libheif/omaf_boxes.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/omaf_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,60 @@ +/* + * libheif OMAF (ISO/IEC 23090-2) + * + * Copyright (c) 2026 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_OMAF_BOXES_H +#define LIBHEIF_OMAF_BOXES_H + +#include "libheif/heif.h" +#include "box.h" + +#include +#include + +// Projection format for OMAF +// See ISO/IEC 23090-2:2023 Section 7.9.3 +class Box_prfr : public FullBox +{ +public: + Box_prfr() + { + set_short_type(fourcc("prfr")); + } + + heif_omaf_image_projection get_omaf_image_projection() const { return m_projection; } + + Error set_image_projection(heif_omaf_image_projection projection); + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Projection Format"; } + + [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + heif_omaf_image_projection m_projection = heif_omaf_image_projection_equirectangular; +}; + +#endif \ No newline at end of file diff -Nru libheif-1.19.8/libheif/pixelimage.cc libheif-1.23.4/libheif/pixelimage.cc --- libheif-1.19.8/libheif/pixelimage.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/pixelimage.cc 1970-01-01 00:00:00.000000000 +0000 @@ -1,1618 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2017 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . -*/ - - -#include "pixelimage.h" -#include "common_utils.h" -#include "security_limits.h" - -#include -#include -#include -#include -#include -#include - - -heif_chroma chroma_from_subsampling(int h, int v) -{ - if (h == 2 && v == 2) { - return heif_chroma_420; - } - else if (h == 2 && v == 1) { - return heif_chroma_422; - } - else if (h == 1 && v == 1) { - return heif_chroma_444; - } - else { - assert(false); - return heif_chroma_undefined; - } -} - - -uint32_t chroma_width(uint32_t w, heif_chroma chroma) -{ - switch (chroma) { - case heif_chroma_420: - case heif_chroma_422: - return (w+1)/2; - default: - return w; - } -} - -uint32_t chroma_height(uint32_t h, heif_chroma chroma) -{ - switch (chroma) { - case heif_chroma_420: - return (h+1)/2; - default: - return h; - } -} - -uint32_t channel_width(uint32_t w, heif_chroma chroma, heif_channel channel) -{ - if (channel == heif_channel_Cb || channel == heif_channel_Cr) { - return chroma_width(w, chroma); - } - else { - return w; - } -} - -uint32_t channel_height(uint32_t h, heif_chroma chroma, heif_channel channel) -{ - if (channel == heif_channel_Cb || channel == heif_channel_Cr) { - return chroma_height(h, chroma); - } - else { - return h; - } -} - -HeifPixelImage::~HeifPixelImage() -{ - for (auto& iter : m_planes) { - delete[] iter.second.allocated_mem; - } -} - - -int num_interleaved_pixels_per_plane(heif_chroma chroma) -{ - switch (chroma) { - case heif_chroma_undefined: - case heif_chroma_monochrome: - case heif_chroma_420: - case heif_chroma_422: - case heif_chroma_444: - return 1; - - case heif_chroma_interleaved_RGB: - case heif_chroma_interleaved_RRGGBB_BE: - case heif_chroma_interleaved_RRGGBB_LE: - return 3; - - case heif_chroma_interleaved_RGBA: - case heif_chroma_interleaved_RRGGBBAA_BE: - case heif_chroma_interleaved_RRGGBBAA_LE: - return 4; - } - - assert(false); - return 0; -} - - -bool is_integer_multiple_of_chroma_size(uint32_t width, - uint32_t height, - heif_chroma chroma) -{ - switch (chroma) { - case heif_chroma_444: - case heif_chroma_monochrome: - return true; - case heif_chroma_422: - return (width & 1) == 0; - case heif_chroma_420: - return (width & 1) == 0 && (height & 1) == 0; - default: - assert(false); - return false; - } -} - - -std::vector get_valid_chroma_values_for_colorspace(heif_colorspace colorspace) -{ - switch (colorspace) { - case heif_colorspace_YCbCr: - return {heif_chroma_420, heif_chroma_422, heif_chroma_444}; - - case heif_colorspace_RGB: - return {heif_chroma_444, - heif_chroma_interleaved_RGB, - heif_chroma_interleaved_RGBA, - heif_chroma_interleaved_RRGGBB_BE, - heif_chroma_interleaved_RRGGBBAA_BE, - heif_chroma_interleaved_RRGGBB_LE, - heif_chroma_interleaved_RRGGBBAA_LE}; - - case heif_colorspace_monochrome: - return {heif_chroma_monochrome}; - - case heif_colorspace_nonvisual: - return {heif_chroma_undefined}; - - default: - return {}; - } -} - - -void HeifPixelImage::create(uint32_t width, uint32_t height, heif_colorspace colorspace, heif_chroma chroma) -{ - m_width = width; - m_height = height; - m_colorspace = colorspace; - m_chroma = chroma; -} - -static uint32_t rounded_size(uint32_t s) -{ - s = (s + 1U) & ~1U; - - if (s < 64) { - s = 64; - } - - return s; -} - -Error HeifPixelImage::add_plane(heif_channel channel, uint32_t width, uint32_t height, int bit_depth, - const heif_security_limits* limits) -{ - assert(!has_channel(channel)); - - ImagePlane plane; - int num_interleaved_pixels = num_interleaved_pixels_per_plane(m_chroma); - - // for backwards compatibility, allow for 24/32 bits for RGB/RGBA interleaved chromas - - if (m_chroma == heif_chroma_interleaved_RGB && bit_depth == 24) { - bit_depth = 8; - } - - if (m_chroma == heif_chroma_interleaved_RGBA && bit_depth == 32) { - bit_depth = 8; - } - - if (auto err = plane.alloc(width, height, heif_channel_datatype_unsigned_integer, bit_depth, num_interleaved_pixels, limits)) { - return err; - } - else { - m_planes.insert(std::make_pair(channel, plane)); - return Error::Ok; - } -} - - -Error HeifPixelImage::add_channel(heif_channel channel, uint32_t width, uint32_t height, heif_channel_datatype datatype, int bit_depth, - const heif_security_limits* limits) -{ - ImagePlane plane; - if (Error err = plane.alloc(width, height, datatype, bit_depth, 1, limits)) { - return err; - } - else { - m_planes.insert(std::make_pair(channel, plane)); - return Error::Ok; - } -} - - -Error HeifPixelImage::ImagePlane::alloc(uint32_t width, uint32_t height, heif_channel_datatype datatype, int bit_depth, - int num_interleaved_components, - const heif_security_limits* limits) -{ - assert(bit_depth >= 1); - assert(bit_depth <= 128); - - // use 16 byte alignment (enough for 128 bit data-types). Every row is an integer number of data-elements. - uint16_t alignment = 16; // must be power of two - - m_width = width; - m_height = height; - - m_mem_width = rounded_size(width); - m_mem_height = rounded_size(height); - - assert(num_interleaved_components > 0 && num_interleaved_components <= 255); - - m_bit_depth = static_cast(bit_depth); - m_num_interleaved_components = static_cast(num_interleaved_components); - m_datatype = datatype; - - - int bytes_per_component = get_bytes_per_pixel(); - int bytes_per_pixel = num_interleaved_components * bytes_per_component; - - stride = m_mem_width * bytes_per_pixel; - stride = (stride + alignment - 1U) & ~(alignment - 1U); - - assert(alignment>=1); - - if (limits && - limits->max_memory_block_size && - (limits->max_memory_block_size < alignment - 1U || - (limits->max_memory_block_size - (alignment - 1U)) / stride < m_mem_height)) { - std::stringstream sstr; - sstr << "Allocating " << static_cast(m_mem_height) * stride + alignment - 1 << " bytes exceeds the security limit of " - << limits->max_memory_block_size << " bytes"; - - return {heif_error_Memory_allocation_error, - heif_suberror_Security_limit_exceeded, - sstr.str()}; - } - - try { - allocated_mem = new uint8_t[static_cast(m_mem_height) * stride + alignment - 1]; - uint8_t* mem_8 = allocated_mem; - - // shift beginning of image data to aligned memory position - - auto mem_start_addr = (uint64_t) mem_8; - auto mem_start_offset = (mem_start_addr & (alignment - 1U)); - if (mem_start_offset != 0) { - mem_8 += alignment - mem_start_offset; - } - - mem = mem_8; - - return Error::Ok; - } - catch (const std::bad_alloc& excpt) { - std::stringstream sstr; - sstr << "Allocating " << static_cast(m_mem_height) * stride + alignment - 1 << " bytes failed"; - - return {heif_error_Memory_allocation_error, - heif_suberror_Unspecified, - sstr.str()}; - } -} - - -Error HeifPixelImage::extend_padding_to_size(uint32_t width, uint32_t height, bool adjust_size, - const heif_security_limits* limits) -{ - for (auto& planeIter : m_planes) { - auto* plane = &planeIter.second; - - uint32_t subsampled_width, subsampled_height; - get_subsampled_size(width, height, planeIter.first, m_chroma, - &subsampled_width, &subsampled_height); - - uint32_t old_width = plane->m_width; - uint32_t old_height = plane->m_height; - - int bytes_per_pixel = get_storage_bits_per_pixel(planeIter.first) / 8; - - if (plane->m_mem_width < subsampled_width || - plane->m_mem_height < subsampled_height) { - - ImagePlane newPlane; - if (auto err = newPlane.alloc(subsampled_width, subsampled_height, plane->m_datatype, plane->m_bit_depth, - num_interleaved_pixels_per_plane(m_chroma), - limits)) - { - return err; - } - - // This is not needed, but we have to silence the clang-tidy false positive. - if (!newPlane.mem) { - return Error::InternalError; - } - - // copy the visible part of the old plane into the new plane - - for (uint32_t y = 0; y < plane->m_height; y++) { - memcpy(static_cast(newPlane.mem) + y * newPlane.stride, - static_cast(plane->mem) + y * plane->stride, - plane->m_width * bytes_per_pixel); - } - - planeIter.second = newPlane; - plane = &planeIter.second; - } - - // extend plane size - - if (old_width != subsampled_width) { - for (uint32_t y = 0; y < old_height; y++) { - for (uint32_t x = old_width; x < subsampled_width; x++) { - memcpy(static_cast(plane->mem) + y * plane->stride + x * bytes_per_pixel, - static_cast(plane->mem) + y * plane->stride + (old_width - 1) * bytes_per_pixel, - bytes_per_pixel); - } - } - } - - for (uint32_t y = old_height; y < subsampled_height; y++) { - memcpy(static_cast(plane->mem) + y * plane->stride, - static_cast(plane->mem) + (old_height - 1) * plane->stride, - subsampled_width * bytes_per_pixel); - } - - - if (adjust_size) { - plane->m_width = subsampled_width; - plane->m_height = subsampled_height; - } - } - - // modify logical image size, if requested - - if (adjust_size) { - m_width = width; - m_height = height; - } - - return Error::Ok; -} - - -Error HeifPixelImage::extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits) -{ - for (auto& planeIter : m_planes) { - auto* plane = &planeIter.second; - - uint32_t subsampled_width, subsampled_height; - get_subsampled_size(width, height, planeIter.first, m_chroma, - &subsampled_width, &subsampled_height); - - uint32_t old_width = plane->m_width; - uint32_t old_height = plane->m_height; - - int bytes_per_pixel = get_storage_bits_per_pixel(planeIter.first) / 8; - - if (plane->m_mem_width < subsampled_width || - plane->m_mem_height < subsampled_height) { - - ImagePlane newPlane; - if (auto err = newPlane.alloc(subsampled_width, subsampled_height, plane->m_datatype, plane->m_bit_depth, num_interleaved_pixels_per_plane(m_chroma), limits)) { - return err; - } - - // This is not needed, but we have to silence the clang-tidy false positive. - if (!newPlane.mem) { - return Error::InternalError; - } - - // copy the visible part of the old plane into the new plane - - for (uint32_t y = 0; y < plane->m_height; y++) { - memcpy(static_cast(newPlane.mem) + y * newPlane.stride, - static_cast(plane->mem) + y * plane->stride, - plane->m_width * bytes_per_pixel); - } - - planeIter.second = newPlane; - plane = &planeIter.second; - } - - // extend plane size - - uint8_t fill = 0; - if (bytes_per_pixel == 1 && (planeIter.first == heif_channel_Cb || planeIter.first == heif_channel_Cr)) { - fill = 128; - } - - if (old_width != subsampled_width) { - for (uint32_t y = 0; y < old_height; y++) { - memset(static_cast(plane->mem) + y * plane->stride + old_width * bytes_per_pixel, - fill, - bytes_per_pixel * (subsampled_width - old_width)); - } - } - - for (uint32_t y = old_height; y < subsampled_height; y++) { - memset(static_cast(plane->mem) + y * plane->stride, - fill, - subsampled_width * bytes_per_pixel); - } - - - plane->m_width = subsampled_width; - plane->m_height = subsampled_height; - } - - // modify the logical image size - - m_width = width; - m_height = height; - - return Error::Ok; -} - -bool HeifPixelImage::has_channel(heif_channel channel) const -{ - return (m_planes.find(channel) != m_planes.end()); -} - - -bool HeifPixelImage::has_alpha() const -{ - return has_channel(heif_channel_Alpha) || - get_chroma_format() == heif_chroma_interleaved_RGBA || - get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE || - get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE; -} - - -uint32_t HeifPixelImage::get_width(enum heif_channel channel) const -{ - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - return 0; - } - - return iter->second.m_width; -} - - -uint32_t HeifPixelImage::get_height(enum heif_channel channel) const -{ - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - return 0; - } - - return iter->second.m_height; -} - - -std::set HeifPixelImage::get_channel_set() const -{ - std::set channels; - - for (const auto& plane : m_planes) { - channels.insert(plane.first); - } - - return channels; -} - - -uint8_t HeifPixelImage::get_storage_bits_per_pixel(enum heif_channel channel) const -{ - if (channel == heif_channel_interleaved) { - auto chroma = get_chroma_format(); - switch (chroma) { - case heif_chroma_interleaved_RGB: - return 24; - case heif_chroma_interleaved_RGBA: - return 32; - case heif_chroma_interleaved_RRGGBB_BE: - case heif_chroma_interleaved_RRGGBB_LE: - return 48; - case heif_chroma_interleaved_RRGGBBAA_BE: - case heif_chroma_interleaved_RRGGBBAA_LE: - return 64; - default: - return -1; // invalid channel/chroma specification - } - } - else { - uint32_t bpp = (get_bits_per_pixel(channel) + 7U) & ~7U; - assert(bpp <= 255); - return static_cast(bpp); - } -} - - -uint8_t HeifPixelImage::get_bits_per_pixel(enum heif_channel channel) const -{ - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - return -1; - } - - return iter->second.m_bit_depth; -} - - -uint8_t HeifPixelImage::get_visual_image_bits_per_pixel() const -{ - switch (m_colorspace) { - case heif_colorspace_monochrome: - return get_bits_per_pixel(heif_channel_Y); - break; - case heif_colorspace_YCbCr: - return std::max(get_bits_per_pixel(heif_channel_Y), - std::max(get_bits_per_pixel(heif_channel_Cb), - get_bits_per_pixel(heif_channel_Cr))); - break; - case heif_colorspace_RGB: - return std::max(get_bits_per_pixel(heif_channel_R), - std::max(get_bits_per_pixel(heif_channel_G), - get_bits_per_pixel(heif_channel_B))); - break; - case heif_colorspace_nonvisual: - return 0; - break; - default: - assert(false); - return 0; - break; - } -} - - -heif_channel_datatype HeifPixelImage::get_datatype(enum heif_channel channel) const -{ - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - return heif_channel_datatype_undefined; - } - - return iter->second.m_datatype; -} - - -int HeifPixelImage::get_number_of_interleaved_components(heif_channel channel) const -{ - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - return 0; - } - - return iter->second.m_num_interleaved_components; -} - - -Error HeifPixelImage::copy_new_plane_from(const std::shared_ptr& src_image, - heif_channel src_channel, - heif_channel dst_channel, - const heif_security_limits* limits) -{ - assert(src_image->has_channel(src_channel)); - assert(!has_channel(dst_channel)); - - uint32_t width = src_image->get_width(src_channel); - uint32_t height = src_image->get_height(src_channel); - - auto src_plane_iter = src_image->m_planes.find(src_channel); - assert(src_plane_iter != src_image->m_planes.end()); - const auto& src_plane = src_plane_iter->second; - - auto err = add_channel(dst_channel, width, height, - src_plane.m_datatype, - src_image->get_bits_per_pixel(src_channel), limits); - if (err) { - return err; - } - - uint8_t* dst; - size_t dst_stride = 0; - - const uint8_t* src; - size_t src_stride = 0; - - src = src_image->get_plane(src_channel, &src_stride); - dst = get_plane(dst_channel, &dst_stride); - - uint32_t bpl = width * (src_image->get_storage_bits_per_pixel(src_channel) / 8); - - for (uint32_t y = 0; y < height; y++) { - memcpy(dst + y * dst_stride, src + y * src_stride, bpl); - } - - return Error::Ok; -} - - -Error HeifPixelImage::extract_alpha_from_RGBA(const std::shared_ptr& src_image, - const heif_security_limits* limits) -{ - uint32_t width = src_image->get_width(); - uint32_t height = src_image->get_height(); - - if (Error err = add_plane(heif_channel_Y, width, height, src_image->get_bits_per_pixel(heif_channel_interleaved), limits)) { - return err; - } - - uint8_t* dst; - size_t dst_stride = 0; - - const uint8_t* src; - size_t src_stride = 0; - - src = src_image->get_plane(heif_channel_interleaved, &src_stride); - dst = get_plane(heif_channel_Y, &dst_stride); - - //int bpl = width * (src_image->get_storage_bits_per_pixel(src_channel) / 8); - - for (uint32_t y = 0; y < height; y++) { - for (uint32_t x = 0; x < width; x++) { - dst[y * dst_stride + x] = src[y * src_stride + 4 * x + 3]; - } - } - - return Error::Ok; -} - - -Error HeifPixelImage::fill_new_plane(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, - const heif_security_limits* limits) -{ - if (Error err = add_plane(dst_channel, width, height, bpp, limits)) { - return err; - } - - fill_plane(dst_channel, value); - - return Error::Ok; -} - - -void HeifPixelImage::fill_plane(heif_channel dst_channel, uint16_t value) -{ - int num_interleaved = num_interleaved_pixels_per_plane(m_chroma); - - int bpp = get_bits_per_pixel(dst_channel); - uint32_t width = get_width(dst_channel); - uint32_t height = get_height(dst_channel); - - if (bpp <= 8) { - uint8_t* dst; - size_t dst_stride = 0; - dst = get_plane(dst_channel, &dst_stride); - uint32_t width_bytes = width * num_interleaved; - - for (uint32_t y = 0; y < height; y++) { - memset(dst + y * dst_stride, value, width_bytes); - } - } - else { - uint16_t* dst; - size_t dst_stride = 0; - dst = (uint16_t*) get_plane(dst_channel, &dst_stride); - - dst_stride /= 2; - - for (uint32_t y = 0; y < height; y++) { - for (uint32_t x = 0; x < width * num_interleaved; x++) { - dst[y * dst_stride + x] = value; - } - } - } -} - - -void HeifPixelImage::transfer_plane_from_image_as(const std::shared_ptr& source, - heif_channel src_channel, - heif_channel dst_channel) -{ - // TODO: check that dst_channel does not exist yet - - ImagePlane plane = source->m_planes[src_channel]; - source->m_planes.erase(src_channel); - - m_planes.insert(std::make_pair(dst_channel, plane)); -} - - -bool is_interleaved_with_alpha(heif_chroma chroma) -{ - switch (chroma) { - case heif_chroma_undefined: - case heif_chroma_monochrome: - case heif_chroma_420: - case heif_chroma_422: - case heif_chroma_444: - case heif_chroma_interleaved_RGB: - case heif_chroma_interleaved_RRGGBB_BE: - case heif_chroma_interleaved_RRGGBB_LE: - return false; - - case heif_chroma_interleaved_RGBA: - case heif_chroma_interleaved_RRGGBBAA_BE: - case heif_chroma_interleaved_RRGGBBAA_LE: - return true; - } - - assert(false); - return false; -} - - -Error HeifPixelImage::copy_image_to(const std::shared_ptr& source, uint32_t x0, uint32_t y0) -{ - std::set channels = source->get_channel_set(); - - uint32_t w = get_width(); - uint32_t h = get_height(); - heif_chroma chroma = get_chroma_format(); - - - for (heif_channel channel : channels) { - - size_t tile_stride; - const uint8_t* tile_data = source->get_plane(channel, &tile_stride); - - size_t out_stride; - uint8_t* out_data = get_plane(channel, &out_stride); - - if (w <= x0 || h <= y0) { - return {heif_error_Invalid_input, - heif_suberror_Invalid_grid_data}; - } - - if (source->get_bits_per_pixel(channel) != get_bits_per_pixel(channel)) { - return {heif_error_Invalid_input, - heif_suberror_Wrong_tile_image_pixel_depth}; - } - - uint32_t src_width = source->get_width(channel); - uint32_t src_height = source->get_height(channel); - - uint32_t copy_width = std::min(src_width, channel_width(w - x0, chroma, channel)); - uint32_t copy_height = std::min(src_height, channel_height(h - y0, chroma, channel)); - - copy_width *= source->get_storage_bits_per_pixel(channel) / 8; - - uint32_t xs = channel_width(x0, chroma, channel); - uint32_t ys = channel_height(y0, chroma, channel); - xs *= source->get_storage_bits_per_pixel(channel) / 8; - - for (uint32_t py = 0; py < copy_height; py++) { - memcpy(out_data + xs + (ys + py) * out_stride, - tile_data + py * tile_stride, - copy_width); - } - } - - return Error::Ok; -} - - -Result> HeifPixelImage::rotate_ccw(int angle_degrees, const heif_security_limits* limits) -{ - // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before rotation - - bool need_conversion = false; - - if (get_chroma_format() == heif_chroma_422) { - if (angle_degrees == 90 || angle_degrees == 270) { - need_conversion = true; - } - else if (angle_degrees == 180 && has_odd_height()) { - need_conversion = true; - } - } - else if (get_chroma_format() == heif_chroma_420) { - if (angle_degrees == 90 && has_odd_width()) { - need_conversion = true; - } - else if (angle_degrees == 180 && (has_odd_width() || has_odd_height())) { - need_conversion = true; - } - else if (angle_degrees == 270 && has_odd_height()) { - need_conversion = true; - } - } - - if (need_conversion) { - heif_color_conversion_options options{}; - heif_color_conversion_options_set_defaults(&options); - - auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, nullptr, get_bits_per_pixel(heif_channel_Y), options, limits); - if (converted_image_result.error) { - return converted_image_result.error; - } - - return (*converted_image_result)->rotate_ccw(angle_degrees, limits); - } - - - // --- create output image - - if (angle_degrees == 0) { - return shared_from_this(); - } - - uint32_t out_width = m_width; - uint32_t out_height = m_height; - - if (angle_degrees == 90 || angle_degrees == 270) { - std::swap(out_width, out_height); - } - - std::shared_ptr out_img = std::make_shared(); - out_img->create(out_width, out_height, m_colorspace, m_chroma); - - - // --- rotate all channels - - for (const auto &plane_pair: m_planes) { - heif_channel channel = plane_pair.first; - const ImagePlane &plane = plane_pair.second; - - /* - if (plane.bit_depth != 8) { - return Error(heif_error_Unsupported_feature, - heif_suberror_Unspecified, - "Can currently only rotate images with 8 bits per pixel"); - } - */ - - uint32_t out_plane_width = plane.m_width; - uint32_t out_plane_height = plane.m_height; - - if (angle_degrees == 90 || angle_degrees == 270) { - std::swap(out_plane_width, out_plane_height); - } - - Error err = out_img->add_channel(channel, out_plane_width, out_plane_height, plane.m_datatype, plane.m_bit_depth, limits); - if (err) { - return err; - } - - auto out_plane_iter = out_img->m_planes.find(channel); - assert(out_plane_iter != out_img->m_planes.end()); - ImagePlane& out_plane = out_plane_iter->second; - - if (plane.m_bit_depth <= 8) { - plane.rotate_ccw(angle_degrees, out_plane); - } - else if (plane.m_bit_depth <= 16) { - plane.rotate_ccw(angle_degrees, out_plane); - } - else if (plane.m_bit_depth <= 32) { - plane.rotate_ccw(angle_degrees, out_plane); - } - else if (plane.m_bit_depth <= 64) { - plane.rotate_ccw(angle_degrees, out_plane); - } - else if (plane.m_bit_depth <= 128) { - plane.rotate_ccw(angle_degrees, out_plane); - } - } - // --- pass the color profiles to the new image - - out_img->set_color_profile_nclx(get_color_profile_nclx()); - out_img->set_color_profile_icc(get_color_profile_icc()); - - return out_img; -} - -template -void HeifPixelImage::ImagePlane::rotate_ccw(int angle_degrees, - ImagePlane& out_plane) const -{ - uint32_t w = m_width; - uint32_t h = m_height; - - uint32_t in_stride = stride / uint32_t(sizeof(T)); - const T* in_data = static_cast(mem); - - uint32_t out_stride = out_plane.stride / uint32_t(sizeof(T)); - T* out_data = static_cast(out_plane.mem); - - if (angle_degrees == 270) { - for (uint32_t x = 0; x < h; x++) - for (uint32_t y = 0; y < w; y++) { - out_data[y * out_stride + x] = in_data[(h - 1 - x) * in_stride + y]; - } - } else if (angle_degrees == 180) { - for (uint32_t y = 0; y < h; y++) - for (uint32_t x = 0; x < w; x++) { - out_data[y * out_stride + x] = in_data[(h - 1 - y) * in_stride + (w - 1 - x)]; - } - } else if (angle_degrees == 90) { - for (uint32_t x = 0; x < h; x++) - for (uint32_t y = 0; y < w; y++) { - out_data[y * out_stride + x] = in_data[x * in_stride + (w - 1 - y)]; - } - } -} - - -template -void HeifPixelImage::ImagePlane::mirror_inplace(heif_transform_mirror_direction direction) -{ - uint32_t w = m_width; - uint32_t h = m_height; - - T* data = static_cast(mem); - - if (direction == heif_transform_mirror_direction_horizontal) { - for (uint32_t y = 0; y < h; y++) { - for (uint32_t x = 0; x < w / 2; x++) - std::swap(data[y * stride / sizeof(T) + x], data[y * stride / sizeof(T) + w - 1 - x]); - } - } else { - for (uint32_t y = 0; y < h / 2; y++) { - for (uint32_t x = 0; x < w; x++) - std::swap(data[y * stride / sizeof(T) + x], data[(h - 1 - y) * stride / sizeof(T) + x]); - } - } -} - - -Result> HeifPixelImage::mirror_inplace(heif_transform_mirror_direction direction, - const heif_security_limits* limits) -{ - // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before rotation - - bool need_conversion = false; - - if (get_chroma_format() == heif_chroma_422) { - if (direction == heif_transform_mirror_direction_horizontal && has_odd_width()) { - need_conversion = true; - } - } - else if (get_chroma_format() == heif_chroma_420) { - if (has_odd_width() || has_odd_height()) { - need_conversion = true; - } - } - - if (need_conversion) { - heif_color_conversion_options options{}; - heif_color_conversion_options_set_defaults(&options); - - auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, nullptr, get_bits_per_pixel(heif_channel_Y), options, limits); - if (converted_image_result.error) { - return converted_image_result.error; - } - - return (*converted_image_result)->mirror_inplace(direction, limits); - } - - - for (auto& plane_pair : m_planes) { - ImagePlane& plane = plane_pair.second; - - if (plane.m_bit_depth <= 8) { - plane.mirror_inplace(direction); - } - else if (plane.m_bit_depth <= 16) { - plane.mirror_inplace(direction); - } - else if (plane.m_bit_depth <= 32) { - plane.mirror_inplace(direction); - } - else if (plane.m_bit_depth <= 64) { - plane.mirror_inplace(direction); - } - else if (plane.m_bit_depth <= 128) { - plane.mirror_inplace(direction); - } - else { - std::stringstream sstr; - sstr << "Cannot mirror images with " << plane.m_bit_depth << " bits per pixel"; - return Error{heif_error_Unsupported_feature, - heif_suberror_Unspecified, - sstr.str()}; - } - } - - return shared_from_this(); -} - - -int HeifPixelImage::ImagePlane::get_bytes_per_pixel() const -{ - if (m_bit_depth <= 8) { - return 1; - } - else if (m_bit_depth <= 16) { - return 2; - } - else if (m_bit_depth <= 32) { - return 4; - } - else if (m_bit_depth <= 64) { - return 8; - } - else { - assert(m_bit_depth <= 128); - return 16; - } -} - - -Result> HeifPixelImage::crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, - const heif_security_limits* limits) const -{ - // --- for some subsampled chroma colorspaces, we have to transform to 4:4:4 before cropping - - bool need_conversion = false; - - if (get_chroma_format() == heif_chroma_422 && (left & 1) == 1) { - need_conversion = true; - } - else if (get_chroma_format() == heif_chroma_420 && - ((left & 1) == 1 || (top & 1) == 1)) { - need_conversion = true; - } - - if (need_conversion) { - heif_color_conversion_options options{}; - heif_color_conversion_options_set_defaults(&options); - - auto converted_image_result = convert_colorspace(shared_from_this(), heif_colorspace_YCbCr, heif_chroma_444, nullptr, get_bits_per_pixel(heif_channel_Y), options, limits); - if (converted_image_result.error) { - return converted_image_result.error; - } - - return (*converted_image_result)->crop(left, right, top, bottom, limits); - } - - - - auto out_img = std::make_shared(); - out_img->create(right - left + 1, bottom - top + 1, m_colorspace, m_chroma); - - - // --- crop all channels - - for (const auto& plane_pair : m_planes) { - heif_channel channel = plane_pair.first; - const ImagePlane& plane = plane_pair.second; - - uint32_t plane_left = get_subsampled_size_h(left, channel, m_chroma, scaling_mode::is_divisible); - uint32_t plane_right = get_subsampled_size_h(right, channel, m_chroma, scaling_mode::round_up); // keep more chroma - uint32_t plane_top = get_subsampled_size_v(top, channel, m_chroma, scaling_mode::is_divisible); // is always divisible - uint32_t plane_bottom = get_subsampled_size_v(bottom, channel, m_chroma, scaling_mode::round_up); // keep more chroma - - auto err = out_img->add_channel(channel, - plane_right - plane_left + 1, - plane_bottom - plane_top + 1, - plane.m_datatype, - plane.m_bit_depth, - limits); - if (err) { - return err; - } - - auto out_plane_iter = out_img->m_planes.find(channel); - assert(out_plane_iter != out_img->m_planes.end()); - ImagePlane& out_plane = out_plane_iter->second; - - int bytes_per_pixel = plane.get_bytes_per_pixel(); - plane.crop(plane_left, plane_right, plane_top, plane_bottom, bytes_per_pixel, out_plane); - } - - // --- pass the color profiles to the new image - - out_img->set_color_profile_nclx(get_color_profile_nclx()); - out_img->set_color_profile_icc(get_color_profile_icc()); - - return out_img; -} - - -void HeifPixelImage::ImagePlane::crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, - int bytes_per_pixel, ImagePlane& out_plane) const -{ - uint32_t in_stride = stride; - auto* in_data = static_cast(mem); - - uint32_t out_stride = out_plane.stride; - auto* out_data = static_cast(out_plane.mem); - - for (uint32_t y = top; y <= bottom; y++) { - memcpy(&out_data[(y - top) * out_stride], - &in_data[y * in_stride + left * bytes_per_pixel], - (right - left + 1) * bytes_per_pixel); - } -} - - -Error HeifPixelImage::fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_t a) -{ - for (const auto& channel : {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}) { - - const auto plane_iter = m_planes.find(channel); - if (plane_iter == m_planes.end()) { - - // alpha channel is optional, R,G,B is required - if (channel == heif_channel_Alpha) { - continue; - } - - return {heif_error_Usage_error, - heif_suberror_Nonexisting_image_channel_referenced}; - - } - - ImagePlane& plane = plane_iter->second; - - if (plane.m_bit_depth != 8) { - return {heif_error_Unsupported_feature, - heif_suberror_Unspecified, - "Can currently only fill images with 8 bits per pixel"}; - } - - size_t h = plane.m_height; - - size_t stride = plane.stride; - auto* data = static_cast(plane.mem); - - uint16_t val16; - switch (channel) { - case heif_channel_R: - val16 = r; - break; - case heif_channel_G: - val16 = g; - break; - case heif_channel_B: - val16 = b; - break; - case heif_channel_Alpha: - val16 = a; - break; - default: - // initialization only to avoid warning of uninitialized variable. - val16 = 0; - // Should already be detected by the check above ("m_planes.find"). - assert(false); - } - - auto val8 = static_cast(val16 >> 8U); - - - // memset() even when h * stride > sizeof(size_t) - - if (std::numeric_limits::max() / stride > h) { - // can fill in one step - memset(data, val8, stride * h); - } - else { - // fill line by line - auto* p = data; - - for (size_t y=0;y(INT32_MAX) + 1; - } - else { - return static_cast(-x); - } -} - - -Error HeifPixelImage::overlay(std::shared_ptr& overlay, int32_t dx, int32_t dy) -{ - std::set channels = overlay->get_channel_set(); - - bool has_alpha = overlay->has_channel(heif_channel_Alpha); - //bool has_alpha_me = has_channel(heif_channel_Alpha); - - size_t alpha_stride = 0; - uint8_t* alpha_p; - alpha_p = overlay->get_plane(heif_channel_Alpha, &alpha_stride); - - for (heif_channel channel : channels) { - if (!has_channel(channel)) { - continue; - } - - size_t in_stride = 0; - const uint8_t* in_p; - - size_t out_stride = 0; - uint8_t* out_p; - - in_p = overlay->get_plane(channel, &in_stride); - out_p = get_plane(channel, &out_stride); - - uint32_t in_w = overlay->get_width(channel); - uint32_t in_h = overlay->get_height(channel); - - uint32_t out_w = get_width(channel); - uint32_t out_h = get_height(channel); - - // top-left points where to start copying in source and destination - uint32_t in_x0; - uint32_t in_y0; - uint32_t out_x0; - uint32_t out_y0; - - if (dx > 0 && static_cast(dx) >= out_w) { - // the overlay image is completely outside the right border -> skip overlaying - return Error::Ok; - } - else if (dx < 0 && in_w <= negate_negative_int32(dx)) { - // the overlay image is completely outside the left border -> skip overlaying - return Error::Ok; - } - - if (dx < 0) { - // overlay image started partially outside of left border - - in_x0 = negate_negative_int32(dx); - out_x0 = 0; - in_w = in_w - in_x0; // in_x0 < in_w because in_w > -dx = in_x0 - } - else { - in_x0 = 0; - out_x0 = static_cast(dx); - } - - // we know that dx >= 0 && dx < out_w - - if (static_cast(dx) > UINT32_MAX - in_w || - dx + in_w > out_w) { - // overlay image extends partially outside of right border - - in_w = out_w - static_cast(dx); // we know that dx < out_w from first condition - } - - - if (dy > 0 && static_cast(dy) >= out_h) { - // the overlay image is completely outside the bottom border -> skip overlaying - return Error::Ok; - } - else if (dy < 0 && in_h <= negate_negative_int32(dy)) { - // the overlay image is completely outside the top border -> skip overlaying - return Error::Ok; - } - - if (dy < 0) { - // overlay image started partially outside of top border - - in_y0 = negate_negative_int32(dy); - out_y0 = 0; - in_h = in_h - in_y0; // in_y0 < in_h because in_h > -dy = in_y0 - } - else { - in_y0 = 0; - out_y0 = static_cast(dy); - } - - // we know that dy >= 0 && dy < out_h - - if (static_cast(dy) > UINT32_MAX - in_h || - dy + in_h > out_h) { - // overlay image extends partially outside of bottom border - - in_h = out_h - static_cast(dy); // we know that dy < out_h from first condition - } - - - for (uint32_t y = in_y0; y < in_h; y++) { - if (!has_alpha) { - memcpy(out_p + out_x0 + (out_y0 + y - in_y0) * out_stride, - in_p + in_x0 + y * in_stride, - in_w - in_x0); - } - else { - for (uint32_t x = in_x0; x < in_w; x++) { - uint8_t* outptr = &out_p[out_x0 + (out_y0 + y - in_y0) * out_stride + x]; - uint8_t in_val = in_p[in_x0 + y * in_stride + x]; - uint8_t alpha_val = alpha_p[in_x0 + y * in_stride + x]; - - *outptr = (uint8_t) ((in_val * alpha_val + *outptr * (255 - alpha_val)) / 255); - } - } - } - } - - return Error::Ok; -} - - -Error HeifPixelImage::scale_nearest_neighbor(std::shared_ptr& out_img, - uint32_t width, uint32_t height, - const heif_security_limits* limits) const -{ - out_img = std::make_shared(); - out_img->create(width, height, m_colorspace, m_chroma); - - - // --- create output image with scaled planes - - if (has_channel(heif_channel_interleaved)) { - if (auto err = out_img->add_plane(heif_channel_interleaved, width, height, get_bits_per_pixel(heif_channel_interleaved), limits)) { - return err; - } - } - else { - if (get_colorspace() == heif_colorspace_RGB) { - if (!has_channel(heif_channel_R) || - !has_channel(heif_channel_G) || - !has_channel(heif_channel_B)) { - return {heif_error_Invalid_input, heif_suberror_Unspecified, "RGB input without R,G,B, planes"}; - } - - if (auto err = out_img->add_plane(heif_channel_R, width, height, get_bits_per_pixel(heif_channel_R), limits)) { - return err; - } - if (auto err = out_img->add_plane(heif_channel_G, width, height, get_bits_per_pixel(heif_channel_G), limits)) { - return err; - } - if (auto err = out_img->add_plane(heif_channel_B, width, height, get_bits_per_pixel(heif_channel_B), limits)) { - return err; - } - } - else if (get_colorspace() == heif_colorspace_monochrome) { - if (!has_channel(heif_channel_Y)) { - return {heif_error_Invalid_input, heif_suberror_Unspecified, "monochrome input with no Y plane"}; - } - - if (auto err = out_img->add_plane(heif_channel_Y, width, height, get_bits_per_pixel(heif_channel_Y), limits)) { - return err; - } - } - else if (get_colorspace() == heif_colorspace_YCbCr) { - if (!has_channel(heif_channel_Y) || - !has_channel(heif_channel_Cb) || - !has_channel(heif_channel_Cr)) { - return {heif_error_Invalid_input, heif_suberror_Unspecified, "YCbCr image without Y,Cb,Cr planes"}; - } - - uint32_t cw, ch; - get_subsampled_size(width, height, heif_channel_Cb, get_chroma_format(), &cw, &ch); - if (auto err = out_img->add_plane(heif_channel_Y, width, height, get_bits_per_pixel(heif_channel_Y), limits)) { - return err; - } - if (auto err = out_img->add_plane(heif_channel_Cb, cw, ch, get_bits_per_pixel(heif_channel_Cb), limits)) { - return err; - } - if (auto err = out_img->add_plane(heif_channel_Cr, cw, ch, get_bits_per_pixel(heif_channel_Cr), limits)) { - return err; - } - } - else { - return {heif_error_Invalid_input, heif_suberror_Unspecified, "unknown color configuration"}; - } - - if (has_channel(heif_channel_Alpha)) { - if (auto err = out_img->add_plane(heif_channel_Alpha, width, height, get_bits_per_pixel(heif_channel_Alpha), limits)) { - return err; - } - } - } - - - // --- scale all channels - - for (const auto& plane_pair : m_planes) { - heif_channel channel = plane_pair.first; - const ImagePlane& plane = plane_pair.second; - - const int bpp = get_storage_bits_per_pixel(channel) / 8; - - if (!out_img->has_channel(channel)) { - return {heif_error_Invalid_input, heif_suberror_Unspecified, "scaling input has extra color plane"}; - } - - - if (plane.m_bit_depth != 8) { - return {heif_error_Unsupported_feature, - heif_suberror_Unspecified, - "Can currently only crop images with 8 bits per pixel"}; - } - - uint32_t out_w = out_img->get_width(channel); - uint32_t out_h = out_img->get_height(channel); - - size_t in_stride = plane.stride; - const auto* in_data = static_cast(plane.mem); - - size_t out_stride = 0; - auto* out_data = static_cast(out_img->get_plane(channel, &out_stride)); - - - for (uint32_t y = 0; y < out_h; y++) { - uint32_t iy = y * m_height / height; - - if (bpp == 1) { - for (uint32_t x = 0; x < out_w; x++) { - uint32_t ix = x * m_width / width; - - out_data[y * out_stride + x] = in_data[iy * in_stride + ix]; - } - } - else { - for (uint32_t x = 0; x < out_w; x++) { - uint32_t ix = x * m_width / width; - - for (int b = 0; b < bpp; b++) { - out_data[y * out_stride + bpp * x + b] = in_data[iy * in_stride + bpp * ix + b]; - } - } - } - } - } - - return Error::Ok; -} - - -void HeifPixelImage::forward_all_metadata_from(const std::shared_ptr& src_image) -{ - set_color_profile_nclx(src_image->get_color_profile_nclx()); - set_color_profile_icc(src_image->get_color_profile_icc()); - - if (src_image->has_nonsquare_pixel_ratio()) { - uint32_t h,v; - src_image->get_pixel_ratio(&h,&v); - set_pixel_ratio(h,v); - } - - if (src_image->has_clli()) { - set_clli(src_image->get_clli()); - } - - if (src_image->has_mdcv()) { - set_mdcv(src_image->get_mdcv()); - } - - set_premultiplied_alpha(src_image->is_premultiplied_alpha()); - - // TODO: TAI timestamp and contentID (once we merge that branch) - - // TODO: should we also forward the warnings? It might be better to do that in ImageItem_Grid. -} - - -void HeifPixelImage::debug_dump() const -{ - auto channels = get_channel_set(); - for (auto c : channels) { - size_t stride = 0; - const uint8_t* p = get_plane(c, &stride); - - for (int y = 0; y < 8; y++) { - for (int x = 0; x < 8; x++) { - printf("%02x ", p[y * stride + x]); - } - printf("\n"); - } - } -} - -Error HeifPixelImage::create_clone_image_at_new_size(const std::shared_ptr& source, uint32_t w, uint32_t h, - const heif_security_limits* limits) -{ - heif_colorspace colorspace = source->get_colorspace(); - heif_chroma chroma = source->get_chroma_format(); - - create(w, h, colorspace, chroma); - - switch (colorspace) { - case heif_colorspace_monochrome: - if (auto err = add_plane(heif_channel_Y, w, h, source->get_bits_per_pixel(heif_channel_Y), limits)) { - return err; - } - break; - case heif_colorspace_YCbCr: - if (auto err = add_plane(heif_channel_Y, w, h, source->get_bits_per_pixel(heif_channel_Y), limits)) { - return err; - } - if (auto err = add_plane(heif_channel_Cb, chroma_width(w, chroma), chroma_height(h, chroma), source->get_bits_per_pixel(heif_channel_Cb), limits)) { - return err; - } - if (auto err = add_plane(heif_channel_Cr, chroma_width(w, chroma), chroma_height(h, chroma), source->get_bits_per_pixel(heif_channel_Cr), limits)) { - return err; - } - break; - case heif_colorspace_RGB: - if (auto err = add_plane(heif_channel_R, w, h, source->get_bits_per_pixel(heif_channel_R), limits)) { - return err; - } - if (auto err = add_plane(heif_channel_G, w, h, source->get_bits_per_pixel(heif_channel_G), limits)) { - return err; - } - if (auto err = add_plane(heif_channel_B, w, h, source->get_bits_per_pixel(heif_channel_B), limits)) { - return err; - } - break; - default: - assert(false); - break; - } - - if (source->has_alpha()) { - if (auto err = add_plane(heif_channel_Alpha, w, h, source->get_bits_per_pixel(heif_channel_Alpha), limits)) { - return err; - } - } - - return Error::Ok; -} - - -Result> -HeifPixelImage::extract_image_area(uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, - const heif_security_limits* limits) const -{ - uint32_t minW = std::min(w, get_width() - x0); - uint32_t minH = std::min(h, get_height() - y0); - - auto areaImg = std::make_shared(); - Error err = areaImg->create_clone_image_at_new_size(shared_from_this(), minW, minH, limits); - if (err) { - return err; - } - - std::set channels = get_channel_set(); - heif_chroma chroma = get_chroma_format(); - - for (heif_channel channel : channels) { - - size_t src_stride; - const uint8_t* src_data = get_plane(channel, &src_stride); - - size_t out_stride; - uint8_t* out_data = areaImg->get_plane(channel, &out_stride); - - if (areaImg->get_bits_per_pixel(channel) != get_bits_per_pixel(channel)) { - return Error{ - heif_error_Invalid_input, - heif_suberror_Wrong_tile_image_pixel_depth - }; - } - - uint32_t copy_width = channel_width(minW, chroma, channel); - uint32_t copy_height = channel_height(minH, chroma, channel); - - copy_width *= get_storage_bits_per_pixel(channel) / 8; - - uint32_t xs = channel_width(x0, chroma, channel); - uint32_t ys = channel_height(y0, chroma, channel); - xs *= get_storage_bits_per_pixel(channel) / 8; - - for (uint32_t py = 0; py < copy_height; py++) { - memcpy(out_data + py * out_stride, - src_data + xs + (ys + py) * src_stride, - copy_width); - } - } - - err = areaImg->extend_to_size_with_zero(w,h,limits); - if (err) { - return err; - } - - return areaImg; -} diff -Nru libheif-1.19.8/libheif/pixelimage.h libheif-1.23.4/libheif/pixelimage.h --- libheif-1.19.8/libheif/pixelimage.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/pixelimage.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,313 +0,0 @@ -/* - * HEIF codec. - * Copyright (c) 2017 Dirk Farin - * - * This file is part of libheif. - * - * libheif is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * libheif is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with libheif. If not, see . -*/ - - -#ifndef LIBHEIF_IMAGE_H -#define LIBHEIF_IMAGE_H - -//#include "heif.h" -#include "error.h" -#include "nclx.h" -#include - -#include -#include -#include -#include -#include -#include - - -heif_chroma chroma_from_subsampling(int h, int v); - -uint32_t chroma_width(uint32_t w, heif_chroma chroma); - -uint32_t chroma_height(uint32_t h, heif_chroma chroma); - -uint32_t channel_width(uint32_t w, heif_chroma chroma, heif_channel channel); - -uint32_t channel_height(uint32_t h, heif_chroma chroma, heif_channel channel); - -bool is_interleaved_with_alpha(heif_chroma chroma); - -int num_interleaved_pixels_per_plane(heif_chroma chroma); - -bool is_integer_multiple_of_chroma_size(uint32_t width, - uint32_t height, - heif_chroma chroma); - -// Returns the list of valid heif_chroma values for a given colorspace. -std::vector get_valid_chroma_values_for_colorspace(heif_colorspace colorspace); - -// TODO: move to public API when used -enum heif_chroma420_sample_position { - // values 0-5 according to ISO 23091-2 / ITU-T H.273 - heif_chroma420_sample_position_00_05 = 0, - heif_chroma420_sample_position_05_05 = 1, - heif_chroma420_sample_position_00_00 = 2, - heif_chroma420_sample_position_05_00 = 3, - heif_chroma420_sample_position_00_10 = 4, - heif_chroma420_sample_position_05_10 = 5, - - // values 6 according to ISO 23001-17 - heif_chroma420_sample_position_00_00_01_00 = 6 -}; - - -class HeifPixelImage : public std::enable_shared_from_this, - public ErrorBuffer -{ -public: - explicit HeifPixelImage() = default; - - ~HeifPixelImage(); - - void create(uint32_t width, uint32_t height, heif_colorspace colorspace, heif_chroma chroma); - - Error create_clone_image_at_new_size(const std::shared_ptr& source, uint32_t w, uint32_t h, - const heif_security_limits* limits); - - Error add_plane(heif_channel channel, uint32_t width, uint32_t height, int bit_depth, const heif_security_limits* limits); - - Error add_channel(heif_channel channel, uint32_t width, uint32_t height, heif_channel_datatype datatype, int bit_depth, - const heif_security_limits* limits); - - bool has_channel(heif_channel channel) const; - - // Has alpha information either as a separate channel or in the interleaved format. - bool has_alpha() const; - - bool is_premultiplied_alpha() const { return m_premultiplied_alpha; } - - void set_premultiplied_alpha(bool flag) { m_premultiplied_alpha = flag; } - - uint32_t get_width() const { return m_width; } - - uint32_t get_height() const { return m_height; } - - uint32_t get_width(enum heif_channel channel) const; - - uint32_t get_height(enum heif_channel channel) const; - - bool has_odd_width() const { return !!(m_width & 1); } - - bool has_odd_height() const { return !!(m_height & 1); } - - heif_chroma get_chroma_format() const { return m_chroma; } - - heif_colorspace get_colorspace() const { return m_colorspace; } - - std::set get_channel_set() const; - - uint8_t get_storage_bits_per_pixel(enum heif_channel channel) const; - - uint8_t get_bits_per_pixel(enum heif_channel channel) const; - - // Get the maximum bit depth of a visual channel (YCbCr or RGB). - uint8_t get_visual_image_bits_per_pixel() const; - - heif_channel_datatype get_datatype(enum heif_channel channel) const; - - int get_number_of_interleaved_components(heif_channel channel) const; - - // Note: we are using size_t as stride type since the stride is usually involved in a multiplication with the line number. - // For very large images (e.g. >2 GB), this can result in an integer overflow and corresponding illegal memory access. - // (see https://github.com/strukturag/libheif/issues/1419) - uint8_t* get_plane(enum heif_channel channel, size_t* out_stride) { return get_channel(channel, out_stride); } - - const uint8_t* get_plane(enum heif_channel channel, size_t* out_stride) const { return get_channel(channel, out_stride); } - - template - T* get_channel(enum heif_channel channel, size_t* out_stride) - { - auto iter = m_planes.find(channel); - if (iter == m_planes.end()) { - if (out_stride) - *out_stride = 0; - - return nullptr; - } - - if (out_stride) { - *out_stride = static_cast(iter->second.stride / sizeof(T)); - } - - //assert(sizeof(T) == iter->second.get_bytes_per_pixel()); - - return static_cast(iter->second.mem); - } - - template - const T* get_channel(enum heif_channel channel, size_t* out_stride) const - { - return const_cast(this)->get_channel(channel, out_stride); - } - - Error copy_new_plane_from(const std::shared_ptr& src_image, - heif_channel src_channel, - heif_channel dst_channel, - const heif_security_limits* limits); - - Error extract_alpha_from_RGBA(const std::shared_ptr& srcimage, const heif_security_limits* limits); - - void fill_plane(heif_channel dst_channel, uint16_t value); - - Error fill_new_plane(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, const heif_security_limits* limits); - - void transfer_plane_from_image_as(const std::shared_ptr& source, - heif_channel src_channel, - heif_channel dst_channel); - - Error copy_image_to(const std::shared_ptr& source, uint32_t x0, uint32_t y0); - - Result> rotate_ccw(int angle_degrees, const heif_security_limits* limits); - - Result> mirror_inplace(heif_transform_mirror_direction, const heif_security_limits* limits); - - Result> crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, - const heif_security_limits* limits) const; - - Error fill_RGB_16bit(uint16_t r, uint16_t g, uint16_t b, uint16_t a); - - Error overlay(std::shared_ptr& overlay, int32_t dx, int32_t dy); - - Error scale_nearest_neighbor(std::shared_ptr& output, uint32_t width, uint32_t height, - const heif_security_limits* limits) const; - - void set_color_profile_nclx(const std::shared_ptr& profile) { m_color_profile_nclx = profile; } - - const std::shared_ptr& get_color_profile_nclx() const { return m_color_profile_nclx; } - - void set_color_profile_icc(const std::shared_ptr& profile) { m_color_profile_icc = profile; } - - const std::shared_ptr& get_color_profile_icc() const { return m_color_profile_icc; } - - void forward_all_metadata_from(const std::shared_ptr& src_image); - - void debug_dump() const; - - Error extend_padding_to_size(uint32_t width, uint32_t height, bool adjust_size, - const heif_security_limits* limits); - - Error extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits); - - Result> extract_image_area(uint32_t x0, uint32_t y0, uint32_t w, uint32_t h, - const heif_security_limits* limits) const; - - - // --- pixel aspect ratio - - bool has_nonsquare_pixel_ratio() const { return m_PixelAspectRatio_h != m_PixelAspectRatio_v; } - - void get_pixel_ratio(uint32_t* h, uint32_t* v) const - { - *h = m_PixelAspectRatio_h; - *v = m_PixelAspectRatio_v; - } - - void set_pixel_ratio(uint32_t h, uint32_t v) - { - m_PixelAspectRatio_h = h; - m_PixelAspectRatio_v = v; - } - - // --- clli - - bool has_clli() const { return m_clli.max_content_light_level != 0 || m_clli.max_pic_average_light_level != 0; } - - heif_content_light_level get_clli() const { return m_clli; } - - void set_clli(const heif_content_light_level& clli) { m_clli = clli; } - - // --- mdcv - - bool has_mdcv() const { return m_mdcv_set; } - - heif_mastering_display_colour_volume get_mdcv() const { return m_mdcv; } - - void set_mdcv(const heif_mastering_display_colour_volume& mdcv) - { - m_mdcv = mdcv; - m_mdcv_set = true; - } - - void unset_mdcv() { m_mdcv_set = false; } - - // --- warnings - - void add_warning(Error warning) { m_warnings.emplace_back(std::move(warning)); } - - void add_warnings(const std::vector& warning) { for (const auto& err : warning) m_warnings.emplace_back(err); } - - const std::vector& get_warnings() const { return m_warnings; } - -private: - struct ImagePlane - { - // limits=nullptr disables the limits - Error alloc(uint32_t width, uint32_t height, heif_channel_datatype datatype, int bit_depth, int num_interleaved_components, - const heif_security_limits* limits); - - heif_channel_datatype m_datatype = heif_channel_datatype_unsigned_integer; - uint8_t m_bit_depth = 0; - uint8_t m_num_interleaved_components = 1; - - // the "visible" area of the plane - uint32_t m_width = 0; - uint32_t m_height = 0; - - // the allocated memory size - uint32_t m_mem_width = 0; - uint32_t m_mem_height = 0; - - void* mem = nullptr; // aligned memory start - uint8_t* allocated_mem = nullptr; // unaligned memory we allocated - uint32_t stride = 0; // bytes per line - - int get_bytes_per_pixel() const; - - template void mirror_inplace(heif_transform_mirror_direction); - - template - void rotate_ccw(int angle_degrees, ImagePlane& out_plane) const; - - void crop(uint32_t left, uint32_t right, uint32_t top, uint32_t bottom, int bytes_per_pixel, ImagePlane& out_plane) const; - }; - - uint32_t m_width = 0; - uint32_t m_height = 0; - heif_colorspace m_colorspace = heif_colorspace_undefined; - heif_chroma m_chroma = heif_chroma_undefined; - bool m_premultiplied_alpha = false; - std::shared_ptr m_color_profile_nclx; - std::shared_ptr m_color_profile_icc; - - std::map m_planes; - - uint32_t m_PixelAspectRatio_h = 1; - uint32_t m_PixelAspectRatio_v = 1; - heif_content_light_level m_clli{}; - heif_mastering_display_colour_volume m_mdcv{}; - bool m_mdcv_set = false; // replace with std::optional<> when we are on C*+17 - - std::vector m_warnings; -}; - -#endif diff -Nru libheif-1.19.8/libheif/plugin_registry.cc libheif-1.23.4/libheif/plugin_registry.cc --- libheif-1.19.8/libheif/plugin_registry.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugin_registry.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,6 +25,9 @@ #include "plugin_registry.h" #include "init.h" +#if HAVE_WEBCODECS +#include "plugins/decoder_webcodecs.h" +#endif #if HAVE_LIBDE265 #include "plugins/decoder_libde265.h" @@ -87,6 +90,10 @@ #include "plugins/encoder_jpeg.h" #endif +#if HAVE_X264 +#include "plugins/encoder_x264.h" +#endif + #if HAVE_OpenH264_DECODER #include "plugins/decoder_openh264.h" #endif @@ -105,19 +112,19 @@ #include "plugins/encoder_openjph.h" #endif -std::set s_decoder_plugins; +std::set s_decoder_plugins; -std::multiset, +std::multiset, encoder_descriptor_priority_order> s_encoder_descriptors; -std::set& get_decoder_plugins() +std::set& get_decoder_plugins() { load_plugins_if_not_initialized_yet(); return s_decoder_plugins; } -extern std::multiset, +extern std::multiset, encoder_descriptor_priority_order>& get_encoder_descriptors() { load_plugins_if_not_initialized_yet(); @@ -140,6 +147,10 @@ void register_default_plugins() { +#if HAVE_WEBCODECS + register_decoder(get_decoder_plugin_webcodecs()); +#endif + #if HAVE_LIBDE265 register_decoder(get_decoder_plugin_libde265()); #endif @@ -212,6 +223,10 @@ register_decoder(get_decoder_plugin_openh264()); #endif +#if HAVE_X264 + register_encoder(get_encoder_plugin_x264()); +#endif + #if WITH_UNCOMPRESSED_CODEC register_encoder(get_encoder_plugin_uncompressed()); register_decoder(get_decoder_plugin_uncompressed()); @@ -231,7 +246,22 @@ } -const struct heif_decoder_plugin* get_decoder(enum heif_compression_format type, const char* name_id) +bool has_decoder(heif_compression_format type, const char* name_id) +{ + load_plugins_if_not_initialized_yet(); + + for (const auto* plugin : s_decoder_plugins) { + int priority = plugin->does_support_format(type); + if (priority > 0 && strcmp(name_id, plugin->id_name) == 0) { + return true; + } + } + + return false; +} + + +const heif_decoder_plugin* get_decoder(heif_compression_format type, const char* name_id) { load_plugins_if_not_initialized_yet(); @@ -264,17 +294,15 @@ (*encoder_plugin->init_plugin)(); } - auto descriptor = std::unique_ptr(new heif_encoder_descriptor); + auto descriptor = std::unique_ptr(new heif_encoder_descriptor); descriptor->plugin = encoder_plugin; s_encoder_descriptors.insert(std::move(descriptor)); } -const struct heif_encoder_plugin* get_encoder(enum heif_compression_format type) +const heif_encoder_plugin* get_encoder(heif_compression_format type) { - load_plugins_if_not_initialized_yet(); - auto filtered_encoder_descriptors = get_filtered_encoder_descriptors(type, nullptr); if (filtered_encoder_descriptors.size() > 0) { return filtered_encoder_descriptors[0]->plugin; @@ -285,14 +313,16 @@ } -std::vector -get_filtered_encoder_descriptors(enum heif_compression_format format, +std::vector +get_filtered_encoder_descriptors(heif_compression_format format, const char* name) { - std::vector filtered_descriptors; + load_plugins_if_not_initialized_yet(); + + std::vector filtered_descriptors; for (const auto& descr : s_encoder_descriptors) { - const struct heif_encoder_plugin* plugin = descr->plugin; + const heif_encoder_plugin* plugin = descr->plugin; if (plugin->compression_format == format || format == heif_compression_undefined) { if (name == nullptr || strcmp(name, plugin->id_name) == 0) { diff -Nru libheif-1.19.8/libheif/plugin_registry.h libheif-1.23.4/libheif/plugin_registry.h --- libheif-1.19.8/libheif/plugin_registry.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugin_registry.h 2026-09-06 14:45:17.000000000 +0000 @@ -35,27 +35,27 @@ struct heif_encoder_descriptor { - const struct heif_encoder_plugin* plugin; + const heif_encoder_plugin* plugin; const char* get_name() const { return plugin->get_plugin_name(); } - enum heif_compression_format get_compression_format() const { return plugin->compression_format; } + heif_compression_format get_compression_format() const { return plugin->compression_format; } }; struct encoder_descriptor_priority_order { - bool operator()(const std::unique_ptr& a, - const std::unique_ptr& b) const + bool operator()(const std::unique_ptr& a, + const std::unique_ptr& b) const { return a->plugin->priority > b->plugin->priority; // highest priority first } }; -extern std::set& get_decoder_plugins(); +extern std::set& get_decoder_plugins(); -extern std::multiset, +extern std::multiset, encoder_descriptor_priority_order>& get_encoder_descriptors(); void register_default_plugins(); @@ -72,12 +72,14 @@ void heif_unregister_encoder_plugin(const heif_encoder_plugin* plugin); #endif -const struct heif_decoder_plugin* get_decoder(enum heif_compression_format type, const char* name_id); +bool has_decoder(heif_compression_format type, const char* name_id); -const struct heif_encoder_plugin* get_encoder(enum heif_compression_format type); +const heif_decoder_plugin* get_decoder(heif_compression_format type, const char* name_id); -std::vector -get_filtered_encoder_descriptors(enum heif_compression_format, +const heif_encoder_plugin* get_encoder(heif_compression_format type); + +std::vector +get_filtered_encoder_descriptors(heif_compression_format, const char* name); #endif diff -Nru libheif-1.19.8/libheif/plugins/CMakeLists.txt libheif-1.23.4/libheif/plugins/CMakeLists.txt --- libheif-1.19.8/libheif/plugins/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -54,7 +54,7 @@ plugin_compilation(libde265 LIBDE265 LIBDE265_FOUND LIBDE265 LIBDE265) set(DAV1D_sources decoder_dav1d.cc decoder_dav1d.h) -set(DAV1D_extra_plugin_sources ../common_utils.cc ../common_utils.h) +set(DAV1D_extra_plugin_sources ../error.cc ../common_utils.cc ../common_utils.h) plugin_compilation(dav1d DAV1D DAV1D_FOUND DAV1D DAV1D) set(AOM_DECODER_sources decoder_aom.cc decoder_aom.h) @@ -66,7 +66,7 @@ plugin_compilation(aomenc AOM AOM_ENCODER_FOUND AOM_ENCODER AOM_ENCODER) set(SvtEnc_sources encoder_svt.cc encoder_svt.h) -set(SvtEnc_extra_plugin_sources ../common_utils.cc ../common_utils.h) +set(SvtEnc_extra_plugin_sources ../error.cc ../common_utils.cc ../common_utils.h) plugin_compilation(svtenc SvtEnc SvtEnc_FOUND SvtEnc SvtEnc) set(RAV1E_sources encoder_rav1e.cc encoder_rav1e.h) @@ -82,7 +82,7 @@ plugin_compilation(jpegenc JPEG JPEG_FOUND JPEG_ENCODER JPEG_ENCODER) set(OpenJPEG_DECODER_sources decoder_openjpeg.cc decoder_openjpeg.h) -set(OpenJPEG_DECODER_extra_plugin_sources ) +set(OpenJPEG_DECODER_extra_plugin_sources ../error.cc ../common_utils.cc ../common_utils.h) plugin_compilation(j2kdec OPENJPEG OPENJPEG_FOUND OpenJPEG_DECODER OPENJPEG_DECODER) set(OpenJPEG_ENCODER_sources encoder_openjpeg.cc encoder_openjpeg.h) @@ -113,6 +113,10 @@ set(VVENC_extra_plugin_sources) plugin_compilation(vvenc vvenc vvenc_FOUND VVENC VVENC) +set(X264_sources encoder_x264.h encoder_x264.cc) +set(X264_extra_plugin_sources) +plugin_compilation(x264 X264 X264_FOUND X264 X264) + set(OpenH264_DECODER_sources decoder_openh264.cc decoder_openh264.h) set(OpenH264_DECODER_extra_plugin_sources) plugin_compilation(openh264dec OpenH264 OpenH264_DECODER_FOUND OpenH264_DECODER OpenH264_DECODER) @@ -130,3 +134,12 @@ decoder_uncompressed.h decoder_uncompressed.cc) endif () + +if (WITH_WEBCODECS) + target_sources(heif PRIVATE + decoder_webcodecs.h + decoder_webcodecs.cc) + target_compile_definitions(heif PRIVATE HAVE_WEBCODECS=1) +endif() + + diff -Nru libheif-1.19.8/libheif/plugins/decoder_aom.cc libheif-1.23.4/libheif/plugins/decoder_aom.cc --- libheif-1.19.8/libheif/plugins/decoder_aom.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_aom.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -31,12 +33,32 @@ struct aom_decoder { + ~aom_decoder() + { + // --- free images that are still in the output queue + + for (auto& pkt : output_queue) { + if (pkt.img) { + heif_image_release(pkt.img); + } + } + } + aom_codec_ctx_t codec; bool codec_initialized = false; aom_codec_iface_t* iface; + struct Packet + { + heif_image* img; + uintptr_t user_data = 0; + }; + + std::deque output_queue; bool strict_decoding = false; + std::string error_message; + const heif_security_limits* limits = nullptr; }; static const char kSuccess[] = "Success"; @@ -72,7 +94,7 @@ } -static int aom_does_support_format(enum heif_compression_format format) +static int aom_does_support_format(heif_compression_format format) { if (format == heif_compression_AV1) { return AOM_PLUGIN_PRIORITY; @@ -82,34 +104,58 @@ } } +static int aom_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return aom_does_support_format(format->format); +} -struct heif_error aom_new_decoder(void** dec) +heif_error aom_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { - struct aom_decoder* decoder = new aom_decoder(); + aom_decoder* decoder = new aom_decoder(); decoder->iface = aom_codec_av1_dx(); - aom_codec_err_t aomerr = aom_codec_dec_init(&decoder->codec, decoder->iface, NULL, 0); + aom_codec_dec_cfg_t cfg{}; + + if (options->num_threads) { + cfg.threads = options->num_threads; + } + cfg.w = 0; + cfg.h = 0; + cfg.allow_lowbitdepth = 1; + + aom_codec_err_t aomerr = aom_codec_dec_init(&decoder->codec, decoder->iface, &cfg, 0); if (aomerr) { *dec = NULL; delete decoder; - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, aom_codec_err_to_string(aomerr)}; + heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, aom_codec_err_to_string(aomerr)}; return err; } decoder->codec_initialized = true; + decoder->limits = options->limits; *dec = decoder; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; return err; } +heif_error aom_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_AV1; + options.strict_decoding = false; + options.num_threads = 0; + + return aom_new_decoder2(dec, &options); +} + void aom_free_decoder(void* decoder_raw) { - struct aom_decoder* decoder = (aom_decoder*) decoder_raw; + aom_decoder* decoder = (aom_decoder*) decoder_raw; if (!decoder) { return; @@ -126,48 +172,38 @@ void aom_set_strict_decoding(void* decoder_raw, int flag) { - struct aom_decoder* decoder = (aom_decoder*) decoder_raw; + aom_decoder* decoder = (aom_decoder*) decoder_raw; decoder->strict_decoding = flag; } -struct heif_error aom_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +static heif_error get_next_image_from_decoder(aom_decoder* decoder, + aom_codec_iter_t* inout_iter, + heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) { - struct aom_decoder* decoder = (struct aom_decoder*) decoder_raw; - - const char* ver = aom_codec_version_str(); - (void)ver; + aom_image_t* img = aom_codec_get_frame(&decoder->codec, inout_iter); - aom_codec_err_t aomerr; - aomerr = aom_codec_decode(&decoder->codec, (const uint8_t*) frame_data, frame_size, NULL); - if (aomerr) { - struct heif_error err = {heif_error_Invalid_input, heif_suberror_Unspecified, aom_codec_err_to_string(aomerr)}; - return err; + if (img == NULL) { + *out_img = NULL; + return {}; } + if (out_user_data) { + *out_user_data = (uintptr_t)img->user_priv; + } - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; -} - - -struct heif_error aom_decode_image(void* decoder_raw, struct heif_image** out_img) -{ - struct aom_decoder* decoder = (struct aom_decoder*) decoder_raw; - - aom_codec_iter_t iter = NULL; - aom_image_t* img = NULL; - - img = aom_codec_get_frame(&decoder->codec, &iter); - + /* if (img == NULL) { - struct heif_error err = {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - kEmptyString}; - return err; + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + kEmptyString + }; } - +*/ if (img->fmt != AOM_IMG_FMT_I420 && img->fmt != AOM_IMG_FMT_I42016 && @@ -175,10 +211,11 @@ img->fmt != AOM_IMG_FMT_I42216 && img->fmt != AOM_IMG_FMT_I444 && img->fmt != AOM_IMG_FMT_I44416) { - struct heif_error err = {heif_error_Decoder_plugin_error, - heif_suberror_Unsupported_image_type, - kEmptyString}; - return err; + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unsupported_image_type, + kEmptyString + }; } heif_chroma chroma; @@ -203,11 +240,11 @@ colorspace = heif_colorspace_YCbCr; } - struct heif_image* heif_img = nullptr; - struct heif_error err = heif_image_create(img->d_w, img->d_h, - colorspace, - chroma, - &heif_img); + heif_image* heif_img = nullptr; + heif_error err = heif_image_create(img->d_w, img->d_h, + colorspace, + chroma, + &heif_img); if (err.code != heif_error_Ok) { assert(heif_img==nullptr); return err; @@ -253,19 +290,23 @@ w = (w + 1) / 2; } - err = heif_image_add_plane(heif_img, channel2plane[c], w, h, bpp); + err = heif_image_add_plane_safe(heif_img, channel2plane[c], w, h, bpp, limits); if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + heif_image_release(heif_img); return err; } - int dst_stride; - uint8_t* dst_mem = heif_image_get_plane(heif_img, channel2plane[c], &dst_stride); + size_t dst_stride; + uint8_t* dst_mem = heif_image_get_plane2(heif_img, channel2plane[c], &dst_stride); int bytes_per_pixel = (bpp + 7) / 8; for (int y = 0; y < h; y++) { - memcpy(dst_mem + y * dst_stride, data + y * stride, w * bytes_per_pixel); + memcpy(dst_mem + y * dst_stride, data + static_cast(y) * stride, static_cast(w) * bytes_per_pixel); } } @@ -274,9 +315,103 @@ } -static const struct heif_decoder_plugin decoder_aom + +heif_error aom_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, uintptr_t user_data) +{ + aom_decoder* decoder = (struct aom_decoder*) decoder_raw; + + const char* ver = aom_codec_version_str(); + (void)ver; + + aom_codec_err_t aomerr; + // The codec hands this opaque pointer back unchanged; it carries an integer, not an address. + aomerr = aom_codec_decode(&decoder->codec, (const uint8_t*) frame_data, frame_size, (void*)user_data); // NOLINT(performance-no-int-to-ptr) + if (aomerr) { + heif_error err = {heif_error_Invalid_input, heif_suberror_Unspecified, aom_codec_err_to_string(aomerr)}; + return err; + } + + + // --- decode images and fill into output queue + + aom_codec_iter_t iter = NULL; + for (;;) { + heif_image* img; + uintptr_t out_user_data; + heif_error err = get_next_image_from_decoder(decoder, &iter, &img, &out_user_data, decoder->limits); + if (err.code) { + return err; + } + + if (img) { + decoder->output_queue.push_back({img, out_user_data}); + } + else { + break; + } + } + + heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + return err; +} + + +heif_error aom_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +{ + return aom_push_data2(decoder_raw, frame_data, frame_size, 0); +} + + +static heif_error aom_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + aom_decoder* decoder = (struct aom_decoder*) decoder_raw; + + // --- if there are images in the output queue, pass them back + + if (!decoder->output_queue.empty()) { + if (out_user_data) { + *out_user_data = decoder->output_queue.front().user_data; + } + + heif_image* next_image = decoder->output_queue.front().img; + decoder->output_queue.pop_front(); + + *out_img = next_image; + return {}; + } + else { + *out_img = nullptr; + return {}; + } +} + + +static heif_error aom_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return aom_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +heif_error aom_decode_image(void* decoder_raw, heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return aom_decode_next_image(decoder_raw, out_img, limits); +} + + +static heif_error aom_flush_data(void* decoder_raw) +{ + // aom_decoder* decoder = (aom_decoder*) decoder_raw; + + return heif_error_ok; +} + + +static const heif_decoder_plugin decoder_aom { - 3, + 6, aom_plugin_name, aom_init_plugin, aom_deinit_plugin, @@ -286,11 +421,18 @@ aom_push_data, aom_decode_image, aom_set_strict_decoding, - "aom" + "aom", + aom_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,22,0), + aom_does_support_format2, + aom_new_decoder2, + aom_push_data2, + aom_flush_data, + aom_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_aom() +const heif_decoder_plugin* get_decoder_plugin_aom() { return &decoder_aom; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_dav1d.cc libheif-1.23.4/libheif/plugins/decoder_dav1d.cc --- libheif-1.19.8/libheif/plugins/decoder_dav1d.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_dav1d.cc 2026-09-06 14:45:17.000000000 +0000 @@ -23,28 +23,30 @@ #include "security_limits.h" #include "common_utils.h" #include "decoder_dav1d.h" -#include #include #include #include +#include #include -#include +#include #include #include + struct dav1d_decoder { - Dav1dSettings settings; - Dav1dContext* context; - Dav1dData data; + Dav1dSettings settings{}; + Dav1dContext* context{}; + std::deque queued_data; bool strict_decoding = false; + std::string error_message; }; -static const char kEmptyString[] = ""; -static const char kSuccess[] = "Success"; +static constexpr char kEmptyString[] = ""; +static constexpr char kSuccess[] = "Success"; -static const int DAV1D_PLUGIN_PRIORITY = 150; +static constexpr int DAV1D_PLUGIN_PRIORITY = 150; #define MAX_PLUGIN_NAME_LENGTH 80 @@ -75,7 +77,7 @@ } -static int dav1d_does_support_format(enum heif_compression_format format) +static int dav1d_does_support_format(heif_compression_format format) { if (format == heif_compression_AV1) { return DAV1D_PLUGIN_PRIORITY; @@ -86,35 +88,50 @@ } -struct heif_error dav1d_new_decoder(void** dec) +static int dav1d_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return dav1d_does_support_format(format->format); +} + +heif_error dav1d_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { auto* decoder = new dav1d_decoder(); dav1d_default_settings(&decoder->settings); - if (heif_get_global_security_limits()->max_image_size_pixels > std::numeric_limits::max()) { + const heif_security_limits* limits = options->limits ? options->limits : heif_get_global_security_limits(); + if (limits->max_image_size_pixels > std::numeric_limits::max()) { decoder->settings.frame_size_limit = 0; } else { - decoder->settings.frame_size_limit = static_cast(heif_get_global_security_limits()->max_image_size_pixels); + decoder->settings.frame_size_limit = static_cast(limits->max_image_size_pixels); } decoder->settings.all_layers = 0; + if (options->num_threads) { + decoder->settings.n_threads = options->num_threads; + } + if (dav1d_open(&decoder->context, &decoder->settings) != 0) { delete decoder; - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, kSuccess}; } - memset(&decoder->data, 0, sizeof(Dav1dData)); - *dec = decoder; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return heif_error_ok; } +heif_error dav1d_new_decoder(void** dec) +{ + struct heif_decoder_plugin_options options{}; + options.format = heif_compression_AV1; + options.strict_decoding = false; + options.num_threads = 0; + + return dav1d_new_decoder2(dec, &options); +} void dav1d_free_decoder(void* decoder_raw) { @@ -124,9 +141,15 @@ return; } - if (decoder->data.sz) { - dav1d_data_unref(&decoder->data); + // free queued data + + for (auto& pkt : decoder->queued_data) { + dav1d_data_unref(&pkt); } + decoder->queued_data.clear(); + + // free decoder context + if (decoder->context) { dav1d_close(&decoder->context); } @@ -137,69 +160,130 @@ void dav1d_set_strict_decoding(void* decoder_raw, int flag) { - struct dav1d_decoder* decoder = (dav1d_decoder*) decoder_raw; + dav1d_decoder* decoder = (dav1d_decoder*) decoder_raw; decoder->strict_decoding = flag; } -struct heif_error dav1d_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) + +static heif_error push_pending_data_into_decoder(dav1d_decoder* decoder) +{ + while (!decoder->queued_data.empty()) { + + // send data + + int res = dav1d_send_data(decoder->context, &decoder->queued_data.front()); + + // decoder does not accept more data at this moment + + if (res == DAV1D_ERR(EAGAIN)) { + break; + } + + if (res < 0) { + // dav1d_send_data failed. Data was not consumed, unref before removing from queue. + dav1d_data_unref(&decoder->queued_data.front()); + decoder->queued_data.pop_front(); + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + kEmptyString + }; + } + + // Decoder has accepted data (Dav1dData is consumed). Remove from queue. + decoder->queued_data.pop_front(); + } + + return heif_error_ok; +} + + +heif_error dav1d_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, uintptr_t user_data) { auto* decoder = (struct dav1d_decoder*) decoder_raw; - assert(decoder->data.sz == 0); + // --- copy input data into Dav1dData packet - uint8_t* d = dav1d_data_create(&decoder->data, frame_size); + Dav1dData packet{}; + + uint8_t* d = dav1d_data_create(&packet, frame_size); if (d == nullptr) { - struct heif_error err = {heif_error_Memory_allocation_error, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Memory_allocation_error, heif_suberror_Unspecified, kSuccess}; } memcpy(d, frame_data, frame_size); - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + // The codec hands this opaque pointer back unchanged; it carries an integer, not an address. + packet.m.user_data.data = (uint8_t*)user_data; // NOLINT(performance-no-int-to-ptr) + + // --- put data into queue + + decoder->queued_data.push_back(packet); + + // --- push pending data to decoder + + return push_pending_data_into_decoder(decoder); } -struct heif_error dav1d_decode_image(void* decoder_raw, struct heif_image** out_img) +heif_error dav1d_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) { - auto* decoder = (struct dav1d_decoder*) decoder_raw; + return dav1d_push_data2(decoder_raw, frame_data, frame_size, 0); +} - struct heif_error err; - Dav1dPicture frame; - memset(&frame, 0, sizeof(Dav1dPicture)); +heif_error dav1d_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + auto* decoder = (struct dav1d_decoder*) decoder_raw; + + heif_error err; - bool flushed = false; + Dav1dPicture frame{}; for (;;) { - int res = dav1d_send_data(decoder->context, &decoder->data); - if ((res < 0) && (res != DAV1D_ERR(EAGAIN))) { - err = {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - kEmptyString}; + // --- send more pending data to decoder + + err = push_pending_data_into_decoder(decoder); + if (err.code) { return err; } - res = dav1d_get_picture(decoder->context, &frame); - if (!flushed && res == DAV1D_ERR(EAGAIN)) { - if (decoder->data.sz == 0) { - flushed = true; - } - continue; + // --- try to get decoded image + + int res = dav1d_get_picture(decoder->context, &frame); + + // We got a picture from the decoder. Continue with processing it. + if (res == 0) { + break; } - else if (res < 0) { - err = {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - kEmptyString}; - return err; + + // decoder wants more data, but queue is empty + if (res == DAV1D_ERR(EAGAIN) && decoder->queued_data.empty()) { + *out_img = nullptr; + return heif_error_ok; } - else { - break; + + // continue feeding more data from queue + if (res == DAV1D_ERR(EAGAIN)) { + continue; + } + + // decoder error + if (res < 0) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + kEmptyString + }; } } + // --- convert image to heif_image + heif_chroma chroma; heif_colorspace colorspace; switch (frame.p.layout) { @@ -220,21 +304,27 @@ colorspace = heif_colorspace_monochrome; break; default: { - err = {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - kEmptyString}; - return err; + dav1d_picture_unref(&frame); + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + kEmptyString + }; } } + if (out_user_data) { + *out_user_data = (uintptr_t)frame.m.user_data.data; + } - struct heif_image* heif_img = nullptr; + heif_image* heif_img = nullptr; err = heif_image_create(frame.p.w, frame.p.h, colorspace, chroma, &heif_img); if (err.code != heif_error_Ok) { assert(heif_img == nullptr); + dav1d_picture_unref(&frame); return err; } @@ -249,9 +339,6 @@ heif_image_set_nclx_color_profile(heif_img, &nclx); - - // --- transfer data from Dav1dPicture to HeifPixelImage - heif_channel channel2plane[3] = { heif_channel_Y, heif_channel_Cb, @@ -273,19 +360,24 @@ get_subsampled_size(frame.p.w, frame.p.h, channel2plane[c], chroma, &w, &h); - err = heif_image_add_plane(heif_img, channel2plane[c], w, h, bpp); + err = heif_image_add_plane_safe(heif_img, channel2plane[c], w, h, bpp, limits); if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + heif_image_release(heif_img); + dav1d_picture_unref(&frame); return err; } - int dst_stride; - uint8_t* dst_mem = heif_image_get_plane(heif_img, channel2plane[c], &dst_stride); + size_t dst_stride; + uint8_t* dst_mem = heif_image_get_plane2(heif_img, channel2plane[c], &dst_stride); - int bytes_per_pixel = (bpp + 7) / 8; + const int bytes_per_pixel = (bpp + 7) / 8; for (uint32_t y = 0; y < h; y++) { - memcpy(dst_mem + y * dst_stride, data + y * stride, w * bytes_per_pixel); + memcpy(dst_mem + y * dst_stride, data + static_cast(y) * stride, static_cast(w) * bytes_per_pixel); } } @@ -294,14 +386,43 @@ *out_img = heif_img; - err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + + +heif_error dav1d_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return dav1d_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + + +heif_error dav1d_decode_image(void* decoder_raw, struct heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return dav1d_decode_next_image(decoder_raw, out_img, limits); +} + + +heif_error dav1d_flush_data(void* decoder_raw) +{ + auto* decoder = (struct dav1d_decoder*) decoder_raw; + + constexpr Dav1dData packet{}; // empty packet + + // --- put data into queue + + decoder->queued_data.push_back(packet); + + // --- push pending data to decoder + + return push_pending_data_into_decoder(decoder); } -static const struct heif_decoder_plugin decoder_dav1d +static const heif_decoder_plugin decoder_dav1d { - 3, + 6, dav1d_plugin_name, dav1d_init_plugin, dav1d_deinit_plugin, @@ -311,11 +432,18 @@ dav1d_push_data, dav1d_decode_image, dav1d_set_strict_decoding, - "dav1d" + "dav1d", + dav1d_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,22,0), + dav1d_does_support_format2, + dav1d_new_decoder2, + dav1d_push_data2, + dav1d_flush_data, + dav1d_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_dav1d() +const heif_decoder_plugin* get_decoder_plugin_dav1d() { return &decoder_dav1d; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_ffmpeg.cc libheif-1.23.4/libheif/plugins/decoder_ffmpeg.cc --- libheif-1.19.8/libheif/plugins/decoder_ffmpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_ffmpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,27 +22,65 @@ #include "libheif/heif_plugin.h" #include "decoder_ffmpeg.h" #include "nalu_utils.h" +#include #if defined(HAVE_CONFIG_H) #include "config.h" #endif +#include +#include #include #include +#include -extern "C" +extern "C" { #include + #include } struct ffmpeg_decoder { - NalMap nalMap; - bool strict_decoding = false; + // --- input data + + struct Packet + { + std::vector data; + uintptr_t user_data; + }; + + std::deque input_data; + bool strict_decoding = false; + + // --- decoder + + const AVCodec* av_codec = NULL; + AVCodecParserContext* av_codec_parser_context = NULL; + AVCodecContext* av_codec_context = NULL; + + std::string error_message; + + ~ffmpeg_decoder() + { + if (av_codec_parser_context) av_parser_close(av_codec_parser_context); + if (av_codec_context) avcodec_free_context(&av_codec_context); + } }; +// H. 264, H. 265, H. 266 require NAL. Others have no NAL. +static bool supportsNal(AVCodecID id) { + return id == AV_CODEC_ID_H264 || id == AV_CODEC_ID_H265 || id == AV_CODEC_ID_H266; +} + +// Codecs whose bitstream carries CICP colour signalling (VUI / sequence header). +static bool codec_has_cicp_signalling(AVCodecID id) +{ + return supportsNal(id) || id == AV_CODEC_ID_AV1; +} + static const int FFMPEG_DECODER_PLUGIN_PRIORITY = 90; #define MAX_PLUGIN_NAME_LENGTH 80 @@ -52,7 +90,7 @@ static const char* ffmpeg_plugin_name() { - snprintf(plugin_name, MAX_PLUGIN_NAME_LENGTH, "FFMPEG HEVC decoder %s", av_version_info()); + snprintf(plugin_name, MAX_PLUGIN_NAME_LENGTH, "FFmpeg decoder %s", av_version_info()); plugin_name[MAX_PLUGIN_NAME_LENGTH - 1] = 0; //null-terminated return plugin_name; @@ -71,28 +109,111 @@ } -static int ffmpeg_does_support_format(enum heif_compression_format format) +static int ffmpeg_does_support_format(heif_compression_format format) { - if (format == heif_compression_HEVC) { - return FFMPEG_DECODER_PLUGIN_PRIORITY; - } - else { + switch(format) { + case heif_compression_HEVC: + return avcodec_find_decoder(AV_CODEC_ID_HEVC) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_AVC: + return avcodec_find_decoder(AV_CODEC_ID_H264) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_VVC: + return avcodec_find_decoder(AV_CODEC_ID_VVC) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_AV1: + return avcodec_find_decoder(AV_CODEC_ID_AV1) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_JPEG: + return avcodec_find_decoder(AV_CODEC_ID_MJPEG) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_JPEG2000: + return avcodec_find_decoder(AV_CODEC_ID_JPEG2000) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; + case heif_compression_HTJ2K: + // FFmpeg gained High-Throughput JPEG 2000 (HT block coder) decoding in 7.0 + // (libavcodec 61). Earlier versions silently misdecode the HT codestream. +#if LIBAVCODEC_VERSION_MAJOR >= 61 + return avcodec_find_decoder(AV_CODEC_ID_JPEG2000) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0; +#else + return 0; +#endif + default: return 0; } } -static struct heif_error ffmpeg_new_decoder(void** dec) +static int ffmpeg_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) { - struct ffmpeg_decoder* decoder = new ffmpeg_decoder(); + return ffmpeg_does_support_format(format->format); +} +static heif_error ffmpeg_new_decoder2(void** dec, const heif_decoder_plugin_options* options) +{ + ffmpeg_decoder* decoder = new ffmpeg_decoder(); *dec = decoder; + + // Find matching video decoder + switch (options->format) { + case heif_compression_AVC: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_H264); + break; + case heif_compression_HEVC: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_HEVC); + break; + case heif_compression_VVC: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_VVC); + break; + case heif_compression_AV1: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_AV1); + break; + case heif_compression_JPEG: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG); + break; + case heif_compression_JPEG2000: + case heif_compression_HTJ2K: + decoder->av_codec = avcodec_find_decoder(AV_CODEC_ID_JPEG2000); + break; + default: + assert(false); + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "FFMPEG plugin started with unsupported codec." + }; + } + + if (!decoder->av_codec) { + return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_find_decoder() returned error" }; + } + + decoder->av_codec_parser_context = av_parser_init(decoder->av_codec->id); + if (!decoder->av_codec_parser_context) { + return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "av_parser_init returned error" }; + } + + decoder->av_codec_context = avcodec_alloc_context3(decoder->av_codec); + if (!decoder->av_codec_context) { + return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "avcodec_alloc_context3 returned error" }; + } + + /* open it */ + if (avcodec_open2(decoder->av_codec_context, decoder->av_codec, NULL) < 0) { + return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_open2 returned error" }; + } + + return heif_error_success; } +static heif_error ffmpeg_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_HEVC; + options.num_threads = 0; + options.strict_decoding = false; + + return ffmpeg_new_decoder2(dec, &options); +} + static void ffmpeg_free_decoder(void* decoder_raw) { - struct ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw; + ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw; delete decoder; } @@ -100,43 +221,166 @@ void ffmpeg_set_strict_decoding(void* decoder_raw, int flag) { - struct ffmpeg_decoder* decoder = (ffmpeg_decoder*) decoder_raw; + ffmpeg_decoder* decoder = (ffmpeg_decoder*) decoder_raw; decoder->strict_decoding = flag; } -static struct heif_error ffmpeg_v1_push_data(void *decoder_raw, const void *data, size_t size) +#if LIBAVCODEC_VERSION_MAJOR < 61 +// Scan a JPEG 2000 codestream main header for the CAP marker (0xFF50), which +// signals the High-Throughput (HT) block coder (JPEG 2000 Part 15). FFmpeg +// gained HT decoding only in 7.0 (libavcodec 61); older versions silently +// misdecode the stream, so we use this to refuse HT input on those. +static bool jpeg2000_codestream_uses_HT(const uint8_t* data, size_t size) { + // main header must start with the SOC marker (FF4F) + if (size < 2 || data[0] != 0xFF || data[1] != 0x4F) { + return false; + } - struct ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw; + size_t pos = 2; + while (pos + 2 <= size) { + if (data[pos] != 0xFF) { + break; // not positioned on a marker -> stop scanning + } - const uint8_t* cdata = (const uint8_t*) data; + uint16_t marker = (uint16_t(data[pos]) << 8) | data[pos + 1]; - return decoder->nalMap.parseHevcNalu(cdata, size); + if (marker == 0xFF50) { // CAP + return true; + } + if (marker == 0xFF90 || // SOT -> main header finished + marker == 0xFFD9) { // EOC + break; + } + + // All remaining main-header markers carry a 2-byte segment length. + if (pos + 4 > size) { + break; + } + uint16_t seg_len = (uint16_t(data[pos + 2]) << 8) | data[pos + 3]; + if (seg_len < 2) { + break; // malformed + } + pos += 2 + seg_len; + } + + return false; } +#endif +static heif_error ffmpeg_push_data2(void *decoder_raw, const void *data, size_t size, uintptr_t user_data) +{ + ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw; -static heif_chroma ffmpeg_get_chroma_format(enum AVPixelFormat pix_fmt) { - if (pix_fmt == AV_PIX_FMT_GRAY8) - { - return heif_chroma_monochrome; - } - else if ((pix_fmt == AV_PIX_FMT_YUV420P) || (pix_fmt == AV_PIX_FMT_YUVJ420P) || - (pix_fmt == AV_PIX_FMT_YUV420P10LE)) - { - return heif_chroma_420; - } - else if (pix_fmt == AV_PIX_FMT_YUV422P) - { - return heif_chroma_422; - } - else if (pix_fmt == AV_PIX_FMT_YUV444P) - { - return heif_chroma_444; + const uint8_t* cdata = (const uint8_t*) data; + +#if LIBAVCODEC_VERSION_MAJOR < 61 + // HEIF reports both plain and HT JPEG 2000 as heif_compression_JPEG2000, so an + // HT codestream can reach this plugin even though does_support_format() gates + // HTJ2K off. Detect it from the codestream and refuse, rather than emit garbage. + if (decoder->av_codec->id == AV_CODEC_ID_JPEG2000 && + jpeg2000_codestream_uses_HT(cdata, size)) { + return { + heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + "High-Throughput JPEG 2000 decoding requires FFmpeg 7.0 or later" + }; + } +#endif + + ffmpeg_decoder::Packet pkt; + + size_t ptr = 0; + if (supportsNal(decoder->av_codec->id)) { + while (ptr < size) + { + if (size - ptr < 4) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + "insufficient data" + }; + } + + uint32_t nal_size = four_bytes_to_uint32(cdata[ptr + 0], + cdata[ptr + 1], + cdata[ptr + 2], + cdata[ptr + 3]); + ptr += 4; + + if (nal_size > size - ptr) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + "insufficient data" + }; + } + + pkt.data.push_back(0); + pkt.data.push_back(0); + pkt.data.push_back(1); + pkt.data.insert(pkt.data.end(), cdata + ptr, cdata + ptr + nal_size); + ptr += nal_size; } - // Unsupported pix_fmt - return heif_chroma_undefined; + } + else { + pkt.data.insert(pkt.data.end(), cdata, cdata + size); + } + + pkt.user_data = user_data; + + decoder->input_data.emplace_back(std::move(pkt)); + + return heif_error_success; +} + +static heif_error ffmpeg_push_data(void *decoder_raw, const void *data, size_t size) +{ + return ffmpeg_push_data2(decoder_raw, data, size, 0); +} + + +// Note: the pixel format names without LE/BE suffix used below are FFmpeg macros that resolve +// to the variant in the host's native byte order (see AV_PIX_FMT_NE in libavutil/pixfmt.h). +// FFmpeg decoders return native-endian formats, and the copy functions below read the samples +// as native uint16_t, so the plugin works unchanged on little- and big-endian hosts. +static heif_chroma ffmpeg_get_chroma_format(AVPixelFormat pix_fmt) { + switch (pix_fmt) { + case AV_PIX_FMT_GRAY8: + case AV_PIX_FMT_GRAY10: + case AV_PIX_FMT_GRAY12: + case AV_PIX_FMT_GRAY14: + case AV_PIX_FMT_GRAY16: + return heif_chroma_monochrome; + + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + case AV_PIX_FMT_YUV420P10: + case AV_PIX_FMT_YUV420P12: + case AV_PIX_FMT_YUV420P14: + case AV_PIX_FMT_YUV420P16: + return heif_chroma_420; + + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV422P10: + case AV_PIX_FMT_YUV422P12: + case AV_PIX_FMT_YUV422P14: + case AV_PIX_FMT_YUV422P16: + return heif_chroma_422; + + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUV444P10: + case AV_PIX_FMT_YUV444P12: + case AV_PIX_FMT_YUV444P14: + case AV_PIX_FMT_YUV444P16: + return heif_chroma_444; + + default: + // Unsupported pix_fmt + return heif_chroma_undefined; + } } static int ffmpeg_get_chroma_width(const AVFrame* frame, heif_channel channel, heif_chroma chroma) @@ -147,7 +391,7 @@ } else if (chroma == heif_chroma_420 || chroma == heif_chroma_422) { - return (frame->width + 1) / 2; + return frame->width / 2 + (frame->width & 1); // note: prevents integer overflow( + 1) / 2; } else { @@ -163,7 +407,7 @@ } else if (chroma == heif_chroma_420) { - return (frame->height + 1) / 2; + return frame->height / 2 + (frame->height & 1); // note: prevents integer overflow( + 1) / 2; } else { @@ -171,313 +415,477 @@ } } -static struct heif_error hevc_decode(AVCodecContext* hevc_dec_ctx, AVFrame* hevc_frame, AVPacket* hevc_pkt, struct heif_image** image) +static int get_ffmpeg_format_bpp(AVPixelFormat pix_fmt) { - int ret; + switch (pix_fmt) { + case AV_PIX_FMT_GRAY8: + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUV444P: + return 8; + case AV_PIX_FMT_GRAY10: + case AV_PIX_FMT_YUV420P10: + case AV_PIX_FMT_YUV422P10: + case AV_PIX_FMT_YUV444P10: + return 10; + case AV_PIX_FMT_GRAY12: + case AV_PIX_FMT_YUV420P12: + case AV_PIX_FMT_YUV422P12: + case AV_PIX_FMT_YUV444P12: + return 12; + case AV_PIX_FMT_GRAY14: + case AV_PIX_FMT_YUV420P14: + case AV_PIX_FMT_YUV422P14: + case AV_PIX_FMT_YUV444P14: + return 14; + case AV_PIX_FMT_GRAY16: + case AV_PIX_FMT_YUV420P16: + case AV_PIX_FMT_YUV422P16: + case AV_PIX_FMT_YUV444P16: + return 16; + default: + return 0; + } +} + + +// A few decoders return a format with an explicit byte order regardless of the host, e.g. +// FFmpeg's JPEG 2000 decoder returns 12-bit grayscale as GRAY16LE. If the format's byte order +// is not the host's, map it to its native-endian counterpart and request byte swapping. +static AVPixelFormat ffmpeg_native_pix_fmt(AVPixelFormat pix_fmt, bool* byte_swap) +{ + *byte_swap = false; + + const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(pix_fmt); + if (!desc || desc->comp[0].depth <= 8) { + return pix_fmt; // single-byte samples have no byte order + } + + const bool format_is_be = (desc->flags & AV_PIX_FMT_FLAG_BE) != 0; + const bool host_is_be = (AV_HAVE_BIGENDIAN != 0); + if (format_is_be == host_is_be) { + return pix_fmt; + } + + AVPixelFormat swapped = av_pix_fmt_swap_endianness(pix_fmt); + if (swapped == AV_PIX_FMT_NONE) { + return pix_fmt; + } + + *byte_swap = true; + return swapped; +} + + +// FFmpeg's MJPEG and JPEG 2000 decoders do not have an exact-depth pixel format for every +// coded bit depth. A 12-bit grayscale JPEG is returned as GRAY16, a 10- or 12-bit grayscale +// JPEG 2000 as GRAY16, a 12-bit YCbCr JPEG as YUV4xxP16, a >8-bit 4:4:4 JPEG 2000 as RGB48, +// etc. In these cases FFmpeg shifts the samples left so that they fill the container (the low +// bits are zero) and reports the coded precision in bits_per_raw_sample. Compute the bit depth +// that the decoded image should have and the right shift that restores the original samples. +// For HEVC/AVC/VVC/AV1 the pixel format depth always equals the coded depth, so nothing is +// shifted there. +static int ffmpeg_coded_bit_depth(const AVCodecContext* av_dec_ctx, int format_bpp, int* shift) +{ + int coded_bpp = av_dec_ctx->bits_per_raw_sample; + if (coded_bpp > 0 && coded_bpp < format_bpp) { + *shift = format_bpp - coded_bpp; + return coded_bpp; + } + + *shift = 0; + return format_bpp; +} + + +// Copy a plane of samples from an FFmpeg frame into a libheif plane. Source samples are +// 'src_bytes_per_sample' bytes wide (byte-swapped first if 'byte_swap'), 'src_pixel_stride' +// bytes apart (> src_bytes_per_sample for packed formats), shifted right by 'shift' bits and +// stored as 'dst_bytes_per_sample'-byte native-endian samples. +static void ffmpeg_copy_samples(uint8_t* dst, size_t dst_stride, int dst_bytes_per_sample, + const uint8_t* src, size_t src_stride, int src_bytes_per_sample, int src_pixel_stride, + int w, int h, int shift, bool byte_swap) +{ + if (src_bytes_per_sample == dst_bytes_per_sample && src_pixel_stride == src_bytes_per_sample && + shift == 0 && !byte_swap) { + for (int y = 0; y < h; y++) { + memcpy(dst + y * dst_stride, src + y * src_stride, static_cast(w) * src_bytes_per_sample); + } + return; + } + + for (int y = 0; y < h; y++) { + const uint8_t* src_row = src + y * src_stride; + uint8_t* dst_row = dst + y * dst_stride; + + for (int x = 0; x < w; x++) { + const uint8_t* s = src_row + static_cast(x) * src_pixel_stride; + + uint16_t v; + if (src_bytes_per_sample == 2) { + memcpy(&v, s, 2); + if (byte_swap) { + v = static_cast((v << 8) | (v >> 8)); + } + } + else { + v = *s; + } - ret = avcodec_send_packet(hevc_dec_ctx, hevc_pkt); - if (ret < 0) { - struct heif_error err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_send_packet" }; - return err; - } + v = static_cast(v >> shift); - ret = avcodec_receive_frame(hevc_dec_ctx, hevc_frame); - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) - { - struct heif_error err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_receive_frame returned EAGAIN or ERROR_EOF" }; - return err; - } - else if (ret < 0) { - struct heif_error err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_receive_frame" }; - return err; + if (dst_bytes_per_sample == 2) { + memcpy(dst_row + static_cast(x) * 2, &v, 2); + } + else { + dst_row[x] = static_cast(v); + } } + } +} - heif_chroma chroma = ffmpeg_get_chroma_format(hevc_dec_ctx->pix_fmt); - if (chroma != heif_chroma_undefined) - { - bool is_mono = (chroma == heif_chroma_monochrome); +static heif_error ffmpeg_av_decode(ffmpeg_decoder* decoder, AVCodecContext* av_dec_ctx, AVFrame* av_frame, heif_image** image, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + int ret; - heif_error err; - err = heif_image_create(hevc_frame->width, - hevc_frame->height, - is_mono ? heif_colorspace_monochrome : heif_colorspace_YCbCr, - chroma, - image); - if (err.code) { - return err; - } + ret = avcodec_receive_frame(av_dec_ctx, av_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR(AVERROR_EOF)) { + *image = nullptr; + return heif_error_ok; + } - heif_channel channel2plane[3] = { - heif_channel_Y, - heif_channel_Cb, - heif_channel_Cr - }; + if (ret < 0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_receive_frame"}; + } - int nPlanes = is_mono ? 1 : 3; + if (out_user_data) { + *out_user_data = av_frame->pts; + } - for (int channel = 0; channel < nPlanes; channel++) { + bool byte_swap = false; + AVPixelFormat pix_fmt = ffmpeg_native_pix_fmt(static_cast(av_frame->format), &byte_swap); - int bpp = (hevc_dec_ctx->pix_fmt == AV_PIX_FMT_YUV420P10LE) ? 10 : 8; - int stride = hevc_frame->linesize[channel]; - const uint8_t* data = hevc_frame->data[channel]; - - int w = ffmpeg_get_chroma_width(hevc_frame, channel2plane[channel], chroma); - int h = ffmpeg_get_chroma_height(hevc_frame, channel2plane[channel], chroma); - if (w <= 0 || h <= 0) { - heif_image_release(*image); - err = { heif_error_Decoder_plugin_error, - heif_suberror_Invalid_image_size, - "invalid image size" }; - return err; - } - - err = heif_image_add_plane(*image, channel2plane[channel], w, h, bpp); - if (err.code) { - heif_image_release(*image); - return err; - } - - int dst_stride; - uint8_t* dst_mem = heif_image_get_plane(*image, channel2plane[channel], &dst_stride); - - int bytes_per_pixel = (bpp + 7) / 8; - - for (int y = 0; y < h; y++) { - memcpy(dst_mem + y * dst_stride, data + y * stride, w * bytes_per_pixel); - } - } + // FFmpeg's JPEG2000 decoder hands back a 3-component 4:4:4 codestream as packed + // RGB24 (8 bit) or RGB48 (9-16 bit), but libheif stores them as sYCC (the + // codestream is tagged sYCC). De-interleave into YCbCr-444 planes so the container's + // nclx color conversion is applied correctly. + if ((pix_fmt == AV_PIX_FMT_RGB24 || pix_fmt == AV_PIX_FMT_RGB48) && + decoder->av_codec->id == AV_CODEC_ID_JPEG2000) { + const int format_bpp = (pix_fmt == AV_PIX_FMT_RGB48) ? 16 : 8; + int shift; + const int bpp = ffmpeg_coded_bit_depth(av_dec_ctx, format_bpp, &shift); - return heif_error_success; + heif_error err = heif_image_create(av_frame->width, av_frame->height, + heif_colorspace_YCbCr, heif_chroma_444, image); + if (err.code) { + return err; } - else - { - struct heif_error err = { heif_error_Unsupported_feature, - heif_suberror_Unsupported_color_conversion, - "Pixel format not implemented" }; + + const heif_channel planes[3] = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; // Y <- "R", Cb <- "G", Cr <- "B" + const int src_bytes_per_sample = (format_bpp + 7) / 8; + const int dst_bytes_per_sample = (bpp + 7) / 8; + + for (int c = 0; c < 3; c++) { + err = heif_image_add_plane_safe(*image, planes[c], av_frame->width, av_frame->height, bpp, limits); + if (err.code) { + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + heif_image_release(*image); return err; + } + + size_t dst_stride; + uint8_t* dst = heif_image_get_plane2(*image, planes[c], &dst_stride); + + ffmpeg_copy_samples(dst, dst_stride, dst_bytes_per_sample, + av_frame->data[0] + c * src_bytes_per_sample, static_cast(av_frame->linesize[0]), + src_bytes_per_sample, 3 * src_bytes_per_sample, + av_frame->width, av_frame->height, shift, byte_swap); } -} -static struct heif_error ffmpeg_v1_decode_image(void* decoder_raw, - struct heif_image** out_img) -{ - struct ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw; + return heif_error_success; + } - int heif_idrpic_size; - int heif_vps_size; - int heif_sps_size; - int heif_pps_size; - const unsigned char* heif_vps_data; - const unsigned char* heif_sps_data; - const unsigned char* heif_pps_data; - const unsigned char* heif_idrpic_data; - - if ((decoder->nalMap.count(NAL_UNIT_VPS_NUT) > 0) - && (decoder->nalMap.count(NAL_UNIT_SPS_NUT) > 0) - && (decoder->nalMap.count(NAL_UNIT_PPS_NUT) > 0) - ) - { - heif_vps_size = decoder->nalMap.size(NAL_UNIT_VPS_NUT); - heif_vps_data = decoder->nalMap.data(NAL_UNIT_VPS_NUT); + heif_chroma chroma = ffmpeg_get_chroma_format(pix_fmt); + if (chroma != heif_chroma_undefined) { + bool is_mono = (chroma == heif_chroma_monochrome); - heif_sps_size = decoder->nalMap.size(NAL_UNIT_SPS_NUT); - heif_sps_data = decoder->nalMap.data(NAL_UNIT_SPS_NUT); + heif_error err; + err = heif_image_create(av_frame->width, + av_frame->height, + is_mono ? heif_colorspace_monochrome : heif_colorspace_YCbCr, + chroma, + image); + if (err.code) { + return err; + } - heif_pps_size = decoder->nalMap.size(NAL_UNIT_PPS_NUT); - heif_pps_data = decoder->nalMap.data(NAL_UNIT_PPS_NUT); - } - else - { - struct heif_error err = { heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - "Unexpected end of data" }; + heif_channel channel2plane[3] = { + heif_channel_Y, + heif_channel_Cb, + heif_channel_Cr + }; + + int nPlanes = is_mono ? 1 : 3; + + const int format_bpp = get_ffmpeg_format_bpp(pix_fmt); + if (format_bpp == 0) { + heif_image_release(*image); + err = { + heif_error_Decoder_plugin_error, + heif_suberror_Unsupported_color_conversion, + "Pixel format not implemented" + }; return err; - } + } - if ((decoder->nalMap.count(NAL_UNIT_IDR_W_RADL) > 0) || (decoder->nalMap.count(NAL_UNIT_IDR_N_LP) > 0)) - { - if (decoder->nalMap.count(NAL_UNIT_IDR_W_RADL) > 0) - { - heif_idrpic_data = decoder->nalMap.data(NAL_UNIT_IDR_W_RADL); - heif_idrpic_size = decoder->nalMap.size(NAL_UNIT_IDR_W_RADL); - } - else - { - heif_idrpic_data = decoder->nalMap.data(NAL_UNIT_IDR_N_LP); - heif_idrpic_size = decoder->nalMap.size(NAL_UNIT_IDR_N_LP); + int shift; + const int bpp = ffmpeg_coded_bit_depth(av_dec_ctx, format_bpp, &shift); + const int src_bytes_per_sample = (format_bpp + 7) / 8; + const int dst_bytes_per_sample = (bpp + 7) / 8; + + for (int channel = 0; channel < nPlanes; channel++) { + int w = ffmpeg_get_chroma_width(av_frame, channel2plane[channel], chroma); + int h = ffmpeg_get_chroma_height(av_frame, channel2plane[channel], chroma); + if (w <= 0 || h <= 0) { + heif_image_release(*image); + err = { + heif_error_Decoder_plugin_error, + heif_suberror_Invalid_image_size, + "invalid image size" + }; + return err; } + + err = heif_image_add_plane_safe(*image, channel2plane[channel], w, h, bpp, limits); + if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(*image); + return err; + } + + size_t dst_stride; + uint8_t* dst_mem = heif_image_get_plane2(*image, channel2plane[channel], &dst_stride); + + ffmpeg_copy_samples(dst_mem, dst_stride, dst_bytes_per_sample, + av_frame->data[channel], static_cast(av_frame->linesize[channel]), + src_bytes_per_sample, src_bytes_per_sample, + w, h, shift, byte_swap); + } + + return heif_error_success; } - else - { - struct heif_error err = { heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - "Unexpected end of data" }; - return err; + else { + const char* fmt_name = av_get_pix_fmt_name(static_cast(av_frame->format)); + decoder->error_message = std::string("Pixel format not implemented: ") + (fmt_name ? fmt_name : "unknown"); + return { + heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + decoder->error_message.c_str() + }; } +} - const char hevc_AnnexB_StartCode[] = { 0x00, 0x00, 0x00, 0x01 }; - int hevc_AnnexB_StartCode_size = 4; - - size_t hevc_data_size = heif_vps_size + heif_sps_size + heif_pps_size + heif_idrpic_size + 4 * hevc_AnnexB_StartCode_size; - uint8_t* hevc_data = (uint8_t*)malloc(hevc_data_size + AV_INPUT_BUFFER_PADDING_SIZE); - //Copy hevc pps data - uint8_t* hevc_data_ptr = hevc_data; - memcpy(hevc_data_ptr, hevc_AnnexB_StartCode, hevc_AnnexB_StartCode_size); - hevc_data_ptr += hevc_AnnexB_StartCode_size; - memcpy(hevc_data_ptr, heif_vps_data, heif_vps_size); - hevc_data_ptr += heif_vps_size; - - //Copy hevc sps data - memcpy(hevc_data_ptr, hevc_AnnexB_StartCode, hevc_AnnexB_StartCode_size); - hevc_data_ptr += hevc_AnnexB_StartCode_size; - memcpy(hevc_data_ptr, heif_sps_data, heif_sps_size); - hevc_data_ptr += heif_sps_size; - - //Copy hevc pps data - memcpy(hevc_data_ptr, hevc_AnnexB_StartCode, hevc_AnnexB_StartCode_size); - hevc_data_ptr += hevc_AnnexB_StartCode_size; - memcpy(hevc_data_ptr, heif_pps_data, heif_pps_size); - hevc_data_ptr += heif_pps_size; - - //Copy hevc idrpic data - memcpy(hevc_data_ptr, hevc_AnnexB_StartCode, hevc_AnnexB_StartCode_size); - hevc_data_ptr += hevc_AnnexB_StartCode_size; - memcpy(hevc_data_ptr, heif_idrpic_data, heif_idrpic_size); - - // decoder->NalMap not needed anymore - decoder->nalMap.clear(); - - const AVCodec* hevc_codec = NULL; - AVCodecParserContext* hevc_parser = NULL; - AVCodecContext* hevc_codecContext = NULL; - AVPacket* hevc_pkt = NULL; - AVFrame* hevc_frame = NULL; - AVCodecParameters* hevc_codecParam = NULL; - struct heif_color_profile_nclx* nclx = NULL; +static heif_error ffmpeg_decode_next_image2(void* decoder_raw, + heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + ffmpeg_decoder* decoder = (ffmpeg_decoder*) decoder_raw; + + AVPacket* av_pkt = NULL; + AVFrame* av_frame = NULL; + AVCodecParameters* av_codecParam = NULL; + heif_color_profile_nclx* nclx = NULL; int ret = 0; - struct heif_error err = heif_error_success; + heif_error err = heif_error_success; - uint8_t* parse_hevc_data = NULL; - int parse_hevc_data_size = 0; + if (!decoder->input_data.empty()) { + uint8_t* parse_av_data = NULL; + int parse_av_data_size = 0; - uint8_t video_full_range_flag = 0; - uint8_t color_primaries = 0; - uint8_t transfer_characteristics = 0; - uint8_t matrix_coefficients = 0; + ffmpeg_decoder::Packet& first_pkt = decoder->input_data.front(); - hevc_pkt = av_packet_alloc(); - if (!hevc_pkt) { - err = { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "av_packet_alloc returned error" }; - goto errexit; - } + if (first_pkt.data.empty()) { + // send 'flush' packet + int ret = avcodec_send_packet(decoder->av_codec_context, nullptr); + if (ret < 0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_send_packet"}; + } - // Find HEVC video decoder - hevc_codec = avcodec_find_decoder(AV_CODEC_ID_HEVC); + decoder->input_data.pop_front(); + } + else { + av_pkt = av_packet_alloc(); + auto pkt_deleter = std::unique_ptr(av_pkt, [](AVPacket* pkt){av_packet_free(&pkt);}); - if (!hevc_codec) { - err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_find_decoder(AV_CODEC_ID_HEVC) returned error" }; - goto errexit; - } + if (!av_pkt) { + return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "av_packet_alloc returned error" }; + } - hevc_parser = av_parser_init(hevc_codec->id); - if (!hevc_parser) { - err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "av_parser_init returned error" }; - goto errexit; - } + parse_av_data = first_pkt.data.data(); + parse_av_data_size = (int) first_pkt.data.size(); + size_t n_bytes_consumed = 0; + + while (parse_av_data_size > 0) { + decoder->av_codec_parser_context->flags = PARSER_FLAG_COMPLETE_FRAMES; + ret = av_parser_parse2(decoder->av_codec_parser_context, decoder->av_codec_context, &av_pkt->data, &av_pkt->size, + parse_av_data, parse_av_data_size, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + if (ret < 0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "av_parser_parse2 returned error"}; + } - hevc_codecContext = avcodec_alloc_context3(hevc_codec); - if (!hevc_codecContext) { - err = { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "avcodec_alloc_context3 returned error" }; - goto errexit; - } + // std::cout << "decode packet of size: " << ret << "\n"; - /* open it */ - if (avcodec_open2(hevc_codecContext, hevc_codec, NULL) < 0) { - err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_open2 returned error" }; - goto errexit; - } - - hevc_frame = av_frame_alloc(); - if (!hevc_frame) { - err = { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "av_frame_alloc returned error" }; - goto errexit; - } - - parse_hevc_data = hevc_data; - parse_hevc_data_size = (int)hevc_data_size; - while (parse_hevc_data_size > 0) { - hevc_parser->flags = PARSER_FLAG_COMPLETE_FRAMES; - ret = av_parser_parse2(hevc_parser, hevc_codecContext, &hevc_pkt->data, &hevc_pkt->size, parse_hevc_data, parse_hevc_data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); - - if (ret < 0) { - err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "av_parser_parse2 returned error" }; - goto errexit; + parse_av_data += ret; + parse_av_data_size -= ret; + n_bytes_consumed += ret; + + if (av_pkt->size) { + av_pkt->pts = first_pkt.user_data; + + ret = avcodec_send_packet(decoder->av_codec_context, av_pkt); + if (ret < 0) { + char buf[100]; + av_make_error_string(buf, 100, ret); + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_send_packet"}; + } + } + else { + break; + } } - parse_hevc_data += ret; - parse_hevc_data_size -= ret; - if (hevc_pkt->size) - { - err = hevc_decode(hevc_codecContext, hevc_frame, hevc_pkt, out_img); - if (err.code != heif_error_Ok) - goto errexit; + if (n_bytes_consumed == first_pkt.data.size()) { + decoder->input_data.pop_front(); + } + else { + if (n_bytes_consumed > 0) { + memmove(first_pkt.data.data(), first_pkt.data.data() + n_bytes_consumed, + first_pkt.data.size() - n_bytes_consumed); + decoder->input_data.resize(decoder->input_data.size() - n_bytes_consumed); + } } + } } - hevc_codecParam = avcodec_parameters_alloc(); - if (!hevc_codecParam) { - err = { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "avcodec_parameters_alloc returned error" }; - goto errexit; + //uint8_t nal_type = (decoder->input_data.front().data[3] >> 1); + + av_frame = av_frame_alloc(); + auto frame_deleter = std::unique_ptr(av_frame, [](AVFrame* frame){av_frame_free(&frame);}); + + if (!av_frame) { + return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "av_frame_alloc returned error" }; } - if (avcodec_parameters_from_context(hevc_codecParam, hevc_codecContext) < 0) + + err = ffmpeg_av_decode(decoder, decoder->av_codec_context, av_frame, out_img, out_user_data, limits); + if (err.code != heif_error_Ok) + return err; + + if (*out_img == nullptr) { + return heif_error_ok; + } + + av_codecParam = avcodec_parameters_alloc(); + auto param_deleter = std::unique_ptr(av_codecParam, [](AVCodecParameters* params){avcodec_parameters_free(¶ms);}); + + if (!av_codecParam) { + return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "avcodec_parameters_alloc returned error" }; + } + if (avcodec_parameters_from_context(av_codecParam, decoder->av_codec_context) < 0) { - err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_parameters_from_context returned error" }; - goto errexit; + return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "avcodec_parameters_from_context returned error" }; } - video_full_range_flag = (hevc_codecParam->color_range == AVCOL_RANGE_JPEG) ? 1 : 0; - color_primaries = hevc_codecParam->color_primaries; - transfer_characteristics = hevc_codecParam->color_trc; - matrix_coefficients = hevc_codecParam->color_space; + // Only AVC/HEVC/VVC (VUI) and AV1 (sequence header) carry CICP colour signalling in the + // bitstream. For JPEG and JPEG 2000, FFmpeg's colour fields are synthesized defaults + // (e.g. "unspecified" and limited range for JPEG 2000), not something read from the + // codestream. Attaching them as a bitstream profile would make libheif convert the + // decoded planes when the file has no 'colr' box. Leave the profile unset for those. + if (!codec_has_cicp_signalling(decoder->av_codec->id)) { + return heif_error_ok; + } + + uint8_t video_full_range_flag = 0; + uint8_t color_primaries = 0; + uint8_t transfer_characteristics = 0; + uint8_t matrix_coefficients = 0; + + video_full_range_flag = (av_codecParam->color_range == AVCOL_RANGE_JPEG) ? 1 : 0; + color_primaries = av_codecParam->color_primaries; + transfer_characteristics = av_codecParam->color_trc; + matrix_coefficients = av_codecParam->color_space; nclx = heif_nclx_color_profile_alloc(); + auto nclx_deleter = std::unique_ptr(nclx, [](heif_color_profile_nclx* nclx){heif_nclx_color_profile_free(nclx);}); + heif_nclx_color_profile_set_color_primaries(nclx, static_cast(color_primaries)); heif_nclx_color_profile_set_transfer_characteristics(nclx, static_cast(transfer_characteristics)); heif_nclx_color_profile_set_matrix_coefficients(nclx, static_cast(matrix_coefficients)); nclx->full_range_flag = (bool)video_full_range_flag; heif_image_set_nclx_color_profile(*out_img, nclx); -errexit: - if (hevc_codecParam) avcodec_parameters_free(&hevc_codecParam); - if (hevc_data) free(hevc_data); - if (hevc_parser) av_parser_close(hevc_parser); - if (hevc_codecContext) avcodec_free_context(&hevc_codecContext); - if (hevc_frame) av_frame_free(&hevc_frame); - if (hevc_pkt) av_packet_free(&hevc_pkt); - if (nclx) heif_nclx_color_profile_free(nclx); + return heif_error_ok; +} + +static heif_error ffmpeg_decode_next_image(void* decoder_raw, + heif_image** out_img, + const heif_security_limits* limits) +{ + return ffmpeg_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + - return err; +static heif_error ffmpeg_decode_image(void* decoder_raw, + heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return ffmpeg_decode_next_image(decoder_raw, out_img, limits); } -static const struct heif_decoder_plugin decoder_ffmpeg +heif_error ffmpeg_flush_data(void* decoder_raw) +{ + auto* decoder = (struct ffmpeg_decoder*) decoder_raw; + + decoder->input_data.push_back({}); + + return heif_error_ok; +} + + +static const heif_decoder_plugin decoder_ffmpeg { - 3, + 5, ffmpeg_plugin_name, ffmpeg_init_plugin, ffmpeg_deinit_plugin, ffmpeg_does_support_format, ffmpeg_new_decoder, ffmpeg_free_decoder, - ffmpeg_v1_push_data, - ffmpeg_v1_decode_image, + ffmpeg_push_data, + ffmpeg_decode_image, ffmpeg_set_strict_decoding, - "ffmpeg" + "ffmpeg", + ffmpeg_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + ffmpeg_does_support_format2, + ffmpeg_new_decoder2, + ffmpeg_push_data2, + ffmpeg_flush_data, + ffmpeg_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_ffmpeg() +const heif_decoder_plugin* get_decoder_plugin_ffmpeg() { return &decoder_ffmpeg; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_jpeg.cc libheif-1.23.4/libheif/plugins/decoder_jpeg.cc --- libheif-1.19.8/libheif/plugins/decoder_jpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_jpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,6 +27,7 @@ #include #include #include +#include extern "C" { #include @@ -36,6 +37,9 @@ struct jpeg_decoder { std::vector data; + uintptr_t user_data; + + std::string error_message; }; static const char kSuccess[] = "Success"; @@ -72,7 +76,7 @@ } -static int jpeg_does_support_format(enum heif_compression_format format) +static int jpeg_does_support_format(heif_compression_format format) { if (format == heif_compression_JPEG) { return JPEG_PLUGIN_PRIORITY; @@ -83,19 +87,34 @@ } -struct heif_error jpeg_new_decoder(void** dec) +static int jpeg_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return jpeg_does_support_format(format->format); +} + +heif_error jpeg_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { struct jpeg_decoder* decoder = new jpeg_decoder(); *dec = decoder; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + + +heif_error jpeg_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_JPEG; + options.num_threads = 0; + options.strict_decoding = false; + + return jpeg_new_decoder2(dec, &options); } void jpeg_free_decoder(void* decoder_raw) { - struct jpeg_decoder* decoder = (jpeg_decoder*) decoder_raw; + jpeg_decoder* decoder = (jpeg_decoder*) decoder_raw; if (!decoder) { return; @@ -111,21 +130,26 @@ } -struct heif_error jpeg_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +heif_error jpeg_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, uintptr_t user_data) { - struct jpeg_decoder* decoder = (struct jpeg_decoder*) decoder_raw; + jpeg_decoder* decoder = (jpeg_decoder*) decoder_raw; const uint8_t* input_data = (const uint8_t*)frame_data; decoder->data.insert(decoder->data.end(), input_data, input_data + frame_size); + decoder->user_data = user_data; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + +heif_error jpeg_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +{ + return jpeg_push_data2(decoder_raw, frame_data, frame_size, 0); } struct my_error_manager { - struct jpeg_error_mgr mgr; + jpeg_error_mgr mgr; jmp_buf setjmp_buffer; }; @@ -145,13 +169,40 @@ } -struct heif_error jpeg_decode_image(void* decoder_raw, struct heif_image** out_img) -{ - struct jpeg_decoder* decoder = (struct jpeg_decoder*) decoder_raw; +// Conservative upper bound on bytes libjpeg will allocate to decode this JPEG. +// Saturates to UINT64_MAX on overflow so callers can compare against limits +// without further overflow checks. The 3x multiplier covers libjpeg's output +// buffer plus internal sample arrays and MCU/row working buffers. +static uint64_t jpeg_estimate_decode_memory_bytes(const jpeg_decompress_struct* cinfo) +{ + auto sat_mul = [](uint64_t a, uint64_t b) -> uint64_t { + if (a == 0 || b == 0) return 0; + if (a > UINT64_MAX / b) return UINT64_MAX; + return a * b; + }; + + uint64_t bytes_per_sample = (cinfo->data_precision > 8) ? 2 : 1; + uint64_t total = sat_mul(uint64_t(cinfo->image_width), uint64_t(cinfo->image_height)); + total = sat_mul(total, uint64_t(cinfo->num_components)); + total = sat_mul(total, bytes_per_sample); + return sat_mul(total, 3); +} + +heif_error jpeg_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + jpeg_decoder* decoder = (jpeg_decoder*) decoder_raw; + + // When there is no input data yet, return NULL image. + if (decoder->data.empty()) { + *out_img = nullptr; + return heif_error_ok; + } - struct jpeg_decompress_struct cinfo; - struct my_error_manager jerr; + jpeg_decompress_struct cinfo; + my_error_manager jerr; // to store embedded icc profile // uint32_t iccLen; @@ -164,6 +215,8 @@ jpeg_create_decompress(&cinfo); + heif_image* heif_img = nullptr; + cinfo.err = jpeg_std_error(&jerr.mgr); jerr.mgr.error_exit = on_jpeg_error; if (setjmp(jerr.setjmp_buffer)) { @@ -171,6 +224,10 @@ jpeg_destroy_decompress(&cinfo); + if (heif_img) { + heif_image_release(heif_img); + } + return heif_error{ heif_error_Decoder_plugin_error, heif_suberror_Unspecified, @@ -188,6 +245,24 @@ jpeg_read_header(&cinfo, TRUE); + // Reject obvious memory bombs before letting libjpeg allocate decode buffers. + // libjpeg has no built-in resource limit API, so we enforce libheif's limits here. + { + uint64_t pixels = uint64_t(cinfo.image_width) * uint64_t(cinfo.image_height); + if (limits->max_image_size_pixels > 0 && pixels > limits->max_image_size_pixels) { + jpeg_destroy_decompress(&cinfo); + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG image exceeds maximum allowed image size"}; + } + + uint64_t estimated_memory = jpeg_estimate_decode_memory_bytes(&cinfo); + if (limits->max_memory_block_size > 0 && estimated_memory > limits->max_memory_block_size) { + jpeg_destroy_decompress(&cinfo); + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG image would require too much memory to decode"}; + } + } + // bool embeddedIccFlag = ReadICCProfileFromJPEG(&cinfo, &iccBuffer, &iccLen); // bool embeddedXMPFlag = ReadXMPFromJPEG(&cinfo, xmpData); // if (embeddedXMPFlag) { @@ -212,20 +287,30 @@ // create destination image - struct heif_image* heif_img = nullptr; - struct heif_error err = heif_image_create(cinfo.output_width, cinfo.output_height, - heif_colorspace_monochrome, - heif_chroma_monochrome, - &heif_img); + heif_error err = heif_image_create(cinfo.output_width, cinfo.output_height, + heif_colorspace_monochrome, + heif_chroma_monochrome, + &heif_img); if (err.code != heif_error_Ok) { assert(heif_img==nullptr); + jpeg_destroy_decompress(&cinfo); return err; } - heif_image_add_plane(heif_img, heif_channel_Y, cinfo.output_width, cinfo.output_height, 8); + err = heif_image_add_plane_safe(heif_img, heif_channel_Y, cinfo.output_width, cinfo.output_height, 8, limits); + if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + heif_img = nullptr; + jpeg_destroy_decompress(&cinfo); + return err; + } - int y_stride; - uint8_t* py = heif_image_get_plane(heif_img, heif_channel_Y, &y_stride); + size_t y_stride; + uint8_t* py = heif_image_get_plane2(heif_img, heif_channel_Y, &y_stride); // read the image @@ -235,8 +320,6 @@ memcpy(py + (cinfo.output_scanline - 1) * y_stride, *buffer, cinfo.output_width); } - - *out_img = heif_img; } else { cinfo.out_color_space = JCS_YCbCr; @@ -250,26 +333,56 @@ // create destination image - struct heif_image* heif_img = nullptr; - struct heif_error err = heif_image_create(cinfo.output_width, cinfo.output_height, - heif_colorspace_YCbCr, - heif_chroma_420, - &heif_img); + heif_error err = heif_image_create(cinfo.output_width, cinfo.output_height, + heif_colorspace_YCbCr, + heif_chroma_420, + &heif_img); if (err.code != heif_error_Ok) { assert(heif_img==nullptr); + jpeg_destroy_decompress(&cinfo); + return err; + } + + err = heif_image_add_plane_safe(heif_img, heif_channel_Y, cinfo.output_width, cinfo.output_height, 8, limits); + if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + heif_img = nullptr; // avoid double free in jpeg error handler + jpeg_destroy_decompress(&cinfo); + return err; + } + err = heif_image_add_plane_safe(heif_img, heif_channel_Cb, (cinfo.output_width + 1) / 2, (cinfo.output_height + 1) / 2, 8, limits); + if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + heif_img = nullptr; // avoid double free in jpeg error handler + jpeg_destroy_decompress(&cinfo); + return err; + } + err = heif_image_add_plane_safe(heif_img, heif_channel_Cr, (cinfo.output_width + 1) / 2, (cinfo.output_height + 1) / 2, 8, limits); + if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + heif_img = nullptr; // avoid double free in jpeg error handler + jpeg_destroy_decompress(&cinfo); return err; } - heif_image_add_plane(heif_img, heif_channel_Y, cinfo.output_width, cinfo.output_height, 8); - heif_image_add_plane(heif_img, heif_channel_Cb, (cinfo.output_width + 1) / 2, (cinfo.output_height + 1) / 2, 8); - heif_image_add_plane(heif_img, heif_channel_Cr, (cinfo.output_width + 1) / 2, (cinfo.output_height + 1) / 2, 8); - - int y_stride; - int cb_stride; - int cr_stride; - uint8_t* py = heif_image_get_plane(heif_img, heif_channel_Y, &y_stride); - uint8_t* pcb = heif_image_get_plane(heif_img, heif_channel_Cb, &cb_stride); - uint8_t* pcr = heif_image_get_plane(heif_img, heif_channel_Cr, &cr_stride); + size_t y_stride; + size_t cb_stride; + size_t cr_stride; + uint8_t* py = heif_image_get_plane2(heif_img, heif_channel_Y, &y_stride); + uint8_t* pcb = heif_image_get_plane2(heif_img, heif_channel_Cb, &cb_stride); + uint8_t* pcr = heif_image_get_plane2(heif_img, heif_channel_Cr, &cr_stride); // read the image @@ -310,8 +423,10 @@ } } } + } - *out_img = heif_img; + if (out_user_data) { + *out_user_data = decoder->user_data; } // if (embeddedIccFlag && iccLen > 0) { @@ -323,15 +438,39 @@ jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); + // Transfer ownership to the caller only after all libjpeg operations have + // completed. jpeg_finish_decompress() can still raise a fatal error (longjmp) + // on corrupt trailing data; until then heif_img must stay owned locally so the + // setjmp error handler can release it. + *out_img = heif_img; + heif_img = nullptr; + decoder->data.clear(); return heif_error_ok; } +heif_error jpeg_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return jpeg_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +heif_error jpeg_decode_image(void* decoder_raw, heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return jpeg_decode_next_image(decoder_raw, out_img, limits); +} + +heif_error jpeg_flush_data(void* decoder) +{ + return heif_error_ok; +} + -static const struct heif_decoder_plugin decoder_jpeg +static const heif_decoder_plugin decoder_jpeg { - 3, + 5, jpeg_plugin_name, jpeg_init_plugin, jpeg_deinit_plugin, @@ -341,11 +480,18 @@ jpeg_push_data, jpeg_decode_image, jpeg_set_strict_decoding, - "jpeg" + "jpeg", + jpeg_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + jpeg_does_support_format2, + jpeg_new_decoder2, + jpeg_push_data2, + jpeg_flush_data, + jpeg_decode_next_image2, }; -const struct heif_decoder_plugin* get_decoder_plugin_jpeg() +const heif_decoder_plugin* get_decoder_plugin_jpeg() { return &decoder_jpeg; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_libde265.cc libheif-1.23.4/libheif/plugins/decoder_libde265.cc --- libheif-1.19.8/libheif/plugins/decoder_libde265.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_libde265.cc 2026-09-06 14:45:17.000000000 +0000 @@ -20,12 +20,12 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" -//#include "libheif/heif_colorconversion.h" -//#include "libheif/heif_api_structs.h" #include "decoder_libde265.h" -#include +#include #include #include +#include +#include #include @@ -35,6 +35,7 @@ { de265_decoder_context* ctx; bool strict_decoding = false; + std::string error_message; }; static const char kEmptyString[] = ""; @@ -45,7 +46,7 @@ #define MAX_PLUGIN_NAME_LENGTH 80 static char plugin_name[MAX_PLUGIN_NAME_LENGTH]; - +static constexpr char version_prefix[] = ", version "; static const char* libde265_plugin_name() { @@ -53,8 +54,8 @@ const char* libde265_version = de265_get_version(); - if (strlen(libde265_version) + 10 < MAX_PLUGIN_NAME_LENGTH) { - strcat(plugin_name, ", version "); + if (strlen(plugin_name) + strlen(libde265_version) + (sizeof(version_prefix) - 1) < MAX_PLUGIN_NAME_LENGTH) { + strcat(plugin_name, version_prefix); strcat(plugin_name, libde265_version); } @@ -74,7 +75,7 @@ } -static int libde265_does_support_format(enum heif_compression_format format) +static int libde265_does_support_format(heif_compression_format format) { if (format == heif_compression_HEVC) { return LIBDE265_PLUGIN_PRIORITY; @@ -85,9 +86,18 @@ } -static struct heif_error convert_libde265_image_to_heif_image(struct libde265_decoder* decoder, - const struct de265_image* de265img, - struct heif_image** image) +[[maybe_unused]] +static int libde265_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return libde265_does_support_format(format->format); +} + + + +static heif_error convert_libde265_image_to_heif_image(libde265_decoder* decoder, + const de265_image* de265img, + heif_image** image, + const heif_security_limits* limits) { bool is_mono = (de265_get_chroma_format(de265img) == de265_chroma_mono); @@ -136,19 +146,23 @@ return err; } - err = heif_image_add_plane(*image, channel2plane[c], w,h, bpp); + err = heif_image_add_plane_safe(*image, channel2plane[c], w,h, bpp, limits); if (err.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + heif_image_release(*image); return err; } - int dst_stride; - uint8_t* dst_mem = heif_image_get_plane(*image, channel2plane[c], &dst_stride); + size_t dst_stride; + uint8_t* dst_mem = heif_image_get_plane2(*image, channel2plane[c], &dst_stride); int bytes_per_pixel = (bpp + 7) / 8; for (int y = 0; y < h; y++) { - memcpy(dst_mem + y * dst_stride, data + y * stride, w * bytes_per_pixel); + memcpy(dst_mem + y * dst_stride, data + static_cast(y) * stride, static_cast(w) * bytes_per_pixel); } } @@ -157,28 +171,64 @@ } -static struct heif_error libde265_new_decoder(void** dec) + +// Create a new decoder context for decoding an image +heif_error libde265_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { - struct libde265_decoder* decoder = new libde265_decoder(); - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + libde265_decoder* decoder = new libde265_decoder(); + heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; decoder->ctx = de265_new_decoder(); + +#if LIBDE265_NUMERIC_VERSION >= 0x01010000 + // Pass libheif's max_image_size_pixels through to libde265 so the decoder + // rejects bitstreams whose SPS declares an image far larger than what the + // surrounding HEIF file claims. libheif tightens this limit per-decode to + // ispe + one coding-unit margin (see tighten_image_size_limit_for_ispe), + // so the padding margin is already baked into the value we see here. + { + const heif_security_limits* limits = options->limits ? options->limits : heif_get_global_security_limits(); + de265_security_limits* de265_limits = de265_get_security_limits(decoder->ctx); + if (limits->max_image_size_pixels > std::numeric_limits::max()) { + de265_limits->max_image_size_pixels = 0; + } + else { + de265_limits->max_image_size_pixels = static_cast(limits->max_image_size_pixels); + } + } +#endif + #if defined(__EMSCRIPTEN__) // Speed up decoding from JavaScript. de265_set_parameter_bool(decoder->ctx, DE265_DECODER_PARAM_DISABLE_DEBLOCKING, 1); de265_set_parameter_bool(decoder->ctx, DE265_DECODER_PARAM_DISABLE_SAO, 1); #else + int nThreads = (options->num_threads ? options->num_threads : 1); + // Worker threads are not supported when running on Emscripten. - de265_start_worker_threads(decoder->ctx, 1); + de265_start_worker_threads(decoder->ctx, nThreads); #endif + decoder->strict_decoding = options->strict_decoding; + *dec = decoder; return err; } + +static heif_error libde265_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_HEVC; + options.num_threads = 0; + options.strict_decoding = false; + + return libde265_new_decoder2(dec, &options); +} + static void libde265_free_decoder(void* decoder_raw) { - struct libde265_decoder* decoder = (struct libde265_decoder*) decoder_raw; + libde265_decoder* decoder = (libde265_decoder*) decoder_raw; de265_error err = de265_free_decoder(decoder->ctx); (void) err; @@ -189,7 +239,7 @@ void libde265_set_strict_decoding(void* decoder_raw, int flag) { - struct libde265_decoder* decoder = (libde265_decoder*) decoder_raw; + libde265_decoder* decoder = (libde265_decoder*) decoder_raw; decoder->strict_decoding = flag; } @@ -197,19 +247,18 @@ #if LIBDE265_NUMERIC_VERSION >= 0x02000000 -static struct heif_error libde265_v2_push_data(void* decoder_raw, const void* data, size_t size) +static heif_error libde265_v2_push_data(void* decoder_raw, const void* data, size_t size) { - struct libde265_decoder* decoder = (struct libde265_decoder*)decoder_raw; + libde265_decoder* decoder = (libde265_decoder*)decoder_raw; const uint8_t* cdata = (const uint8_t*)data; size_t ptr=0; while (ptr < size) { if (4 > size - ptr) { - struct heif_error err = { heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - kEmptyString }; - return err; + return { heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString }; } // TODO: the size of the NAL unit length variable is defined in the hvcC header. @@ -221,10 +270,9 @@ //sstr << "NAL size (" << size32 << ") exceeds available data in file (" //<< data_bytes_left_to_read << ")"; - struct heif_error err = { heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - kEmptyString }; - return err; + return { heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString }; } de265_push_NAL(decoder->ctx, cdata+ptr, nal_size, 0, nullptr); @@ -232,15 +280,15 @@ } - struct heif_error err = { heif_error_Ok, heif_suberror_Unspecified, kSuccess }; - return err; + return { heif_error_Ok, heif_suberror_Unspecified, kSuccess }; } -static struct heif_error libde265_v2_decode_image(void* decoder_raw, - struct heif_image** out_img) +static heif_error libde265_v2_decode_next_image(void* decoder_raw, + heif_image** out_img, + const heif_security_limits* limits) { - struct libde265_decoder* decoder = (struct libde265_decoder*)decoder_raw; + libde265_decoder* decoder = (libde265_decoder*)decoder_raw; de265_push_end_of_stream(decoder->ctx); @@ -252,65 +300,100 @@ if (action==de265_action_get_image) { const de265_image* img = de265_get_next_picture(decoder->ctx); if (img) { - struct heif_error err = convert_libde265_image_to_heif_image(decoder, img, - out_img); + heif_error err = convert_libde265_image_to_heif_image(decoder, img, + out_img, limits); de265_release_picture(img); return err; } } - struct heif_error err = { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, kEmptyString }; - return err; + return { heif_error_Decoder_plugin_error, heif_suberror_Unspecified, kEmptyString }; } +static heif_error libde265_v2_decode_image(void* decoder_raw, + heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return libde265_v2_decode_next_image(decoder_raw, out_img, limits); +} #else -static struct heif_error libde265_v1_push_data(void* decoder_raw, const void* data, size_t size) +static heif_error libde265_v1_push_data2(void* decoder_raw, const void* data, size_t size, uintptr_t user_data) { - struct libde265_decoder* decoder = (struct libde265_decoder*) decoder_raw; + libde265_decoder* decoder = (libde265_decoder*) decoder_raw; const uint8_t* cdata = (const uint8_t*) data; size_t ptr = 0; while (ptr < size) { if (4 > size - ptr) { - struct heif_error err = {heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - kEmptyString}; - return err; + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString + }; } uint32_t nal_size = static_cast((cdata[ptr] << 24) | (cdata[ptr + 1] << 16) | (cdata[ptr + 2] << 8) | (cdata[ptr + 3])); ptr += 4; if (nal_size > size - ptr) { - struct heif_error err = {heif_error_Decoder_plugin_error, - heif_suberror_End_of_data, - kEmptyString}; - return err; - } + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString + }; + } + +#if 0 + FILE* fh = fopen("data.h265", "a"); + fputc(0, fh); + fputc(0, fh); + fputc(1, fh); + fwrite(cdata + ptr, nal_size, 1, fh); + fclose(fh); - de265_push_NAL(decoder->ctx, cdata + ptr, nal_size, 0, nullptr); + printf("put nal with size %d %x\n", nal_size, *(cdata+ptr)); +#endif + + // The codec hands this opaque pointer back unchanged; it carries an integer, not an address. + de265_push_NAL(decoder->ctx, cdata + ptr, nal_size, 0, (void*)user_data); // NOLINT(performance-no-int-to-ptr) ptr += nal_size; } // TODO(farindk): Set "err" if data could not be pushed //de265_push_data(decoder->ctx, data, size, 0, nullptr); - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; } +static heif_error libde265_v1_push_data(void* decoder_raw, const void* data, size_t size) +{ + return libde265_v1_push_data2(decoder_raw, data, size, 0); +} -static struct heif_error libde265_v1_decode_image(void* decoder_raw, - struct heif_image** out_img) +static heif_error libde265_flush_data(void* decoder_raw) { - struct libde265_decoder* decoder = (struct libde265_decoder*) decoder_raw; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + libde265_decoder* decoder = (libde265_decoder*) decoder_raw; de265_flush_data(decoder->ctx); + return heif_error_ok; +} + + + +static heif_error libde265_v1_decode_next_image2(void* decoder_raw, + heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + libde265_decoder* decoder = (libde265_decoder*) decoder_raw; + heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + + // TODO(251119) : de265_flush_data(decoder->ctx); + // TODO(farindk): Set "err" if no image was decoded. int more; de265_error decode_err; @@ -325,18 +408,23 @@ // TODO: read NCLX from h265 bitstream - const struct de265_image* image = de265_get_next_picture(decoder->ctx); + const de265_image* image = de265_get_next_picture(decoder->ctx); if (image) { // TODO(farindk): Should we return the first image instead? if (*out_img) { heif_image_release(*out_img); } - err = convert_libde265_image_to_heif_image(decoder, image, out_img); + + if (out_user_data) { + *out_user_data = (uintptr_t)de265_get_image_user_data(image); + } + + err = convert_libde265_image_to_heif_image(decoder, image, out_img, limits); if (err.code != heif_error_Ok) { return err; } - struct heif_color_profile_nclx* nclx = heif_nclx_color_profile_alloc(); + heif_color_profile_nclx* nclx = heif_nclx_color_profile_alloc(); #if LIBDE265_NUMERIC_VERSION >= 0x01000700 HEIF_WARN_OR_FAIL(decoder->strict_decoding, *out_img, heif_nclx_color_profile_set_color_primaries(nclx, static_cast(de265_get_image_colour_primaries(image))), { @@ -362,6 +450,7 @@ heif_nclx_color_profile_free(nclx); de265_release_next_picture(decoder->ctx); + return heif_error_ok; } } while (more); @@ -369,12 +458,26 @@ } +static heif_error libde265_v1_decode_next_image(void* decoder_raw, + heif_image** out_img, + const heif_security_limits* limits) +{ + return libde265_v1_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +static heif_error libde265_v1_decode_image(void* decoder_raw, + heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return libde265_v1_decode_next_image(decoder_raw, out_img, limits); +} + #endif #if LIBDE265_NUMERIC_VERSION >= 0x02000000 -static const struct heif_decoder_plugin decoder_libde265 +static const heif_decoder_plugin decoder_libde265 { 1, libde265_plugin_name, @@ -384,14 +487,17 @@ libde265_new_decoder, libde265_free_decoder, libde265_v2_push_data, - libde265_v2_decode_image + libde265_v2_decode_image, + libde265_set_strict_decoding, + "libde265", + libde265_v2_decode_next_image }; #else -static const struct heif_decoder_plugin decoder_libde265 +static const heif_decoder_plugin decoder_libde265 { - 3, + 5, libde265_plugin_name, libde265_init_plugin, libde265_deinit_plugin, @@ -401,12 +507,19 @@ libde265_v1_push_data, libde265_v1_decode_image, libde265_set_strict_decoding, - "libde265" + "libde265", + libde265_v1_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + libde265_does_support_format2, + libde265_new_decoder2, + libde265_v1_push_data2, + libde265_flush_data, + libde265_v1_decode_next_image2 }; #endif -const struct heif_decoder_plugin* get_decoder_plugin_libde265() +const heif_decoder_plugin* get_decoder_plugin_libde265() { return &decoder_libde265; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_openh264.cc libheif-1.23.4/libheif/plugins/decoder_openh264.cc --- libheif-1.19.8/libheif/plugins/decoder_openh264.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_openh264.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,29 +21,64 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "decoder_openh264.h" -#include #include #include #include #include +#include #include +#include +#include +// TODO: the openh264 decoder seems to fail with some images. +// I have not figured out yet which images are affected. +// Maybe it has something to do with the image size. I got the error with large images. struct openh264_decoder { - std::vector data; + struct Packet + { + std::vector data; + uintptr_t pts; + }; + + std::deque input_data; + bool m_eof_reached = false; + + std::string error_message; + + + // --- decoder + + ISVCDecoder* decoder = nullptr; + + ~openh264_decoder() + { + if (decoder) { + // Step 6:uninitialize the decoder and memory free + + decoder->Uninitialize(); // TODO: do we have to Uninitialize when an error is returned? + + + WelsDestroyDecoder(decoder); + } + } }; static const char kSuccess[] = "Success"; -static const int OpenH264_PLUGIN_PRIORITY = 100; +// Reduced priority because OpenH264 cannot pass through user-data. +// We need this feature for decoding sequences with SAI. +// Prefer to use the FFMPEG plugin. +static const int OpenH264_PLUGIN_PRIORITY = 70; #define MAX_PLUGIN_NAME_LENGTH 80 static char plugin_name[MAX_PLUGIN_NAME_LENGTH]; static heif_error kError_EOF = {heif_error_Decoder_plugin_error, heif_suberror_End_of_data, "Insufficient input data"}; +static heif_error kError_invalid_data = {heif_error_Decoder_plugin_error, heif_suberror_Invalid_parameter_value, "Invalid input data"}; static const char* openh264_plugin_name() @@ -66,7 +101,7 @@ } -static int openh264_does_support_format(enum heif_compression_format format) +static int openh264_does_support_format(heif_compression_format format) { if (format == heif_compression_AVC) { return OpenH264_PLUGIN_PRIORITY; @@ -76,17 +111,51 @@ } } +static int openh264_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return openh264_does_support_format(format->format); +} + -struct heif_error openh264_new_decoder(void** dec) +heif_error openh264_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { auto* decoder = new openh264_decoder(); *dec = decoder; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + // Step 2:decoder creation + WelsCreateDecoder(&decoder->decoder); + if (!decoder->decoder) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Cannot create OpenH264 decoder" + }; + } + + // Step 3:declare required parameter, used to differentiate Decoding only and Parsing only + SDecodingParam sDecParam{}; + sDecParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC; + + //for Parsing only, the assignment is mandatory + // sDecParam.bParseOnly = true; + + // Step 4:initialize the parameter and decoder context, allocate memory + decoder->decoder->Initialize(&sDecParam); + + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; } +heif_error openh264_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_AVC; + options.num_threads = 0; + options.strict_decoding = false; + + return openh264_new_decoder2(dec, &options); +} + void openh264_free_decoder(void* decoder_raw) { auto* decoder = (openh264_decoder*) decoder_raw; @@ -101,134 +170,161 @@ void openh264_set_strict_decoding(void* decoder_raw, int flag) { -// auto* decoder = (openh264_decoder*) decoder_raw; + // auto* decoder = (openh264_decoder*) decoder_raw; } -struct heif_error openh264_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +heif_error openh264_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, uintptr_t user_data) { - auto* decoder = (struct openh264_decoder*) decoder_raw; + auto* decoder = (openh264_decoder*) decoder_raw; const auto* input_data = (const uint8_t*) frame_data; - decoder->data.insert(decoder->data.end(), input_data, input_data + frame_size); + if (frame_size < 4) { + return kError_invalid_data; + } + + openh264_decoder::Packet pkt; + pkt.data.insert(pkt.data.end(), input_data, input_data + frame_size); + pkt.pts = user_data; + decoder->input_data.push_back(std::move(pkt)); + + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; +heif_error openh264_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +{ + return openh264_push_data2(decoder_raw, frame_data, frame_size, 0); } -struct heif_error openh264_decode_image(void* decoder_raw, struct heif_image** out_img) +heif_error openh264_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) { - auto* decoder = (struct openh264_decoder*) decoder_raw; + auto* decoder = (openh264_decoder*) decoder_raw; + ISVCDecoder* pSvcDecoder = decoder->decoder; - if (decoder->data.size() < 4) { - return kError_EOF; + // --- Not enough data, but no EOF yet. + + if (decoder->input_data.empty() && !decoder->m_eof_reached) { + *out_img = nullptr; + return heif_error_ok; } - const std::vector& indata = decoder->data; - std::vector scdata; - size_t idx = 0; - while (idx < indata.size()) { - if (indata.size() - 4 < idx) { - return kError_EOF; - } + //output: [0~2] for Y,U,V buffer for Decoding only + unsigned char* pData[3] = {nullptr, nullptr, nullptr}; - uint32_t size = ((indata[idx] << 24) | (indata[idx + 1] << 16) | (indata[idx + 2] << 8) | indata[idx + 3]); - idx += 4; + // in-out: for Decoding only: declare and initialize the output buffer info, this should never co-exist with Parsing only - if (indata.size() < size || indata.size() - size < idx) { - return kError_EOF; - } + SBufferInfo sDstBufInfo; + memset(&sDstBufInfo, 0, sizeof(SBufferInfo)); - scdata.push_back(0); - scdata.push_back(0); - scdata.push_back(1); - - // check for need of start code emulation prevention - - bool do_start_code_emulation_check = true; - - while (do_start_code_emulation_check && size >= 3) { - - bool found_start_code_emulation = false; - - for (size_t i = 0; i < size - 3; i++) { - if (indata[idx + 0] == 0 && - indata[idx + 1] == 0 && - (indata[idx + 2] >= 0 && indata[idx + 2] <= 3)) { - scdata.push_back(0); - scdata.push_back(0); - scdata.push_back(3); - - scdata.insert(scdata.end(), &indata[idx + 2], indata.data() + idx + i + 2); - idx += i + 2; - size -= (uint32_t)(i + 2); - found_start_code_emulation = true; - break; - } - } + int iRet; - do_start_code_emulation_check = found_start_code_emulation; - } + if (!decoder->input_data.empty()) { + sDstBufInfo.uiInBsTimeStamp = decoder->input_data.front().pts; - assert(size > 0); - // Note: we cannot write &indata[idx + size] since that would use the operator[] on an element beyond the vector range. - scdata.insert(scdata.end(), &indata[idx], indata.data() + idx + size); + const std::vector& indata = decoder->input_data.front().data; + std::vector scdata; - idx += size; - } + size_t idx = 0; + while (idx < indata.size()) { + if (indata.size() - 4 < idx) { + return kError_EOF; + } - if (idx != indata.size()) { - return kError_EOF; - } + // Compute in uint32_t. The bytes promote to 'int', so a leading byte >= 0x80 + // shifted left by 24 lands in the sign bit and the result would be negative. + uint32_t size = (static_cast(indata[idx]) << 24) | + (static_cast(indata[idx + 1]) << 16) | + (static_cast(indata[idx + 2]) << 8) | + static_cast(indata[idx + 3]); + idx += 4; - // input: encoded bitstream start position; should include start code prefix - unsigned char* pBuf = scdata.data(); + if (indata.size() < size || indata.size() - size < idx) { + return kError_EOF; + } - // input: encoded bit stream length; should include the size of start code prefix - int iSize = static_cast(scdata.size()); + scdata.push_back(0); + scdata.push_back(0); + scdata.push_back(1); + + // check for need of start code emulation prevention + + bool do_start_code_emulation_check = true; + + while (do_start_code_emulation_check && size >= 3) { + bool found_start_code_emulation = false; + + for (size_t i = 0; i < size - 3; i++) { + if (indata[idx + 0] == 0 && + indata[idx + 1] == 0 && + (indata[idx + 2] >= 0 && indata[idx + 2] <= 3)) { + scdata.push_back(0); + scdata.push_back(0); + scdata.push_back(3); + + scdata.insert(scdata.end(), &indata[idx + 2], indata.data() + idx + i + 2); + idx += i + 2; + size -= (uint32_t) (i + 2); + found_start_code_emulation = true; + break; + } + } - //output: [0~2] for Y,U,V buffer for Decoding only - unsigned char* pData[3] = {nullptr, nullptr, nullptr}; + do_start_code_emulation_check = found_start_code_emulation; + } - // in-out: for Decoding only: declare and initialize the output buffer info, this should never co-exist with Parsing only + if (size == 0) { + return kError_invalid_data; + } + // Note: we cannot write &indata[idx + size] since that would use the operator[] on an element beyond the vector range. + scdata.insert(scdata.end(), &indata[idx], indata.data() + idx + size); - SBufferInfo sDstBufInfo; - memset(&sDstBufInfo, 0, sizeof(SBufferInfo)); + idx += size; + } - // Step 2:decoder creation - ISVCDecoder* pSvcDecoder; - WelsCreateDecoder(&pSvcDecoder); - if (!pSvcDecoder) { - return {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - "Cannot create OpenH264 decoder"}; - } + if (idx != indata.size()) { + decoder->input_data.pop_front(); + return kError_EOF; + } - std::unique_ptr dummy_h264_decoder_ptr(pSvcDecoder, WelsDestroyDecoder); + decoder->input_data.pop_front(); + // input: encoded bitstream start position; should include start code prefix + unsigned char* pBuf = scdata.data(); - // Step 3:declare required parameter, used to differentiate Decoding only and Parsing only - SDecodingParam sDecParam{}; - sDecParam.sVideoProperty.eVideoBsType = VIDEO_BITSTREAM_AVC; + // input: encoded bit stream length; should include the size of start code prefix + int iSize = static_cast(scdata.size()); - //for Parsing only, the assignment is mandatory - // sDecParam.bParseOnly = true; - // Step 4:initialize the parameter and decoder context, allocate memory - pSvcDecoder->Initialize(&sDecParam); + // Step 5:do actual decoding process in slice level; this can be done in a loop until data ends - // Step 5:do actual decoding process in slice level; this can be done in a loop until data ends - //for Decoding only - int iRet = pSvcDecoder->DecodeFrameNoDelay(pBuf, iSize, pData, &sDstBufInfo); + //for Decoding only + iRet = pSvcDecoder->DecodeFrameNoDelay(pBuf, iSize, pData, &sDstBufInfo); + } + else { + iRet = pSvcDecoder->FlushFrame(pData, &sDstBufInfo); + } if (iRet != 0) { - return {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - "OpenH264 decoder error"}; + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "OpenH264 decoder error" + }; + } + + if (sDstBufInfo.iBufferStatus != 1) { + *out_img = nullptr; + return heif_error_ok; + } + + if (out_user_data) { + *out_user_data = sDstBufInfo.uiOutYuvTimeStamp; } /* @@ -243,8 +339,8 @@ uint32_t width = sDstBufInfo.UsrData.sSystemBuffer.iWidth; uint32_t height = sDstBufInfo.UsrData.sSystemBuffer.iHeight; - struct heif_image* heif_img; - struct heif_error err{}; + heif_image* heif_img; + heif_error err{}; uint32_t cwidth, cheight; @@ -263,16 +359,42 @@ *out_img = heif_img; - heif_image_add_plane(heif_img, heif_channel_Y, width, height, 8); - heif_image_add_plane(heif_img, heif_channel_Cb, cwidth, cheight, 8); - heif_image_add_plane(heif_img, heif_channel_Cr, cwidth, cheight, 8); - - int y_stride; - int cb_stride; - int cr_stride; - uint8_t* py = heif_image_get_plane(heif_img, heif_channel_Y, &y_stride); - uint8_t* pcb = heif_image_get_plane(heif_img, heif_channel_Cb, &cb_stride); - uint8_t* pcr = heif_image_get_plane(heif_img, heif_channel_Cr, &cr_stride); + err = heif_image_add_plane_safe(heif_img, heif_channel_Y, width, height, 8, limits); + if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + return err; + } + + err = heif_image_add_plane_safe(heif_img, heif_channel_Cb, cwidth, cheight, 8, limits); + if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + return err; + } + + err = heif_image_add_plane_safe(heif_img, heif_channel_Cr, cwidth, cheight, 8, limits); + if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + + heif_image_release(heif_img); + return err; + } + + size_t y_stride; + size_t cb_stride; + size_t cr_stride; + uint8_t* py = heif_image_get_plane2(heif_img, heif_channel_Y, &y_stride); + uint8_t* pcb = heif_image_get_plane2(heif_img, heif_channel_Cb, &cb_stride); + uint8_t* pcr = heif_image_get_plane2(heif_img, heif_channel_Cr, &cr_stride); int ystride = sDstBufInfo.UsrData.sSystemBuffer.iStride[0]; int cstride = sDstBufInfo.UsrData.sSystemBuffer.iStride[1]; @@ -287,37 +409,65 @@ } } else { - return {heif_error_Decoder_plugin_error, - heif_suberror_Unspecified, - "Unsupported image pixel format"}; + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Unsupported image pixel format" + }; } - // Step 6:uninitialize the decoder and memory free + // decoder->data.clear(); + + return heif_error_ok; +} + + +heif_error openh264_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return openh264_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +heif_error openh264_decode_image(void* decoder_raw, heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return openh264_decode_next_image(decoder_raw, out_img, limits); +} - pSvcDecoder->Uninitialize(); // TODO: do we have to Uninitialize when an error is returned? - decoder->data.clear(); +heif_error openh264_flush_data(void* decoder_raw) +{ + auto* decoder = (struct openh264_decoder*) decoder_raw; + + decoder->m_eof_reached = true; return heif_error_ok; } -static const struct heif_decoder_plugin decoder_openh264{ - 3, - openh264_plugin_name, - openh264_init_plugin, - openh264_deinit_plugin, - openh264_does_support_format, - openh264_new_decoder, - openh264_free_decoder, - openh264_push_data, - openh264_decode_image, - openh264_set_strict_decoding, - "openh264" +static const heif_decoder_plugin decoder_openh264{ + 5, + openh264_plugin_name, + openh264_init_plugin, + openh264_deinit_plugin, + openh264_does_support_format, + openh264_new_decoder, + openh264_free_decoder, + openh264_push_data, + openh264_decode_image, + openh264_set_strict_decoding, + "openh264", + openh264_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + openh264_does_support_format2, + openh264_new_decoder2, + openh264_push_data2, + openh264_flush_data, + openh264_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_openh264() +const heif_decoder_plugin* get_decoder_plugin_openh264() { return &decoder_openh264; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_openjpeg.cc libheif-1.23.4/libheif/plugins/decoder_openjpeg.cc --- libheif-1.19.8/libheif/plugins/decoder_openjpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_openjpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -22,12 +22,14 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "decoder_openjpeg.h" +#include "common_utils.h" #include #include #include #include #include +#include static const int OPENJPEG_PLUGIN_PRIORITY = 100; static const int OPENJPEG_PLUGIN_PRIORITY_HTJ2K = 90; @@ -35,7 +37,10 @@ struct openjpeg_decoder { std::vector encoded_data; + uintptr_t user_data; + size_t read_position = 0; + std::string error_message; }; @@ -61,7 +66,7 @@ } -static int openjpeg_does_support_format(enum heif_compression_format format) +static int openjpeg_does_support_format(heif_compression_format format) { if (format == heif_compression_JPEG2000) { return OPENJPEG_PLUGIN_PRIORITY; @@ -74,20 +79,33 @@ } } +static int openjpeg_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return openjpeg_does_support_format(format->format); +} -struct heif_error openjpeg_new_decoder(void** dec) +heif_error openjpeg_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { - struct openjpeg_decoder* decoder = new openjpeg_decoder(); + openjpeg_decoder* decoder = new openjpeg_decoder(); *dec = decoder; return heif_error_ok; } +heif_error openjpeg_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_JPEG2000; + options.num_threads = 0; + options.strict_decoding = false; + + return openjpeg_new_decoder2(dec, &options); +} void openjpeg_free_decoder(void* decoder_raw) { - struct openjpeg_decoder* decoder = (openjpeg_decoder*) decoder_raw; + openjpeg_decoder* decoder = (openjpeg_decoder*) decoder_raw; if (!decoder) { return; @@ -103,16 +121,22 @@ } -struct heif_error openjpeg_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +heif_error openjpeg_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, + uintptr_t user_data) { - struct openjpeg_decoder* decoder = (struct openjpeg_decoder*) decoder_raw; + openjpeg_decoder* decoder = (openjpeg_decoder*) decoder_raw; const uint8_t* frame_data_src = (const uint8_t*) frame_data; decoder->encoded_data.insert(decoder->encoded_data.end(), frame_data_src, frame_data_src + frame_size); + decoder->user_data = user_data; return heif_error_ok; } +heif_error openjpeg_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +{ + return openjpeg_push_data2(decoder_raw, frame_data, frame_size, 0); +} //************************************************************************** @@ -253,10 +277,48 @@ //************************************************************************** -struct heif_error openjpeg_decode_image(void* decoder_raw, struct heif_image** out_img) +// Conservative upper bound on bytes OpenJPEG will allocate to decode this +// codestream. Saturates to UINT64_MAX on overflow. OpenJPEG stores each sample +// internally as OPJ_INT32 regardless of the codestream bit depth; the 3x +// multiplier covers the final image planes plus in-flight tile and DWT +// working buffers. +static uint64_t openjpeg_estimate_decode_memory_bytes(const opj_image_t* image) +{ + auto sat_mul = [](uint64_t a, uint64_t b) -> uint64_t { + if (a == 0 || b == 0) return 0; + if (a > UINT64_MAX / b) return UINT64_MAX; + return a * b; + }; + + auto sat_add = [](uint64_t a, uint64_t b) -> uint64_t { + uint64_t s = a + b; + return (s < a) ? UINT64_MAX : s; + }; + + uint64_t total = 0; + for (uint32_t c = 0; c < image->numcomps; c++) { + const opj_image_comp_t& comp = image->comps[c]; + uint64_t plane = sat_mul(uint64_t(comp.w), uint64_t(comp.h)); + plane = sat_mul(plane, sizeof(OPJ_INT32)); + total = sat_add(total, plane); + } + + return sat_mul(total, 3); +} + + +heif_error openjpeg_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) { auto* decoder = (struct openjpeg_decoder*) decoder_raw; + if (decoder->encoded_data.empty()) { + *out_img = nullptr; + return heif_error_ok; + } + + OPJ_BOOL success; opj_dparameters_t decompression_parameters; std::unique_ptr l_codec(opj_create_decompress(OPJ_CODEC_J2K), @@ -266,8 +328,7 @@ opj_set_default_decoder_parameters(&decompression_parameters); success = opj_setup_decoder(l_codec.get(), &decompression_parameters); if (!success) { - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_setup_decoder()"}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_setup_decoder()"}; } @@ -282,21 +343,44 @@ opj_image_t* image_ptr = nullptr; success = opj_read_header(stream.get(), l_codec.get(), &image_ptr); if (!success) { - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_read_header()"}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_read_header()"}; } std::unique_ptr image(image_ptr, opj_image_destroy); + // Reject obvious memory bombs before letting OpenJPEG allocate decode buffers. + // OpenJPEG has no built-in resource limit API, so we enforce libheif's limits here. + if (image->x1 < image->x0 || image->y1 < image->y0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, + "Invalid JPEG 2000 image bounding box"}; + } + + uint64_t img_w = image->x1 - image->x0; + uint64_t img_h = image->y1 - image->y0; + + // image->x0,x1,y0,y1 are uint32, thus no overflow here + uint64_t pixels = img_w * img_h; + if (limits->max_image_size_pixels > 0 && pixels > limits->max_image_size_pixels) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG 2000 image exceeds maximum allowed image size"}; + } + + uint64_t estimated_memory = openjpeg_estimate_decode_memory_bytes(image.get()); + if (limits->max_memory_block_size > 0 && estimated_memory > limits->max_memory_block_size) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG 2000 image would require too much memory to decode"}; + } + + // TODO: also enforce limits->max_components against image->numcomps, and + // limits->max_number_of_tiles against opj_get_cstr_info()->tw * th. + if (image->numcomps != 3 && image->numcomps != 1) { //TODO - Handle other numbers of components - struct heif_error err = {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Number of components must be 3 or 1"}; - return err; + return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Number of components must be 3 or 1"}; } else if ((image->color_space != OPJ_CLRSPC_UNSPECIFIED) && (image->color_space != OPJ_CLRSPC_SRGB)) { //TODO - Handle other colorspaces - struct heif_error err = {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Colorspace must be SRGB"}; - return err; + return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Colorspace must be SRGB"}; } const int width = (image->x1 - image->x0); @@ -306,15 +390,13 @@ /* Get the decoded image */ success = opj_decode(l_codec.get(), stream.get(), image.get()); if (!success) { - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_decode()"}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_decode()"}; } success = opj_end_decompress(l_codec.get(), stream.get()); if (!success) { - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_end_decompress()"}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "opj_end_decompress()"}; } @@ -350,12 +432,25 @@ channels = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; } else { - struct heif_error err = {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "unsupported image format"}; - return err; + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "unsupported image format"}; } - struct heif_error error = heif_image_create(width, height, colorspace, chroma, out_img); + // Validate per-component sizes against the chroma format derived above. A malformed + // JPEG 2000 stream may set comp[1].dx/dy consistently with a chroma format yet declare + // comp[c].w/h that do not match the subsampled dimensions; using such planes downstream + // causes out-of-bounds reads in color conversion (issue #1796). + for (size_t c = 0; c < image->numcomps; c++) { + uint32_t expected_w, expected_h; + get_subsampled_size(static_cast(width), static_cast(height), + channels[c], chroma, &expected_w, &expected_h); + if (image->comps[c].w != expected_w || image->comps[c].h != expected_h) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, + "JPEG 2000 component size does not match the image's chroma subsampling"}; + } + } + + heif_error error = heif_image_create(width, height, colorspace, chroma, out_img); if (error.code) { return error; } @@ -367,10 +462,19 @@ int cwidth = opj_comp.w; int cheight = opj_comp.h; - error = heif_image_add_plane(*out_img, channels[c], cwidth, cheight, bit_depth); + error = heif_image_add_plane_safe(*out_img, channels[c], cwidth, cheight, bit_depth, limits); + if (error.code) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = error.message; + error.message = decoder->error_message.c_str(); + + heif_image_release(*out_img); + *out_img = nullptr; + return error; + } - int stride = -1; - uint8_t* p = heif_image_get_plane(*out_img, channels[c], &stride); + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(*out_img, channels[c], &stride); // TODO: a SIMD implementation to convert int32 to uint8 would speed this up @@ -393,12 +497,36 @@ } } + if (out_user_data) { + *out_user_data = decoder->user_data; + } + + decoder->encoded_data.clear(); + decoder->read_position = 0; + + return heif_error_ok; +} + +heif_error openjpeg_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return openjpeg_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +heif_error openjpeg_decode_image(void* decoder_raw, heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return openjpeg_decode_next_image(decoder_raw, out_img, limits); +} + +heif_error openjpeg_flush_data(void* decoder) +{ return heif_error_ok; } -static const struct heif_decoder_plugin decoder_openjpeg{ - 3, +static const heif_decoder_plugin decoder_openjpeg{ + 5, openjpeg_plugin_name, openjpeg_init_plugin, openjpeg_deinit_plugin, @@ -408,10 +536,17 @@ openjpeg_push_data, openjpeg_decode_image, openjpeg_set_strict_decoding, - "openjpeg" + "openjpeg", + openjpeg_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + openjpeg_does_support_format2, + openjpeg_new_decoder2, + openjpeg_push_data2, + openjpeg_flush_data, + openjpeg_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_openjpeg() +const heif_decoder_plugin* get_decoder_plugin_openjpeg() { return &decoder_openjpeg; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_uncompressed.cc libheif-1.23.4/libheif/plugins/decoder_uncompressed.cc --- libheif-1.19.8/libheif/plugins/decoder_uncompressed.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_uncompressed.cc 2026-09-06 14:45:17.000000000 +0000 @@ -48,9 +48,9 @@ -static const struct heif_decoder_plugin decoder_uncompressed +static const heif_decoder_plugin decoder_uncompressed { - 3, + 5, uncompressed_plugin_name, nullptr, nullptr, @@ -60,11 +60,14 @@ nullptr, nullptr, nullptr, - "uncompressed" + "uncompressed", + nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,0,0), + nullptr }; -const struct heif_decoder_plugin* get_decoder_plugin_uncompressed() +const heif_decoder_plugin* get_decoder_plugin_uncompressed() { return &decoder_uncompressed; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_vvdec.cc libheif-1.23.4/libheif/plugins/decoder_vvdec.cc --- libheif-1.19.8/libheif/plugins/decoder_vvdec.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_vvdec.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,8 +25,11 @@ #include #include #include +#include +#include #include +#include #if 0 #include @@ -41,9 +44,18 @@ bool strict_decoding = false; - std::vector> nalus; + struct Packet + { + std::vector data; + uintptr_t user_data; + }; + std::deque nalus; + bool end_of_stream_reached = false; + + std::string error_message; }; +static const char kEmptyString[] = ""; static const char kSuccess[] = "Success"; static const int VVDEC_PLUGIN_PRIORITY = 100; @@ -78,7 +90,7 @@ } -static int vvdec_does_support_format(enum heif_compression_format format) +static int vvdec_does_support_format(heif_compression_format format) { if (format == heif_compression_VVC) { return VVDEC_PLUGIN_PRIORITY; @@ -89,13 +101,22 @@ } -struct heif_error vvdec_new_decoder(void** dec) +static int vvdec_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return vvdec_does_support_format(format->format); +} + +heif_error vvdec_new_decoder2(void** dec, const heif_decoder_plugin_options* options) { auto* decoder = new vvdec_decoder(); vvdecParams params; vvdec_params_default(¶ms); params.logLevel = VVDEC_INFO; + + if (options->num_threads) { + params.threads = options->num_threads; + } decoder->decoder = vvdec_decoder_open(¶ms); const int MaxNaluSize = 256 * 1024; @@ -105,8 +126,20 @@ *dec = decoder; - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + + +heif_error vvdec_new_decoder(void** dec) +{ + heif_decoder_plugin_options options{}; + options.format = heif_compression_VVC; + options.num_threads = 0; + options.strict_decoding = false; + + vvdec_new_decoder2(dec, &options); + + return {}; } @@ -140,19 +173,34 @@ } -struct heif_error vvdec_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +heif_error vvdec_push_data2(void* decoder_raw, const void* frame_data, size_t frame_size, + uintptr_t user_data) { - auto* decoder = (struct vvdec_decoder*) decoder_raw; + auto* decoder = (vvdec_decoder*) decoder_raw; const auto* data = (const uint8_t*) frame_data; - for (;;) { - uint32_t size = ((((uint32_t) data[0]) << 24) | - (((uint32_t) data[1]) << 16) | - (((uint32_t) data[2]) << 8) | - (data[3])); + while (frame_size > 0) { + if (frame_size < 4) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString + }; + } + + uint32_t size = four_bytes_to_uint32(data[0], data[1], data[2], data[3]); + + if (frame_size - 4 < size) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString + }; + } data += 4; + frame_size -= 4; std::vector nalu; nalu.push_back(0); @@ -160,22 +208,24 @@ nalu.push_back(1); nalu.insert(nalu.end(), data, data + size); - decoder->nalus.push_back(nalu); + decoder->nalus.push_back({std::move(nalu), user_data}); data += size; - frame_size -= 4 + size; - if (frame_size == 0) { - break; - } + frame_size -= size; } - struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; - return err; + return heif_error_ok; } +heif_error vvdec_push_data(void* decoder_raw, const void* frame_data, size_t frame_size) +{ + return vvdec_push_data2(decoder_raw, frame_data, frame_size, 0); +} -struct heif_error vvdec_decode_image(void* decoder_raw, struct heif_image** out_img) +heif_error vvdec_decode_next_image2(void* decoder_raw, heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) { - auto* decoder = (struct vvdec_decoder*) decoder_raw; + auto* decoder = (vvdec_decoder*) decoder_raw; vvdecFrame* frame = nullptr; @@ -183,7 +233,7 @@ size_t max_payload_size = 0; for (const auto& nalu : decoder->nalus) { - max_payload_size = std::max(max_payload_size, nalu.size()); + max_payload_size = std::max(max_payload_size, nalu.data.size()); } if (decoder->au == nullptr || max_payload_size > (size_t) decoder->au->payloadSize) { @@ -198,36 +248,49 @@ // --- feed NALUs into decoder, flush when done - for (int i = 0;; i++) { + for (;;) { int ret; - if (i < (int) decoder->nalus.size()) { - const auto& nalu = decoder->nalus[i]; - - memcpy(decoder->au->payload, nalu.data(), nalu.size()); - decoder->au->payloadUsedSize = (int) nalu.size(); - - ret = vvdec_decode(decoder->decoder, decoder->au, &frame); - } - else { + // -> end of stream reached + if (decoder->nalus.empty() && decoder->end_of_stream_reached) { ret = vvdec_flush(decoder->decoder, &frame); } + // -> not enough data to decode an image + else if (decoder->nalus.empty()) { + *out_img = nullptr; + return heif_error_ok; + } + // -> push NALs from queue into decoder + else { + const auto& nalu = decoder->nalus.front(); - if (ret != VVDEC_OK && ret != VVDEC_EOF && ret != VVDEC_TRY_AGAIN) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "vvdec decoding error"}; + memcpy(decoder->au->payload, nalu.data.data(), nalu.data.size()); + decoder->au->payloadUsedSize = (int) nalu.data.size(); + decoder->au->cts = nalu.user_data; + decoder->au->ctsValid = true; + decoder->nalus.pop_front(); + ret = vvdec_decode(decoder->decoder, decoder->au, &frame); } - if (frame) { + if (ret == VVDEC_OK && frame) { break; } if (ret == VVDEC_EOF) { - return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "no frame decoded"}; + assert(!frame); + *out_img = nullptr; + return heif_error_ok; + } + + if (ret != VVDEC_OK && ret != VVDEC_TRY_AGAIN) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "vvdec decoding error"}; } } + if (out_user_data) { + *out_user_data = frame->cts; + } - decoder->nalus.clear(); // --- convert decoded frame to heif_image @@ -251,12 +314,12 @@ colorspace = heif_colorspace_YCbCr; } - struct heif_image* heif_img = nullptr; - struct heif_error err = heif_image_create((int)frame->width, - (int)frame->height, - colorspace, - chroma, - &heif_img); + heif_image* heif_img = nullptr; + heif_error err = heif_image_create((int) frame->width, + (int) frame->height, + colorspace, + chroma, + &heif_img); if (err.code != heif_error_Ok) { assert(heif_img == nullptr); return err; @@ -296,19 +359,23 @@ int w = (int)plane.width; int h = (int)plane.height; - err = heif_image_add_plane(heif_img, channel2plane[c], w, h, bpp); + err = heif_image_add_plane_safe(heif_img, channel2plane[c], w, h, bpp, limits); if (err.code != heif_error_Ok) { + // copy error message to decoder object because heif_image will be released + decoder->error_message = err.message; + err.message = decoder->error_message.c_str(); + heif_image_release(heif_img); return err; } - int dst_stride; - uint8_t* dst_mem = heif_image_get_plane(heif_img, channel2plane[c], &dst_stride); + size_t dst_stride; + uint8_t* dst_mem = heif_image_get_plane2(heif_img, channel2plane[c], &dst_stride); int bytes_per_pixel = (bpp + 7) / 8; for (int y = 0; y < h; y++) { - memcpy(dst_mem + y * dst_stride, data + y * stride, w * bytes_per_pixel); + memcpy(dst_mem + y * dst_stride, data + static_cast(y) * stride, static_cast(w) * bytes_per_pixel); } #if 0 @@ -325,24 +392,49 @@ return err; } +heif_error vvdec_decode_next_image(void* decoder_raw, heif_image** out_img, + const heif_security_limits* limits) +{ + return vvdec_decode_next_image2(decoder_raw, out_img, nullptr, limits); +} + +heif_error vvdec_decode_image(void* decoder_raw, heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return vvdec_decode_next_image(decoder_raw, out_img, limits); +} + +heif_error vvdec_flush_data(void* decoder_raw) +{ + auto* decoder = (vvdec_decoder*) decoder_raw; + decoder->end_of_stream_reached = true; + return heif_error_ok; +} -static const struct heif_decoder_plugin decoder_vvdec +static const heif_decoder_plugin decoder_vvdec { - 3, - vvdec_plugin_name, - vvdec_init_plugin, - vvdec_deinit_plugin, - vvdec_does_support_format, - vvdec_new_decoder, - vvdec_free_decoder, - vvdec_push_data, - vvdec_decode_image, - vvdec_set_strict_decoding, - "vvdec" + 5, + vvdec_plugin_name, + vvdec_init_plugin, + vvdec_deinit_plugin, + vvdec_does_support_format, + vvdec_new_decoder, + vvdec_free_decoder, + vvdec_push_data, + vvdec_decode_image, + vvdec_set_strict_decoding, + "vvdec", + vvdec_decode_next_image, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + vvdec_does_support_format2, + vvdec_new_decoder2, + vvdec_push_data2, + vvdec_flush_data, + vvdec_decode_next_image2 }; -const struct heif_decoder_plugin* get_decoder_plugin_vvdec() +const heif_decoder_plugin* get_decoder_plugin_vvdec() { return &decoder_vvdec; } diff -Nru libheif-1.19.8/libheif/plugins/decoder_webcodecs.cc libheif-1.23.4/libheif/plugins/decoder_webcodecs.cc --- libheif-1.19.8/libheif/plugins/decoder_webcodecs.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_webcodecs.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,959 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "decoder_webcodecs.h" +#include "libheif/heif_plugin.h" +#include "codecs/hevc_boxes.h" +#include "codecs/decoder.h" +#include "bitstream.h" +#include "nalu_utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +struct NALUnit { + std::vector data; +}; + +struct webcodecs_decoder +{ + std::queue data_queue; +}; + +static const char kEmptyString[] = ""; +static const char kSuccess[] = "Success"; + +static const int WEBCODECS_PLUGIN_PRIORITY = 80; + +#define MAX_PLUGIN_NAME_LENGTH 80 + +static char plugin_name[MAX_PLUGIN_NAME_LENGTH]; + +/** + * Decodes a HEVC frame using the browser's WebCodecs API. This implementation + * prefers hardware decoding when available. + * + * As of this writing, most HEIC images will be decoded directly into the NV12 + * pixel format. For images returned in NV12 or planar YUV format (I420, I422, + * I444), the format will be preserved when returning the data to C++. + * + * Any other image format returned by the WebCodecs API will be converted to + * RGBA before being returned to C++ to ensure that the result can be + * properly interpreted by the plugin. + * + * Note that the WebCodecs API don't support converting into NV12 format in + * cases where the native pixel format is something else. That's why RGBA is + * used as a fallback format, b/c the browser can always convert to it. + */ +EM_JS(emscripten::EM_VAL, decode_with_browser_hevc, (const char *codec_ptr, uintptr_t hvcc_record_ptr, size_t hvcc_record_size, uintptr_t data_ptr, size_t data_size), { + return Asyncify.handleSleep((callback) => { + const codec = UTF8ToString(codec_ptr); + const data = HEAPU8.subarray(data_ptr, data_ptr + data_size); + const description = HEAPU8.subarray(hvcc_record_ptr, hvcc_record_ptr + hvcc_record_size); + let returnedError = false; + + function returnError(err) { + if (!returnedError) { + returnedError = true; + + console.error(err); + callback({'error': err.stack}); + } + } + + function handleEmptyFormat(decoded) { + // Use the visible rectangle, not the coded rectangle. The coded rectangle + // may include non-visible padding (HEVC conformance window) that is not + // part of the image. + const width = decoded.visibleRect.width; + const height = decoded.visibleRect.height; + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext('2d'); + context.drawImage(decoded, 0, 0, width, height); + const imageData = context.getImageData(0, 0, width, height); + const data = imageData.data; + const format = 'RGBA'; + const planes = [{offset: 0, stride: width * 4}]; + callback(Emval.toHandle({ + 'buffer': data, + 'format': format, + 'planes': planes, + 'width': width, + 'height': height, + })); + + decoded.close(); + } + + if (typeof VideoDecoder === 'undefined') { + returnError(new Error('VideoDecoder API is not available')); + + return; + } + + const decoder = new VideoDecoder({ + output: (decoded) => { + // For 10-bit color images, the format is observed to be null. In this + // case the VideoFrame.copyTo API doesn't work, however, it does work + // to draw the VideoFrame to a Canvas and then extract the image bytes. + // Drawing to a canvas is slower than copyTo, so only use it when + // necessary. + if (!decoded.format) { + handleEmptyFormat(decoded); + return; + } + + const nativeFormats = ['NV12', 'I420', 'I422', 'I444']; + const format = nativeFormats.includes(decoded.format) ? decoded.format : 'RGBA'; + const fullRange = decoded.colorSpace ? decoded.colorSpace.fullRange : false; + + // Always operate on the visible rectangle. allocationSize() and + // copyTo() default to it anyway, but pass it explicitly so that the + // buffer size, the plane layout and the dimensions reported to C++ + // are guaranteed to describe the same rectangle. The coded rectangle + // (codedWidth x codedHeight) can be larger because of the HEVC + // conformance window, which is set by the file being decoded. It must + // never be used as the geometry of the copied buffer. + const rect = decoded.visibleRect; + const width = rect.width; + const height = rect.height; + const formatOptions = nativeFormats.includes(format) ? + {'rect': rect} : + {'rect': rect, 'format': format, 'colorSpace': 'srgb'}; + const bufferSize = nativeFormats.includes(format) ? + decoded.allocationSize(formatOptions) : + width * height * 4; + + const buffer = new Uint8Array(bufferSize); + + Promise.resolve().then( + () => decoded.copyTo(buffer, formatOptions) + ).then((planes) => { + callback(Emval.toHandle({ + 'buffer': buffer, + 'format': format, + 'planes': planes, + 'width': width, + 'height': height, + 'fullRange': fullRange, + })); + + decoded.close(); + }).catch((e) => { + returnError(e); + }); + }, + error: (e) => { + returnError(e); + } + }); + + try { + decoder.configure({ + codec, + hardwareAcceleration: 'prefer-hardware', + optimizeForLatency: true, + description, + }); + + const chunk = new EncodedVideoChunk({ + timestamp: 0, + type: 'key', + data: data, + }); + + decoder.decode(chunk); + decoder.flush(); + } catch (e) { + returnError(e); + } + }); +}); + + +static const char* webcodecs_plugin_name() +{ + strcpy(plugin_name, "Webcodecs HEVC decoder"); + + const char* webcodecs_version = "1"; + + if (strlen(webcodecs_version) + 10 < MAX_PLUGIN_NAME_LENGTH) { + strcat(plugin_name, ", version "); + strcat(plugin_name, webcodecs_version); + } + + return plugin_name; +} + + +static void webcodecs_init_plugin() +{ + +} + + +static void webcodecs_deinit_plugin() +{ + +} + + +static int webcodecs_does_support_format(enum heif_compression_format format) +{ + if (format == heif_compression_HEVC) { + return WEBCODECS_PLUGIN_PRIORITY; + } + else { + return 0; + } +} + + +static struct heif_error webcodecs_new_decoder(void** dec) +{ + struct webcodecs_decoder* decoder = new webcodecs_decoder(); + struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + + *dec = decoder; + return err; +} + + +static void webcodecs_free_decoder(void* decoder_raw) +{ + struct webcodecs_decoder* decoder = (struct webcodecs_decoder*) decoder_raw; + + delete decoder; +} + + +static struct heif_error webcodecs_push_data(void* decoder_raw, const void* data, size_t size) +{ + struct webcodecs_decoder* decoder = (struct webcodecs_decoder*) decoder_raw; + + const uint8_t* cdata = (const uint8_t*) data; + + size_t ptr = 0; + while (ptr < size) { + if (4 > size - ptr) { + struct heif_error err = {heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString}; + return err; + } + + uint32_t nal_size = static_cast((cdata[ptr] << 24) | (cdata[ptr + 1] << 16) | (cdata[ptr + 2] << 8) | (cdata[ptr + 3])); + ptr += 4; + + if (nal_size > size - ptr) { + struct heif_error err = {heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + kEmptyString}; + return err; + } + + NALUnit nal_unit; + nal_unit.data.assign(cdata + ptr, cdata + ptr + nal_size); + decoder->data_queue.push(std::move(nal_unit)); + ptr += nal_size; + } + + struct heif_error err = {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + return err; +} + + +static void normalize_luma_range(uint8_t* dst, int stride, int width, int height) { + // Luma data coming from the browser's VideoDecoder API may be using a + // limited range (16-235) instead of the full range (0-255). If this is the + // case, we need to normalize the data to the full range. + for (int y = 0; y < height; y++) { + uint8_t* p = dst + y * stride; + for (int x = 0; x < width; x++) { + float v = (static_cast(p[x]) - 16.0f) * 255.0f / 219.0f; + p[x] = static_cast(std::min(255.0f, std::max(0.0f, v + 0.5f))); + } + } +} + +static void normalize_chroma_range(uint8_t* dst, int stride, int width, int height) { + // Chroma data coming from the browser's VideoDecoder API may be using a + // limited range (16-240) instead of the full range (0-255). If this is the + // case, we need to normalize the data to the full range. + for (int y = 0; y < height; y++) { + uint8_t* p = dst + y * stride; + for (int x = 0; x < width; x++) { + float v = (static_cast(p[x]) - 16.0f) * 255.0f / 224.0f; + p[x] = static_cast(std::min(255.0f, std::max(0.0f, v + 0.5f))); + } + } +} + +// Returns the size of the chroma planes for the given chroma format. Odd luma +// sizes are rounded up, matching the convention used throughout libheif. +static void get_chroma_plane_size(heif_chroma chroma, int width, int height, + int* chroma_w, int* chroma_h) +{ + *chroma_w = width; + *chroma_h = height; + if (chroma == heif_chroma_420 || chroma == heif_chroma_monochrome) { + *chroma_w = (width + 1) / 2; + *chroma_h = (height + 1) / 2; + } + else if (chroma == heif_chroma_422) { + *chroma_w = (width + 1) / 2; + } +} + + +// Checks that a plane consisting of 'rows' rows of 'row_bytes' bytes each, +// starting at 'offset' and with 'stride' bytes between consecutive rows, lies +// entirely within a buffer of 'buffer_size' bytes. +// +// The plane layout is reported by the browser and the image geometry +// ultimately comes from the file being decoded, so none of these values may be +// trusted. Every copy loop that reads from the browser's buffer must be +// preceded by this check. +static bool plane_fits_in_buffer(size_t buffer_size, + int offset, int stride, + int rows, int row_bytes) +{ + if (offset < 0 || stride < 0 || rows <= 0 || row_bytes <= 0) { + return false; + } + + if (stride < row_bytes) { + return false; + } + + const uint64_t end = static_cast(offset) + + static_cast(rows - 1) * static_cast(stride) + + static_cast(row_bytes); + return end <= buffer_size; +} + + +static const heif_error kPlaneOutOfBoundsError = { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: plane layout reported by the browser exceeds the decoded buffer"}; + + +// The caller must have verified with plane_fits_in_buffer() that each source +// plane covers 'height' rows of 'width' bytes (luma) and the corresponding +// chroma plane size (see get_chroma_plane_size()). +static struct heif_error convert_planar_yuv_to_heif_image( + const uint8_t* y_src, int y_src_stride, + const uint8_t* u_src, int u_src_stride, + const uint8_t* v_src, int v_src_stride, + int width, int height, + struct heif_image** out_img, + heif_chroma chroma, + bool is_full_range, + const heif_security_limits* limits) { + heif_error err; + bool is_mono = chroma == heif_chroma_monochrome; + + int chroma_w, chroma_h; + get_chroma_plane_size(chroma, width, height, &chroma_w, &chroma_h); + + err = heif_image_create( + width, height, + is_mono ? heif_colorspace_monochrome + : heif_colorspace_YCbCr, + is_mono ? heif_chroma_monochrome : chroma, + out_img); + if (err.code) { + return err; + } + + err = heif_image_add_plane_safe( + *out_img, heif_channel_Y, width, height, 8, limits); + if (err.code) { + heif_image_release(*out_img); + return err; + } + + int y_stride; + uint8_t* y_dst = heif_image_get_plane( + *out_img, heif_channel_Y, &y_stride); + for (int i = 0; i < height; ++i) { + memcpy(y_dst + i * y_stride, + y_src + i * y_src_stride, + width); + } + + if (!is_full_range) { + normalize_luma_range(y_dst, y_stride, width, height); + } + + if (!is_mono) { + err = heif_image_add_plane_safe( + *out_img, heif_channel_Cb, + chroma_w, chroma_h, 8, limits); + if (err.code) { + heif_image_release(*out_img); + return err; + } + + err = heif_image_add_plane_safe( + *out_img, heif_channel_Cr, + chroma_w, chroma_h, 8, limits); + if (err.code) { + heif_image_release(*out_img); + return err; + } + + int cb_stride; + uint8_t* cb_dst = heif_image_get_plane( + *out_img, heif_channel_Cb, &cb_stride); + for (int i = 0; i < chroma_h; ++i) { + memcpy(cb_dst + i * cb_stride, + u_src + i * u_src_stride, + chroma_w); + } + + int cr_stride; + uint8_t* cr_dst = heif_image_get_plane( + *out_img, heif_channel_Cr, &cr_stride); + for (int i = 0; i < chroma_h; ++i) { + memcpy(cr_dst + i * cr_stride, + v_src + i * v_src_stride, + chroma_w); + } + + if (!is_full_range) { + normalize_chroma_range( + cb_dst, cb_stride, chroma_w, chroma_h); + normalize_chroma_range( + cr_dst, cr_stride, chroma_w, chroma_h); + } + } + + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + +static struct heif_error convert_nv12_to_heif_image( + const std::unique_ptr& buffer, size_t buffer_size, + int width, int height, + int y_offset, int y_src_stride, + int uv_offset, int uv_src_stride, + struct heif_image** out_img, + bool is_mono, + bool is_full_range, + const heif_security_limits* limits) { + if (!plane_fits_in_buffer(buffer_size, y_offset, y_src_stride, height, width)) { + return kPlaneOutOfBoundsError; + } + + if (is_mono) { + return convert_planar_yuv_to_heif_image( + buffer.get() + y_offset, y_src_stride, + nullptr, 0, nullptr, 0, + width, height, out_img, + heif_chroma_monochrome, is_full_range, limits); + } + + int chroma_w, chroma_h; + get_chroma_plane_size(heif_chroma_420, width, height, &chroma_w, &chroma_h); + + // The interleaved UV plane has chroma_w Cb/Cr sample pairs per row. + if (!plane_fits_in_buffer(buffer_size, uv_offset, uv_src_stride, chroma_h, chroma_w * 2)) { + return kPlaneOutOfBoundsError; + } + + std::vector u_buf(static_cast(chroma_w) * chroma_h); + std::vector v_buf(static_cast(chroma_w) * chroma_h); + + for (int i = 0; i < chroma_h; ++i) { + const uint8_t* uv_row = + buffer.get() + uv_offset + i * uv_src_stride; + for (int j = 0; j < chroma_w; ++j) { + u_buf[i * chroma_w + j] = uv_row[j * 2]; + v_buf[i * chroma_w + j] = uv_row[j * 2 + 1]; + } + } + + return convert_planar_yuv_to_heif_image( + buffer.get() + y_offset, y_src_stride, + u_buf.data(), chroma_w, + v_buf.data(), chroma_w, + width, height, out_img, + heif_chroma_420, is_full_range, limits); +} + +/** + * Generates a HEVC codec string as defined in ISO/IEC 14496-15 specification, + * Annex E.3. + */ +static std::string get_hevc_codec_string(const HEVCDecoderConfigurationRecord& config) { + std::string codec_string = "hvc1."; + + // Profile IDC + codec_string += std::to_string(config.general_profile_idc); + codec_string += "."; + + // Profile Compatibility Flags + uint32_t profile_compatibility_flags = config.general_profile_compatibility_flags; + char buffer[9]; + snprintf(buffer, sizeof(buffer), "%X", profile_compatibility_flags); + codec_string += buffer; + codec_string += "."; + + // Tier and Level + codec_string += (config.general_tier_flag ? "H" : "L"); + codec_string += std::to_string(config.general_level_idc); + codec_string += "."; + + // Constraint Indicator Flags + uint64_t constraint_flags = 0; + for (int i = 0; i < 48; ++i) { + if (config.general_constraint_indicator_flags[i]) { + constraint_flags |= (1ULL << (47 - i)); + } + } + snprintf(buffer, sizeof(buffer), "%06X", + (unsigned int)(constraint_flags >> 24)); + codec_string += buffer; + + return codec_string; +} + + + + +static void get_nal_units(struct webcodecs_decoder* decoder, + NALUnit& vps_nal_unit, + NALUnit& sps_nal_unit, + NALUnit& pps_nal_unit, + NALUnit& data_unit) { + // This code parses the NAL units to find the VPS, SPS, PPS, and data NAL + // units. It handles cases where the NAL units are not in the expected order + // and where there are extra NAL units that should be ignored. The last seen + // VPS, SPS, PPS, and VCL data are used. + while (!decoder->data_queue.empty()) { + NALUnit nal_unit = decoder->data_queue.front(); + decoder->data_queue.pop(); + + if (nal_unit.data.empty()) { + continue; + } + + const uint8_t nal_type = (nal_unit.data[0] >> 1) & 0x3F; + + if (nal_type == HEVC_NAL_UNIT_VPS_NUT) { + vps_nal_unit = nal_unit; + } else if (nal_type == HEVC_NAL_UNIT_SPS_NUT) { + sps_nal_unit = nal_unit; + } else if (nal_type == HEVC_NAL_UNIT_PPS_NUT) { + pps_nal_unit = nal_unit; + } else if (nal_type <= HEVC_NAL_UNIT_MAX_VCL) { + // Assume the plugin will only receive one VCL NAL unit. + data_unit = nal_unit; + } + } +} + + +static struct heif_error webcodecs_decode_image_with_limits(void* decoder_raw, + struct heif_image** out_img, + const heif_security_limits* limits) +{ + struct webcodecs_decoder* decoder = (struct webcodecs_decoder*) decoder_raw; + *out_img = nullptr; + + NALUnit vps_nal_unit; + NALUnit sps_nal_unit; + NALUnit pps_nal_unit; + NALUnit data_unit; + + get_nal_units(decoder, vps_nal_unit, sps_nal_unit, pps_nal_unit, data_unit); + + if (vps_nal_unit.data.empty() || sps_nal_unit.data.empty() || pps_nal_unit.data.empty() || data_unit.data.empty()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_End_of_data, + "Missing required NAL units (VPS, SPS, PPS, or data)"}; + } + + HEVCDecoderConfigurationRecord config; + uint32_t w, h; // visible size after applying the conformance window + ImageSize coded{}; // size of the coded picture + Error err = parse_sps_for_hvcC_configuration(sps_nal_unit.data.data(), sps_nal_unit.data.size(), + &config, &w, &h, &coded); + if (err != Error::Ok) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Failed to parse SPS"}; + } + + // Reject coded picture sizes beyond the security limits before handing the + // bitstream to the browser. libheif checks the SPS stored in the hvcC box, + // but this plugin honours the last SPS in the NAL stream, which may differ. + if (coded.width == 0 || coded.height == 0 || w == 0 || h == 0) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Invalid_image_size, + "SPS declares a zero-sized image"}; + } + + if (limits && limits->max_image_size_pixels > 0) { + const auto max_dim = static_cast(std::numeric_limits::max()); + if (coded.width > max_dim || coded.height > max_dim || + coded.width > limits->max_image_size_pixels / coded.height) { + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "SPS coded picture size exceeds the maximum image size"}; + } + } + + config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_VPS_NUT, {vps_nal_unit.data}}); + config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_SPS_NUT, {sps_nal_unit.data}}); + config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_PPS_NUT, {pps_nal_unit.data}}); + + StreamWriter writer; + config.write(writer); + std::vector hvcc_record = writer.get_data(); + + // The WebCodecs API expects the NAL unit to be prefixed with its size (4 bytes, big-endian). + uint32_t nal_size = static_cast(data_unit.data.size()); + std::vector data_with_size(4 + nal_size); + // Write length in Big Endian + data_with_size[0] = (nal_size >> 24) & 0xFF; + data_with_size[1] = (nal_size >> 16) & 0xFF; + data_with_size[2] = (nal_size >> 8) & 0xFF; + data_with_size[3] = nal_size & 0xFF; + // Append NAL payload + memcpy(data_with_size.data() + 4, data_unit.data.data(), nal_size); + + std::string codec_string = get_hevc_codec_string(config); + + emscripten::val result = emscripten::val::take_ownership( + decode_with_browser_hevc( + codec_string.c_str(), + (uintptr_t)hvcc_record.data(), + hvcc_record.size(), + (uintptr_t)data_with_size.data(), + data_with_size.size() + ) + ); + + if (!result["error"].isUndefined()) { + static char error_message[256]; + std::string error_str = result["error"].as(); + snprintf(error_message, sizeof(error_message), "%s", error_str.c_str()); + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + error_message}; + } + + if (result.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: decode_with_browser_hevc returned undefined"}; + } + + emscripten::val js_array = result["buffer"]; + if (js_array.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.buffer is undefined"}; + } + + const size_t len = js_array["length"].as(); + std::unique_ptr buffer(new uint8_t[len]); + emscripten::val memory_view(emscripten::typed_memory_view(len, buffer.get())); + memory_view.call("set", js_array); + + // These are the dimensions of the visible rectangle that the JavaScript side + // copied into 'buffer'. They are only used together with the plane layout + // after plane_fits_in_buffer() has confirmed that the layout stays within + // 'len' bytes. + if (result["width"].isUndefined() || result["height"].isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.width or result.height is undefined"}; + } + + // The upper bound keeps the per-row byte counts computed below (up to four + // bytes per pixel) and the rounded-up chroma sizes representable as int. + const int max_dim = std::numeric_limits::max() / 4; + const int width = result["width"].as(); + const int height = result["height"].as(); + if (width <= 0 || height <= 0 || width > max_dim || height > max_dim) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Invalid_image_size, + "Decoding failed: invalid image size reported by the browser"}; + } + + std::string format = result["format"].as(); + + emscripten::val planes = result["planes"]; + if (planes.isUndefined() || !planes.isArray()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.planes is undefined or not an array"}; + } + + bool is_full_range = !result["fullRange"].isUndefined() && result["fullRange"].as(); + + // Most HEIC images in the browser will be decoded natively in NV12 pixel + // format. Using the bytes directly helps retain the original image fidelity. + if (format == "NV12") { + bool is_mono = config.chroma_format == 0; + if (!is_mono && planes["length"].as() < 2) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: NV12 format requires at least 2 planes"}; + } else if (is_mono && planes["length"].as() < 1) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: NV12 monochrome format requires at least 1 plane"}; + } + + emscripten::val y_plane = planes[0]; + if (y_plane.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.planes[0] is undefined"}; + } + + const int y_offset = y_plane["offset"].as(); + const int y_src_stride = y_plane["stride"].as(); + int uv_offset = 0; + int uv_src_stride = 0; + + if (!is_mono) { + emscripten::val uv_plane = planes[1]; + if (uv_plane.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.planes[1] is undefined"}; + } + + uv_offset = uv_plane["offset"].as(); + uv_src_stride = uv_plane["stride"].as(); + } + + return convert_nv12_to_heif_image(buffer, len, width, height, + y_offset, y_src_stride, uv_offset, uv_src_stride, + out_img, is_mono, is_full_range, limits); + } else if (format == "I420" || format == "I422" || format == "I444") { + if (planes["length"].as() < 3) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: planar YUV format requires 3 planes"}; + } + + emscripten::val y_plane = planes[0]; + emscripten::val u_plane = planes[1]; + emscripten::val v_plane = planes[2]; + if (y_plane.isUndefined() || u_plane.isUndefined() || v_plane.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: one or more YUV planes are undefined"}; + } + + heif_chroma chroma = heif_chroma_420; + if (format == "I422") { + chroma = heif_chroma_422; + } else if (format == "I444") { + chroma = heif_chroma_444; + } + + const int y_offset = y_plane["offset"].as(); + const int y_src_stride = y_plane["stride"].as(); + const int u_offset = u_plane["offset"].as(); + const int u_src_stride = u_plane["stride"].as(); + const int v_offset = v_plane["offset"].as(); + const int v_src_stride = v_plane["stride"].as(); + + int chroma_w, chroma_h; + get_chroma_plane_size(chroma, width, height, &chroma_w, &chroma_h); + + if (!plane_fits_in_buffer(len, y_offset, y_src_stride, height, width) || + !plane_fits_in_buffer(len, u_offset, u_src_stride, chroma_h, chroma_w) || + !plane_fits_in_buffer(len, v_offset, v_src_stride, chroma_h, chroma_w)) { + return kPlaneOutOfBoundsError; + } + + return convert_planar_yuv_to_heif_image( + buffer.get() + y_offset, y_src_stride, + buffer.get() + u_offset, u_src_stride, + buffer.get() + v_offset, v_src_stride, + width, height, + out_img, chroma, is_full_range, limits); + } else if (format == "RGBA") { + if (planes["length"].as() < 1) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: RGBA format requires at least 1 plane"}; + } + + emscripten::val rgba_plane = planes[0]; + if (rgba_plane.isUndefined()) { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Decoding failed: result.planes[0] is undefined"}; + } + + const int rgba_offset = rgba_plane["offset"].as(); + const int rgba_src_stride = rgba_plane["stride"].as(); + + if (!plane_fits_in_buffer(len, rgba_offset, rgba_src_stride, height, width * 4)) { + return kPlaneOutOfBoundsError; + } + + heif_error err; + err = heif_image_create(width, + height, + heif_colorspace_RGB, + heif_chroma_interleaved_RGBA, + out_img); + if (err.code) { + return err; + } + + err = heif_image_add_plane_safe(*out_img, heif_channel_interleaved, width, height, 8, limits); + if (err.code) { + heif_image_release(*out_img); + return err; + } + + int stride; + uint8_t* dst = heif_image_get_plane(*out_img, heif_channel_interleaved, &stride); + + for (int i = 0; i < height; ++i) { + memcpy(dst + i * stride, + buffer.get() + rgba_offset + i * rgba_src_stride, + width * 4); + } + + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; + } else { + return {heif_error_Decoder_plugin_error, + heif_suberror_Unsupported_color_conversion, + "Decoding failed: unsupported pixel format"}; + } +} + + +void webcodecs_set_strict_decoding(void* decoder_raw, int flag) +{ +} + + +static int webcodecs_does_support_format2(const heif_decoder_plugin_compressed_format_description* format) +{ + return webcodecs_does_support_format(format->format); +} + + +static struct heif_error webcodecs_new_decoder2(void** dec, const heif_decoder_plugin_options* options) +{ + return webcodecs_new_decoder(dec); +} + + +static struct heif_error webcodecs_push_data2(void* decoder_raw, const void* data, size_t size, uintptr_t user_data) +{ + return webcodecs_push_data(decoder_raw, data, size); +} + + +static struct heif_error webcodecs_flush_data(void* decoder_raw) +{ + return {heif_error_Ok, heif_suberror_Unspecified, kSuccess}; +} + + +static struct heif_error webcodecs_decode_next_image(void* decoder_raw, + struct heif_image** out_img, + const heif_security_limits* limits) +{ + return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits); +} + + +static struct heif_error webcodecs_decode_next_image2(void* decoder_raw, + struct heif_image** out_img, + uintptr_t* out_user_data, + const heif_security_limits* limits) +{ + if (out_user_data) { + *out_user_data = 0; + } + return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits); +} + + +static struct heif_error webcodecs_decode_image(void* decoder_raw, + struct heif_image** out_img) +{ + auto* limits = heif_get_global_security_limits(); + return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits); +} + + +static const struct heif_decoder_plugin decoder_webcodecs + { + 5, + webcodecs_plugin_name, + webcodecs_init_plugin, + webcodecs_deinit_plugin, + webcodecs_does_support_format, + webcodecs_new_decoder, + webcodecs_free_decoder, + webcodecs_push_data, + webcodecs_decode_image, + webcodecs_set_strict_decoding, + "webcodecs", + webcodecs_decode_next_image, + 0, + webcodecs_does_support_format2, + webcodecs_new_decoder2, + webcodecs_push_data2, + webcodecs_flush_data, + webcodecs_decode_next_image2 + }; + + + +const struct heif_decoder_plugin* get_decoder_plugin_webcodecs() +{ + return &decoder_webcodecs; +} + +#if PLUGIN_WEBCODECS +heif_plugin_info plugin_info { + 1, + heif_plugin_type_decoder, + &decoder_webcodecs +}; +#endif diff -Nru libheif-1.19.8/libheif/plugins/decoder_webcodecs.h libheif-1.23.4/libheif/plugins/decoder_webcodecs.h --- libheif-1.19.8/libheif/plugins/decoder_webcodecs.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/decoder_webcodecs.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,34 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef THIRD_PARTY_LIBHEIF_LIBHEIF_PLUGINS_DECODER_WEBCODECS_H_ +#define THIRD_PARTY_LIBHEIF_LIBHEIF_PLUGINS_DECODER_WEBCODECS_H_ + +#include "common_utils.h" + +const struct heif_decoder_plugin* get_decoder_plugin_webcodecs(); + +#if PLUGIN_WEBCODECS +extern "C" { +MAYBE_UNUSED LIBHEIF_API extern heif_plugin_info plugin_info; +} +#endif + +#endif // THIRD_PARTY_LIBHEIF_LIBHEIF_PLUGINS_DECODER_WEBCODECS_H_ diff -Nru libheif-1.19.8/libheif/plugins/encoder_aom.cc libheif-1.23.4/libheif/plugins/encoder_aom.cc --- libheif-1.19.8/libheif/plugins/encoder_aom.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_aom.cc 2026-09-06 14:45:17.000000000 +0000 @@ -31,7 +31,9 @@ #include #include #include "encoder_aom.h" +#include "encoder_input_check.h" +#include #include #include #include @@ -57,8 +59,15 @@ for (auto* error : aom_errors) { delete[] error; } + + // automatically destroy aom_codec_ctx_t when we leave the function +// auto codec_ctx_deleter = std::unique_ptr(&codec, aom_codec_destroy); + + aom_codec_destroy(&codec); } + aom_codec_ctx_t codec; + // --- parameters bool realtime_mode; @@ -85,9 +94,13 @@ #endif aom_tune_metric tune; + bool tune_auto = true; heif_chroma chroma = heif_chroma_420; + // bit depth the codec was initialized with, to check the later frames of a sequence against + int bit_depth = 8; + // --- input bool alpha_quality_set = false; @@ -96,8 +109,17 @@ // --- output - std::vector compressedData; - bool data_read = false; + struct Packet + { + std::vector compressedData; + uintptr_t frameNr = 0; + bool is_keyframe = false; + }; + + std::deque output_packets; + std::vector active_output_data; + + //bool data_read = false; // --- error message copies @@ -176,9 +198,51 @@ static const char* kParam_tune = "tune"; static const char* const kParam_tune_valid_values[] = { - "psnr", "ssim", "iq", nullptr + "auto", "psnr", "ssim", "iq", nullptr +}; + +#if defined(AOM_HAVE_TUNE_IQ) +static heif_error heif_error_lossless_with_tune_iq = { + heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "AOM 'tune=iq' cannot be combined with lossless encoding because libaom enables " + "chroma delta-q for this tune. Use 'tune=ssim' or 'tune=psnr' for lossless images." }; +// This table has been copied from libavif/src/codec_aom.c + +// Quality (q) to quantizer (qp) formula for tune=iq (Image Quality), expressed as a look-up table for more clarity. +// Copied from libavif (src/codec_aom.c). The formula is a piecewise linear function empirically selected +// to correct for the non-linear bitrate increase of tune=iq relative to tune=ssim with the same qp. +// +// | Quality | Quantizer | Step size | +// |---------|------------------------------------|-----------| +// | 0 - 6 | 63 - floor(quality / 3) | 3 | +// | 7 - 28 | 61 - round((quality - 7) / 2) | 2 | +// | 29 - 53 | 50 - round((quality - 29) * 3 / 5) | 1.66 | +// | 54 - 99 | 35 - round((quality - 54) * 3 / 4) | 1.33 | +// | 100 | 0 (lossless) | 1 | +// +// The x axis of the table represents the ones digit, while the y axis represents the tens digit +// of the q value [0-100], which is then mapped to a qp value [0-63]. +// clang-format off +static const int tuneIqQualityToQuantizer[101] = { +// 1s digit: *0 *1 *2 *3 *4 *5 *6 *7 *8 *9 10s digit: + 63, 63, 63, 62, 62, 62, 61, 61, 60, 60, // 0* + 59, 59, 58, 58, 57, 57, 56, 56, 55, 55, // 1* + 54, 54, 53, 53, 52, 52, 51, 51, 50, 50, // 2* + 49, 49, 48, 48, 47, 46, 46, 45, 45, 44, // 3* + 43, 43, 42, 42, 41, 40, 40, 39, 39, 38, // 4* + 37, 37, 36, 36, 35, 34, 33, 33, 32, 31, // 5* + 30, 30, 29, 28, 27, 27, 26, 25, 24, 24, // 6* + 23, 22, 21, 21, 20, 19, 18, 18, 17, 16, // 7* + 15, 15, 14, 13, 12, 12, 11, 10, 9, 9, // 8* + 8, 7, 6, 6, 5, 4, 3, 3, 2, 1, // 9* + 0 // quality 100 +}; +// clang-format on +#endif + static const int AOM_PLUGIN_PRIORITY = 60; #define MAX_PLUGIN_NAME_LENGTH 80 @@ -205,13 +269,13 @@ #define MAX_NPARAMETERS 16 -static struct heif_encoder_parameter aom_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* aom_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter aom_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* aom_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void aom_init_parameters() { - struct heif_encoder_parameter* p = aom_encoder_params; - const struct heif_encoder_parameter** d = aom_encoder_parameter_ptrs; + heif_encoder_parameter* p = aom_encoder_params; + const heif_encoder_parameter** d = aom_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -293,7 +357,7 @@ p->version = 2; p->name = kParam_tune; p->type = heif_encoder_parameter_type_string; - p->string.default_value = "ssim"; + p->string.default_value = "auto"; p->has_default = true; p->string.valid_values = kParam_tune_valid_values; d[i++] = p++; @@ -389,7 +453,7 @@ } -const struct heif_encoder_parameter** aom_list_parameters(void* encoder) +const heif_encoder_parameter** aom_list_parameters(void* encoder) { return aom_encoder_parameter_ptrs; } @@ -404,10 +468,10 @@ { } -struct heif_error aom_new_encoder(void** enc) +heif_error aom_new_encoder(void** enc) { - struct encoder_struct_aom* encoder = new encoder_struct_aom(); - struct heif_error err = heif_error_ok; + encoder_struct_aom* encoder = new encoder_struct_aom(); + heif_error err = heif_error_ok; *enc = encoder; @@ -420,15 +484,15 @@ void aom_free_encoder(void* encoder_raw) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + struct encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; delete encoder; } -struct heif_error aom_set_parameter_quality(void* encoder_raw, int quality) +heif_error aom_set_parameter_quality(void* encoder_raw, int quality) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -439,18 +503,18 @@ return heif_error_ok; } -struct heif_error aom_get_parameter_quality(void* encoder_raw, int* quality) +heif_error aom_get_parameter_quality(void* encoder_raw, int* quality) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -struct heif_error aom_set_parameter_lossless(void* encoder_raw, int enable) +heif_error aom_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (enable) { encoder->min_q = 0; @@ -466,9 +530,9 @@ return heif_error_ok; } -struct heif_error aom_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error aom_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; *enable = encoder->lossless; @@ -507,9 +571,9 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error aom_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error aom_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return aom_set_parameter_quality(encoder, value); @@ -545,9 +609,9 @@ return heif_error_unsupported_parameter; } -struct heif_error aom_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error aom_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return aom_get_parameter_quality(encoder, value); @@ -577,9 +641,9 @@ } -struct heif_error aom_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error aom_set_parameter_boolean(void* encoder_raw, const char* name, int value) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return aom_set_parameter_lossless(encoder, value); @@ -606,9 +670,9 @@ return heif_error_unsupported_parameter; } -struct heif_error aom_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error aom_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return aom_get_parameter_lossless(encoder, value); @@ -623,9 +687,9 @@ } -struct heif_error aom_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error aom_set_parameter_string(void* encoder_raw, const char* name, const char* value) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { if (strcmp(value, "420") == 0) { @@ -646,17 +710,24 @@ } if (strcmp(name, kParam_tune) == 0) { - if (strcmp(value, "psnr") == 0) { + if (strcmp(value, "auto") == 0) { + encoder->tune_auto = true; + return heif_error_ok; + } + else if (strcmp(value, "psnr") == 0) { encoder->tune = AOM_TUNE_PSNR; + encoder->tune_auto = false; return heif_error_ok; } else if (strcmp(value, "ssim") == 0) { encoder->tune = AOM_TUNE_SSIM; + encoder->tune_auto = false; return heif_error_ok; } #if defined(AOM_HAVE_TUNE_IQ) else if (strcmp(value, "iq") == 0) { encoder->tune = AOM_TUNE_IQ; + encoder->tune_auto = false; return heif_error_ok; } #endif @@ -683,10 +754,10 @@ } -struct heif_error aom_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +heif_error aom_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { switch (encoder->chroma) { @@ -706,6 +777,10 @@ return heif_error_ok; } else if (strcmp(name, kParam_tune) == 0) { + if (encoder->tune_auto) { + save_strcpy(value, value_size, "auto"); + return heif_error_ok; + } switch (encoder->tune) { case AOM_TUNE_PSNR: save_strcpy(value, value_size, "psnr"); @@ -731,8 +806,8 @@ static void aom_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = aom_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = aom_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -760,7 +835,7 @@ void aom_query_input_colorspace2(void* encoder_raw, heif_colorspace* colorspace, heif_chroma* chroma) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; if (*colorspace == heif_colorspace_monochrome) { // keep the monochrome colorspace @@ -794,147 +869,109 @@ return err; \ } - -struct heif_error aom_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +struct chroma_info { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; - - struct heif_error err; - - const int source_width = heif_image_get_width(image, heif_channel_Y); - const int source_height = heif_image_get_height(image, heif_channel_Y); - - const heif_chroma chroma = heif_image_get_chroma_format(image); - - int bpp_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); - - - // --- check for AOM 3.6.0 bug - - bool is_aom_3_6_0 = (aom_codec_version() == 0x030600); - - if (is_aom_3_6_0) { - // This bound might be too tight, as I still could encode images with 8193 x 4353 correctly. Even 8200x4400, but 8200x4800 fails. - // Let's still keep it as most images will be smaller anyway. - if (!(source_width <= 8192 * 2 && source_height <= 4352 * 2 && source_width * source_height <= 8192 * 4352)) { - err = {heif_error_Encoding_error, - heif_suberror_Encoder_encoding, - "AOM v3.6.0 has a bug when encoding large images. Please upgrade to at least AOM v3.6.1."}; - return err; - } - } - - - // --- copy libheif image to aom image - aom_img_fmt_t img_format = AOM_IMG_FMT_NONE; - int chroma_height = 0; int chroma_sample_position = AOM_CSP_UNKNOWN; +}; + + +chroma_info get_chroma_info(heif_chroma chroma, + int bpp_y, int source_height) +{ + chroma_info info; switch (chroma) { case heif_chroma_420: case heif_chroma_monochrome: - img_format = AOM_IMG_FMT_I420; - chroma_height = (source_height+1)/2; - chroma_sample_position = AOM_CSP_UNKNOWN; // TODO: change this to CSP_CENTER in the future (https://github.com/AOMediaCodec/av1-avif/issues/88) + info.img_format = AOM_IMG_FMT_I420; + info.chroma_height = (source_height+1)/2; + info.chroma_sample_position = AOM_CSP_UNKNOWN; // TODO: change this to CSP_CENTER in the future (https://github.com/AOMediaCodec/av1-avif/issues/88) break; case heif_chroma_422: - img_format = AOM_IMG_FMT_I422; - chroma_height = (source_height+1)/2; - chroma_sample_position = AOM_CSP_COLOCATED; + info.img_format = AOM_IMG_FMT_I422; + info.chroma_height = source_height; // 4:2:2 is subsampled horizontally only + info.chroma_sample_position = AOM_CSP_COLOCATED; break; case heif_chroma_444: - img_format = AOM_IMG_FMT_I444; - chroma_height = source_height; - chroma_sample_position = AOM_CSP_COLOCATED; + info.img_format = AOM_IMG_FMT_I444; + info.chroma_height = source_height; + info.chroma_sample_position = AOM_CSP_COLOCATED; break; default: - img_format = AOM_IMG_FMT_NONE; - chroma_sample_position = AOM_CSP_UNKNOWN; + info.img_format = AOM_IMG_FMT_NONE; + info.chroma_sample_position = AOM_CSP_UNKNOWN; assert(false); break; } if (bpp_y > 8) { - img_format = (aom_img_fmt_t) (img_format | AOM_IMG_FMT_HIGHBITDEPTH); + // aom_img_fmt_t is a set of flags, so the combined value is intentionally not an enumerator. + info.img_format = (aom_img_fmt_t) (info.img_format | AOM_IMG_FMT_HIGHBITDEPTH); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) } - std::unique_ptr input_image(aom_img_alloc(nullptr, img_format, - source_width, source_height, 1), - aom_img_free); - if (!input_image) { - err = {heif_error_Memory_allocation_error, - heif_suberror_Unspecified, - "Failed to allocate image"}; - return err; - } + return info; +} - for (int plane = 0; plane < 3; plane++) { - unsigned char* buf = input_image->planes[plane]; - const int stride = input_image->stride[plane]; - if (chroma == heif_chroma_monochrome && plane != 0) { - if (bpp_y == 8) { - memset(buf, 128, chroma_height * stride); - } - else { - uint16_t* buf16 = (uint16_t*) buf; - uint16_t half_range = (uint16_t) (1 << (bpp_y - 1)); - for (int i = 0; i < chroma_height * stride / 2; i++) { - buf16[i] = half_range; - } - } +static heif_error aom_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; - continue; - } + // destroy the codec in case it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + aom_codec_destroy(&encoder->codec); - /* - const int w = aom_img_plane_width(img, plane) * - ((img->fmt & AOM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1); - const int h = aom_img_plane_height(img, plane); - */ + heif_error err; - int in_stride = 0; - const uint8_t* in_p = heif_image_get_plane_readonly(image, (heif_channel) plane, &in_stride); + const int source_width = heif_image_get_width(image, heif_channel_Y); + const int source_height = heif_image_get_height(image, heif_channel_Y); - int w = source_width; - int h = source_height; + const heif_chroma chroma = heif_image_get_chroma_format(image); - if (plane != 0) { - if (chroma != heif_chroma_444) { w = (w + 1) / 2; } - if (chroma == heif_chroma_420) { h = (h + 1) / 2; } + int bpp_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); - assert(w == heif_image_get_width(image, (heif_channel) plane)); - assert(h == heif_image_get_height(image, (heif_channel) plane)); - } - if (bpp_y > 8) { - w *= 2; - } + // --- check for AOM 3.6.0 bug - for (int y = 0; y < h; y++) { - memcpy(buf, &in_p[y * in_stride], w); - buf += stride; + bool is_aom_3_6_0 = (aom_codec_version() == 0x030600); + + if (is_aom_3_6_0) { + // This bound might be too tight, as I still could encode images with 8193 x 4353 correctly. Even 8200x4400, but 8200x4800 fails. + // Let's still keep it as most images will be smaller anyway. + if (!(source_width <= 8192 * 2 && source_height <= 4352 * 2 && source_width * source_height <= 8192 * 4352)) { + err = {heif_error_Encoding_error, + heif_suberror_Encoder_encoding, + "AOM v3.6.0 has a bug when encoding large images. Please upgrade to at least AOM v3.6.1."}; + return err; } } + // --- copy libheif image to aom image + + chroma_info chroma_info = get_chroma_info(chroma, bpp_y, source_height); + // --- configure codec aom_codec_iface_t* iface; - aom_codec_ctx_t codec; + aom_codec_ctx_t& codec = encoder->codec; iface = aom_codec_av1_cx(); //encoder->encoder = get_aom_encoder_by_name("av1"); if (!iface) { - err = {heif_error_Unsupported_feature, - heif_suberror_Unsupported_codec, - "Unsupported codec: AOMedia Project AV1 Encoder"}; - return err; + return { + heif_error_Unsupported_feature, + heif_suberror_Unsupported_codec, + "Unsupported codec: AOMedia Project AV1 Encoder" + }; } @@ -945,6 +982,13 @@ // aom 2.0 unsigned int aomUsage = AOM_USAGE_GOOD_QUALITY; #endif + + if (image_sequence && + options->gop_structure != heif_sequence_gop_structure_intra_only && + options->keyframe_distance_max != 1) { + aomUsage = AOM_USAGE_GOOD_QUALITY; + } + if (encoder->realtime_mode) { aomUsage = AOM_USAGE_REALTIME; } @@ -966,7 +1010,9 @@ // Set the max number of frames to encode to 1. This makes the libaom encoder // set still_picture and reduced_still_picture_header to 1 in the AV1 sequence // header OBU. - cfg.g_limit = 1; + if (!image_sequence) { + cfg.g_limit = 1; + } // Use the default settings of the new AOM_USAGE_ALL_INTRA (added in // https://crbug.com/aomedia/2959). @@ -977,8 +1023,23 @@ cfg.g_lag_in_frames = 0; // Disable automatic placement of key frames by the encoder. cfg.kf_mode = AOM_KF_DISABLED; - // Tell libaom that all frames will be key frames. - cfg.kf_max_dist = 0; + + if (!image_sequence) { + // Tell libaom that all frames will be key frames. + cfg.kf_max_dist = 0; + } + else if (options->gop_structure == heif_sequence_gop_structure_intra_only) { + cfg.kf_max_dist = 0; + } + else { + if (options->keyframe_distance_min) { + cfg.kf_min_dist = options->keyframe_distance_min; + } + + if (options->keyframe_distance_max) { + cfg.kf_max_dist = options->keyframe_distance_max; + } + } cfg.g_profile = seq_profile; cfg.g_bit_depth = (aom_bit_depth_t) bpp_y; @@ -990,17 +1051,92 @@ int max_q = encoder->max_q; if (input_class == heif_image_input_class_alpha && encoder->alpha_min_q_set && encoder->alpha_max_q_set) { - min_q = encoder->alpha_min_q; - max_q = encoder->alpha_max_q; + min_q = encoder->alpha_min_q; + max_q = encoder->alpha_max_q; } int quality = encoder->quality; if (input_class == heif_image_input_class_alpha && encoder->alpha_quality_set) { - quality = encoder->alpha_quality; + quality = encoder->alpha_quality; + } + + // Fetch NCLX and determine the effective tune metric early, since the + // quality-to-quantizer mapping for AOM_TUNE_IQ uses a different (non-linear) table. + + heif_color_profile_nclx* nclx = nullptr; + err = heif_image_get_nclx_color_profile(image, &nclx); + if (err.code != heif_error_Ok) { + assert(nclx == nullptr); + } + + // make sure NCLX profile is deleted at end of function + auto nclx_deleter = std::unique_ptr(nclx, heif_nclx_color_profile_free); + + // A slide-show-style image sequence is treated like a still image (favor + // perceptual quality / AOM_TUNE_IQ when supported); only true video content + // keeps SSIM-tuned encoding. + bool tune_as_video = (image_sequence && + options && + options->version >= 3 && + options->content_kind == heif_sequence_content_kind_video); + + bool is_lossless = (encoder->lossless || + (input_class == heif_image_input_class_alpha && encoder->lossless_alpha)); + + aom_tune_metric effective_tune = encoder->tune; + if (encoder->tune_auto) { + if (tune_as_video) { + effective_tune = AOM_TUNE_SSIM; + } + else if (input_class == heif_image_input_class_alpha) { + // AOM_TUNE_SSIM causes ringing on alpha; PSNR avoids that. + effective_tune = AOM_TUNE_PSNR; + } + else { + effective_tune = AOM_TUNE_SSIM; + +#if defined(AOM_HAVE_TUNE_IQ) + // AOM_TUNE_IQ is tuned for the YCbCr family of color spaces (and other YUV-like + // spaces such as YCgCo, ICtCp, including monochrome). It does NOT generalize to + // GBR samples (matrix_coefficients = IDENTITY), so we keep SSIM for that case. + // AOM_TUNE_IQ stabilized in libaom v3.13.0 (all-intra only); v3.14.0 added + // support for the good-quality and realtime inter-frame modes. + + static const int aom_version_3_13_0 = (3 << 16) | (13 << 8); + static const int aom_version_3_14_0 = (3 << 16) | (14 << 8); + + bool is_identity_matrix = nclx && (nclx->matrix_coefficients == heif_matrix_coefficients_RGB_GBR); + int aom_version = aom_codec_version(); + bool iq_supports_inter = (aom_version >= aom_version_3_14_0); + + // libaom turns on chroma delta-q for AOM_TUNE_IQ and then refuses to combine + // that with lossless coding, so keep AOM_TUNE_SSIM for lossless images. + if (!is_identity_matrix && !is_lossless && + (cfg.g_usage == AOM_USAGE_ALL_INTRA || iq_supports_inter) && + aom_version >= aom_version_3_13_0) { + effective_tune = AOM_TUNE_IQ; + } +#endif + } } - int cq_level = ((100 - quality) * 63 + 50) / 100; +#if defined(AOM_HAVE_TUNE_IQ) + if (is_lossless && effective_tune == AOM_TUNE_IQ) { + return heif_error_lossless_with_tune_iq; + } +#endif + + int cq_level; +#if defined(AOM_HAVE_TUNE_IQ) + if (effective_tune == AOM_TUNE_IQ) { + cq_level = tuneIqQualityToQuantizer[quality]; + } + else +#endif + { + cq_level = ((100 - quality) * 63 + 50) / 100; + } // Work around the bug in libaom v2.0.2 or older fixed by // https://aomedia-review.googlesource.com/c/aom/+/113064. If using a libaom @@ -1017,6 +1153,9 @@ cfg.monochrome = 1; } + cfg.g_timebase.num = static_cast(framerate_num); + cfg.g_timebase.den = static_cast(framerate_denom); + // --- initialize codec aom_codec_flags_t encoder_flags = 0; @@ -1034,8 +1173,7 @@ return err; } - // automatically destroy aom_codec_ctx_t when we leave the function - auto codec_ctx_deleter = std::unique_ptr(&codec, aom_codec_destroy); + encoder->bit_depth = bpp_y; aom_codec_err_t aom_error; @@ -1058,18 +1196,9 @@ // TODO: set AV1E_SET_TILE_ROWS and AV1E_SET_TILE_COLUMNS. - struct heif_color_profile_nclx* nclx = nullptr; - err = heif_image_get_nclx_color_profile(image, &nclx); - if (err.code != heif_error_Ok) { - assert(nclx == nullptr); - } - - // make sure NCLX profile is deleted at end of function - auto nclx_deleter = std::unique_ptr(nclx, heif_nclx_color_profile_free); - // In aom, color_range defaults to limited range (0). Set it to full range (1). aom_error = aom_codec_control(&codec, AV1E_SET_COLOR_RANGE, nclx ? nclx->full_range_flag : 1); CHECK_ERROR; - aom_error = aom_codec_control(&codec, AV1E_SET_CHROMA_SAMPLE_POSITION, chroma_sample_position); CHECK_ERROR; + aom_error = aom_codec_control(&codec, AV1E_SET_CHROMA_SAMPLE_POSITION, chroma_info.chroma_sample_position); CHECK_ERROR; if (nclx && (input_class == heif_image_input_class_normal || @@ -1077,11 +1206,11 @@ aom_error = aom_codec_control(&codec, AV1E_SET_COLOR_PRIMARIES, nclx->color_primaries); CHECK_ERROR aom_error = aom_codec_control(&codec, AV1E_SET_MATRIX_COEFFICIENTS, nclx->matrix_coefficients); CHECK_ERROR; aom_error = aom_codec_control(&codec, AV1E_SET_TRANSFER_CHARACTERISTICS, nclx->transfer_characteristics); CHECK_ERROR; - } + } - aom_error = aom_codec_control(&codec, AOME_SET_TUNING, encoder->tune); CHECK_ERROR; + aom_error = aom_codec_control(&codec, AOME_SET_TUNING, effective_tune); CHECK_ERROR; - if (encoder->lossless || (input_class == heif_image_input_class_alpha && encoder->lossless_alpha)) { + if (is_lossless) { aom_error = aom_codec_control(&codec, AV1E_SET_LOSSLESS, 1); CHECK_ERROR; } @@ -1114,12 +1243,125 @@ } #endif + return {}; +} + + + +static heif_error aom_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return aom_start_sequence_encoding_intern(encoder_raw, image, input_class, framerate_num, framerate_denom, options, + true); +} + + +static heif_error aom_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t frame_nr) +{ + // AV1 signals one bit depth for all planes, so an image whose color + // channels disagree cannot be encoded. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {8, 10, 12}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; + aom_codec_ctx_t& codec = encoder->codec; + + // AOM_CODEC_USE_HIGHBITDEPTH was decided when the codec was initialized from the + // first frame of the sequence. libaom refuses a frame that disagrees with it, but + // with an error that says nothing about the cause. + input_error = check_sequence_frame_bit_depth(image, encoder->bit_depth); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + heif_error err; + + const int source_width = heif_image_get_width(image, heif_channel_Y); + const int source_height = heif_image_get_height(image, heif_channel_Y); + + const heif_chroma chroma = heif_image_get_chroma_format(image); + + int bpp_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + chroma_info chroma_info = get_chroma_info(chroma, bpp_y, source_height); + + std::unique_ptr input_image(aom_img_alloc(nullptr, + chroma_info.img_format, + source_width, + source_height, + 1), + aom_img_free); + if (!input_image) { + err = {heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + "Failed to allocate image"}; + return err; + } + + + for (int plane = 0; plane < 3; plane++) { + unsigned char* buf = input_image->planes[plane]; + const int stride = input_image->stride[plane]; + + if (chroma == heif_chroma_monochrome && plane != 0) { + if (bpp_y == 8) { + memset(buf, 128, chroma_info.chroma_height * stride); + } + else { + uint16_t* buf16 = (uint16_t*) buf; + uint16_t half_range = (uint16_t) (1 << (bpp_y - 1)); + for (int i = 0; i < chroma_info.chroma_height * stride / 2; i++) { + buf16[i] = half_range; + } + } + + continue; + } + + /* + const int w = aom_img_plane_width(img, plane) * + ((img->fmt & AOM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1); + const int h = aom_img_plane_height(img, plane); + */ + + size_t in_stride = 0; + const uint8_t* in_p = heif_image_get_plane_readonly2(image, (heif_channel) plane, &in_stride); + + int w = source_width; + int h = source_height; + + if (plane != 0) { + if (chroma != heif_chroma_444) { w = (w + 1) / 2; } + if (chroma == heif_chroma_420) { h = (h + 1) / 2; } + + assert(w == heif_image_get_width(image, (heif_channel) plane)); + assert(h == heif_image_get_height(image, (heif_channel) plane)); + } + + if (bpp_y > 8) { + w *= 2; + } + + for (int y = 0; y < h; y++) { + memcpy(buf, &in_p[y * in_stride], w); + buf += stride; + } + } + + //input_image->user_priv = (void*)frame_nr; + // --- encode frame - res = aom_codec_encode(&codec, input_image.get(), - 0, // only encoding a single frame - 1, - 0); // no flags + aom_codec_err_t res = aom_codec_encode(&codec, input_image.get(), + frame_nr, // PTS (only encoding a single frame) TODO + 1, + 0); // no flags if (res != AOM_CODEC_OK) { err = { @@ -1130,7 +1372,8 @@ return err; } - encoder->compressedData.clear(); + // TODO: do we need this ? encoder->compressedData.clear(); + const aom_codec_cx_pkt_t* pkt = NULL; aom_codec_iter_t iter = NULL; // for extracting the compressed packets @@ -1146,19 +1389,34 @@ // This allows libheif to easily extract the sequence header for the av1C header size_t n = pkt->data.frame.sz; - size_t oldSize = encoder->compressedData.size(); - encoder->compressedData.resize(oldSize + n); - memcpy(encoder->compressedData.data() + oldSize, + encoder_struct_aom::Packet output_packet; + output_packet.frameNr = pkt->data.frame.pts; + output_packet.is_keyframe = (pkt->data.frame.flags & AOM_FRAME_IS_INTRAONLY); + + encoder->output_packets.emplace_back(output_packet); + encoder->output_packets.back().compressedData.resize(n); + + memcpy(encoder->output_packets.back().compressedData.data(), pkt->data.frame.buf, n); - - encoder->data_read = false; } } + + return heif_error_ok; +} + + +static heif_error aom_end_sequence_encoding(void *encoder_raw) +{ + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; + aom_codec_ctx_t& codec = encoder->codec; + + heif_error err; + int flags = 0; - res = aom_codec_encode(&codec, NULL, -1, 0, flags); + aom_codec_err_t res = aom_codec_encode(&codec, NULL, -1, 0, flags); if (res != AOM_CODEC_OK) { err = {heif_error_Encoder_plugin_error, heif_suberror_Encoder_encoding, @@ -1166,8 +1424,10 @@ return err; } - iter = NULL; + aom_codec_iter_t iter = NULL; // for extracting the compressed packets + + const aom_codec_cx_pkt_t* pkt = NULL; while ((pkt = aom_codec_get_cx_data(&codec, &iter)) != NULL) { if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) { @@ -1180,43 +1440,83 @@ // This allows libheif to easily extract the sequence header for the av1C header size_t n = pkt->data.frame.sz; - size_t oldSize = encoder->compressedData.size(); - encoder->compressedData.resize(oldSize + n); - memcpy(encoder->compressedData.data() + oldSize, + encoder_struct_aom::Packet output_packet; + output_packet.frameNr = pkt->data.frame.pts; + + encoder->output_packets.emplace_back(output_packet); + encoder->output_packets.back().compressedData.resize(n); + + memcpy(encoder->output_packets.back().compressedData.data(), pkt->data.frame.buf, n); - - encoder->data_read = false; } } - return heif_error_ok; + return {}; } -struct heif_error aom_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error aom_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) { - struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw; + heif_error err; + err = aom_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } - if (encoder->data_read) { + err = aom_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } + + return aom_end_sequence_encoding(encoder_raw); +} + + +heif_error aom_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* out_framenr, int* out_is_keyframe, + int* more_frame_packets) +{ + encoder_struct_aom* encoder = (encoder_struct_aom*) encoder_raw; + + encoder->active_output_data.clear(); + + if (encoder->output_packets.empty()) { *size = 0; *data = nullptr; } else { - *size = (int) encoder->compressedData.size(); - *data = encoder->compressedData.data(); - encoder->data_read = true; + encoder->active_output_data = std::move(encoder->output_packets.front().compressedData); + if (out_framenr) { + *out_framenr = encoder->output_packets.front().frameNr; + } + + if (out_is_keyframe) { + *out_is_keyframe = encoder->output_packets.front().is_keyframe; + } + + encoder->output_packets.pop_front(); + + *size = (int) encoder->active_output_data.size(); + *data = encoder->active_output_data.data(); } return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_aom +static heif_error aom_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return aom_get_compressed_data2(encoder_raw, data, size, nullptr, nullptr, nullptr); +} + + +static const heif_encoder_plugin encoder_plugin_aom { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_AV1, /* id_name */ "aom", /* priority */ AOM_PLUGIN_PRIORITY, @@ -1244,10 +1544,16 @@ /* encode_image */ aom_encode_image, /* get_compressed_data */ aom_get_compressed_data, /* query_input_colorspace (v2) */ aom_query_input_colorspace2, - /* query_encoded_size (v3) */ nullptr + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ aom_start_sequence_encoding, + /* encode_sequence_frame (v4) */ aom_encode_sequence_frame, + /* end_sequence_encoding (v4) */ aom_end_sequence_encoding, + /* get_compressed_data2 (v4) */ aom_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_aom() +const heif_encoder_plugin* get_encoder_plugin_aom() { return &encoder_plugin_aom; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_input_check.h libheif-1.23.4/libheif/plugins/encoder_input_check.h --- libheif-1.19.8/libheif/plugins/encoder_input_check.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_input_check.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,163 @@ +/* + * HEIF codec. + * Copyright (c) 2026 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_ENCODER_INPUT_CHECK_H +#define LIBHEIF_ENCODER_INPUT_CHECK_H + +#include "libheif/heif.h" +#include "libheif/heif_plugin.h" + +#include + +/* + * Input validation for encoder plugins that take planar YCbCr or monochrome + * images with one bit depth for all channels. + * + * A plugin cannot assume that it is handed an image it can encode. The image + * arrives from Encoder::convert_colorspace_for_encoding(), which returns the + * image unchanged whenever it already has the colorspace and chroma format the + * plugin asked for, so nothing on the way in inspects the per-channel bit + * depths. Input images may legitimately differ per channel: the 'unci' codec + * (ISO/IEC 23001-17) declares component_bit_depth once per component, so + * decoding such a file and re-encoding it produces, for example, Y at 10 bits + * and Cb/Cr at 8. HeifPixelImage allocates one byte per sample up to 8 bits and + * two bytes above that, so a plugin that derives the sample width from the luma + * channel and applies it to the chroma planes reads past the end of them. + * + * Whether an image can be encoded depends on the codec and on the specific + * encoder implementation, which is why this takes the constraints as arguments + * rather than hard-coding them: + * + * - H.265 and H.264 signal bit_depth_luma_minus8 and bit_depth_chroma_minus8 + * separately, so the formats can represent differing luma and chroma bit + * depths, but neither x265 nor x264 nor kvazaar can produce it: their APIs + * carry a single bit depth. + * - AV1 and VVC signal one bit depth for all planes, so the formats cannot + * represent it at all. + * - JPEG 2000 signals a precision per component and genuinely supports it, + * which is why the OpenJPEG and OpenJPH plugins do not use this check. + * + * 'supported_bit_depths' is the set this plugin can actually encode. Where that + * is narrower than what the codec allows because of the encoder library, the + * plugin has to pass the narrower set: only x265 answers the question at run + * time, where x265_api_get() returns NULL for a depth the linked build does not + * provide. uvg_api_get() and kvz_api_get() look like they do the same, but both + * are 'return &..._8bit_api;' upstream and never fail, so the uvg266 and kvazaar + * builds are pinned at compile time by UVG_BIT_DEPTH / KVZ_BIT_DEPTH and those + * are what the two plugins pass here. + * + * TODO: this per-plugin check is a stopgap. What is really needed is a proper + * plugin API through which an encoder describes the input formats it accepts + * (bit depth per channel, chroma formats, and so on). That would let libheif + * query how an image has to be color-converted to fit a given encoder, instead + * of the plugin refusing the image outright. It would also feed into encoder + * selection: two encoders for the same output format may well support different + * bit depth combinations, so the choice of plugin should depend on what the + * input image actually needs. Until that API exists, each plugin has to check + * its own input. + */ +static inline heif_error check_encoder_input_image(const heif_image* image, + bool supports_monochrome, + std::initializer_list supported_bit_depths) +{ + const heif_channel channels[3] = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; + int num_color_channels; + + switch (heif_image_get_colorspace(image)) { + case heif_colorspace_monochrome: + if (!supports_monochrome) { + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, + "Encoder cannot encode monochrome images"}; + } + num_color_channels = 1; + break; + + case heif_colorspace_YCbCr: + num_color_channels = 3; + break; + + default: + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, + "Encoder can only encode YCbCr and monochrome images"}; + } + + for (int i = 0; i < num_color_channels; i++) { + if (!heif_image_has_channel(image, channels[i])) { + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, + "Input image is missing one of its color channels"}; + } + } + + int bpp = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + for (int i = 1; i < num_color_channels; i++) { + if (heif_image_get_bits_per_pixel_range(image, channels[i]) != bpp) { + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + "Encoder cannot encode images in which the color channels have different bit depths"}; + } + } + + for (int supported_bpp : supported_bit_depths) { + if (bpp == supported_bpp) { + return heif_error_ok; + } + } + + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + "Encoder cannot encode images at this bit depth"}; +} + + +/* + * An encoder is opened once per sequence, in *_start_sequence_encoding(), and is + * configured from the first frame. Encoder_HEVC / Encoder_AVC / Encoder_AVIF / + * Encoder_VVC::encode_sequence_frame() call it only while the encoder is not + * running yet, so every later frame goes straight to *_encode_sequence_frame(). + * A plugin that latched a bit depth there and applies it to the planes of a later + * frame walks a one byte per sample plane at two bytes per sample: x265, for + * example, hands the plane pointers of the current frame to libx265 together with + * pic->bitDepth taken from the first frame, and reads past the end of them. + * + * So a plugin that keeps the bit depth across frames has to check every frame + * against that configuration and not only against the codec level set above. + * Plugins whose depth is fixed at compile time need nothing extra: passing that + * constant to check_encoder_input_image() already pins all frames to one value. + * + * Call this after check_encoder_input_image(), which has already established that + * the luma channel exists and that the chroma channels have the same depth. + */ +static inline heif_error check_sequence_frame_bit_depth(const heif_image* image, + int configured_bit_depth) +{ + if (heif_image_get_bits_per_pixel_range(image, heif_channel_Y) != configured_bit_depth) { + return heif_error{heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + "All frames of a sequence must have the bit depth of the first frame"}; + } + + return heif_error_ok; +} + +#endif // LIBHEIF_ENCODER_INPUT_CHECK_H diff -Nru libheif-1.19.8/libheif/plugins/encoder_jpeg.cc libheif-1.23.4/libheif/plugins/encoder_jpeg.cc --- libheif-1.19.8/libheif/plugins/encoder_jpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_jpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,6 +21,7 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_jpeg.h" +#include "encoder_input_check.h" #include #include #include @@ -72,13 +73,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter jpeg_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* jpeg_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter jpeg_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* jpeg_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void jpeg_init_parameters() { - struct heif_encoder_parameter* p = jpeg_encoder_params; - const struct heif_encoder_parameter** d = jpeg_encoder_parameter_ptrs; + heif_encoder_parameter* p = jpeg_encoder_params; + const heif_encoder_parameter** d = jpeg_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -108,7 +109,7 @@ } -const struct heif_encoder_parameter** jpeg_list_parameters(void* encoder) +const heif_encoder_parameter** jpeg_list_parameters(void* encoder) { return jpeg_encoder_parameter_ptrs; } @@ -123,10 +124,10 @@ { } -struct heif_error jpeg_new_encoder(void** enc) +heif_error jpeg_new_encoder(void** enc) { auto* encoder = new encoder_struct_jpeg(); - struct heif_error err = heif_error_ok; + heif_error err = heif_error_ok; *enc = encoder; @@ -139,15 +140,15 @@ void jpeg_free_encoder(void* encoder_raw) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; delete encoder; } -struct heif_error jpeg_set_parameter_quality(void* encoder_raw, int quality) +heif_error jpeg_set_parameter_quality(void* encoder_raw, int quality) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -158,18 +159,18 @@ return heif_error_ok; } -struct heif_error jpeg_get_parameter_quality(void* encoder_raw, int* quality) +heif_error jpeg_get_parameter_quality(void* encoder_raw, int* quality) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -struct heif_error jpeg_set_parameter_lossless(void* encoder_raw, int enable) +heif_error jpeg_set_parameter_lossless(void* encoder_raw, int enable) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (enable) { encoder->quality = 100; // not really lossless, but the best we can do @@ -178,21 +179,21 @@ return heif_error_ok; } -struct heif_error jpeg_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error jpeg_get_parameter_lossless(void* encoder_raw, int* enable) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; *enable = (encoder->quality == 100); // not really correct, but matches the setting above return heif_error_ok; } -struct heif_error jpeg_set_parameter_logging_level(void* encoder_raw, int logging) +heif_error jpeg_set_parameter_logging_level(void* encoder_raw, int logging) { return heif_error_ok; } -struct heif_error jpeg_get_parameter_logging_level(void* encoder_raw, int* loglevel) +heif_error jpeg_get_parameter_logging_level(void* encoder_raw, int* loglevel) { *loglevel = 0; @@ -203,9 +204,9 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error jpeg_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error jpeg_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_jpeg* encoder = (struct encoder_struct_jpeg*) encoder_raw; + encoder_struct_jpeg* encoder = (encoder_struct_jpeg*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return jpeg_set_parameter_quality(encoder, value); @@ -217,9 +218,9 @@ return heif_error_unsupported_parameter; } -struct heif_error jpeg_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error jpeg_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return jpeg_get_parameter_quality(encoder, value); @@ -232,9 +233,9 @@ } -struct heif_error jpeg_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error jpeg_set_parameter_boolean(void* encoder_raw, const char* name, int value) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return jpeg_set_parameter_lossless(encoder, value); @@ -245,9 +246,9 @@ return heif_error_unsupported_parameter; } -struct heif_error jpeg_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error jpeg_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return jpeg_get_parameter_lossless(encoder, value); @@ -259,7 +260,7 @@ } -struct heif_error jpeg_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error jpeg_set_parameter_string(void* encoder_raw, const char* name, const char* value) { //auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; @@ -267,8 +268,8 @@ } -struct heif_error jpeg_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +heif_error jpeg_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { //auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; @@ -278,8 +279,8 @@ static void jpeg_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = jpeg_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = jpeg_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -327,7 +328,7 @@ struct ErrorHandler { - struct jpeg_error_mgr pub; /* "public" fields */ + jpeg_error_mgr pub; /* "public" fields */ jmp_buf setjmp_buffer; /* for return to caller */ }; @@ -338,15 +339,27 @@ } -struct heif_error jpeg_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +heif_error jpeg_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + // JPEG signals one sample precision for all components. The plugin always + // requests YCbCr input and does not implement greyscale JPEG encoding yet. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/false, + {8}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + auto* encoder = (encoder_struct_jpeg*) encoder_raw; - struct jpeg_compress_struct cinfo; - struct ErrorHandler jerr; - cinfo.err = jpeg_std_error(reinterpret_cast(&jerr)); + if (heif_image_get_bits_per_pixel(image, heif_channel_Y) != 8) { + return heif_error{heif_error_Encoding_error, heif_suberror_Encoder_encoding, "Cannot write JPEG image with >8 bpp."}; + } + + jpeg_compress_struct cinfo{}; + ErrorHandler jerr; + cinfo.err = jpeg_std_error(reinterpret_cast(&jerr)); jerr.pub.error_exit = &OnJpegError; if (setjmp(jerr.setjmp_buffer)) { cinfo.err->output_message(reinterpret_cast(&cinfo)); @@ -354,11 +367,6 @@ return heif_error{heif_error_Encoding_error, heif_suberror_Encoder_encoding, "JPEG encoding error"}; } - if (heif_image_get_bits_per_pixel(image, heif_channel_Y) != 8) { - jpeg_destroy_compress(&cinfo); - return heif_error{heif_error_Encoding_error, heif_suberror_Encoder_encoding, "Cannot write JPEG image with >8 bpp."}; - } - uint8_t* outbuffer = nullptr; #if defined(LIBJPEG_TURBO_VERSION) || (JPEG_LIB_VERSION_MAJOR < 9 || (JPEG_LIB_VERSION_MAJOR == 9 && JPEG_LIB_VERSION_MINOR < 4)) unsigned long outlength = 0; @@ -376,20 +384,37 @@ jpeg_set_defaults(&cinfo); static const boolean kForceBaseline = TRUE; jpeg_set_quality(&cinfo, encoder->quality, kForceBaseline); + + // Write the pixel aspect ratio into the JFIF APP0 density fields. With density_unit=0, + // Xdensity:Ydensity specifies the pixel aspect ratio (ITU-T T.871), in the same + // hSpacing:vSpacing order as the pasp property (matching the FFmpeg convention). + // The fields are u(16), so ratios that do not fit are only signalled through pasp. + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + cinfo.density_unit = 0; // no absolute units, aspect ratio only + cinfo.X_density = (UINT16) aspect_h; + cinfo.Y_density = (UINT16) aspect_v; + } static const boolean kWriteAllTables = TRUE; jpeg_start_compress(&cinfo, kWriteAllTables); - int stride_y; - const uint8_t* row_y = heif_image_get_plane_readonly(image, heif_channel_Y, - &stride_y); - int stride_u; - const uint8_t* row_u = heif_image_get_plane_readonly(image, heif_channel_Cb, - &stride_u); - int stride_v; - const uint8_t* row_v = heif_image_get_plane_readonly(image, heif_channel_Cr, - &stride_v); + size_t stride_y; + const uint8_t* row_y = heif_image_get_plane_readonly2(image, heif_channel_Y, + &stride_y); + size_t stride_u; + const uint8_t* row_u = heif_image_get_plane_readonly2(image, heif_channel_Cb, + &stride_u); + size_t stride_v; + const uint8_t* row_v = heif_image_get_plane_readonly2(image, heif_channel_Cr, + &stride_v); JSAMPARRAY buffer = cinfo.mem->alloc_sarray( reinterpret_cast(&cinfo), JPOOL_IMAGE, @@ -425,10 +450,10 @@ } -struct heif_error jpeg_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +heif_error jpeg_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) { - auto* encoder = (struct encoder_struct_jpeg*) encoder_raw; + auto* encoder = (encoder_struct_jpeg*) encoder_raw; if (encoder->data_read) { *data = nullptr; @@ -444,9 +469,45 @@ } -static const struct heif_encoder_plugin encoder_plugin_jpeg +heif_error jpeg_start_sequence_encoding(void* encoder, const heif_image* image, + enum heif_image_input_class image_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return heif_error_ok; +} + +heif_error jpeg_encode_sequence_frame(void* encoder, const heif_image* image, uintptr_t frame_nr) +{ + return jpeg_encode_image(encoder, image, heif_image_input_class_normal); +} + +heif_error jpeg_end_sequence_encoding(void* encoder) +{ + return heif_error_ok; +} + +heif_error jpeg_get_compressed_data2(void* encoder, uint8_t** data, int* size, + uintptr_t* frame_nr, + int* is_keyframe, int* more_frame_packets) +{ + heif_error err = jpeg_get_compressed_data(encoder, data, size, nullptr); + + if (is_keyframe) { + *is_keyframe = true; + } + + if (more_frame_packets) { + *more_frame_packets = true; + } + + return err; +} + + +static const heif_encoder_plugin encoder_plugin_jpeg { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_JPEG, /* id_name */ "jpeg", /* priority */ JPEG_PLUGIN_PRIORITY, @@ -474,10 +535,16 @@ /* encode_image */ jpeg_encode_image, /* get_compressed_data */ jpeg_get_compressed_data, /* query_input_colorspace (v2) */ jpeg_query_input_colorspace2, - /* query_encoded_size (v3) */ jpeg_query_encoded_size + /* query_encoded_size (v3) */ jpeg_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ jpeg_start_sequence_encoding, + /* encode_sequence_frame (v4) */ jpeg_encode_sequence_frame, + /* end_sequence_encoding (v4) */ jpeg_end_sequence_encoding, + /* get_compressed_data2 (v4) */ jpeg_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_jpeg() +const heif_encoder_plugin* get_encoder_plugin_jpeg() { return &encoder_plugin_jpeg; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_kvazaar.cc libheif-1.23.4/libheif/plugins/encoder_kvazaar.cc --- libheif-1.19.8/libheif/plugins/encoder_kvazaar.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_kvazaar.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,12 +21,16 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_kvazaar.h" +#include "encoder_input_check.h" #include #include // apparently, this is a false positive of cpplint #include #include #include #include +#include +#include +#include extern "C" { #include @@ -41,11 +45,50 @@ struct encoder_struct_kvazaar { + // --- parameters + int quality = 75; bool lossless = false; - std::vector output_data; - size_t output_idx = 0; + // --- input config + + //heif_color_profile_nclx* nclx = nullptr; + + // --- encoder + + const kvz_api* api = nullptr; + kvz_config* config = nullptr; + kvz_encoder* kvzencoder = nullptr; + + // --- output + + struct Packet + { + std::vector data; + uintptr_t frameNr = 0; + }; + + std::deque output_data; + + std::vector active_data; // holds the data that we just returned + + + ~encoder_struct_kvazaar() + { + /* + if (nclx) { + heif_nclx_color_profile_free(nclx); + } + */ + + if (kvzencoder) { + api->encoder_close(kvzencoder); + } + + if (config) { + api->config_destroy(config); + } + } }; static const int kvazaar_PLUGIN_PRIORITY = 100; @@ -60,20 +103,24 @@ static const char* kvazaar_plugin_name() { +#if HAVE_KVAZAAR_VERSION_STRING + snprintf(plugin_name, MAX_PLUGIN_NAME_LENGTH, "kvazaar HEVC encoder %s", kvz_get_version_string()); +#else strcpy(plugin_name, "kvazaar HEVC encoder"); +#endif return plugin_name; } #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter kvazaar_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* kvazaar_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter kvazaar_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* kvazaar_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void kvazaar_init_parameters() { - struct heif_encoder_parameter* p = kvazaar_encoder_params; - const struct heif_encoder_parameter** d = kvazaar_encoder_parameter_ptrs; + heif_encoder_parameter* p = kvazaar_encoder_params; + const heif_encoder_parameter** d = kvazaar_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -101,7 +148,7 @@ } -const struct heif_encoder_parameter** kvazaar_list_parameters(void* encoder) +const heif_encoder_parameter** kvazaar_list_parameters(void* encoder) { return kvazaar_encoder_parameter_ptrs; } @@ -118,10 +165,10 @@ } -static struct heif_error kvazaar_new_encoder(void** enc) +static heif_error kvazaar_new_encoder(void** enc) { - struct encoder_struct_kvazaar* encoder = new encoder_struct_kvazaar(); - struct heif_error err = heif_error_ok; + encoder_struct_kvazaar* encoder = new encoder_struct_kvazaar(); + heif_error err = heif_error_ok; *enc = encoder; @@ -134,14 +181,14 @@ static void kvazaar_free_encoder(void* encoder_raw) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; delete encoder; } -static struct heif_error kvazaar_set_parameter_quality(void* encoder_raw, int quality) +static heif_error kvazaar_set_parameter_quality(void* encoder_raw, int quality) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -152,34 +199,34 @@ return heif_error_ok; } -static struct heif_error kvazaar_get_parameter_quality(void* encoder_raw, int* quality) +static heif_error kvazaar_get_parameter_quality(void* encoder_raw, int* quality) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -static struct heif_error kvazaar_set_parameter_lossless(void* encoder_raw, int enable) +static heif_error kvazaar_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; encoder->lossless = enable ? 1 : 0; return heif_error_ok; } -static struct heif_error kvazaar_get_parameter_lossless(void* encoder_raw, int* enable) +static heif_error kvazaar_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; *enable = encoder->lossless; return heif_error_ok; } -static struct heif_error kvazaar_set_parameter_logging_level(void* encoder_raw, int logging) +static heif_error kvazaar_set_parameter_logging_level(void* encoder_raw, int logging) { // struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; @@ -188,7 +235,7 @@ return heif_error_ok; } -static struct heif_error kvazaar_get_parameter_logging_level(void* encoder_raw, int* loglevel) +static heif_error kvazaar_get_parameter_logging_level(void* encoder_raw, int* loglevel) { // struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; @@ -198,9 +245,9 @@ } -static struct heif_error kvazaar_set_parameter_integer(void* encoder_raw, const char* name, int value) +static heif_error kvazaar_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return kvazaar_set_parameter_quality(encoder, value); @@ -212,9 +259,9 @@ return heif_error_unsupported_parameter; } -static struct heif_error kvazaar_get_parameter_integer(void* encoder_raw, const char* name, int* value) +static heif_error kvazaar_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return kvazaar_get_parameter_quality(encoder, value); @@ -227,7 +274,7 @@ } -static struct heif_error kvazaar_set_parameter_boolean(void* encoder, const char* name, int value) +static heif_error kvazaar_set_parameter_boolean(void* encoder, const char* name, int value) { if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return kvazaar_set_parameter_lossless(encoder, value); @@ -249,13 +296,13 @@ */ -static struct heif_error kvazaar_set_parameter_string(void* encoder_raw, const char* name, const char* value) +static heif_error kvazaar_set_parameter_string(void* encoder_raw, const char* name, const char* value) { return heif_error_unsupported_parameter; } -static struct heif_error kvazaar_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +static heif_error kvazaar_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { return heif_error_unsupported_parameter; } @@ -263,8 +310,8 @@ static void kvazaar_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = kvazaar_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = kvazaar_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -314,8 +361,8 @@ void kvazaar_query_encoded_size(void* encoder_raw, uint32_t input_width, uint32_t input_height, uint32_t* encoded_width, uint32_t* encoded_height) { - *encoded_width = (input_width + 7) & ~0x7; - *encoded_height = (input_height + 7) & ~0x7; + *encoded_width = (input_width + 7) & ~0x7U; + *encoded_height = (input_height + 7) & ~0x7U; } @@ -332,28 +379,68 @@ } #endif -static void append_chunk_data(kvz_data_chunk* data, std::vector& out) +static void append_chunk_data(kvz_data_chunk* data, encoder_struct_kvazaar* encoder, uintptr_t pts) { if (!data || data->len == 0) { return; } - size_t old_size = out.size(); - out.resize(old_size + data->len); - memcpy(out.data() + old_size, data->data, data->len); + std::vector input_data; + while (data) { + input_data.insert(input_data.end(), data->data, data->data + data->len); + data = data->next; + } + + //std::vector& pktdata = encoder->output_data.front().data; + size_t start_idx = 0; + + for (;;) + { + while (start_idx < input_data.size() - 3 && + (input_data[start_idx] != 0 || + input_data[start_idx + 1] != 0 || + input_data[start_idx + 2] != 1)) { + start_idx++; + } + + size_t end_idx = start_idx + 1; + + while (end_idx < input_data.size() - 3 && + (input_data[end_idx] != 0 || + input_data[end_idx + 1] != 0 || + input_data[end_idx + 2] != 1)) { + end_idx++; + } + + if (end_idx == input_data.size() - 3) { + end_idx = input_data.size(); + } + + encoder_struct_kvazaar::Packet pkt; + pkt.data.resize(end_idx - start_idx - 3); + memcpy(pkt.data.data(), input_data.data() + start_idx + 3, pkt.data.size()); + pkt.frameNr = pts; + + // std::cout << "append frameNr=" << pts << " NAL:" << ((int)pkt.data[0]>>1) << " size:" << pkt.data.size() << "\n"; + + encoder->output_data.emplace_back(std::move(pkt)); - if (data->next) { - append_chunk_data(data->next, out); + if (end_idx == input_data.size()) { + break; + } + + start_idx = end_idx; } } -static void copy_plane(kvz_pixel* out_p, uint32_t out_stride, const uint8_t* in_p, uint32_t in_stride, int w, int h, int padded_width, int padded_height, +static void copy_plane(kvz_pixel* out_p, size_t out_stride, const uint8_t* in_p, size_t in_stride, + uint32_t w, uint32_t h, uint32_t padded_width, uint32_t padded_height, int bit_depth) { int bpp = (bit_depth > 8) ? 2 : 1; - for (int y = 0; y < padded_height; y++) { + for (uint32_t y = 0; y < padded_height; y++) { int sy = std::min(y, h - 1); // source y memcpy(out_p + y * out_stride, in_p + sy * in_stride, w * bpp); @@ -369,53 +456,51 @@ return std::unique_ptr(ptr, deleter); } -static struct heif_error kvazaar_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) -{ - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; - - int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); - int bit_depth_chroma = 0; - bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); - heif_chroma chroma = heif_image_get_chroma_format(image); - - if (!isGreyscale) { - bit_depth_chroma = heif_image_get_bits_per_pixel_range(image, heif_channel_Cb); - if (bit_depth != bit_depth_chroma) { - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Unsupported_bit_depth, - "Luma bit depth must equal the chroma bit depth" - }; - return err; +static heif_error kvazaar_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; + + // close the encoder if it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + if (encoder->api) { + if (encoder->kvzencoder) { + encoder->api->encoder_close(encoder->kvzencoder); + encoder->kvzencoder = nullptr; + } + if (encoder->config) { + encoder->api->config_destroy(encoder->config); + encoder->config = nullptr; } } + int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + // Kvazaar uses a hard-coded bit depth (https://github.com/ultravideo/kvazaar/issues/399). // Check whether this matches to the image bit depth. // TODO: we should check the bit depth supported by the encoder (like query_input_colorspace2()) and output a warning // if we have to encode at a different bit depth than requested. if (bit_depth != KVZ_BIT_DEPTH) { - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Unsupported_bit_depth, - kError_unsupported_bit_depth - }; - return err; + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + kError_unsupported_bit_depth + }; } - const kvz_api* api = kvz_api_get(bit_depth); + const kvz_api* api = encoder->api = kvz_api_get(bit_depth); if (api == nullptr) { - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Unspecified, - "Could not initialize Kvazaar API" - }; - return err; + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "Could not initialize Kvazaar API" + }; } - auto uconfig = make_guard(api->config_alloc(), [api](kvz_config* cfg) { api->config_destroy(cfg); }); - kvz_config* config = uconfig.get(); + kvz_config* config = encoder->config = api->config_alloc(); api->config_init(config); // param, encoder->preset.c_str(), encoder->tune.c_str()); #if HAVE_KVAZAAR_ENABLE_LOGGING @@ -428,104 +513,29 @@ config->threads = 0; #endif -#if 1 -#if 0 - while (ctuSize > 16 && - (heif_image_get_width(image, heif_channel_Y) < ctuSize || - heif_image_get_height(image, heif_channel_Y) < ctuSize)) { - ctuSize /= 2; - } - - if (ctuSize < 16) { - api->config_destroy(config); - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } -#endif -#else - // TODO: There seems to be a bug in kvazaar where increasing the CTU size between - // multiple encoding jobs causes a segmentation fault. E.g. encoding multiple - // times with a CTU of 16 works, the next encoding with a CTU of 32 crashes. - // Use hardcoded value of 64 and reject images that are too small. - - if (heif_image_get_width(image, heif_channel_Y) < ctuSize || - heif_image_get_height(image, heif_channel_Y) < ctuSize) { - api->param_free(param); - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } -#endif - -#if 0 - // ctuSize should be a power of 2 in [16;64] - switch (ctuSize) { - case 64: - ctu = "64"; - break; - case 32: - ctu = "32"; - break; - case 16: - ctu = "16"; - break; - default: - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } - (void) ctu; -#endif + heif_chroma chroma = heif_image_get_chroma_format(image); + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); int input_width = heif_image_get_width(image, heif_channel_Y); int input_height = heif_image_get_height(image, heif_channel_Y); - int input_chroma_width = 0; - int input_chroma_height = 0; uint32_t encoded_width, encoded_height; kvazaar_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height); - kvz_chroma_format kvzChroma; - int chroma_stride_shift = 0; - int chroma_height_shift = 0; + config->framerate_num = framerate_num; + config->framerate_denom = framerate_denom; if (isGreyscale) { config->input_format = KVZ_FORMAT_P400; - kvzChroma = KVZ_CSP_400; } else if (chroma == heif_chroma_420) { config->input_format = KVZ_FORMAT_P420; - kvzChroma = KVZ_CSP_420; - chroma_stride_shift = 1; - chroma_height_shift = 1; - input_chroma_width = (input_width + 1) / 2; - input_chroma_height = (input_height + 1) / 2; } else if (chroma == heif_chroma_422) { config->input_format = KVZ_FORMAT_P422; - kvzChroma = KVZ_CSP_422; - chroma_stride_shift = 1; - chroma_height_shift = 0; - input_chroma_width = (input_width + 1) / 2; - input_chroma_height = input_height; } else if (chroma == heif_chroma_444) { config->input_format = KVZ_FORMAT_P444; - kvzChroma = KVZ_CSP_444; - chroma_stride_shift = 0; - chroma_height_shift = 0; - input_chroma_width = input_width; - input_chroma_height = input_height; } else { return heif_error{ @@ -535,21 +545,8 @@ }; } - if (chroma != heif_chroma_monochrome) { - int w = heif_image_get_width(image, heif_channel_Y); - int h = heif_image_get_height(image, heif_channel_Y); - if (chroma != heif_chroma_444) { w = (w + 1) / 2; } - if (chroma == heif_chroma_420) { h = (h + 1) / 2; } - - assert(heif_image_get_width(image, heif_channel_Cb) == w); - assert(heif_image_get_width(image, heif_channel_Cr) == w); - assert(heif_image_get_height(image, heif_channel_Cb) == h); - assert(heif_image_get_height(image, heif_channel_Cr) == h); - (void) w; - (void) h; - } - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; heif_error err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { nclx = nullptr; @@ -573,12 +570,103 @@ config->vui.colormatrix = nclx->matrix_coefficients; } + // Write the pixel aspect ratio into the VUI. Kvazaar emits aspect_ratio_info when both + // sar fields are >0. Extended_SAR stores sar_width/sar_height as u(16), so ratios that + // do not fit are only signalled through the pasp property. A 1:1 ratio is omitted + // (VUI absence already means unspecified/square). + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + config->vui.sar_width = (int32_t) aspect_h; + config->vui.sar_height = (int32_t) aspect_v; + } + config->qp = ((100 - encoder->quality) * 51 + 50) / 100; config->lossless = encoder->lossless ? 1 : 0; config->width = encoded_width; config->height = encoded_height; + if (image_sequence) { + // TODO + /* + config->target_bitrate = options->sequence_bitrate; + */ + + if (options->keyframe_distance_max) { + config->intra_period = options->keyframe_distance_max; + } + + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + config->intra_period = 1; + break; + case heif_sequence_gop_structure_lowdelay: + config->gop_lowdelay = 1; + break; + case heif_sequence_gop_structure_unrestricted: + break; + } + } + + kvz_encoder* kvzencoder = encoder->kvzencoder = api->encoder_open(config); + if (!kvzencoder) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + + // --- encode headers + + // Not needed. Headers are also output by kvazaar together with the images. +#if 0 + kvz_data_chunk* data = nullptr; + uint32_t data_len; + int success; + success = api->encoder_headers(encoder->kvzencoder, &data, &data_len); + if (!success) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + append_chunk_data(data, encoder, 0); + + if (data) { + api->chunk_free(data); + data = nullptr; + } +#endif + + return heif_error_ok; +} + + + + +static heif_error kvazaar_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t frame_nr) +{ + // HEVC can signal different luma and chroma bit depths, but kvazaar has a + // single hard-coded bit depth and cannot produce such a stream. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {KVZ_BIT_DEPTH}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; + // Note: it is ok to cast away the const, as the image content is not changed. // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. /* @@ -590,6 +678,71 @@ } */ + const kvz_api* api = encoder->api; + + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + heif_chroma chroma = heif_image_get_chroma_format(image); + + int input_width = heif_image_get_width(image, heif_channel_Y); + int input_height = heif_image_get_height(image, heif_channel_Y); + + uint32_t encoded_width, encoded_height; + kvazaar_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height); + + kvz_chroma_format kvzChroma; + int chroma_stride_shift = 0; + int chroma_height_shift = 0; + int input_chroma_width = 0; + int input_chroma_height = 0; + + if (isGreyscale) { + kvzChroma = KVZ_CSP_400; + } + else if (chroma == heif_chroma_420) { + kvzChroma = KVZ_CSP_420; + chroma_stride_shift = 1; + chroma_height_shift = 1; + input_chroma_width = (input_width + 1) / 2; + input_chroma_height = (input_height + 1) / 2; + } + else if (chroma == heif_chroma_422) { + kvzChroma = KVZ_CSP_422; + chroma_stride_shift = 1; + chroma_height_shift = 0; + input_chroma_width = (input_width + 1) / 2; + input_chroma_height = input_height; + } + else if (chroma == heif_chroma_444) { + kvzChroma = KVZ_CSP_444; + chroma_stride_shift = 0; + chroma_height_shift = 0; + input_chroma_width = input_width; + input_chroma_height = input_height; + } + else { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, + kError_unsupported_chroma + }; + } + + + if (chroma != heif_chroma_monochrome) { + int w = heif_image_get_width(image, heif_channel_Y); + int h = heif_image_get_height(image, heif_channel_Y); + if (chroma != heif_chroma_444) { w = (w + 1) / 2; } + if (chroma == heif_chroma_420) { h = (h + 1) / 2; } + + assert(heif_image_get_width(image, heif_channel_Cb) == w); + assert(heif_image_get_width(image, heif_channel_Cr) == w); + assert(heif_image_get_height(image, heif_channel_Cb) == h); + assert(heif_image_get_height(image, heif_channel_Cr) == h); + (void) w; + (void) h; + } + + auto upic = make_guard(api->picture_alloc_csp(kvzChroma, encoded_width, encoded_height), [api](kvz_picture* pic) { api->picture_free(pic); }); kvz_picture* pic = upic.get(); if (!pic) { @@ -600,65 +753,47 @@ }; } + int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + int bit_depth_chroma = 0; + + if (!isGreyscale) { + bit_depth_chroma = heif_image_get_bits_per_pixel_range(image, heif_channel_Cb); + } + if (isGreyscale) { - int stride; - const uint8_t* data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); + size_t stride; + const uint8_t* data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height, bit_depth); } else { - int stride; + size_t stride; const uint8_t* data; - data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); + data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height, bit_depth); - data = heif_image_get_plane_readonly(image, heif_channel_Cb, &stride); + data = heif_image_get_plane_readonly2(image, heif_channel_Cb, &stride); copy_plane(pic->u, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height, encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift, bit_depth_chroma); - data = heif_image_get_plane_readonly(image, heif_channel_Cr, &stride); + data = heif_image_get_plane_readonly2(image, heif_channel_Cr, &stride); copy_plane(pic->v, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height, encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift, bit_depth_chroma); } - auto uencoder = make_guard(api->encoder_open(config), [api](kvz_encoder* e) { api->encoder_close(e); }); - kvz_encoder* kvzencoder = uencoder.get(); - if (!kvzencoder) { - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } + pic->pts = frame_nr; kvz_data_chunk* data = nullptr; - auto free_data = [api](kvz_data_chunk** data){ - if(*data) { - api->chunk_free(*data); - *data = nullptr; - } - }; - auto data_deleter = std::unique_ptr(&data, free_data); - uint32_t data_len; - int success; - success = api->encoder_headers(kvzencoder, &data, &data_len); - if (!success) { - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } - append_chunk_data(data, encoder->output_data); - free_data(&data); + kvz_picture* picture_out = nullptr; - success = api->encoder_encode(kvzencoder, + int success; + success = api->encoder_encode(encoder->kvzencoder, pic, &data, &data_len, - nullptr, nullptr, nullptr); + &picture_out, nullptr, nullptr); if (!success) { return heif_error{ heif_error_Encoder_plugin_error, @@ -667,81 +802,162 @@ }; } - append_chunk_data(data, encoder->output_data); - free_data(&data); + append_chunk_data(data, encoder, picture_out ? picture_out->pts : 0); + + if (data) { + api->chunk_free(data); + data = nullptr; + } + + api->picture_free(picture_out); + + (void) success; + return heif_error_ok; +} + + +static heif_error kvazaar_end_sequence_encoding(void* encoder_raw) +{ + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; + const kvz_api* api = encoder->api; + for (;;) { - success = api->encoder_encode(kvzencoder, + kvz_data_chunk* data = nullptr; + uint32_t data_len; + + kvz_picture* picture_out; + + int success = api->encoder_encode(encoder->kvzencoder, nullptr, &data, &data_len, - nullptr, nullptr, nullptr); + &picture_out, nullptr, nullptr); if (!success) { return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; } if (data == nullptr || data->len == 0) { break; } - append_chunk_data(data, encoder->output_data); - free_data(&data); + append_chunk_data(data, encoder, picture_out->pts); + + api->picture_free(picture_out); + api->chunk_free(data); } - (void) success; return heif_error_ok; } -static struct heif_error kvazaar_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error kvazaar_get_compressed_data_intern(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* more_frame_packets) { - struct encoder_struct_kvazaar* encoder = (struct encoder_struct_kvazaar*) encoder_raw; + encoder_struct_kvazaar* encoder = (encoder_struct_kvazaar*) encoder_raw; - if (encoder->output_idx == encoder->output_data.size()) { + if (encoder->output_data.empty()) { *data = nullptr; *size = 0; return heif_error_ok; } - size_t start_idx = encoder->output_idx; + std::vector& pktdata = encoder->output_data.front().data; +#if 0 + size_t start_idx = 0; - while (start_idx < encoder->output_data.size() - 3 && - (encoder->output_data[start_idx] != 0 || - encoder->output_data[start_idx + 1] != 0 || - encoder->output_data[start_idx + 2] != 1)) { + while (start_idx < pktdata.size() - 3 && + (pktdata[start_idx] != 0 || + pktdata[start_idx + 1] != 0 || + pktdata[start_idx + 2] != 1)) { start_idx++; } size_t end_idx = start_idx + 1; - while (end_idx < encoder->output_data.size() - 3 && - (encoder->output_data[end_idx] != 0 || - encoder->output_data[end_idx + 1] != 0 || - encoder->output_data[end_idx + 2] != 1)) { + while (end_idx < pktdata.size() - 3 && + (pktdata[end_idx] != 0 || + pktdata[end_idx + 1] != 0 || + pktdata[end_idx + 2] != 1)) { end_idx++; } if (end_idx == encoder->output_data.size() - 3) { end_idx = encoder->output_data.size(); } +#endif + + if (frame_nr) { + *frame_nr = encoder->output_data.front().frameNr; + } - *data = encoder->output_data.data() + start_idx + 3; - *size = (int) (end_idx - start_idx - 3); + if (more_frame_packets) { + if (encoder->output_data.size() > 1 && + encoder->output_data[0].frameNr == encoder->output_data[1].frameNr) { + *more_frame_packets = 1; + } + else { + *more_frame_packets = 0; + } + } - encoder->output_idx = end_idx; + encoder->active_data = std::move(pktdata); + encoder->output_data.pop_front(); + + *data = encoder->active_data.data(); + *size = static_cast(encoder->active_data.size()); return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_kvazaar +static heif_error kvazaar_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return kvazaar_get_compressed_data_intern(encoder_raw, data, size, nullptr, nullptr); +} + +static heif_error kvazaar_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, int* more_frame_packets) +{ + return kvazaar_get_compressed_data_intern(encoder_raw, data, size, frame_nr, more_frame_packets); +} + + +static heif_error kvazaar_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return kvazaar_start_sequence_encoding_intern(encoder_raw, image, input_class, framerate_num, framerate_denom, options, true); +} + + +static heif_error kvazaar_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = kvazaar_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } + + err = kvazaar_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } + + return kvazaar_end_sequence_encoding(encoder_raw); +} + + +static const heif_encoder_plugin encoder_plugin_kvazaar { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_HEVC, /* id_name */ "kvazaar", /* priority */ kvazaar_PLUGIN_PRIORITY, @@ -769,10 +985,16 @@ /* encode_image */ kvazaar_encode_image, /* get_compressed_data */ kvazaar_get_compressed_data, /* query_input_colorspace (v2) */ kvazaar_query_input_colorspace2, - /* query_encoded_size (v3) */ kvazaar_query_encoded_size + /* query_encoded_size (v3) */ kvazaar_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ kvazaar_start_sequence_encoding, + /* encode_sequence_frame (v4) */ kvazaar_encode_sequence_frame, + /* end_sequence_encoding (v4) */ kvazaar_end_sequence_encoding, + /* get_compressed_data2 (v4) */ kvazaar_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 0 }; -const struct heif_encoder_plugin* get_encoder_plugin_kvazaar() +const heif_encoder_plugin* get_encoder_plugin_kvazaar() { return &encoder_plugin_kvazaar; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_mask.cc libheif-1.23.4/libheif/plugins/encoder_mask.cc --- libheif-1.19.8/libheif/plugins/encoder_mask.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_mask.cc 2026-09-06 14:45:17.000000000 +0000 @@ -49,13 +49,13 @@ #define MAX_NPARAMETERS 14 -static struct heif_encoder_parameter mask_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* mask_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter mask_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* mask_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void mask_init_parameters() { - struct heif_encoder_parameter* p = mask_encoder_params; - const struct heif_encoder_parameter** d = mask_encoder_parameter_ptrs; + heif_encoder_parameter* p = mask_encoder_params; + const heif_encoder_parameter** d = mask_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -71,7 +71,7 @@ } -const struct heif_encoder_parameter** mask_list_parameters(void* encoder) +const heif_encoder_parameter** mask_list_parameters(void* encoder) { return mask_encoder_parameter_ptrs; } @@ -86,10 +86,10 @@ { } -struct heif_error mask_new_encoder(void** enc) +heif_error mask_new_encoder(void** enc) { - struct encoder_struct_mask* encoder = new encoder_struct_mask(); - struct heif_error err = heif_error_ok; + encoder_struct_mask* encoder = new encoder_struct_mask(); + heif_error err = heif_error_ok; *enc = encoder; @@ -102,44 +102,44 @@ void mask_free_encoder(void* encoder_raw) { - struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; + encoder_struct_mask* encoder = (encoder_struct_mask*) encoder_raw; delete encoder; } -struct heif_error mask_set_parameter_quality(void* encoder_raw, int quality) +heif_error mask_set_parameter_quality(void* encoder_raw, int quality) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; return heif_error_ok; } -struct heif_error mask_get_parameter_quality(void* encoder_raw, int* quality) +heif_error mask_get_parameter_quality(void* encoder_raw, int* quality) { *quality = 100; return heif_error_ok; } -struct heif_error mask_set_parameter_lossless(void* encoder_raw, int enable) +heif_error mask_set_parameter_lossless(void* encoder_raw, int enable) { return heif_error_ok; } -struct heif_error mask_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error mask_get_parameter_lossless(void* encoder_raw, int* enable) { *enable = true; return heif_error_ok; } -struct heif_error mask_set_parameter_logging_level(void* encoder_raw, int logging) +heif_error mask_set_parameter_logging_level(void* encoder_raw, int logging) { return heif_error_ok; } -struct heif_error mask_get_parameter_logging_level(void* encoder_raw, int* loglevel) +heif_error mask_get_parameter_logging_level(void* encoder_raw, int* loglevel) { *loglevel = 0; @@ -150,14 +150,14 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error mask_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error mask_set_parameter_integer(void* encoder_raw, const char* name, int value) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; return heif_error_unsupported_parameter; } -struct heif_error mask_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error mask_get_parameter_integer(void* encoder_raw, const char* name, int* value) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; @@ -165,14 +165,14 @@ } -struct heif_error mask_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error mask_set_parameter_boolean(void* encoder_raw, const char* name, int value) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; return heif_error_unsupported_parameter; } -struct heif_error mask_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error mask_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; @@ -180,7 +180,7 @@ } -struct heif_error mask_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error mask_set_parameter_string(void* encoder_raw, const char* name, const char* value) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; @@ -188,7 +188,7 @@ } -struct heif_error mask_get_parameter_string(void* encoder_raw, const char* name, +heif_error mask_get_parameter_string(void* encoder_raw, const char* name, char* value, int value_size) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; @@ -199,8 +199,8 @@ static void mask_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = mask_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = mask_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -238,8 +238,8 @@ } -struct heif_error mask_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +heif_error mask_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) { //struct encoder_struct_mask* encoder = (struct encoder_struct_mask*) encoder_raw; @@ -249,14 +249,14 @@ } -struct heif_error mask_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +heif_error mask_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) { return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_mask +static const heif_encoder_plugin encoder_plugin_mask { /* plugin_api_version */ 3, /* compression_format */ heif_compression_mask, @@ -289,7 +289,7 @@ /* query_encoded_size (v3) */ nullptr }; -const struct heif_encoder_plugin* get_encoder_plugin_mask() +const heif_encoder_plugin* get_encoder_plugin_mask() { return &encoder_plugin_mask; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_openjpeg.cc libheif-1.23.4/libheif/plugins/encoder_openjpeg.cc --- libheif-1.19.8/libheif/plugins/encoder_openjpeg.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_openjpeg.cc 2026-09-06 14:45:17.000000000 +0000 @@ -87,13 +87,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter opj_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* opj_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter opj_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* opj_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void opj_init_parameters() { - struct heif_encoder_parameter* p = opj_encoder_params; - const struct heif_encoder_parameter** d = opj_encoder_parameter_ptrs; + heif_encoder_parameter* p = opj_encoder_params; + const heif_encoder_parameter** d = opj_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -119,9 +119,9 @@ static void opj_set_default_parameters(void* encoder); -struct heif_error opj_new_encoder(void** encoder_out) +heif_error opj_new_encoder(void** encoder_out) { - struct encoder_struct_opj* encoder = new encoder_struct_opj(); + encoder_struct_opj* encoder = new encoder_struct_opj(); *encoder_out = encoder; opj_set_default_parameters(encoder); @@ -132,13 +132,13 @@ void opj_free_encoder(void* encoder_raw) { - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; delete encoder; } -struct heif_error opj_set_parameter_quality(void* encoder_raw, int quality) +heif_error opj_set_parameter_quality(void* encoder_raw, int quality) { - auto* encoder = (struct encoder_struct_opj*) encoder_raw; + auto* encoder = (encoder_struct_opj*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -149,47 +149,47 @@ return heif_error_ok; } -struct heif_error opj_get_parameter_quality(void* encoder_raw, int* quality) +heif_error opj_get_parameter_quality(void* encoder_raw, int* quality) { - auto* encoder = (struct encoder_struct_opj*) encoder_raw; + auto* encoder = (encoder_struct_opj*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -struct heif_error opj_set_parameter_lossless(void* encoder_raw, int lossless) +heif_error opj_set_parameter_lossless(void* encoder_raw, int lossless) { - auto* encoder = (struct encoder_struct_opj*) encoder_raw; + auto* encoder = (encoder_struct_opj*) encoder_raw; encoder->parameters.irreversible = lossless ? 0 : 1; return heif_error_ok; } -struct heif_error opj_get_parameter_lossless(void* encoder_raw, int* lossless) +heif_error opj_get_parameter_lossless(void* encoder_raw, int* lossless) { - auto* encoder = (struct encoder_struct_opj*) encoder_raw; + auto* encoder = (encoder_struct_opj*) encoder_raw; *lossless = (encoder->parameters.irreversible == 0); return heif_error_ok; } -struct heif_error opj_set_parameter_logging_level(void* encoder, int logging) +heif_error opj_set_parameter_logging_level(void* encoder, int logging) { return heif_error_ok; } -struct heif_error opj_get_parameter_logging_level(void* encoder, int* logging) +heif_error opj_get_parameter_logging_level(void* encoder, int* logging) { return heif_error_ok; } -const struct heif_encoder_parameter** opj_list_parameters(void* encoder) +const heif_encoder_parameter** opj_list_parameters(void* encoder) { return opj_encoder_parameter_ptrs; } -struct heif_error opj_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error opj_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return opj_set_parameter_quality(encoder, value); @@ -198,9 +198,9 @@ return heif_error_unsupported_parameter; } -struct heif_error opj_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error opj_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return opj_get_parameter_quality(encoder, value); @@ -209,19 +209,19 @@ return heif_error_ok; } -struct heif_error opj_set_parameter_boolean(void* encoder, const char* name, int value) +heif_error opj_set_parameter_boolean(void* encoder, const char* name, int value) { return heif_error_ok; } -struct heif_error opj_get_parameter_boolean(void* encoder, const char* name, int* value) +heif_error opj_get_parameter_boolean(void* encoder, const char* name, int* value) { return heif_error_ok; } -struct heif_error opj_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error opj_set_parameter_string(void* encoder_raw, const char* name, const char* value) { - auto* encoder = (struct encoder_struct_opj*) encoder_raw; + auto* encoder = (encoder_struct_opj*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { if (strcmp(value, "420") == 0) { @@ -250,9 +250,9 @@ dst[dst_size - 1] = 0; } -struct heif_error opj_get_parameter_string(void* encoder_raw, const char* name, char* value, int value_size) +heif_error opj_get_parameter_string(void* encoder_raw, const char* name, char* value, int value_size) { - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { switch (encoder->chroma) { @@ -281,8 +281,8 @@ static void opj_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = opj_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = opj_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -301,7 +301,7 @@ } -void opj_query_input_colorspace(enum heif_colorspace* inout_colorspace, enum heif_chroma* inout_chroma) +void opj_query_input_colorspace(heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) { // Replace the input colorspace/chroma with the one that is supported by the encoder and that // comes as close to the input colorspace/chroma as possible. @@ -316,7 +316,7 @@ } } -void opj_query_input_colorspace2(void* encoder_raw, enum heif_colorspace* inout_colorspace, enum heif_chroma* inout_chroma) +void opj_query_input_colorspace2(void* encoder_raw, heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) { auto* encoder = (struct encoder_struct_opj*) encoder_raw; @@ -341,11 +341,11 @@ // @param src_data_raw - Newly encoded bytes provided by OpenJPEG // @param nb_bytes - The number of bytes or size of src_data_raw // @param encoder_raw - Out the new -// @return - The number of bytes successfuly transfered +// @return - The number of bytes successfully transferred static OPJ_SIZE_T opj_write_from_buffer(void* src_data_raw, OPJ_SIZE_T nb_bytes, void* encoder_raw) { uint8_t* src_data = (uint8_t*) src_data_raw; - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; for (size_t i = 0; i < nb_bytes; i++) { encoder->codestream.push_back(src_data[i]); @@ -363,14 +363,21 @@ // compressed image pixel data and very basic metadata. // @param data - Uncompressed image pixel data // @param encoder - The function will output codestream in encoder->codestream -static heif_error generate_codestream(opj_image_t* image, struct encoder_struct_opj* encoder) +static heif_error generate_codestream(opj_image_t* image, encoder_struct_opj* encoder) { heif_error error; OPJ_BOOL success; encoder->parameters.cp_disto_alloc = 1; encoder->parameters.tcp_numlayers = 1; - encoder->parameters.tcp_rates[0] = (float)(1 + (100 - encoder->quality)/2); + if (encoder->parameters.irreversible == 0) { + // Lossless: the reversible wavelet alone is not sufficient, the rate allocation must not + // truncate the codestream either. A rate of 0 means "no rate limit" in OpenJPEG. + encoder->parameters.tcp_rates[0] = 0; + } + else { + encoder->parameters.tcp_rates[0] = (float)(1 + (100 - encoder->quality)/2); + } #if 0 //Insert a human readable comment into the codestream @@ -449,10 +456,10 @@ } -struct heif_error opj_encode_image(void* encoder_raw, const struct heif_image* image, enum heif_image_input_class image_class) +heif_error opj_encode_image(void* encoder_raw, const heif_image* image, heif_image_input_class image_class) { - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; - struct heif_error err; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; + heif_error err; heif_chroma chroma = heif_image_get_chroma_format(image); heif_colorspace colorspace = heif_image_get_colorspace(image); @@ -478,7 +485,7 @@ break; default: assert(false); - return heif_error{heif_error_Encoding_error, heif_suberror_Unspecified, "OpenJPEG encoder plugin received image with invalid colorspace."}; + return {heif_error_Encoding_error, heif_suberror_Unspecified, "OpenJPEG encoder plugin received image with invalid colorspace."}; } int band_count = (int) channels.size(); @@ -563,11 +570,11 @@ return err; } -struct heif_error opj_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, enum heif_encoded_data_type* type) +heif_error opj_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, enum heif_encoded_data_type* type) { // Get a packet of decoded data. The data format depends on the codec. - struct encoder_struct_opj* encoder = (struct encoder_struct_opj*) encoder_raw; + encoder_struct_opj* encoder = (encoder_struct_opj*) encoder_raw; if (encoder->data_read) { *size = 0; @@ -596,8 +603,43 @@ } -static const struct heif_encoder_plugin encoder_plugin_openjpeg{ - /* plugin_api_version */ 3, +heif_error opj_start_sequence_encoding(void* encoder, const heif_image* image, + enum heif_image_input_class image_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return heif_error_ok; +} + +heif_error opj_encode_sequence_frame(void* encoder, const heif_image* image, uintptr_t frame_nr) +{ + return opj_encode_image(encoder, image, heif_image_input_class_normal); +} + +heif_error opj_end_sequence_encoding(void* encoder) +{ + return heif_error_ok; +} + +heif_error opj_get_compressed_data2(void* encoder, uint8_t** data, int* size, + uintptr_t* frame_nr, + int* is_keyframe, int* more_frame_packets) +{ + heif_error err = opj_get_compressed_data(encoder, data, size, nullptr); + + if (is_keyframe) { + *is_keyframe = true; + } + + if (more_frame_packets) { + *more_frame_packets = true; + } + + return err; +} + +static const heif_encoder_plugin encoder_plugin_openjpeg{ + /* plugin_api_version */ 4, /* compression_format */ heif_compression_JPEG2000, /* id_name */ "openjpeg", /* priority */ OPJ_PLUGIN_PRIORITY, @@ -625,10 +667,16 @@ /* encode_image */ opj_encode_image, /* get_compressed_data */ opj_get_compressed_data, /* query_input_colorspace (v2) */ opj_query_input_colorspace2, - /* query_encoded_size (v3) */ opj_query_encoded_size + /* query_encoded_size (v3) */ opj_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ opj_start_sequence_encoding, + /* encode_sequence_frame (v4) */ opj_encode_sequence_frame, + /* end_sequence_encoding (v4) */ opj_end_sequence_encoding, + /* get_compressed_data2 (v4) */ opj_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_openjpeg() +const heif_encoder_plugin* get_encoder_plugin_openjpeg() { return &encoder_plugin_openjpeg; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_openjph.cc libheif-1.23.4/libheif/plugins/encoder_openjph.cc --- libheif-1.19.8/libheif/plugins/encoder_openjph.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_openjph.cc 2026-09-06 14:45:17.000000000 +0000 @@ -96,8 +96,8 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter ojph_encoder_params[MAX_NPARAMETERS]; -const static struct heif_encoder_parameter* ojph_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter ojph_encoder_params[MAX_NPARAMETERS]; +const static heif_encoder_parameter* ojph_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static const char* kParam_chroma = "chroma"; static const char* const kParam_chroma_valid_values[] = { @@ -132,8 +132,8 @@ static void ojph_init_encoder_parameters() { - struct heif_encoder_parameter* p = ojph_encoder_params; - const struct heif_encoder_parameter** d = ojph_encoder_parameter_ptrs; + heif_encoder_parameter* p = ojph_encoder_params; + const heif_encoder_parameter** d = ojph_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -241,9 +241,9 @@ // Note quality is part of the plugin API. -struct heif_error ojph_set_parameter_quality(void* encoder_raw, int quality) +heif_error ojph_set_parameter_quality(void* encoder_raw, int quality) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; encoder->quality = quality; @@ -259,9 +259,9 @@ return heif_error_ok; } -struct heif_error ojph_set_parameter_integer(void *encoder_raw, const char *name, int value) +heif_error ojph_set_parameter_integer(void *encoder_raw, const char *name, int value) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return ojph_set_parameter_quality(encoder, value); @@ -277,9 +277,9 @@ // Note quality is part of the plugin API -struct heif_error ojph_get_parameter_quality(void* encoder_raw, int* quality) +heif_error ojph_get_parameter_quality(void* encoder_raw, int* quality) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; *quality = encoder->quality; @@ -292,9 +292,9 @@ return heif_error_ok; } -struct heif_error ojph_get_parameter_integer(void *encoder_raw, const char *name, int *value) +heif_error ojph_get_parameter_integer(void *encoder_raw, const char *name, int *value) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return ojph_get_parameter_quality(encoder, value); @@ -310,9 +310,9 @@ // Note lossless is part of the plugin API -struct heif_error ojph_set_parameter_lossless(void* encoder_raw, int lossless) +heif_error ojph_set_parameter_lossless(void* encoder_raw, int lossless) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; encoder->codestream.access_cod().set_reversible(lossless); return heif_error_ok; } @@ -325,9 +325,9 @@ } #endif -struct heif_error ojph_set_parameter_boolean(void *encoder_raw, const char *name, int value) +heif_error ojph_set_parameter_boolean(void *encoder_raw, const char *name, int value) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return ojph_set_parameter_lossless(encoder, value); #if OPENJPH_MAJOR_VERSION > 1 || OPENJPH_MINOR_VERSION > 10 @@ -342,9 +342,9 @@ // Note lossless is part of the plugin API -struct heif_error ojph_get_parameter_lossless(void* encoder_raw, int* lossless) +heif_error ojph_get_parameter_lossless(void* encoder_raw, int* lossless) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; *lossless = encoder->codestream.access_cod().is_reversible(); return heif_error_ok; } @@ -357,9 +357,9 @@ } #endif -struct heif_error ojph_get_parameter_boolean(void *encoder_raw, const char *name, int *value) +heif_error ojph_get_parameter_boolean(void *encoder_raw, const char *name, int *value) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return ojph_get_parameter_lossless(encoder, value); #if OPENJPH_MAJOR_VERSION > 1 || OPENJPH_MINOR_VERSION > 10 @@ -450,9 +450,9 @@ return heif_error_ok; } -struct heif_error ojph_get_parameter_string(void *encoder_raw, const char *name, char *value, int value_size) +heif_error ojph_get_parameter_string(void *encoder_raw, const char *name, char *value, int value_size) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { return ojph_get_parameter_chroma(encoder, value, value_size); @@ -523,7 +523,7 @@ static const heif_error &ojph_set_tile_size(encoder_struct_ojph *encoder, const char *value) { std::string valueStr(value); - size_t commaOffset = valueStr.find(","); + size_t commaOffset = valueStr.find(','); if (commaOffset == std::string::npos) { return heif_error_invalid_parameter_value; } @@ -594,7 +594,7 @@ static const heif_error &ojph_set_block_dimensions(encoder_struct_ojph *encoder, const char *value) { std::string valueStr(value); - size_t commaOffset = valueStr.find(","); + size_t commaOffset = valueStr.find(','); if (commaOffset == std::string::npos) { return heif_error_invalid_parameter_value; } @@ -611,9 +611,9 @@ return heif_error_ok; } -struct heif_error ojph_set_parameter_string(void *encoder_raw, const char *name, const char *value) +heif_error ojph_set_parameter_string(void *encoder_raw, const char *name, const char *value) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { return ojph_set_chroma(encoder, value); @@ -636,9 +636,9 @@ static void ojph_set_default_parameters(void* encoder_raw) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; - for (const struct heif_encoder_parameter** p = ojph_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; + for (const heif_encoder_parameter** p = ojph_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -658,9 +658,9 @@ ///// Actual encoding functionality -struct heif_error ojph_new_encoder(void** encoder_out) +heif_error ojph_new_encoder(void** encoder_out) { - struct encoder_struct_ojph* encoder = new encoder_struct_ojph(); + encoder_struct_ojph* encoder = new encoder_struct_ojph(); encoder->outfile.open(); *encoder_out = encoder; @@ -671,30 +671,30 @@ void ojph_free_encoder(void* encoder_raw) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; encoder->codestream.close(); delete encoder; } -struct heif_error ojph_set_parameter_logging_level(void* encoder, int logging) +heif_error ojph_set_parameter_logging_level(void* encoder, int logging) { // No logging level options in OpenJPH return heif_error_ok; } -struct heif_error ojph_get_parameter_logging_level(void* encoder, int* logging) +heif_error ojph_get_parameter_logging_level(void* encoder, int* logging) { // No logging level options in OpenJPH return heif_error_ok; } -const struct heif_encoder_parameter** ojph_list_parameters(void* encoder_raw) +const heif_encoder_parameter** ojph_list_parameters(void* encoder_raw) { return ojph_encoder_parameter_ptrs; } -void ojph_query_input_colorspace(enum heif_colorspace* inout_colorspace, enum heif_chroma* inout_chroma) +void ojph_query_input_colorspace(heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) { // Replace the input colorspace/chroma with the one that is supported by the encoder and that // comes as close to the input colorspace/chroma as possible. @@ -709,9 +709,9 @@ } } -void ojph_query_input_colorspace2(void* encoder_raw, enum heif_colorspace* inout_colorspace, enum heif_chroma* inout_chroma) +void ojph_query_input_colorspace2(void* encoder_raw, heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) { - auto* encoder = (struct encoder_struct_ojph*) encoder_raw; + auto* encoder = (encoder_struct_ojph*) encoder_raw; if (*inout_colorspace == heif_colorspace_monochrome) { *inout_colorspace = heif_colorspace_monochrome; @@ -740,7 +740,13 @@ heif_chroma chroma = heif_image_get_chroma_format(image); encoder->codestream.set_planar(true); - sourceChannels = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; + + if (chroma == heif_chroma_monochrome) { + sourceChannels = {heif_channel_Y}; + } else { + sourceChannels = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; + } + siz.set_num_components((ojph::ui32)sourceChannels.size()); for (ojph::ui32 i = 0; i < siz.get_num_components(); i++) { int bit_depth = heif_image_get_bits_per_pixel_range(image, sourceChannels[i]); @@ -767,15 +773,22 @@ cod.set_color_transform(false); } -struct heif_error ojph_encode_image(void *encoder_raw, const struct heif_image *image, enum heif_image_input_class image_class) +heif_error ojph_encode_image(void *encoder_raw, const heif_image *image, heif_image_input_class image_class) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; - if (heif_image_get_colorspace(image) != heif_colorspace_YCbCr) { - return heif_error{heif_error_Encoding_error, - heif_suberror_Unspecified, - "OpenJPH encoder plugin received image with invalid colorspace."}; - } + if (heif_image_get_colorspace(image) != heif_colorspace_YCbCr && + heif_image_get_colorspace(image) != heif_colorspace_monochrome) { + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "OpenJPH encoder plugin received image with invalid colorspace." + }; + } + + // reset output position to start + encoder->outfile.seek(0, ojph::outfile_base::seek::OJPH_SEEK_SET); + encoder->data_read = false; std::vector sourceChannels = build_SIZ(encoder, image); build_COD(encoder); @@ -793,15 +806,24 @@ ojph::line_buf* cur_line = encoder->codestream.exchange(NULL, next_comp); for (const auto& sourceChannel : sourceChannels) { - int stride; - const uint8_t *data = heif_image_get_plane_readonly(image, sourceChannel, &stride); + size_t stride; + const uint8_t *data = heif_image_get_plane_readonly2(image, sourceChannel, &stride); uint32_t component_height = heif_image_get_height(image, sourceChannel); + int bit_depth = heif_image_get_bits_per_pixel_range(image, sourceChannel); for (uint32_t y = 0; y < component_height; y++) { - const uint8_t *sourceLine = data + y * stride; size_t outputWidth = cur_line->size; ojph::si32 *targetLine = cur_line->i32; - for (uint32_t x = 0; x < outputWidth; x++) { - targetLine[x] = sourceLine[x]; + if (bit_depth > 8) { + const uint16_t *sourceLine = (const uint16_t*) (data + y * stride); + for (uint32_t x = 0; x < outputWidth; x++) { + targetLine[x] = sourceLine[x]; + } + } + else { + const uint8_t *sourceLine = data + y * stride; + for (uint32_t x = 0; x < outputWidth; x++) { + targetLine[x] = sourceLine[x]; + } } cur_line = encoder->codestream.exchange(cur_line, next_comp); } @@ -811,9 +833,9 @@ return heif_error_ok; } -struct heif_error ojph_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, enum heif_encoded_data_type* type) +heif_error ojph_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, heif_encoded_data_type* type) { - struct encoder_struct_ojph* encoder = (struct encoder_struct_ojph*) encoder_raw; + encoder_struct_ojph* encoder = (encoder_struct_ojph*) encoder_raw; if (encoder->data_read) { *size = 0; @@ -844,8 +866,44 @@ return plugin_name; } -static const struct heif_encoder_plugin encoder_plugin_openjph { - /* plugin_api_version */ 3, + +heif_error ojph_start_sequence_encoding(void* encoder, const heif_image* image, + enum heif_image_input_class image_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return heif_error_ok; +} + +heif_error ojph_encode_sequence_frame(void* encoder, const heif_image* image, uintptr_t frame_nr) +{ + return ojph_encode_image(encoder, image, heif_image_input_class_normal); +} + +heif_error ojph_end_sequence_encoding(void* encoder) +{ + return heif_error_ok; +} + +heif_error ojph_get_compressed_data2(void* encoder, uint8_t** data, int* size, + uintptr_t* frame_nr, + int* is_keyframe, int* more_frame_packets) +{ + heif_error err = ojph_get_compressed_data(encoder, data, size, nullptr); + + if (is_keyframe) { + *is_keyframe = true; + } + + if (more_frame_packets) { + *more_frame_packets = true; + } + + return err; +} + +static const heif_encoder_plugin encoder_plugin_openjph { + /* plugin_api_version */ 4, /* compression_format */ heif_compression_HTJ2K, /* id_name */ "openjph", /* priority */ OJPH_PLUGIN_PRIORITY, @@ -873,10 +931,16 @@ /* encode_image */ ojph_encode_image, /* get_compressed_data */ ojph_get_compressed_data, /* query_input_colorspace (v2) */ ojph_query_input_colorspace2, - /* query_encoded_size (v3) */ nullptr + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ ojph_start_sequence_encoding, + /* encode_sequence_frame (v4) */ ojph_encode_sequence_frame, + /* end_sequence_encoding (v4) */ ojph_end_sequence_encoding, + /* get_compressed_data2 (v4) */ ojph_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_openjph() +const heif_encoder_plugin* get_encoder_plugin_openjph() { return &encoder_plugin_openjph; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_rav1e.cc libheif-1.23.4/libheif/plugins/encoder_rav1e.cc --- libheif-1.19.8/libheif/plugins/encoder_rav1e.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_rav1e.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,20 +21,30 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_rav1e.h" +#include "encoder_input_check.h" #include #include #include #include #include #include +#include #include // TODO: remove me #include "rav1e.h" +#include struct encoder_struct_rav1e { + ~encoder_struct_rav1e() + { + if (rav1eContextRaw) { + rav1e_context_unref(rav1eContextRaw); + } + } + int speed; // 0-10 int quality; // TODO: not sure yet how to map quality to min/max q @@ -46,14 +56,32 @@ heif_chroma chroma; + // --- encoder + + RaContext* rav1eContextRaw = nullptr; + uint8_t yShift = 0; + + // bit depth the context was created with, to check the later frames of a sequence against + int bit_depth = 8; + // --- output - std::vector compressed_data; - bool data_read = false; + struct Packet + { + std::vector compressedData; + uintptr_t frameNr = 0; + bool is_keyframe = false; + }; + + std::deque output_packets; + std::vector active_output_data; }; //static const char* kError_out_of_memory = "Out of memory"; +static heif_error get_encoder_packets(void* encoder_raw); + + static const char* kParam_min_q = "min-q"; static const char* kParam_threads = "threads"; static const char* kParam_speed = "speed"; @@ -65,9 +93,11 @@ static int valid_tile_num_values[] = {1, 2, 4, 8, 16, 32, 64}; -static struct heif_error heif_error_codec_library_error = {heif_error_Encoder_plugin_error, - heif_suberror_Unspecified, - "rav1e error"}; +static heif_error heif_error_codec_library_error = { + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "rav1e error" +}; static const int RAV1E_PLUGIN_PRIORITY = 20; @@ -89,13 +119,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter rav1e_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* rav1e_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter rav1e_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* rav1e_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void rav1e_init_parameters() { - struct heif_encoder_parameter* p = rav1e_encoder_params; - const struct heif_encoder_parameter** d = rav1e_encoder_parameter_ptrs; + heif_encoder_parameter* p = rav1e_encoder_params; + const heif_encoder_parameter** d = rav1e_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -198,7 +228,7 @@ } -const struct heif_encoder_parameter** rav1e_list_parameters(void* encoder) +const heif_encoder_parameter** rav1e_list_parameters(void* encoder) { return rav1e_encoder_parameter_ptrs; } @@ -213,10 +243,10 @@ { } -struct heif_error rav1e_new_encoder(void** enc) +heif_error rav1e_new_encoder(void** enc) { auto* encoder = new encoder_struct_rav1e(); - struct heif_error err = heif_error_ok; + heif_error err = heif_error_ok; *enc = encoder; @@ -229,15 +259,15 @@ void rav1e_free_encoder(void* encoder_raw) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; delete encoder; } -struct heif_error rav1e_set_parameter_quality(void* encoder_raw, int quality) +heif_error rav1e_set_parameter_quality(void* encoder_raw, int quality) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -248,18 +278,18 @@ return heif_error_ok; } -struct heif_error rav1e_get_parameter_quality(void* encoder_raw, int* quality) +heif_error rav1e_get_parameter_quality(void* encoder_raw, int* quality) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -struct heif_error rav1e_set_parameter_lossless(void* encoder_raw, int enable) +heif_error rav1e_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_rav1e* encoder = (struct encoder_struct_rav1e*) encoder_raw; + encoder_struct_rav1e* encoder = (encoder_struct_rav1e*) encoder_raw; if (enable) { encoder->min_q = 0; @@ -268,16 +298,16 @@ return heif_error_ok; } -struct heif_error rav1e_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error rav1e_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_rav1e* encoder = (struct encoder_struct_rav1e*) encoder_raw; + encoder_struct_rav1e* encoder = (encoder_struct_rav1e*) encoder_raw; *enable = (encoder->min_q == 0); return heif_error_ok; } -struct heif_error rav1e_set_parameter_logging_level(void* encoder_raw, int logging) +heif_error rav1e_set_parameter_logging_level(void* encoder_raw, int logging) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -292,7 +322,7 @@ return heif_error_ok; } -struct heif_error rav1e_get_parameter_logging_level(void* encoder_raw, int* loglevel) +heif_error rav1e_get_parameter_logging_level(void* encoder_raw, int* loglevel) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -309,9 +339,9 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error rav1e_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error rav1e_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_rav1e* encoder = (struct encoder_struct_rav1e*) encoder_raw; + encoder_struct_rav1e* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return rav1e_set_parameter_quality(encoder, value); @@ -329,9 +359,9 @@ return heif_error_unsupported_parameter; } -struct heif_error rav1e_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error rav1e_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_rav1e* encoder = (struct encoder_struct_rav1e*) encoder_raw; + encoder_struct_rav1e* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return rav1e_get_parameter_quality(encoder, value); @@ -350,9 +380,9 @@ } -struct heif_error rav1e_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error rav1e_set_parameter_boolean(void* encoder_raw, const char* name, int value) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return rav1e_set_parameter_lossless(encoder, value); @@ -363,9 +393,9 @@ return heif_error_unsupported_parameter; } -struct heif_error rav1e_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error rav1e_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return rav1e_get_parameter_lossless(encoder, value); @@ -377,9 +407,9 @@ } -struct heif_error rav1e_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error rav1e_set_parameter_string(void* encoder_raw, const char* name, const char* value) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + auto* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { if (strcmp(value, "420") == 0) { @@ -410,10 +440,10 @@ } -struct heif_error rav1e_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +heif_error rav1e_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { - struct encoder_struct_rav1e* encoder = (struct encoder_struct_rav1e*) encoder_raw; + encoder_struct_rav1e* encoder = (encoder_struct_rav1e*) encoder_raw; if (strcmp(name, kParam_chroma) == 0) { switch (encoder->chroma) { @@ -439,8 +469,8 @@ static void rav1e_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = rav1e_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = rav1e_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -475,14 +505,23 @@ } -struct heif_error rav1e_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) -{ - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; +heif_error rav1e_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + auto* encoder = (encoder_struct_rav1e*) encoder_raw; + + // unref the context if it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + if (encoder->rav1eContextRaw) { + rav1e_context_unref(encoder->rav1eContextRaw); + encoder->rav1eContextRaw = nullptr; + } const heif_chroma chroma = heif_image_get_chroma_format(image); - uint8_t yShift = 0; RaChromaSampling chromaSampling; RaChromaSamplePosition chromaPosition; RaPixelRange rav1eRange; @@ -490,7 +529,7 @@ if (input_class == heif_image_input_class_alpha) { chromaSampling = RA_CHROMA_SAMPLING_CS420; // I can't seem to get RA_CHROMA_SAMPLING_CS400 to work right now, unfortunately chromaPosition = RA_CHROMA_SAMPLE_POSITION_UNKNOWN; // TODO: set to CENTER when AV1 and rav1e supports this - yShift = 1; + encoder->yShift = 1; } else { switch (chroma) { @@ -505,14 +544,14 @@ case heif_chroma_420: chromaSampling = RA_CHROMA_SAMPLING_CS420; chromaPosition = RA_CHROMA_SAMPLE_POSITION_UNKNOWN; // TODO: set to CENTER when AV1 and rav1e supports this - yShift = 1; + encoder->yShift = 1; break; default: return heif_error_codec_library_error; } } - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; heif_error err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { nclx = nullptr; @@ -526,7 +565,7 @@ rav1eRange = nclx->full_range_flag ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED; } - int bitDepth = heif_image_get_bits_per_pixel(image, heif_channel_Y); + int bitDepth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); auto rav1eConfigRaw = rav1e_config_default(); auto rav1eConfig = std::shared_ptr(rav1eConfigRaw, [](RaConfig* c) { rav1e_config_unref(c); }); @@ -535,9 +574,35 @@ return heif_error_codec_library_error; } - if (rav1e_config_parse(rav1eConfig.get(), "still_picture", "true") == -1) { - return heif_error_codec_library_error; + rav1e_config_set_time_base(rav1eConfig.get(), RaRational{framerate_num, framerate_denom}); + + if (!image_sequence) { + if (rav1e_config_parse(rav1eConfig.get(), "still_picture", "true") == -1) { + return heif_error_codec_library_error; + } } + + if (image_sequence) { + if (options->gop_structure == heif_sequence_gop_structure_intra_only) { + if (rav1e_config_parse(rav1eConfig.get(), "key_frame_interval", "1") == -1) { + return heif_error_codec_library_error; + } + } + else if (options->keyframe_distance_max) { + if (rav1e_config_parse(rav1eConfig.get(), "key_frame_interval", + std::to_string(options->keyframe_distance_max).c_str()) == -1) { + return heif_error_codec_library_error; + } + } + + if (options->keyframe_distance_min) { + if (rav1e_config_parse(rav1eConfig.get(), "min_key_frame_interval", + std::to_string(options->keyframe_distance_min).c_str()) == -1) { + return heif_error_codec_library_error; + } + } + } + if (rav1e_config_parse_int(rav1eConfig.get(), "width", heif_image_get_width(image, heif_channel_Y)) == -1) { return heif_error_codec_library_error; } @@ -592,28 +657,68 @@ (RaTransferCharacteristics) nclx->transfer_characteristics); } - RaContext* rav1eContextRaw = rav1e_context_new(rav1eConfig.get()); - if (!rav1eContextRaw) { + encoder->rav1eContextRaw = rav1e_context_new(rav1eConfig.get()); + if (!encoder->rav1eContextRaw) { return heif_error_codec_library_error; } - auto rav1eContext = std::shared_ptr(rav1eContextRaw, [](RaContext* ctx) { rav1e_context_unref(ctx); }); + + encoder->bit_depth = bitDepth; + + return {}; +} + + +heif_error rav1e_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return rav1e_start_sequence_encoding_intern(encoder_raw, image, input_class, + framerate_num, framerate_denom, options, true); +} + + +heif_error rav1e_encode_sequence_frame(void* encoder_raw, const heif_image* image, uintptr_t frame_nr) +{ + // AV1 signals one bit depth for all planes. rav1e always requests YCbCr + // input, so a monochrome image would be missing the Cb/Cr planes read below. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/false, + {8, 10, 12}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + auto* encoder = (encoder_struct_rav1e*) encoder_raw; + auto& rav1eContext = encoder->rav1eContextRaw; + + // rav1e_frame_new() below builds a frame for the pixel format the context was + // created with, from the first frame of the sequence, while byteWidth is this + // frame's. + input_error = check_sequence_frame_bit_depth(image, encoder->bit_depth); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + int bitDepth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + int yShift = encoder->yShift; // --- copy libheif image to rav1e image - auto rav1eFrameRaw = rav1e_frame_new(rav1eContext.get()); + auto rav1eFrameRaw = rav1e_frame_new(rav1eContext); auto rav1eFrame = std::shared_ptr(rav1eFrameRaw, [](RaFrame* frm) { rav1e_frame_unref(frm); }); int byteWidth = (bitDepth > 8) ? 2 : 1; // if (input_class == heif_image_input_class_alpha) { //} else { - int strideY; - const uint8_t* Y = heif_image_get_plane_readonly(image, heif_channel_Y, &strideY); - int strideCb; - const uint8_t* Cb = heif_image_get_plane_readonly(image, heif_channel_Cb, &strideCb); - int strideCr; - const uint8_t* Cr = heif_image_get_plane_readonly(image, heif_channel_Cr, &strideCr); + size_t strideY; + const uint8_t* Y = heif_image_get_plane_readonly2(image, heif_channel_Y, &strideY); + size_t strideCb; + const uint8_t* Cb = heif_image_get_plane_readonly2(image, heif_channel_Cb, &strideCb); + size_t strideCr; + const uint8_t* Cr = heif_image_get_plane_readonly2(image, heif_channel_Cr, &strideCr); uint32_t height = heif_image_get_height(image, heif_channel_Y); @@ -624,58 +729,138 @@ rav1e_frame_fill_plane(rav1eFrame.get(), 2, Cr, strideCr * uvHeight, strideCr, byteWidth); } - RaEncoderStatus encoderStatus = rav1e_send_frame(rav1eContext.get(), rav1eFrame.get()); + RaEncoderStatus encoderStatus = rav1e_send_frame(rav1eContext, rav1eFrame.get()); if (encoderStatus != 0) { return heif_error_codec_library_error; } + return get_encoder_packets(encoder_raw); +} + + +heif_error rav1e_end_sequence_encoding(void* encoder_raw) +{ + auto* encoder = (encoder_struct_rav1e*) encoder_raw; + auto& rav1eContext = encoder->rav1eContextRaw; + // flush encoder - encoderStatus = rav1e_send_frame(rav1eContext.get(), nullptr); + RaEncoderStatus encoderStatus = rav1e_send_frame(rav1eContext, nullptr); if (encoderStatus != 0) { return heif_error_codec_library_error; } - RaPacket* pkt = nullptr; - encoderStatus = rav1e_receive_packet(rav1eContext.get(), &pkt); - if (encoderStatus != 0) { - return heif_error_codec_library_error; + return get_encoder_packets(encoder_raw); +} + + +static heif_error get_encoder_packets(void* encoder_raw) +{ + auto* encoder = (encoder_struct_rav1e*) encoder_raw; + auto& rav1eContext = encoder->rav1eContextRaw; + + for (;;) { + RaPacket* pkt = nullptr; + RaEncoderStatus encoderStatus = rav1e_receive_packet(rav1eContext, &pkt); + + if (encoderStatus == RA_ENCODER_STATUS_NEED_MORE_DATA) { + return {}; + } + + if (encoderStatus == RA_ENCODER_STATUS_ENCODED) { + continue; + } + + if (encoderStatus != 0) { + return heif_error_codec_library_error; + } + + if (pkt && pkt->data && (pkt->len > 0)) { + encoder_struct_rav1e::Packet output_packet; + output_packet.frameNr = pkt->input_frameno; + output_packet.is_keyframe = (pkt->frame_type == RaFrameType::RA_FRAME_TYPE_KEY); + + encoder->output_packets.emplace_back(output_packet); + encoder->output_packets.back().compressedData.resize(pkt->len); + + memcpy(encoder->output_packets.back().compressedData.data(), + pkt->data, + pkt->len); + } + + if (pkt) { + rav1e_packet_unref(pkt); + } + else { + break; + } } - if (pkt && pkt->data && (pkt->len > 0)) { - encoder->compressed_data.resize(pkt->len); - memcpy(encoder->compressed_data.data(), pkt->data, pkt->len); - encoder->data_read = false; + return {}; +} + + +heif_error rav1e_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* out_framenr, int* out_is_keyframe, + int* more_frame_packets) +{ + auto* encoder = (encoder_struct_rav1e*) encoder_raw; + + encoder->active_output_data.clear(); + + if (encoder->output_packets.empty()) { + *size = 0; + *data = nullptr; } + else { + encoder->active_output_data = std::move(encoder->output_packets.front().compressedData); + if (out_framenr) { + *out_framenr = encoder->output_packets.front().frameNr; + } - if (pkt) { - rav1e_packet_unref(pkt); + if (out_is_keyframe) { + *out_is_keyframe = encoder->output_packets.front().is_keyframe; + } + + encoder->output_packets.pop_front(); + + *size = (int) encoder->active_output_data.size(); + *data = encoder->active_output_data.data(); } return heif_error_ok; } -struct heif_error rav1e_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +heif_error rav1e_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) { - auto* encoder = (struct encoder_struct_rav1e*) encoder_raw; + return rav1e_get_compressed_data2(encoder_raw, data, size, nullptr, nullptr, nullptr); +} - if (encoder->data_read) { - *data = nullptr; - *size = 0; + +heif_error rav1e_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = rav1e_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; } - else { - *data = encoder->compressed_data.data(); - *size = (int) encoder->compressed_data.size(); - encoder->data_read = true; + + err = rav1e_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; } + rav1e_end_sequence_encoding(encoder_raw); + return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_rav1e + +static const heif_encoder_plugin encoder_plugin_rav1e { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_AV1, /* id_name */ "rav1e", /* priority */ RAV1E_PLUGIN_PRIORITY, @@ -703,10 +888,16 @@ /* encode_image */ rav1e_encode_image, /* get_compressed_data */ rav1e_get_compressed_data, /* query_input_colorspace (v2) */ rav1e_query_input_colorspace2, - /* query_encoded_size (v3) */ nullptr + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ rav1e_start_sequence_encoding, + /* encode_sequence_frame (v4) */ rav1e_encode_sequence_frame, + /* end_sequence_encoding (v4) */ rav1e_end_sequence_encoding, + /* get_compressed_data2 (v4) */ rav1e_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_rav1e() +const heif_encoder_plugin* get_encoder_plugin_rav1e() { return &encoder_plugin_rav1e; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_svt.cc libheif-1.23.4/libheif/plugins/encoder_svt.cc --- libheif-1.19.8/libheif/plugins/encoder_svt.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_svt.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,18 +21,34 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_svt.h" +#include "encoder_input_check.h" #include #include #include #include +#include #include +#include +#include +#include #include "svt-av1/EbSvtAv1.h" #include "svt-av1/EbSvtAv1Enc.h" +#include +// TODO: for some reason, the SVT encoder outputs only keyframes for me. +// It appears to work in libavif, but I didn't find the difference yet. struct encoder_struct_svt { + ~encoder_struct_svt() + { + if (svt_encoder) { + svt_av1_enc_deinit(svt_encoder); + svt_av1_enc_deinit_handle(svt_encoder); + } + } + int speed = 12; // 0-13 int quality; @@ -51,20 +67,48 @@ int tile_cols = 1; // 1,2,4,8,16,32,64 #if SVT_AV1_CHECK_VERSION(0, 9, 1) - enum Tune { + enum Tune + { Tune_VQ = 0, Tune_PSNR = 1, - Tune_SSIM = 2 + Tune_SSIM = 2, +#if SVT_AV1_CHECK_VERSION(4, 0, 0) + Tune_IQ = 3, + Tune_MS_SSIM = 4 +#endif }; + uint8_t tune = Tune_PSNR; #endif - heif_chroma chroma = heif_chroma_420; // SVT-AV1 only supports 4:2:0 as of v3.0.0 + heif_chroma chroma = heif_chroma_420; // SVT-AV1 only supports 4:2:0 as of v3.0.0 + heif_image_input_class input_class = heif_image_input_class_normal; + + // int keyframe_interval = 0; TODO: this does not seem to work (see all [1]) + + // --- Encoder + + EbComponentType* svt_encoder = nullptr; + EbSvtAv1EncConfiguration svt_config{}; // value-initialized: svt_encode_sequence_frame() reads encoder_bit_depth from it + EbBufferHeaderType input_buffer; + + bool still_image_mode = false; + bool low_delay_mode = false; // --- output - std::vector compressed_data; - bool data_read = false; + struct Packet + { + std::vector compressedData; + uintptr_t frameNr = 0; + bool is_keyframe = false; + }; + + std::deque output_packets; + std::vector active_output_data; + + //std::vector compressed_data; + //bool data_read = false; }; //static const char* kError_out_of_memory = "Out of memory"; @@ -77,7 +121,11 @@ #if SVT_AV1_CHECK_VERSION(0, 9, 1) static const char* kParam_tune = "tune"; -static const char* const kParam_tune_valid_values[] = {"vq","psnr","ssim", nullptr}; +#if SVT_AV1_CHECK_VERSION(4, 0, 0) +static const char* const kParam_tune_valid_values[] = {"vq", "psnr", "ssim", "iq", "ms-ssim", nullptr}; +#else +static const char* const kParam_tune_valid_values[] = {"vq", "psnr", "ssim", nullptr}; +#endif #endif /* @@ -89,9 +137,11 @@ static int valid_tile_num_values[] = {1, 2, 4, 8, 16, 32, 64}; -static struct heif_error heif_error_codec_library_error = {heif_error_Encoder_plugin_error, - heif_suberror_Unspecified, - "SVT-AV1 error"}; +static heif_error heif_error_codec_library_error = { + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "SVT-AV1 error" +}; static const int SVT_PLUGIN_PRIORITY = 40; @@ -130,13 +180,13 @@ #define MAX_NPARAMETERS 11 -static struct heif_encoder_parameter svt_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* svt_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter svt_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* svt_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void svt_init_parameters() { - struct heif_encoder_parameter* p = svt_encoder_params; - const struct heif_encoder_parameter** d = svt_encoder_parameter_ptrs; + heif_encoder_parameter* p = svt_encoder_params; + const heif_encoder_parameter** d = svt_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -276,7 +326,7 @@ } -const struct heif_encoder_parameter** svt_list_parameters(void* encoder) +const heif_encoder_parameter** svt_list_parameters(void* encoder) { return svt_encoder_parameter_ptrs; } @@ -291,7 +341,7 @@ { } -struct heif_error svt_new_encoder(void** enc) +heif_error svt_new_encoder(void** enc) { auto* encoder = new encoder_struct_svt(); struct heif_error err = heif_error_ok; @@ -313,9 +363,9 @@ } -struct heif_error svt_set_parameter_quality(void* encoder_raw, int quality) +heif_error svt_set_parameter_quality(void* encoder_raw, int quality) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -326,18 +376,18 @@ return heif_error_ok; } -struct heif_error svt_get_parameter_quality(void* encoder_raw, int* quality) +heif_error svt_get_parameter_quality(void* encoder_raw, int* quality) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -struct heif_error svt_set_parameter_lossless(void* encoder_raw, int enable) +heif_error svt_set_parameter_lossless(void* encoder_raw, int enable) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; #if SVT_AV1_CHECK_VERSION(3, 0, 0) encoder->lossless = enable; @@ -354,9 +404,9 @@ return heif_error_ok; } -struct heif_error svt_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error svt_get_parameter_lossless(void* encoder_raw, int* enable) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; #if SVT_AV1_CHECK_VERSION(3, 0, 0) *enable = encoder->lossless; @@ -368,7 +418,7 @@ return heif_error_ok; } -struct heif_error svt_set_parameter_logging_level(void* encoder_raw, int logging) +heif_error svt_set_parameter_logging_level(void* encoder_raw, int logging) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -383,7 +433,7 @@ return heif_error_ok; } -struct heif_error svt_get_parameter_logging_level(void* encoder_raw, int* loglevel) +heif_error svt_get_parameter_logging_level(void* encoder_raw, int* loglevel) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -400,9 +450,9 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error svt_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error svt_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_svt* encoder = (struct encoder_struct_svt*) encoder_raw; + encoder_struct_svt* encoder = (encoder_struct_svt*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return svt_set_parameter_quality(encoder, value); @@ -426,9 +476,9 @@ return heif_error_unsupported_parameter; } -struct heif_error svt_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error svt_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return svt_get_parameter_quality(encoder, value); @@ -436,10 +486,16 @@ else if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return svt_get_parameter_lossless(encoder, value); } + else if (strcmp(name, kParam_qp) == 0) { + if (!encoder->qp_set) { + return heif_error_unsupported_parameter; + } + *value = encoder->qp; + return heif_error_ok; + } get_value(kParam_min_q, min_q); get_value(kParam_max_q, max_q); - get_value(kParam_qp, qp); // TODO: what if qp was not set ? get_value(kParam_threads, threads); get_value(kParam_speed, speed); get_value("tile-rows", tile_rows); @@ -449,9 +505,9 @@ } -struct heif_error svt_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error svt_set_parameter_boolean(void* encoder_raw, const char* name, int value) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return svt_set_parameter_lossless(encoder, value); @@ -462,9 +518,9 @@ return heif_error_unsupported_parameter; } -struct heif_error svt_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error svt_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return svt_get_parameter_lossless(encoder, value); @@ -476,10 +532,10 @@ } -struct heif_error svt_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error svt_set_parameter_string(void* encoder_raw, const char* name, const char* value) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; - (void)encoder; + auto* encoder = (encoder_struct_svt*) encoder_raw; + (void) encoder; #if 0 if (strcmp(name, kParam_chroma) == 0) { @@ -515,6 +571,16 @@ encoder->tune = encoder_struct_svt::Tune_SSIM; return heif_error_ok; } +#if SVT_AV1_CHECK_VERSION(4, 0, 0) + else if (strcmp(value, "iq") == 0) { + encoder->tune = encoder_struct_svt::Tune_IQ; + return heif_error_ok; + } + else if (strcmp(value, "ms-ssim") == 0) { + encoder->tune = encoder_struct_svt::Tune_MS_SSIM; + return heif_error_ok; + } +#endif } #endif @@ -531,11 +597,11 @@ #endif -struct heif_error svt_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +heif_error svt_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; - (void)encoder; + auto* encoder = (encoder_struct_svt*) encoder_raw; + (void) encoder; #if 0 if (strcmp(name, kParam_chroma) == 0) { @@ -562,17 +628,25 @@ switch (encoder->tune) { case encoder_struct_svt::Tune_VQ: save_strcpy(value, value_size, "vq"); - break; + break; case encoder_struct_svt::Tune_PSNR: save_strcpy(value, value_size, "psnr"); - break; + break; case encoder_struct_svt::Tune_SSIM: save_strcpy(value, value_size, "ssim"); - break; + break; +#if SVT_AV1_CHECK_VERSION(4, 0, 0) + case encoder_struct_svt::Tune_IQ: + save_strcpy(value, value_size, "iq"); + break; + case encoder_struct_svt::Tune_MS_SSIM: + save_strcpy(value, value_size, "ms-ssim"); + break; +#endif default: assert(false); - return heif_error_invalid_parameter_value; + return heif_error_invalid_parameter_value; } return heif_error_ok; } @@ -584,8 +658,8 @@ static void svt_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = svt_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = svt_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -624,7 +698,7 @@ void svt_query_encoded_size(void* encoder_raw, uint32_t input_width, uint32_t input_height, uint32_t* encoded_width, uint32_t* encoded_height) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; // SVT-AV1 (as of version 1.2.1) can only create image sizes matching the chroma format. Add padding if necessary. @@ -650,13 +724,26 @@ } -struct heif_error svt_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +static heif_error svt_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + auto* encoder = (encoder_struct_svt*) encoder_raw; + encoder->input_class = input_class; EbErrorType res = EB_ErrorNone; + heif_error err; + + // deinit the encoder if it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + if (encoder->svt_encoder) { + svt_av1_enc_deinit(encoder->svt_encoder); + svt_av1_enc_deinit_handle(encoder->svt_encoder); + encoder->svt_encoder = nullptr; + } - encoder->compressed_data.clear(); + // encoder->compressed_data.clear(); int w = heif_image_get_width(image, heif_channel_Y); int h = heif_image_get_height(image, heif_channel_Y); @@ -664,25 +751,14 @@ uint32_t encoded_width, encoded_height; svt_query_encoded_size(encoder_raw, w, h, &encoded_width, &encoded_height); - // Note: it is ok to cast away the const, as the image content is not changed. - // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. - heif_error err = heif_image_extend_padding_to_size(const_cast(image), - (int) encoded_width, - (int) encoded_height); - if (err.code) { - return err; - } - const heif_chroma chroma = heif_image_get_chroma_format(image); int bitdepth_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); - uint8_t yShift = 0; EbColorFormat color_format = EB_YUV420; if (input_class == heif_image_input_class_alpha) { color_format = EB_YUV420; //chromaPosition = RA_CHROMA_SAMPLE_POSITION_UNKNOWN; - yShift = 1; } else { switch (chroma) { @@ -697,7 +773,6 @@ case heif_chroma_420: color_format = EB_YUV420; //chromaPosition = RA_CHROMA_SAMPLE_POSITION_UNKNOWN; // TODO: set to CENTER when AV1 and svt supports this - yShift = 1; break; default: return heif_error_codec_library_error; @@ -707,8 +782,8 @@ // --- initialize the encoder - EbComponentType* svt_encoder = nullptr; - EbSvtAv1EncConfiguration svt_config; + EbComponentType*& svt_encoder = encoder->svt_encoder; + EbSvtAv1EncConfiguration& svt_config = encoder->svt_config; memset(&svt_config, 0, sizeof(EbSvtAv1EncConfiguration)); #if SVT_AV1_CHECK_VERSION(3, 0, 0) @@ -729,7 +804,7 @@ svt_config.lossless = encoder->lossless; #endif - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { nclx = nullptr; @@ -779,18 +854,29 @@ #endif // disable 2-pass - svt_config.rc_stats_buffer = SvtAv1FixedBuf {nullptr, 0}; + svt_config.rc_stats_buffer = SvtAv1FixedBuf{nullptr, 0}; svt_config.rate_control_mode = 0; // constant rate factor //svt_config.enable_adaptive_quantization = 0; // 2 is CRF (the default), 0 would be CQP - int qp; + float qp; if (encoder->qp_set) { - qp = encoder->qp; + qp = (float) encoder->qp; } else { - qp = ((100 - encoder->quality) * 63 + 50) / 100; + qp = (float) (100 - encoder->quality) * 63.0f / 100.0f; } - svt_config.qp = qp; +#if SVT_AV1_CHECK_VERSION(4, 0, 0) + // Use quarter-QP precision. The fractional QP part is passed in 'extended_crf_qindex_offset' + // in units of quarter QP steps (one AV1 qindex step). + // We set these configuration fields directly instead of using + // svt_av1_enc_parse_parameter(&svt_config, "cqp", ...), because that would also + // set aq_mode=0, which switches the rate control from CRF to plain CQP. + uint32_t quarter_qp_steps = (uint32_t) std::lround(qp * 4.0f); + svt_config.qp = quarter_qp_steps / 4; + svt_config.extended_crf_qindex_offset = (uint8_t) (quarter_qp_steps % 4); +#else + svt_config.qp = (uint32_t) std::lround(qp); +#endif svt_config.min_qp_allowed = encoder->min_q; svt_config.max_qp_allowed = encoder->max_q; @@ -803,6 +889,60 @@ svt_config.enc_mode = (int8_t) encoder->speed; + if (image_sequence) { + // svt_config.force_key_frames = true; TODO: this does not seem to work (see all [1]), tested with SVT 3.0.1 + +#if 1 +#if SVT_AV1_CHECK_VERSION(4, 1, 0) + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + svt_config.pred_structure = PredStructure::ALL_INTRA; + break; + case heif_sequence_gop_structure_lowdelay: + svt_config.pred_structure = PredStructure::LOW_DELAY; + encoder->low_delay_mode = true; + break; + case heif_sequence_gop_structure_unrestricted: + svt_config.pred_structure = PredStructure::RANDOM_ACCESS; + break; + } +#elif SVT_AV1_CHECK_VERSION(4, 0, 0) + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + case heif_sequence_gop_structure_lowdelay: + svt_config.pred_structure = 1; // LOW_DELAY + encoder->low_delay_mode = true; + break; + case heif_sequence_gop_structure_unrestricted: + svt_config.pred_structure = 2; // RANDOM_ACCESS + break; + } +#else + // TODO: setting pref_structure to SVT_AV1_PRED_LOW_DELAY_B hangs the encoder + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + case heif_sequence_gop_structure_lowdelay: + svt_config.pred_structure = SvtAv1PredStructure::SVT_AV1_PRED_LOW_DELAY_B; + encoder->low_delay_mode = true; + break; + case heif_sequence_gop_structure_unrestricted: + svt_config.pred_structure = SvtAv1PredStructure::SVT_AV1_PRED_RANDOM_ACCESS; + break; + } +#endif + + if (options->keyframe_distance_max) { + svt_config.intra_period_length = options->keyframe_distance_max - 1; + } +#endif + } + else { +#if SVT_AV1_CHECK_VERSION(4, 0, 0) + svt_config.avif = true; + encoder->still_image_mode = true; +#endif + } + if (color_format == EB_YUV422 || bitdepth_y > 10) { svt_config.profile = PROFESSIONAL_PROFILE; } @@ -810,63 +950,192 @@ svt_config.profile = HIGH_PROFILE; } + svt_config.frame_rate_numerator = framerate_num; + svt_config.frame_rate_denominator = framerate_denom; + res = svt_av1_enc_set_parameter(svt_encoder, &svt_config); if (res == EB_ErrorBadParameter) { - svt_av1_enc_deinit(svt_encoder); - svt_av1_enc_deinit_handle(svt_encoder); return heif_error_codec_library_error; } res = svt_av1_enc_init(svt_encoder); if (res != EB_ErrorNone) { - svt_av1_enc_deinit(svt_encoder); - svt_av1_enc_deinit_handle(svt_encoder); return heif_error_codec_library_error; } + return {}; +} + + +static heif_error read_encoder_output_packets(void* encoder_raw, bool done_sending_pics) +{ + auto* encoder = (encoder_struct_svt*) encoder_raw; + EbComponentType*& svt_encoder = encoder->svt_encoder; + EbErrorType res = EB_ErrorNone; + + + // --- read compressed picture + + int encode_at_eos = 0; + + do { + EbBufferHeaderType* output_buf = nullptr; + + res = svt_av1_enc_get_packet(svt_encoder, &output_buf, (uint8_t) done_sending_pics); + if (output_buf != nullptr) { + encode_at_eos = ((output_buf->flags & EB_BUFFERFLAG_EOS) == EB_BUFFERFLAG_EOS); + if (output_buf->p_buffer && (output_buf->n_filled_len > 0)) { + uint8_t* data = output_buf->p_buffer; + uint32_t n = output_buf->n_filled_len; + + encoder_struct_svt::Packet output_packet; + output_packet.frameNr = output_buf->pts; + output_packet.is_keyframe = (output_buf->pic_type == EbAv1PictureType::EB_AV1_KEY_PICTURE); + + encoder->output_packets.emplace_back(output_packet); + encoder->output_packets.back().compressedData.resize(n); + + memcpy(encoder->output_packets.back().compressedData.data(), + data, n); + } + svt_av1_enc_release_out_buffer(&output_buf); + + // In LOW_DELAY mode, svt_av1_enc_get_packet() is always a blocking call + // (enforcing a "picture in, picture out" model). We must exit after + // retrieving one packet per frame, otherwise the next call blocks forever. + if (encoder->low_delay_mode && !done_sending_pics) { + break; + } + } + } while (res == EB_ErrorNone && !encode_at_eos); + + + // delete input_buffer.p_buffer; + + if (!done_sending_pics && ((res == EB_ErrorNone) || (res == EB_NoErrorEmptyQueue))) { + return heif_error_ok; + } + else { + return (res == EB_ErrorNone ? heif_error_ok : heif_error_codec_library_error); + } +} + + +static heif_error svt_encode_sequence_frame(void* encoder_raw, const heif_image* image, uintptr_t frame_nr) +{ + // AV1 signals one bit depth for all planes. SVT always requests YCbCr + // input, so a monochrome image would be missing the Cb/Cr planes read below. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/false, + {8, 10, 12}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + auto* encoder = (encoder_struct_svt*) encoder_raw; + EbComponentType*& svt_encoder = encoder->svt_encoder; + EbErrorType res = EB_ErrorNone; + + // svt_config.encoder_bit_depth was taken from the first frame of the sequence, + // while the plane pointers handed to SVT below are this frame's. + input_error = check_sequence_frame_bit_depth(image, encoder->svt_config.encoder_bit_depth); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + int w = heif_image_get_width(image, heif_channel_Y); + int h = heif_image_get_height(image, heif_channel_Y); + const heif_chroma chroma = heif_image_get_chroma_format(image); + int bitdepth_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + + uint32_t encoded_width, encoded_height; + svt_query_encoded_size(encoder_raw, w, h, &encoded_width, &encoded_height); + + // Note: it is ok to cast away the const, as the image content is not changed. + // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. + heif_error err = heif_image_extend_padding_to_size(const_cast(image), + (int) encoded_width, + (int) encoded_height); + if (err.code) { + return err; + } + + + uint8_t yShift = 0; + + if (encoder->input_class == heif_image_input_class_alpha) { + yShift = 1; + } + else if (chroma == heif_chroma_420) { + yShift = 1; + } + // --- copy libheif image to svt image - EbBufferHeaderType input_buffer; - input_buffer.p_buffer = (uint8_t*) (new EbSvtIOFormat()); + EbBufferHeaderType& input_buffer = encoder->input_buffer; + auto svt_io_format = std::make_unique(); + input_buffer.p_buffer = (uint8_t*) svt_io_format.get(); memset(input_buffer.p_buffer, 0, sizeof(EbSvtIOFormat)); + input_buffer.size = sizeof(EbBufferHeaderType); input_buffer.p_app_private = nullptr; input_buffer.pic_type = EB_AV1_INVALID_PICTURE; input_buffer.metadata = nullptr; + input_buffer.pts = frame_nr; + + /* TODO: this does not seem to work (see all [1]) + if (encoder->keyframe_interval) { + if (frame_nr % encoder->keyframe_interval == 0) { + input_buffer.pic_type = EB_AV1_KEY_PICTURE; + } + } + */ auto* input_picture_buffer = (EbSvtIOFormat*) input_buffer.p_buffer; int bytesPerPixel = bitdepth_y > 8 ? 2 : 1; std::vector dummy_color_plane; - if (input_class == heif_image_input_class_alpha) { - int stride; - input_picture_buffer->luma = (uint8_t*) heif_image_get_plane_readonly(image, heif_channel_Y, &stride); - input_picture_buffer->y_stride = stride / bytesPerPixel; - input_buffer.n_filled_len = stride * encoded_height; + if (encoder->input_class == heif_image_input_class_alpha) { + size_t stride64; + input_picture_buffer->luma = (uint8_t*) heif_image_get_plane_readonly2(image, heif_channel_Y, &stride64); + + uint32_t stride32; + if (stride64 > std::numeric_limits::max()) { + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "Image too wide for encoder" + }; + } + else { + stride32 = static_cast(stride64); + } + + input_picture_buffer->y_stride = stride32 / bytesPerPixel; + input_buffer.n_filled_len = stride32 * encoded_height; uint32_t uvWidth = get_subsampled_size_h(encoded_width, heif_channel_Cb, heif_chroma_420, scaling_mode::round_up); uint32_t uvHeight = get_subsampled_size_v(encoded_height, heif_channel_Cb, heif_chroma_420, scaling_mode::round_up); - dummy_color_plane.resize(uvWidth * uvHeight); + dummy_color_plane.resize(uvWidth * uvHeight * bytesPerPixel); if (bitdepth_y <= 8) { - uint8_t val = 1 << (bitdepth_y - 1); + uint8_t val = static_cast(1 << (bitdepth_y - 1)); memset(dummy_color_plane.data(), val, uvWidth * uvHeight * bytesPerPixel); } else { assert(bitdepth_y > 8 && bitdepth_y <= 16); - uint16_t val = 1 << (bitdepth_y - 1); + uint16_t val = static_cast(1 << (bitdepth_y - 1)); uint8_t high = static_cast((val >> 8) & 0xFF); - uint8_t low = static_cast(val & 0xFF); + uint8_t low = static_cast(val & 0xFF); - for (uint32_t i=0;icb_stride = uvWidth; input_picture_buffer->cr_stride = uvWidth; @@ -874,36 +1143,65 @@ input_picture_buffer->cr = dummy_color_plane.data(); } else { - int stride; - input_picture_buffer->luma = (uint8_t*) heif_image_get_plane_readonly(image, heif_channel_Y, &stride); - input_picture_buffer->y_stride = stride / bytesPerPixel; - input_buffer.n_filled_len = stride * encoded_height; + size_t stride64; + input_picture_buffer->luma = (uint8_t*) heif_image_get_plane_readonly2(image, heif_channel_Y, &stride64); + + uint32_t stride32; + if (stride64 > std::numeric_limits::max()) { + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "Image too wide for encoder" + }; + } + else { + stride32 = static_cast(stride64); + } + + input_picture_buffer->y_stride = stride32 / bytesPerPixel; + input_buffer.n_filled_len = stride32 * encoded_height; uint32_t uvHeight = (encoded_height + yShift) >> yShift; - input_picture_buffer->cb = (uint8_t*) heif_image_get_plane_readonly(image, heif_channel_Cb, &stride); - input_buffer.n_filled_len += stride * uvHeight; - input_picture_buffer->cb_stride = stride / bytesPerPixel; - - input_picture_buffer->cr = (uint8_t*) heif_image_get_plane_readonly(image, heif_channel_Cr, &stride); - input_buffer.n_filled_len += stride * uvHeight; - input_picture_buffer->cr_stride = stride / bytesPerPixel; + input_picture_buffer->cb = (uint8_t*) heif_image_get_plane_readonly2(image, heif_channel_Cb, &stride64); + stride32 = static_cast(stride64); // chroma stride should always be smaller than luma stride + input_buffer.n_filled_len += stride32 * uvHeight; + input_picture_buffer->cb_stride = stride32 / bytesPerPixel; + + input_picture_buffer->cr = (uint8_t*) heif_image_get_plane_readonly2(image, heif_channel_Cr, &stride64); + stride32 = static_cast(stride64); // chroma stride should always be smaller than luma stride + input_buffer.n_filled_len += stride32 * uvHeight; + input_picture_buffer->cr_stride = stride32 / bytesPerPixel; } input_buffer.flags = 0; - input_buffer.pts = 0; - - EbAv1PictureType frame_type = EB_AV1_KEY_PICTURE; + // input_buffer.pts = 0; - input_buffer.pic_type = frame_type; + //EbAv1PictureType frame_type = EB_AV1_KEY_PICTURE; + //input_buffer.pic_type = frame_type; res = svt_av1_enc_send_picture(svt_encoder, &input_buffer); if (res != EB_ErrorNone) { - delete input_buffer.p_buffer; - svt_av1_enc_deinit(svt_encoder); - svt_av1_enc_deinit_handle(svt_encoder); + // input_buffer.p_buffer is owned by the svt_io_format unique_ptr, which frees it on return. return heif_error_codec_library_error; } + return read_encoder_output_packets(encoder_raw, false); +} + + +static heif_error svt_end_sequence_encoding(void* encoder_raw) +{ + auto* encoder = (encoder_struct_svt*) encoder_raw; + + // In LOW_DELAY mode, all packets have already been retrieved during per-frame + // encoding (picture-in, picture-out model). No flush is needed, and sending an + // EOS buffer would deadlock because SVT-AV1's LOW_DELAY path does not release + // the internal resources allocated for the EOS picture. + if (encoder->low_delay_mode) { + return heif_error_ok; + } + + EbComponentType*& svt_encoder = encoder->svt_encoder; // --- flush encoder @@ -921,111 +1219,130 @@ ret = svt_av1_enc_send_picture(svt_encoder, &flush_input_buffer); if (ret != EB_ErrorNone) { - delete input_buffer.p_buffer; - svt_av1_enc_deinit(svt_encoder); - svt_av1_enc_deinit_handle(svt_encoder); + // delete input_buffer.p_buffer; return heif_error_codec_library_error; } + return read_encoder_output_packets(encoder_raw, true); +} - // --- read compressed picture - int encode_at_eos = 0; - uint8_t done_sending_pics = true; +heif_error svt_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* out_framenr, int* out_is_keyframe, + int* more_frame_packets) +{ + auto* encoder = (encoder_struct_svt*) encoder_raw; - do { - EbBufferHeaderType* output_buf = nullptr; + encoder->active_output_data.clear(); - res = svt_av1_enc_get_packet(svt_encoder, &output_buf, (uint8_t) done_sending_pics); - if (output_buf != nullptr) { - encode_at_eos = ((output_buf->flags & EB_BUFFERFLAG_EOS) == EB_BUFFERFLAG_EOS); - if (output_buf->p_buffer && (output_buf->n_filled_len > 0)) { - uint8_t* data = output_buf->p_buffer; - uint32_t n = output_buf->n_filled_len; + if (encoder->output_packets.empty()) { + *size = 0; + *data = nullptr; + } + else { + encoder->active_output_data = std::move(encoder->output_packets.front().compressedData); + if (out_framenr) { + *out_framenr = encoder->output_packets.front().frameNr; + } - size_t oldSize = encoder->compressed_data.size(); - encoder->compressed_data.resize(oldSize + n); + if (out_is_keyframe) { + *out_is_keyframe = encoder->output_packets.front().is_keyframe; + } - memcpy(encoder->compressed_data.data() + oldSize, data, n); + encoder->output_packets.pop_front(); - encoder->data_read = false; - // (output_buf->pic_type == EB_AV1_KEY_PICTURE)); - } - svt_av1_enc_release_out_buffer(&output_buf); - } - } while (res == EB_ErrorNone && !encode_at_eos); + *size = (int) encoder->active_output_data.size(); + *data = encoder->active_output_data.data(); + } + return heif_error_ok; +} - delete input_buffer.p_buffer; - svt_av1_enc_deinit(svt_encoder); - svt_av1_enc_deinit_handle(svt_encoder); - if (!done_sending_pics && ((res == EB_ErrorNone) || (res == EB_NoErrorEmptyQueue))) { - return heif_error_ok; - } - else { - return (res == EB_ErrorNone ? heif_error_ok : heif_error_codec_library_error); - } +heif_error svt_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return svt_get_compressed_data2(encoder_raw, data, size, nullptr, nullptr, nullptr); } -struct heif_error svt_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error svt_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) { - auto* encoder = (struct encoder_struct_svt*) encoder_raw; + return svt_start_sequence_encoding_intern(encoder_raw, image, input_class, + framerate_num, framerate_denom, options, true); +} - if (encoder->data_read) { - *data = nullptr; - *size = 0; + +heif_error svt_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = svt_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; } - else { - *data = encoder->compressed_data.data(); - *size = (int) encoder->compressed_data.size(); - encoder->data_read = true; + + err = svt_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; } - return heif_error_ok; + err = svt_end_sequence_encoding(encoder_raw); + if (err.code) { + return err; + } + + return {}; } -static const struct heif_encoder_plugin encoder_plugin_svt - { - /* plugin_api_version */ 3, - /* compression_format */ heif_compression_AV1, - /* id_name */ "svt", - /* priority */ SVT_PLUGIN_PRIORITY, - /* supports_lossy_compression */ true, +static const heif_encoder_plugin encoder_plugin_svt +{ + /* plugin_api_version */ 4, + /* compression_format */ heif_compression_AV1, + /* id_name */ "svt", + /* priority */ SVT_PLUGIN_PRIORITY, + /* supports_lossy_compression */ true, #if SVT_AV1_CHECK_VERSION(3, 0, 0) - /* supports_lossless_compression */ true, + /* supports_lossless_compression */ true, #else /* supports_lossless_compression */ false, #endif - /* get_plugin_name */ svt_plugin_name, - /* init_plugin */ svt_init_plugin, - /* cleanup_plugin */ svt_cleanup_plugin, - /* new_encoder */ svt_new_encoder, - /* free_encoder */ svt_free_encoder, - /* set_parameter_quality */ svt_set_parameter_quality, - /* get_parameter_quality */ svt_get_parameter_quality, - /* set_parameter_lossless */ svt_set_parameter_lossless, - /* get_parameter_lossless */ svt_get_parameter_lossless, - /* set_parameter_logging_level */ svt_set_parameter_logging_level, - /* get_parameter_logging_level */ svt_get_parameter_logging_level, - /* list_parameters */ svt_list_parameters, - /* set_parameter_integer */ svt_set_parameter_integer, - /* get_parameter_integer */ svt_get_parameter_integer, - /* set_parameter_boolean */ svt_set_parameter_boolean, - /* get_parameter_boolean */ svt_get_parameter_boolean, - /* set_parameter_string */ svt_set_parameter_string, - /* get_parameter_string */ svt_get_parameter_string, - /* query_input_colorspace */ svt_query_input_colorspace, - /* encode_image */ svt_encode_image, - /* get_compressed_data */ svt_get_compressed_data, - /* query_input_colorspace (v2) */ svt_query_input_colorspace2, - /* query_encoded_size (v3) */ svt_query_encoded_size - }; + /* get_plugin_name */ svt_plugin_name, + /* init_plugin */ svt_init_plugin, + /* cleanup_plugin */ svt_cleanup_plugin, + /* new_encoder */ svt_new_encoder, + /* free_encoder */ svt_free_encoder, + /* set_parameter_quality */ svt_set_parameter_quality, + /* get_parameter_quality */ svt_get_parameter_quality, + /* set_parameter_lossless */ svt_set_parameter_lossless, + /* get_parameter_lossless */ svt_get_parameter_lossless, + /* set_parameter_logging_level */ svt_set_parameter_logging_level, + /* get_parameter_logging_level */ svt_get_parameter_logging_level, + /* list_parameters */ svt_list_parameters, + /* set_parameter_integer */ svt_set_parameter_integer, + /* get_parameter_integer */ svt_get_parameter_integer, + /* set_parameter_boolean */ svt_set_parameter_boolean, + /* get_parameter_boolean */ svt_get_parameter_boolean, + /* set_parameter_string */ svt_set_parameter_string, + /* get_parameter_string */ svt_get_parameter_string, + /* query_input_colorspace */ svt_query_input_colorspace, + /* encode_image */ svt_encode_image, + /* get_compressed_data */ svt_get_compressed_data, + /* query_input_colorspace (v2) */ svt_query_input_colorspace2, + /* query_encoded_size (v3) */ svt_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ svt_start_sequence_encoding, + /* encode_sequence_frame (v4) */ svt_encode_sequence_frame, + /* end_sequence_encoding (v4) */ svt_end_sequence_encoding, + /* get_compressed_data2 (v4) */ svt_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 +}; -const struct heif_encoder_plugin* get_encoder_plugin_svt() +const heif_encoder_plugin* get_encoder_plugin_svt() { return &encoder_plugin_svt; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_uncompressed.cc libheif-1.23.4/libheif/plugins/encoder_uncompressed.cc --- libheif-1.19.8/libheif/plugins/encoder_uncompressed.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_uncompressed.cc 2026-09-06 14:45:17.000000000 +0000 @@ -49,13 +49,13 @@ #define MAX_NPARAMETERS 14 -static struct heif_encoder_parameter uncompressed_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* uncompressed_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter uncompressed_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* uncompressed_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void uncompressed_init_parameters() { - struct heif_encoder_parameter* p = uncompressed_encoder_params; - const struct heif_encoder_parameter** d = uncompressed_encoder_parameter_ptrs; + heif_encoder_parameter* p = uncompressed_encoder_params; + const heif_encoder_parameter** d = uncompressed_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -71,7 +71,7 @@ } -const struct heif_encoder_parameter** uncompressed_list_parameters(void* encoder) +const heif_encoder_parameter** uncompressed_list_parameters(void* encoder) { return uncompressed_encoder_parameter_ptrs; } @@ -86,10 +86,10 @@ { } -struct heif_error uncompressed_new_encoder(void** enc) +heif_error uncompressed_new_encoder(void** enc) { - struct encoder_struct_uncompressed* encoder = new encoder_struct_uncompressed(); - struct heif_error err = heif_error_ok; + encoder_struct_uncompressed* encoder = new encoder_struct_uncompressed(); + heif_error err = heif_error_ok; *enc = encoder; @@ -102,32 +102,32 @@ void uncompressed_free_encoder(void* encoder_raw) { - struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; + encoder_struct_uncompressed* encoder = (encoder_struct_uncompressed*) encoder_raw; delete encoder; } -struct heif_error uncompressed_set_parameter_quality(void* encoder_raw, int quality) +heif_error uncompressed_set_parameter_quality(void* encoder_raw, int quality) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; return heif_error_ok; } -struct heif_error uncompressed_get_parameter_quality(void* encoder_raw, int* quality) +heif_error uncompressed_get_parameter_quality(void* encoder_raw, int* quality) { *quality = 100; return heif_error_ok; } -struct heif_error uncompressed_set_parameter_lossless(void* encoder_raw, int enable) +heif_error uncompressed_set_parameter_lossless(void* encoder_raw, int enable) { return heif_error_ok; } -struct heif_error uncompressed_get_parameter_lossless(void* encoder_raw, int* enable) +heif_error uncompressed_get_parameter_lossless(void* encoder_raw, int* enable) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -136,7 +136,7 @@ return heif_error_ok; } -struct heif_error uncompressed_set_parameter_logging_level(void* encoder_raw, int logging) +heif_error uncompressed_set_parameter_logging_level(void* encoder_raw, int logging) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -151,7 +151,7 @@ return heif_error_ok; } -struct heif_error uncompressed_get_parameter_logging_level(void* encoder_raw, int* loglevel) +heif_error uncompressed_get_parameter_logging_level(void* encoder_raw, int* loglevel) { #if 0 struct encoder_struct_x265* encoder = (struct encoder_struct_x265*)encoder_raw; @@ -168,14 +168,14 @@ #define get_value(paramname, paramvar) if (strcmp(name, paramname)==0) { *value = encoder->paramvar; return heif_error_ok; } -struct heif_error uncompressed_set_parameter_integer(void* encoder_raw, const char* name, int value) +heif_error uncompressed_set_parameter_integer(void* encoder_raw, const char* name, int value) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; return heif_error_unsupported_parameter; } -struct heif_error uncompressed_get_parameter_integer(void* encoder_raw, const char* name, int* value) +heif_error uncompressed_get_parameter_integer(void* encoder_raw, const char* name, int* value) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -183,14 +183,14 @@ } -struct heif_error uncompressed_set_parameter_boolean(void* encoder_raw, const char* name, int value) +heif_error uncompressed_set_parameter_boolean(void* encoder_raw, const char* name, int value) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; return heif_error_unsupported_parameter; } -struct heif_error uncompressed_get_parameter_boolean(void* encoder_raw, const char* name, int* value) +heif_error uncompressed_get_parameter_boolean(void* encoder_raw, const char* name, int* value) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -198,7 +198,7 @@ } -struct heif_error uncompressed_set_parameter_string(void* encoder_raw, const char* name, const char* value) +heif_error uncompressed_set_parameter_string(void* encoder_raw, const char* name, const char* value) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -206,8 +206,8 @@ } -struct heif_error uncompressed_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +heif_error uncompressed_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -217,8 +217,8 @@ static void uncompressed_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = uncompressed_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = uncompressed_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -258,8 +258,8 @@ } -struct heif_error uncompressed_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +heif_error uncompressed_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) { //struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -269,8 +269,8 @@ } -struct heif_error uncompressed_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +heif_error uncompressed_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) { #if 0 struct encoder_struct_uncompressed* encoder = (struct encoder_struct_uncompressed*) encoder_raw; @@ -290,9 +290,35 @@ } -static const struct heif_encoder_plugin encoder_plugin_uncompressed +heif_error uncompressed_start_sequence_encoding(void* encoder, const heif_image* image, + enum heif_image_input_class image_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return heif_error_ok; +} + +heif_error uncompressed_encode_sequence_frame(void* encoder, const heif_image* image, uintptr_t frame_nr) +{ + return heif_error_ok; +} + +heif_error uncompressed_end_sequence_encoding(void* encoder) +{ + return heif_error_ok; +} + +heif_error uncompressed_get_compressed_data2(void* encoder, uint8_t** data, int* size, + uintptr_t* frame_nr, + int* is_keyframe, int* more_frame_packets) +{ + return heif_error_ok; +} + + +static const heif_encoder_plugin encoder_plugin_uncompressed { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_uncompressed, /* id_name */ "uncompressed", /* priority */ PLUGIN_PRIORITY, @@ -320,10 +346,16 @@ /* encode_image */ uncompressed_encode_image, /* get_compressed_data */ uncompressed_get_compressed_data, /* query_input_colorspace (v2) */ uncompressed_query_input_colorspace2, - /* query_encoded_size (v3) */ nullptr + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ uncompressed_start_sequence_encoding, + /* encode_sequence_frame (v4) */ uncompressed_encode_sequence_frame, + /* end_sequence_encoding (v4) */ uncompressed_end_sequence_encoding, + /* get_compressed_data2 (v4) */ uncompressed_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 1 }; -const struct heif_encoder_plugin* get_encoder_plugin_uncompressed() +const heif_encoder_plugin* get_encoder_plugin_uncompressed() { return &encoder_plugin_uncompressed; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_uvg266.cc libheif-1.23.4/libheif/plugins/encoder_uvg266.cc --- libheif-1.19.8/libheif/plugins/encoder_uvg266.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_uvg266.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,12 +21,19 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_uvg266.h" +#include "encoder_input_check.h" #include #include // apparently, this is a false positive of cpplint #include #include #include #include +#include +#include + +#include "logging.h" +#include "nalu_utils.h" +#include extern "C" { #include @@ -44,8 +51,46 @@ int quality = 75; bool lossless = false; - std::vector output_data; - size_t output_idx = 0; + // --- encoder + + const uvg_api* api = nullptr; + uvg_config* config = nullptr; + uvg_encoder* kvzencoder = nullptr; + + uvg_chroma_format kvzChroma; + int chroma_stride_shift = 0; + int chroma_height_shift = 0; + int input_chroma_width = 0; + int input_chroma_height = 0; + + // --- output data + + //std::vector output_data; + //size_t output_idx = 0; + + struct Packet + { + std::vector data; + uintptr_t frameNr = 0; + bool more_nals = false; + }; + + std::deque output_data; + + std::vector active_data; // holds the data that we just returned + + void append_chunk_data(uvg_data_chunk* data, int framenr); + + ~encoder_struct_uvg266() + { + if (kvzencoder) { + api->encoder_close(kvzencoder); + } + + if (config) { + api->config_destroy(config); + } + } }; static const int uvg266_PLUGIN_PRIORITY = 50; @@ -67,13 +112,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter uvg266_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* uvg266_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter uvg266_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* uvg266_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void uvg266_init_parameters() { - struct heif_encoder_parameter* p = uvg266_encoder_params; - const struct heif_encoder_parameter** d = uvg266_encoder_parameter_ptrs; + heif_encoder_parameter* p = uvg266_encoder_params; + const heif_encoder_parameter** d = uvg266_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -101,7 +146,7 @@ } -const struct heif_encoder_parameter** uvg266_list_parameters(void* encoder) +const heif_encoder_parameter** uvg266_list_parameters(void* encoder) { return uvg266_encoder_parameter_ptrs; } @@ -118,10 +163,10 @@ } -static struct heif_error uvg266_new_encoder(void** enc) +static heif_error uvg266_new_encoder(void** enc) { - struct encoder_struct_uvg266* encoder = new encoder_struct_uvg266(); - struct heif_error err = heif_error_ok; + encoder_struct_uvg266* encoder = new encoder_struct_uvg266(); + heif_error err = heif_error_ok; *enc = encoder; @@ -134,14 +179,14 @@ static void uvg266_free_encoder(void* encoder_raw) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + struct encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; delete encoder; } -static struct heif_error uvg266_set_parameter_quality(void* encoder_raw, int quality) +static heif_error uvg266_set_parameter_quality(void* encoder_raw, int quality) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -152,34 +197,34 @@ return heif_error_ok; } -static struct heif_error uvg266_get_parameter_quality(void* encoder_raw, int* quality) +static heif_error uvg266_get_parameter_quality(void* encoder_raw, int* quality) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -static struct heif_error uvg266_set_parameter_lossless(void* encoder_raw, int enable) +static heif_error uvg266_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; encoder->lossless = enable ? 1 : 0; return heif_error_ok; } -static struct heif_error uvg266_get_parameter_lossless(void* encoder_raw, int* enable) +static heif_error uvg266_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; *enable = encoder->lossless; return heif_error_ok; } -static struct heif_error uvg266_set_parameter_logging_level(void* encoder_raw, int logging) +static heif_error uvg266_set_parameter_logging_level(void* encoder_raw, int logging) { // struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; @@ -188,7 +233,7 @@ return heif_error_ok; } -static struct heif_error uvg266_get_parameter_logging_level(void* encoder_raw, int* loglevel) +static heif_error uvg266_get_parameter_logging_level(void* encoder_raw, int* loglevel) { // struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; @@ -198,9 +243,9 @@ } -static struct heif_error uvg266_set_parameter_integer(void* encoder_raw, const char* name, int value) +static heif_error uvg266_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return uvg266_set_parameter_quality(encoder, value); @@ -212,9 +257,9 @@ return heif_error_unsupported_parameter; } -static struct heif_error uvg266_get_parameter_integer(void* encoder_raw, const char* name, int* value) +static heif_error uvg266_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return uvg266_get_parameter_quality(encoder, value); @@ -227,7 +272,7 @@ } -static struct heif_error uvg266_set_parameter_boolean(void* encoder, const char* name, int value) +static heif_error uvg266_set_parameter_boolean(void* encoder, const char* name, int value) { if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return uvg266_set_parameter_lossless(encoder, value); @@ -249,13 +294,13 @@ */ -static struct heif_error uvg266_set_parameter_string(void* encoder_raw, const char* name, const char* value) +static heif_error uvg266_set_parameter_string(void* encoder_raw, const char* name, const char* value) { return heif_error_unsupported_parameter; } -static struct heif_error uvg266_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +static heif_error uvg266_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { return heif_error_unsupported_parameter; } @@ -263,8 +308,8 @@ static void uvg266_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = uvg266_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = uvg266_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -313,10 +358,10 @@ } void uvg266_query_encoded_size(void* encoder_raw, uint32_t input_width, uint32_t input_height, - uint32_t* encoded_width, uint32_t* encoded_height) + uint32_t* encoded_width, uint32_t* encoded_height) { - *encoded_width = (input_width + 7) & ~0x7; - *encoded_height = (input_height + 7) & ~0x7; + *encoded_width = (input_width + 7) & ~0x7U; + *encoded_height = (input_height + 7) & ~0x7U; } @@ -333,63 +378,135 @@ } #endif -static void append_chunk_data(uvg_data_chunk* data, std::vector& out) +void encoder_struct_uvg266::append_chunk_data(uvg_data_chunk* data, int framenr) { if (!data || data->len == 0) { return; } + std::vector input_data; + while (data) { + input_data.insert(input_data.end(), data->data, data->data + data->len); + data = data->next; + } + #if 0 std::cout << "DATA\n"; - std::cout << write_raw_data_as_hex(data->data, data->len, {}, {}); + std::cout << write_raw_data_as_hex(input_data.data(), input_data.size(), {}, {}); std::cout << "---\n"; #endif - size_t old_size = out.size(); - out.resize(old_size + data->len); - memcpy(out.data() + old_size, data->data, data->len); + //std::vector& pktdata = encoder->output_data.front().data; + size_t start_idx = 0; + + for (;;) + { + while (start_idx < input_data.size() - 3 && + (input_data[start_idx] != 0 || + input_data[start_idx + 1] != 0 || + input_data[start_idx + 2] != 1)) { + start_idx++; + } + + size_t end_idx = start_idx + 1; + + while (end_idx < input_data.size() - 3 && + (input_data[end_idx] != 0 || + input_data[end_idx + 1] != 0 || + input_data[end_idx + 2] != 1)) { + end_idx++; + } + + if (end_idx == input_data.size() - 3) { + end_idx = input_data.size(); + } + + encoder_struct_uvg266::Packet pkt; + pkt.data.resize(end_idx - start_idx - 3); + memcpy(pkt.data.data(), input_data.data() + start_idx + 3, pkt.data.size()); + pkt.frameNr = framenr; + pkt.more_nals = true; // will be set to 'false' for last NAL below - if (data->next) { - append_chunk_data(data->next, out); + // uint8_t nal_type = (pkt.data[1] >> 3); + // std::cout << "append frameNr=" << framenr << " NAL:" << ((int)nal_type) << " size:" << pkt.data.size() << "\n"; + + output_data.emplace_back(std::move(pkt)); + + if (end_idx == input_data.size()) { + break; + } + + start_idx = end_idx; + } + + if (!output_data.empty()) { + output_data.back().more_nals = false; } } -static void copy_plane(uvg_pixel* out_p, uint32_t out_stride, const uint8_t* in_p, uint32_t in_stride, int w, int h, int padded_width, int padded_height) +static void copy_plane(uvg_pixel* out_p, size_t out_stride, const uint8_t* in_p, size_t in_stride, + int w, int h, int padded_width, int padded_height, int bit_depth) { + int bpp = (bit_depth > 8) ? 2 : 1; + for (int y = 0; y < padded_height; y++) { int sy = std::min(y, h - 1); // source y - memcpy(out_p + y * out_stride, in_p + sy * in_stride, w); + memcpy(out_p + y * out_stride, in_p + sy * in_stride, w * bpp); if (padded_width > w) { - memset(out_p + y * out_stride + w, *(in_p + sy * in_stride + w - 1), padded_width - w); + memset(out_p + y * out_stride + w, *(in_p + sy * in_stride + w - 1), (padded_width - w) * bpp); } } } -static struct heif_error uvg266_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) -{ - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; +static heif_error uvg266_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; + + // close the encoder if it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + if (encoder->api) { + if (encoder->kvzencoder) { + encoder->api->encoder_close(encoder->kvzencoder); + encoder->kvzencoder = nullptr; + } + if (encoder->config) { + encoder->api->config_destroy(encoder->config); + encoder->config = nullptr; + } + } int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); - bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); - heif_chroma chroma = heif_image_get_chroma_format(image); + if (bit_depth != UVG_BIT_DEPTH) { + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + kError_unsupported_bit_depth + }; + } const uvg_api* api = uvg_api_get(bit_depth); if (api == nullptr) { - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Unsupported_bit_depth, - kError_unsupported_bit_depth + return { + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + kError_unsupported_bit_depth }; - return err; } + encoder->api = api; + uvg_config* config = api->config_alloc(); api->config_init(config); // param, encoder->preset.c_str(), encoder->tune.c_str()); + encoder->config = config; + #if HAVE_UVG266_ENABLE_LOGGING config->enable_logging_output = 0; #endif @@ -400,104 +517,39 @@ config->threads = 0; #endif -#if 1 -#if 0 - while (ctuSize > 16 && - (heif_image_get_width(image, heif_channel_Y) < ctuSize || - heif_image_get_height(image, heif_channel_Y) < ctuSize)) { - ctuSize /= 2; - } - - if (ctuSize < 16) { - api->config_destroy(config); - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } -#endif -#else - // TODO: There seems to be a bug in uvg266 where increasing the CTU size between - // multiple encoding jobs causes a segmentation fault. E.g. encoding multiple - // times with a CTU of 16 works, the next encoding with a CTU of 32 crashes. - // Use hardcoded value of 64 and reject images that are too small. - - if (heif_image_get_width(image, heif_channel_Y) < ctuSize || - heif_image_get_height(image, heif_channel_Y) < ctuSize) { - api->param_free(param); - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } -#endif - -#if 0 - // ctuSize should be a power of 2 in [16;64] - switch (ctuSize) { - case 64: - ctu = "64"; - break; - case 32: - ctu = "32"; - break; - case 16: - ctu = "16"; - break; - default: - struct heif_error err = { - heif_error_Encoder_plugin_error, - heif_suberror_Invalid_parameter_value, - kError_unsupported_image_size - }; - return err; - } - (void) ctu; -#endif + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + heif_chroma chroma = heif_image_get_chroma_format(image); int input_width = heif_image_get_width(image, heif_channel_Y); int input_height = heif_image_get_height(image, heif_channel_Y); - int input_chroma_width = 0; - int input_chroma_height = 0; - - uint32_t encoded_width, encoded_height; - uvg266_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height); - - uvg_chroma_format kvzChroma; - int chroma_stride_shift = 0; - int chroma_height_shift = 0; if (isGreyscale) { config->input_format = UVG_FORMAT_P400; - kvzChroma = UVG_CSP_400; + encoder->kvzChroma = UVG_CSP_400; } else if (chroma == heif_chroma_420) { config->input_format = UVG_FORMAT_P420; - kvzChroma = UVG_CSP_420; - chroma_stride_shift = 1; - chroma_height_shift = 1; - input_chroma_width = (input_width + 1) / 2; - input_chroma_height = (input_height + 1) / 2; + encoder->kvzChroma = UVG_CSP_420; + encoder->chroma_stride_shift = 1; + encoder->chroma_height_shift = 1; + encoder->input_chroma_width = (input_width + 1) / 2; + encoder->input_chroma_height = (input_height + 1) / 2; } else if (chroma == heif_chroma_422) { config->input_format = UVG_FORMAT_P422; - kvzChroma = UVG_CSP_422; - chroma_stride_shift = 1; - chroma_height_shift = 0; - input_chroma_width = (input_width + 1) / 2; - input_chroma_height = input_height; + encoder->kvzChroma = UVG_CSP_422; + encoder->chroma_stride_shift = 1; + encoder->chroma_height_shift = 0; + encoder->input_chroma_width = (input_width + 1) / 2; + encoder->input_chroma_height = input_height; } else if (chroma == heif_chroma_444) { config->input_format = UVG_FORMAT_P444; - kvzChroma = UVG_CSP_444; - chroma_stride_shift = 0; - chroma_height_shift = 0; - input_chroma_width = input_width; - input_chroma_height = input_height; + encoder->kvzChroma = UVG_CSP_444; + encoder->chroma_stride_shift = 0; + encoder->chroma_height_shift = 0; + encoder->input_chroma_width = input_width; + encoder->input_chroma_height = input_height; } else { return heif_error{ @@ -521,7 +573,7 @@ (void) h; } - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; heif_error err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { nclx = nullptr; @@ -545,12 +597,92 @@ config->vui.colormatrix = nclx->matrix_coefficients; } + // Write the pixel aspect ratio into the VUI. uvg266 emits aspect_ratio_info when both + // sar fields are >0. Extended_SAR stores sar_width/sar_height as u(16), so ratios that + // do not fit are only signalled through the pasp property. A 1:1 ratio is omitted + // (VUI absence already means unspecified/square). + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + config->vui.sar_width = (int32_t) aspect_h; + config->vui.sar_height = (int32_t) aspect_v; + } + config->qp = ((100 - encoder->quality) * 51 + 50) / 100; config->lossless = encoder->lossless ? 1 : 0; + if (image_sequence) { + if (options->keyframe_distance_max) { + config->intra_period = options->keyframe_distance_max; + } + + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + config->intra_period = 1; + break; + case heif_sequence_gop_structure_lowdelay: + config->gop_lowdelay = 1; + break; + case heif_sequence_gop_structure_unrestricted: + break; + } + } + + config->framerate_num = framerate_num; + config->framerate_denom = framerate_denom; + + uint32_t encoded_width, encoded_height; + uvg266_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height); + config->width = encoded_width; config->height = encoded_height; + uvg_encoder* kvzencoder = api->encoder_open(config); + if (!kvzencoder) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + encoder->kvzencoder = kvzencoder; + + return heif_error_ok; +} + + +static heif_error uvg266_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t framenr) +{ + // VVC signals one bit depth for all planes, and uvg266 is built for a single + // depth: uvg_pixel follows UVG_BIT_DEPTH. This has to be checked here and not + // only in uvg266_start_sequence_encoding_intern(), because that one runs for + // the first frame of a sequence only. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {UVG_BIT_DEPTH}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; + + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + + int input_width = heif_image_get_width(image, heif_channel_Y); + int input_height = heif_image_get_height(image, heif_channel_Y); + + int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + int bit_depth_chroma = isGreyscale ? bit_depth : heif_image_get_bits_per_pixel_range(image, heif_channel_Cb); + + uint32_t encoded_width, encoded_height; + uvg266_query_encoded_size(encoder_raw, input_width, input_height, &encoded_width, &encoded_height); + // Note: it is ok to cast away the const, as the image content is not changed. // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. /* @@ -562,9 +694,8 @@ } */ - uvg_picture* pic = api->picture_alloc_csp(kvzChroma, encoded_width, encoded_height); + uvg_picture* pic = encoder->api->picture_alloc_csp(encoder->kvzChroma, encoded_width, encoded_height); if (!pic) { - api->config_destroy(config); return heif_error{ heif_error_Encoder_plugin_error, heif_suberror_Encoder_encoding, @@ -573,47 +704,38 @@ } if (isGreyscale) { - int stride; - const uint8_t* data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); + size_t stride; + const uint8_t* data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); - copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height); + copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height, bit_depth); } else { - int stride; + size_t stride; const uint8_t* data; - data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); - copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height); + data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); + copy_plane(pic->y, pic->stride, data, stride, input_width, input_height, encoded_width, encoded_height, bit_depth); - data = heif_image_get_plane_readonly(image, heif_channel_Cb, &stride); - copy_plane(pic->u, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height, - encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift); - - data = heif_image_get_plane_readonly(image, heif_channel_Cr, &stride); - copy_plane(pic->v, pic->stride >> chroma_stride_shift, data, stride, input_chroma_width, input_chroma_height, - encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift); + data = heif_image_get_plane_readonly2(image, heif_channel_Cb, &stride); + copy_plane(pic->u, pic->stride >> encoder->chroma_stride_shift, data, stride, + encoder->input_chroma_width, encoder->input_chroma_height, + encoded_width >> encoder->chroma_stride_shift, + encoded_height >> encoder->chroma_height_shift, bit_depth_chroma); + + data = heif_image_get_plane_readonly2(image, heif_channel_Cr, &stride); + copy_plane(pic->v, pic->stride >> encoder->chroma_stride_shift, data, stride, + encoder->input_chroma_width, encoder->input_chroma_height, + encoded_width >> encoder->chroma_stride_shift, encoded_height >> encoder->chroma_height_shift, bit_depth_chroma); } - uvg_encoder* kvzencoder = api->encoder_open(config); - if (!kvzencoder) { - api->picture_free(pic); - api->config_destroy(config); - - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } uvg_data_chunk* data = nullptr; uint32_t data_len; int success; - success = api->encoder_headers(kvzencoder, &data, &data_len); +#if 0 + success = encoder->api->encoder_headers(encoder->kvzencoder, &data, &data_len); if (!success) { - api->picture_free(pic); - api->config_destroy(config); - api->encoder_close(kvzencoder); + encoder->api->picture_free(pic); return heif_error{ heif_error_Encoder_plugin_error, @@ -621,19 +743,23 @@ kError_unspecified_error }; } +#endif + + pic->pts = framenr; // If we write this, the data is twice in the output //append_chunk_data(data, encoder->output_data); - success = api->encoder_encode(kvzencoder, + uvg_picture* src_out = nullptr; + + success = encoder->api->encoder_encode(encoder->kvzencoder, pic, &data, &data_len, - nullptr, nullptr, nullptr); + nullptr, &src_out, nullptr); if (!success) { - api->chunk_free(data); - api->picture_free(pic); - api->config_destroy(config); - api->encoder_close(kvzencoder); + encoder->api->chunk_free(data); + encoder->api->picture_free(pic); + encoder->api->picture_free(src_out); return heif_error{ heif_error_Encoder_plugin_error, @@ -642,57 +768,51 @@ }; } - append_chunk_data(data, encoder->output_data); - - for (;;) { - success = api->encoder_encode(kvzencoder, - nullptr, - &data, &data_len, - nullptr, nullptr, nullptr); - if (!success) { - api->chunk_free(data); - api->picture_free(pic); - api->config_destroy(config); - api->encoder_close(kvzencoder); - - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } - - if (data == nullptr || data->len == 0) { - break; - } + if (src_out) { + encoder->append_chunk_data(data, (int)src_out->pts); - append_chunk_data(data, encoder->output_data); + encoder->api->picture_free(src_out); + src_out = nullptr; } - (void) success; + encoder->api->picture_free(pic); - api->chunk_free(data); - api->encoder_close(kvzencoder); - api->picture_free(pic); - api->config_destroy(config); + encoder->api->chunk_free(data); return heif_error_ok; } -static struct heif_error uvg266_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error uvg266_get_compressed_data_intern(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, int* more_frame_packets) { - struct encoder_struct_uvg266* encoder = (struct encoder_struct_uvg266*) encoder_raw; + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; - if (encoder->output_idx == encoder->output_data.size()) { + if (encoder->output_data.empty()) { *data = nullptr; *size = 0; return heif_error_ok; } + auto& pkg = encoder->output_data.front(); + + if (frame_nr) { + *frame_nr = pkg.frameNr; + } + + if (more_frame_packets) { + *more_frame_packets = pkg.more_nals; + } + + encoder->active_data = std::move(pkg.data); + encoder->output_data.pop_front(); + + *data = encoder->active_data.data(); + *size = (int)encoder->active_data.size(); + +#if 0 size_t start_idx = encoder->output_idx; while (start_idx < encoder->output_data.size() - 3 && @@ -719,14 +839,103 @@ *size = (int) (end_idx - start_idx - 3); encoder->output_idx = end_idx; +#endif + + return heif_error_ok; +} + +static heif_error uvg266_start_sequence_encoding(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return uvg266_start_sequence_encoding_intern(encoder_raw, image, input_class, + framerate_num, framerate_denom, options, true); +} + +static heif_error uvg266_end_sequence_encoding(void* encoder_raw) +{ + encoder_struct_uvg266* encoder = (encoder_struct_uvg266*) encoder_raw; + + // --- flush + + uvg_data_chunk* data = nullptr; + uint32_t data_len; + + // uvg_picture* src_out = nullptr; + + int success; + uvg_picture* src_out = nullptr; + + for (;;) { + success = encoder->api->encoder_encode(encoder->kvzencoder, + nullptr, + &data, &data_len, + nullptr, &src_out, nullptr); + if (!success) { + encoder->api->chunk_free(data); + encoder->api->picture_free(src_out); + + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + if (data == nullptr || data->len == 0) { + encoder->api->chunk_free(data); + encoder->api->picture_free(src_out); + break; + } + + encoder->append_chunk_data(data, (int)src_out->pts); + + encoder->api->chunk_free(data); + encoder->api->picture_free(src_out); + src_out = nullptr; + } + + + return heif_error_ok; +} + +static heif_error uvg266_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = uvg266_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } + + err = uvg266_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } + + uvg266_end_sequence_encoding(encoder_raw); return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_uvg266 +static heif_error uvg266_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return uvg266_get_compressed_data_intern(encoder_raw, data, size, nullptr, nullptr, nullptr); +} + +static heif_error uvg266_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, int* more_frame_packets) +{ + return uvg266_get_compressed_data_intern(encoder_raw, data, size, frame_nr, is_keyframe, more_frame_packets); +} + + +static const heif_encoder_plugin encoder_plugin_uvg266 { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_VVC, /* id_name */ "uvg266", /* priority */ uvg266_PLUGIN_PRIORITY, @@ -754,11 +963,16 @@ /* encode_image */ uvg266_encode_image, /* get_compressed_data */ uvg266_get_compressed_data, /* query_input_colorspace (v2) */ uvg266_query_input_colorspace2, - /* query_encoded_size (v3) */ uvg266_query_encoded_size + /* query_encoded_size (v3) */ uvg266_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ uvg266_start_sequence_encoding, + /* encode_sequence_frame (v4) */ uvg266_encode_sequence_frame, + /* end_sequence_encoding (v4) */ uvg266_end_sequence_encoding, + /* get_compressed_data2 (v4) */ uvg266_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 0 }; -const struct heif_encoder_plugin* get_encoder_plugin_uvg266() -{ +const struct heif_encoder_plugin* get_encoder_plugin_uvg266() { return &encoder_plugin_uvg266; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_vvenc.cc libheif-1.23.4/libheif/plugins/encoder_vvenc.cc --- libheif-1.19.8/libheif/plugins/encoder_vvenc.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_vvenc.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,12 +21,15 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_vvenc.h" +#include "encoder_input_check.h" #include #include // apparently, this is a false positive of cpplint #include #include #include #include +#include +#include extern "C" { #include @@ -46,8 +49,31 @@ int quality = 32; bool lossless = false; - std::vector output_data; - size_t output_idx = 0; + // --- encoder + + vvencEncoder* vvencoder = nullptr; + vvencChromaFormat vvencChroma; + uint32_t encoded_width=0, encoded_height=0; + + // --- output + + struct Packet + { + std::vector data; + uintptr_t frameNr = 0; + bool more_nals = false; + }; + + std::deque output_data; + + std::vector active_data; // holds the data that we just returned + + ~encoder_struct_vvenc() + { + if (vvencoder) { + vvenc_encoder_close(vvencoder); + } + } }; static const int vvenc_PLUGIN_PRIORITY = 100; @@ -69,13 +95,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter vvenc_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* vvenc_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter vvenc_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* vvenc_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void vvenc_init_parameters() { - struct heif_encoder_parameter* p = vvenc_encoder_params; - const struct heif_encoder_parameter** d = vvenc_encoder_parameter_ptrs; + heif_encoder_parameter* p = vvenc_encoder_params; + const heif_encoder_parameter** d = vvenc_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -103,7 +129,7 @@ } -const struct heif_encoder_parameter** vvenc_list_parameters(void* encoder) +const heif_encoder_parameter** vvenc_list_parameters(void* encoder) { return vvenc_encoder_parameter_ptrs; } @@ -120,10 +146,10 @@ } -static struct heif_error vvenc_new_encoder(void** enc) +static heif_error vvenc_new_encoder(void** enc) { - struct encoder_struct_vvenc* encoder = new encoder_struct_vvenc(); - struct heif_error err = heif_error_ok; + encoder_struct_vvenc* encoder = new encoder_struct_vvenc(); + heif_error err = heif_error_ok; *enc = encoder; @@ -136,14 +162,14 @@ static void vvenc_free_encoder(void* encoder_raw) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; delete encoder; } -static struct heif_error vvenc_set_parameter_quality(void* encoder_raw, int quality) +static heif_error vvenc_set_parameter_quality(void* encoder_raw, int quality) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -154,34 +180,34 @@ return heif_error_ok; } -static struct heif_error vvenc_get_parameter_quality(void* encoder_raw, int* quality) +static heif_error vvenc_get_parameter_quality(void* encoder_raw, int* quality) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; *quality = encoder->quality; return heif_error_ok; } -static struct heif_error vvenc_set_parameter_lossless(void* encoder_raw, int enable) +static heif_error vvenc_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; encoder->lossless = enable ? 1 : 0; return heif_error_ok; } -static struct heif_error vvenc_get_parameter_lossless(void* encoder_raw, int* enable) +static heif_error vvenc_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; *enable = encoder->lossless; return heif_error_ok; } -static struct heif_error vvenc_set_parameter_logging_level(void* encoder_raw, int logging) +static heif_error vvenc_set_parameter_logging_level(void* encoder_raw, int logging) { // struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; @@ -190,7 +216,7 @@ return heif_error_ok; } -static struct heif_error vvenc_get_parameter_logging_level(void* encoder_raw, int* loglevel) +static heif_error vvenc_get_parameter_logging_level(void* encoder_raw, int* loglevel) { // struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; @@ -200,9 +226,9 @@ } -static struct heif_error vvenc_set_parameter_integer(void* encoder_raw, const char* name, int value) +static heif_error vvenc_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return vvenc_set_parameter_quality(encoder, value); @@ -214,9 +240,9 @@ return heif_error_unsupported_parameter; } -static struct heif_error vvenc_get_parameter_integer(void* encoder_raw, const char* name, int* value) +static heif_error vvenc_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return vvenc_get_parameter_quality(encoder, value); @@ -229,7 +255,7 @@ } -static struct heif_error vvenc_set_parameter_boolean(void* encoder, const char* name, int value) +static heif_error vvenc_set_parameter_boolean(void* encoder, const char* name, int value) { if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return vvenc_set_parameter_lossless(encoder, value); @@ -251,13 +277,13 @@ */ -static struct heif_error vvenc_set_parameter_string(void* encoder_raw, const char* name, const char* value) +static heif_error vvenc_set_parameter_string(void* encoder_raw, const char* name, const char* value) { return heif_error_unsupported_parameter; } -static struct heif_error vvenc_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +static heif_error vvenc_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { return heif_error_unsupported_parameter; } @@ -265,8 +291,8 @@ static void vvenc_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = vvenc_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = vvenc_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -317,64 +343,235 @@ void vvenc_query_encoded_size(void* encoder_raw, uint32_t input_width, uint32_t input_height, uint32_t* encoded_width, uint32_t* encoded_height) { - *encoded_width = (input_width + 7) & ~0x7; - *encoded_height = (input_height + 7) & ~0x7; + *encoded_width = (input_width + 7) & ~0x7U; + *encoded_height = (input_height + 7) & ~0x7U; } #include #include -static void append_chunk_data(struct encoder_struct_vvenc* encoder, vvencAccessUnit* au) +static void append_chunk_data(struct encoder_struct_vvenc* encoder, vvencAccessUnit* au, + uintptr_t pts) { #if 0 - std::cout << "DATA\n"; + std::cout << "DATA pts=" << pts << "\n"; std::cout << write_raw_data_as_hex(au->payload, au->payloadUsedSize, {}, {}); std::cout << "---\n"; #endif - size_t old_size = encoder->output_data.size(); - encoder->output_data.resize(old_size + au->payloadUsedSize); - memcpy(encoder->output_data.data() + old_size, au->payload, au->payloadUsedSize); + std::vector dat; + dat.resize(au->payloadUsedSize); + memcpy(dat.data(), au->payload, au->payloadUsedSize); + + int start_idx = 0; + + for (;;) + { + while (start_idx < au->payloadUsedSize - 3 && + (au->payload[start_idx] != 0 || + au->payload[start_idx + 1] != 0 || + au->payload[start_idx + 2] != 1)) { + start_idx++; + } + + int end_idx = start_idx + 1; + + while (end_idx < au->payloadUsedSize - 3 && + (au->payload[end_idx] != 0 || + au->payload[end_idx + 1] != 0 || + au->payload[end_idx + 2] != 1)) { + end_idx++; + } + + if (end_idx == au->payloadUsedSize - 3) { + end_idx = au->payloadUsedSize; + } + + encoder_struct_vvenc::Packet pkt; + pkt.data.resize(end_idx - start_idx - 3); + memcpy(pkt.data.data(), au->payload + start_idx + 3, pkt.data.size()); + pkt.frameNr = pts; + pkt.more_nals = true; // will be set to 'false' for last NAL below. + + //std::cout << "append frameNr=" << pts << " NAL:" << ((int)pkt.data[1]>>3) << " size:" << pkt.data.size() << "\n"; + + encoder->output_data.emplace_back(std::move(pkt)); + + if (end_idx == au->payloadUsedSize) { + break; + } + + start_idx = end_idx; + } + + if (!encoder->output_data.empty()) { + encoder->output_data.back().more_nals = false; + } } -static void copy_plane(int16_t*& out_p, int& out_stride, const uint8_t* in_p, uint32_t in_stride, int w, int h, int padded_width, int padded_height) +static void copy_plane(int16_t* dst_p, size_t dst_stride, const uint8_t* in_p, size_t in_stride, int w, int h, int padded_width, int padded_height) { - out_stride = padded_width; - out_p = new int16_t[out_stride * w * h]; - for (int y = 0; y < padded_height; y++) { int sy = std::min(y, h - 1); // source y for (int x = 0; x < w; x++) { - out_p[y * out_stride + x] = in_p[sy * in_stride + x]; + dst_p[y * dst_stride + x] = in_p[sy * in_stride + x]; } for (int x = w; x < padded_width; x++) { - out_p[y * out_stride + x] = in_p[sy * in_stride + w - 1]; + dst_p[y * dst_stride + x] = in_p[sy * in_stride + w - 1]; } } } -static struct heif_error vvenc_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) -{ - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; +static heif_error vvenc_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; + + // close the encoder if it was already initialized + // (e.g. when the encoder is reused for alpha encoding after being used for YUV encoding) + if (encoder->vvencoder) { + vvenc_encoder_close(encoder->vvencoder); + encoder->vvencoder = nullptr; + } + + vvenc_config params; int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + if (bit_depth != 8) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, + kError_unsupported_bit_depth + }; + } + + int input_width = heif_image_get_width(image, heif_channel_Y); + int input_height = heif_image_get_height(image, heif_channel_Y); + + vvenc_query_encoded_size(encoder_raw, input_width, input_height, + &encoder->encoded_width, &encoder->encoded_height); + + + // invert encoder quality range and scale to 0-63 + int encoder_quality = 63 - encoder->quality*63/100; + + int ret = vvenc_init_default(¶ms, + static_cast(encoder->encoded_width), + static_cast(encoder->encoded_height), + static_cast((framerate_denom + framerate_num / 2) / framerate_num), + 0, + encoder_quality, + VVENC_MEDIUM); + if (ret != VVENC_OK) { + // TODO: cleanup memory + + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + params.m_inputBitDepth[0] = bit_depth; + params.m_inputBitDepth[1] = bit_depth; + params.m_outputBitDepth[0] = bit_depth; + params.m_outputBitDepth[1] = bit_depth; + params.m_internalBitDepth[0] = bit_depth; + params.m_internalBitDepth[1] = bit_depth; + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); - heif_chroma chroma = heif_image_get_chroma_format(image); + if (isGreyscale) { + params.m_internChromaFormat = VVENC_CHROMA_400; + } + + params.m_FrameRate = framerate_denom; + params.m_FrameScale = framerate_num; + + if (image_sequence) { + if (options->keyframe_distance_max) { + params.m_IntraPeriod = options->keyframe_distance_max; + } + + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + params.m_IntraPeriod = 1; + break; + case heif_sequence_gop_structure_lowdelay: + params.m_picReordering = 0; + params.m_GOPSize = 8; // as of vvcenc 1.13.1, this only works with GOPSize=8. see https://github.com/fraunhoferhhi/vvenc/issues/284 + params.m_DecodingRefreshType = VVENC_DRT_NONE; + params.m_poc0idr = true; + break; + case heif_sequence_gop_structure_unrestricted: + ; + } + } + + // Write the pixel aspect ratio into the VUI. Extended_SAR (idc 255) stores + // sar_width/sar_height as u(16), so ratios that do not fit are only signalled through + // the pasp property. A 1:1 ratio is omitted (VUI absence already means unspecified/square). + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + params.m_vuiParametersPresent = 1; + params.m_aspectRatioInfoPresent = true; + params.m_aspectRatioIdc = 255; // Extended_SAR + params.m_sarWidth = static_cast(aspect_h); + params.m_sarHeight = static_cast(aspect_v); + } + + vvencEncoder* vvencoder = encoder->vvencoder = vvenc_encoder_create(); + + //ret = vvenc_check_config(vvencoder, ¶ms); + //const char* err = vvenc_get_last_error(vvencoder); + + ret = vvenc_encoder_open(vvencoder, ¶ms); + //err = vvenc_get_last_error(vvencoder); + if (ret != VVENC_OK) { + // TODO: cleanup memory - if (bit_depth != 8) { return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Unsupported_image_type, - kError_unsupported_bit_depth - }; + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; + } + + return heif_error_ok; +} + + +static heif_error vvenc_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t framenr) +{ + // VVC signals one bit depth for all planes, and this plugin only + // implements 8 bit encoding. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {8}); + if (input_error.code != heif_error_Ok) { + return input_error; } + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; + vvencEncoder* vvencoder = encoder->vvencoder; + + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + heif_chroma chroma = heif_image_get_chroma_format(image); + + int input_width = heif_image_get_width(image, heif_channel_Y); int input_height = heif_image_get_height(image, heif_channel_Y); @@ -421,6 +618,8 @@ }; } + encoder->vvencChroma = vvencChroma; + if (chroma != heif_chroma_monochrome) { int w = heif_image_get_width(image, heif_channel_Y); int h = heif_image_get_height(image, heif_channel_Y); @@ -436,45 +635,7 @@ } - vvenc_config params; - - // invert encoder quality range and scale to 0-63 - int encoder_quality = 63 - encoder->quality*63/100; - - int ret = vvenc_init_default(¶ms, encoded_width, encoded_height, 25, 0, - encoder_quality, - VVENC_MEDIUM); - if (ret != VVENC_OK) { - // TODO: cleanup memory - - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } - - params.m_inputBitDepth[0] = bit_depth; - params.m_inputBitDepth[1] = bit_depth; - params.m_outputBitDepth[0] = bit_depth; - params.m_outputBitDepth[1] = bit_depth; - params.m_internalBitDepth[0] = bit_depth; - params.m_internalBitDepth[1] = bit_depth; - - vvencEncoder* vvencoder = vvenc_encoder_create(); - ret = vvenc_encoder_open(vvencoder, ¶ms); - if (ret != VVENC_OK) { - // TODO: cleanup memory - - return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; - } - - - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; heif_error err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { nclx = nullptr; @@ -527,65 +688,40 @@ // vvenc_init_pass( encoder, pass, statsfilename ); - int16_t* yptr = nullptr; - int16_t* cbptr = nullptr; - int16_t* crptr = nullptr; - int ystride = 0; - int cbstride = 0; - int crstride = 0; - if (isGreyscale) { - int stride; - const uint8_t* data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); + size_t stride; + const uint8_t* data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); - copy_plane(yptr, ystride, data, stride, input_width, input_height, encoded_width, encoded_height); - - yuvbuf->planes[0].ptr = yptr; - yuvbuf->planes[0].width = encoded_width; - yuvbuf->planes[0].height = encoded_height; - yuvbuf->planes[0].stride = ystride; + copy_plane(yuvbuf->planes[0].ptr, yuvbuf->planes[0].stride, data, stride, input_width, input_height, encoded_width, encoded_height); } else { - int stride; + size_t stride; const uint8_t* data; - data = heif_image_get_plane_readonly(image, heif_channel_Y, &stride); - copy_plane(yptr, ystride, data, stride, input_width, input_height, encoded_width, encoded_height); + data = heif_image_get_plane_readonly2(image, heif_channel_Y, &stride); + copy_plane(yuvbuf->planes[0].ptr, yuvbuf->planes[0].stride, data, stride, input_width, input_height, encoded_width, encoded_height); - data = heif_image_get_plane_readonly(image, heif_channel_Cb, &stride); - copy_plane(cbptr, cbstride, data, stride, input_chroma_width, input_chroma_height, + data = heif_image_get_plane_readonly2(image, heif_channel_Cb, &stride); + copy_plane(yuvbuf->planes[1].ptr, yuvbuf->planes[1].stride, data, stride, input_chroma_width, input_chroma_height, encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift); - data = heif_image_get_plane_readonly(image, heif_channel_Cr, &stride); - copy_plane(crptr, crstride, data, stride, input_chroma_width, input_chroma_height, + data = heif_image_get_plane_readonly2(image, heif_channel_Cr, &stride); + copy_plane(yuvbuf->planes[2].ptr, yuvbuf->planes[2].stride, data, stride, input_chroma_width, input_chroma_height, encoded_width >> chroma_stride_shift, encoded_height >> chroma_height_shift); - - yuvbuf->planes[0].ptr = yptr; - yuvbuf->planes[0].width = encoded_width; - yuvbuf->planes[0].height = encoded_height; - yuvbuf->planes[0].stride = ystride; - - yuvbuf->planes[1].ptr = cbptr; - yuvbuf->planes[1].width = encoded_width >> chroma_stride_shift; - yuvbuf->planes[1].height = encoded_height >> chroma_height_shift; - yuvbuf->planes[1].stride = cbstride; - - yuvbuf->planes[2].ptr = crptr; - yuvbuf->planes[2].width = encoded_width >> chroma_stride_shift; - yuvbuf->planes[2].height = encoded_height >> chroma_height_shift; - yuvbuf->planes[2].stride = crstride; } - //yuvbuf->cts = frame->pts; - //yuvbuf->ctsValid = true; + + yuvbuf->cts = framenr; + yuvbuf->ctsValid = true; bool encDone; - ret = vvenc_encode(vvencoder, yuvbuf, au, &encDone); + int ret = vvenc_encode(vvencoder, yuvbuf, au, &encDone); if (ret != VVENC_OK) { - vvenc_encoder_close(vvencoder); - vvenc_YUVBuffer_free(yuvbuf, true); // release storage and payload memory + //vvenc_encoder_close(vvencoder); + vvenc_YUVBuffer_free_buffer(yuvbuf); + vvenc_YUVBuffer_free(yuvbuf, false); // release storage and payload memory vvenc_accessUnit_free(au, true); // release storage and payload memory return heif_error{ @@ -596,88 +732,143 @@ } if (au->payloadUsedSize > 0) { - append_chunk_data(encoder, au); + append_chunk_data(encoder, au, au->cts); } + vvenc_YUVBuffer_free_buffer(yuvbuf); + vvenc_YUVBuffer_free(yuvbuf, false); // release storage and payload memory + vvenc_accessUnit_free(au, true); // release storage and payload memory + + /* + delete[] yptr; + delete[] cbptr; + delete[] crptr; +*/ + + return heif_error_ok; +} + + +static heif_error vvenc_end_sequence_encoding(void* encoder_raw) +{ + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; + vvencEncoder* vvencoder = encoder->vvencoder; + + vvencAccessUnit* au = vvenc_accessUnit_alloc(); + + const int auSizeScale = (encoder->vvencChroma <= VVENC_CHROMA_420 ? 2 : 3); + vvenc_accessUnit_alloc_payload(au, auSizeScale * encoder->encoded_width * encoder->encoded_height + 1024); + + bool encDone = false; + while (!encDone) { - ret = vvenc_encode(vvencoder, nullptr, au, &encDone); + int ret = vvenc_encode(vvencoder, nullptr, au, &encDone); if (ret != VVENC_OK) { - vvenc_encoder_close(vvencoder); - vvenc_YUVBuffer_free(yuvbuf, true); // release storage and payload memory + //vvenc_encoder_close(vvencoder); vvenc_accessUnit_free(au, true); // release storage and payload memory return heif_error{ - heif_error_Encoder_plugin_error, - heif_suberror_Encoder_encoding, - kError_unspecified_error - }; + heif_error_Encoder_plugin_error, + heif_suberror_Encoder_encoding, + kError_unspecified_error + }; } if (au->payloadUsedSize > 0) { - append_chunk_data(encoder, au); + append_chunk_data(encoder, au, au->cts); } } - vvenc_encoder_close(vvencoder); - vvenc_YUVBuffer_free(yuvbuf, true); // release storage and payload memory vvenc_accessUnit_free(au, true); // release storage and payload memory - /* - delete[] yptr; - delete[] cbptr; - delete[] crptr; -*/ + return heif_error_ok; +} + + +static heif_error vvenc_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = vvenc_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } + + err = vvenc_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } + + vvenc_end_sequence_encoding(encoder_raw); return heif_error_ok; } +static heif_error vvenc_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return vvenc_start_sequence_encoding_intern(encoder_raw, image, input_class, framerate_num, framerate_denom, options, true); +} + + -static struct heif_error vvenc_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error vvenc_get_compressed_data_intern(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* more_frame_packets) { - struct encoder_struct_vvenc* encoder = (struct encoder_struct_vvenc*) encoder_raw; + encoder_struct_vvenc* encoder = (encoder_struct_vvenc*) encoder_raw; - if (encoder->output_idx == encoder->output_data.size()) { + if (encoder->output_data.empty()) { *data = nullptr; *size = 0; return heif_error_ok; } - size_t start_idx = encoder->output_idx; + std::vector& pktdata = encoder->output_data.front().data; - while (start_idx < encoder->output_data.size() - 3 && - (encoder->output_data[start_idx] != 0 || - encoder->output_data[start_idx + 1] != 0 || - encoder->output_data[start_idx + 2] != 1)) { - start_idx++; + if (frame_nr) { + *frame_nr = encoder->output_data.front().frameNr; } - - size_t end_idx = start_idx + 1; - - while (end_idx < encoder->output_data.size() - 3 && - (encoder->output_data[end_idx] != 0 || - encoder->output_data[end_idx + 1] != 0 || - encoder->output_data[end_idx + 2] != 1)) { - end_idx++; +/* + if (more_frame_packets && + encoder->output_data.size() > 1 && + encoder->output_data[0].frameNr == encoder->output_data[1].frameNr) { + *more_frame_packets = 1; } +*/ - if (end_idx == encoder->output_data.size() - 3) { - end_idx = encoder->output_data.size(); + if (more_frame_packets) { + *more_frame_packets = encoder->output_data.front().more_nals; } - *data = encoder->output_data.data() + start_idx + 3; - *size = (int) (end_idx - start_idx - 3); + encoder->active_data = std::move(pktdata); + encoder->output_data.pop_front(); - encoder->output_idx = end_idx; + *data = encoder->active_data.data(); + *size = static_cast(encoder->active_data.size()); return heif_error_ok; } -static const struct heif_encoder_plugin encoder_plugin_vvenc +static heif_error vvenc_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return vvenc_get_compressed_data_intern(encoder_raw, data, size, nullptr, nullptr); +} + +static heif_error vvenc_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, int* more_frame_packets) +{ + return vvenc_get_compressed_data_intern(encoder_raw, data, size, frame_nr, more_frame_packets); +} + + +static const heif_encoder_plugin encoder_plugin_vvenc { - /* plugin_api_version */ 3, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_VVC, /* id_name */ "vvenc", /* priority */ vvenc_PLUGIN_PRIORITY, @@ -705,10 +896,16 @@ /* encode_image */ vvenc_encode_image, /* get_compressed_data */ vvenc_get_compressed_data, /* query_input_colorspace (v2) */ vvenc_query_input_colorspace2, - /* query_encoded_size (v3) */ vvenc_query_encoded_size + /* query_encoded_size (v3) */ vvenc_query_encoded_size, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ vvenc_start_sequence_encoding, + /* encode_sequence_frame (v4) */ vvenc_encode_sequence_frame, + /* end_sequence_encoding (v4) */ vvenc_end_sequence_encoding, + /* get_compressed_data2 (v4) */ vvenc_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 0 }; -const struct heif_encoder_plugin* get_encoder_plugin_vvenc() +const heif_encoder_plugin* get_encoder_plugin_vvenc() { return &encoder_plugin_vvenc; } diff -Nru libheif-1.19.8/libheif/plugins/encoder_x264.cc libheif-1.23.4/libheif/plugins/encoder_x264.cc --- libheif-1.19.8/libheif/plugins/encoder_x264.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_x264.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,1288 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "libheif/heif.h" +#include "libheif/heif_plugin.h" +#include "encoder_x264.h" +#include "encoder_input_check.h" +#include +#include +#include +#include +#include +#include + +#include "logging.h" +#include + +extern "C" { +#include +} + +#if 0 +static const char* naltype_table[] = { + /* 0 */ "unspecified", + /* 1 */ "non-IDR", + /* 2 */ "partition A", + /* 3 */ "partition B", + /* 4 */ "partition C", + /* 5 */ "IDR", + /* 6 */ "SEI", + /* 7 */ "SPS", + /* 8 */ "PPS", + /* 9 */ "AU-delimiter", + /* 10 */ "EOSequence", + /* 11 */ "EOStream", + /* 12 */ "FillerData", + /* 13 */ "SPS-extension", + /* 14 */ "Prefix-NAL", + /* 15 */ "Subset-SPS", + /* 16 */ "reserved", + /* 17 */ "reserved", + /* 18 */ "reserved", + /* 19 */ "aux-coded-picture", + /* 20 */ "slice-in-scalable-extension" +}; + + +static const char* naltype(uint8_t type) +{ + if (type <= 20) { + return naltype_table[type]; + } + else { + return "reserved"; + } +} +#endif + + +enum parameter_type +{ + UndefinedType, Int, Bool, String +}; + +struct parameter +{ + parameter_type type = UndefinedType; + std::string name; + + int value_int = 0; // also used for boolean + std::string value_string; +}; + + +struct encoder_struct_x264 +{ + x264_t* encoder = nullptr; + x264_param_t param{}; + + uintptr_t out_frameNr = 0; + int bit_depth = 0; + + heif_chroma chroma; + + + // --- parameters + + std::vector parameters; + + void add_param(const parameter&); + + void add_param(const std::string& name, int value); + + void add_param(const std::string& name, bool value); + + void add_param(const std::string& name, const std::string& value); + + parameter get_param(const std::string& name) const; + + std::string preset; + std::string tune; + + int logLevel = X264_LOG_NONE; + + // --- output + + struct Packet + { + std::vector data; + uintptr_t frameNr = 0; + bool more_packets = false; + }; + + std::deque m_output_packets; + std::vector m_active_output; + + void append_nals(const x264_nal_t* nals, int num_nals, uintptr_t frameNr); + + std::string last_error_message; +}; + + +void encoder_struct_x264::append_nals(const x264_nal_t* nals, int num_nals, uintptr_t frameNr) +{ +#if 0 + std::cout << "append " << num_nals << " NALs for frame " << frameNr << "\n"; + + for (int i=0;iversion = 2; + p->name = heif_encoder_parameter_name_quality; + p->type = heif_encoder_parameter_type_integer; + p->integer.default_value = 50; + p->has_default = true; + p->integer.have_minimum_maximum = true; + p->integer.minimum = 0; + p->integer.maximum = 100; + p->integer.valid_values = NULL; + p->integer.num_valid_values = 0; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = heif_encoder_parameter_name_lossless; + p->type = heif_encoder_parameter_type_boolean; + p->boolean.default_value = false; + p->has_default = true; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = kParam_preset; + p->type = heif_encoder_parameter_type_string; + p->string.default_value = "slow"; // increases computation time + p->has_default = true; + p->string.valid_values = kParam_preset_valid_values; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = kParam_tune; + p->type = heif_encoder_parameter_type_string; + p->string.default_value = "ssim"; + p->has_default = true; + p->string.valid_values = kParam_tune_valid_values; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = kParam_TU_intra_depth; + p->type = heif_encoder_parameter_type_integer; + p->integer.default_value = 2; // increases computation time + p->has_default = true; + p->integer.have_minimum_maximum = true; + p->integer.minimum = 1; + p->integer.maximum = 4; + p->integer.valid_values = NULL; + p->integer.num_valid_values = 0; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = kParam_complexity; + p->type = heif_encoder_parameter_type_integer; + p->integer.default_value = 50; + p->has_default = false; + p->integer.have_minimum_maximum = true; + p->integer.minimum = 0; + p->integer.maximum = 100; + p->integer.valid_values = NULL; + p->integer.num_valid_values = 0; + d[i++] = p++; + + assert(i < MAX_NPARAMETERS); + p->version = 2; + p->name = kParam_chroma; + p->type = heif_encoder_parameter_type_string; + p->string.default_value = "420"; + p->has_default = true; + p->string.valid_values = kParam_chroma_valid_values; + d[i++] = p++; + + d[i++] = nullptr; +} + + +const heif_encoder_parameter** x264_list_parameters(void* encoder) +{ + return x264_encoder_parameter_ptrs; +} + + +static void x264_init_plugin() +{ + x264_init_parameters(); +} + + +static void x264_cleanup_plugin() +{ +} + + +static heif_error x264_new_encoder(void** enc) +{ + encoder_struct_x264* encoder = new encoder_struct_x264(); + heif_error err = heif_error_ok; + + + // encoder has to be allocated in x264_encode_image, because it needs to know the image size + encoder->encoder = nullptr; + + encoder->bit_depth = 8; + + *enc = encoder; + + + // set default parameters + + x264_set_default_parameters(encoder); + + return err; +} + +static void x264_free_encoder(void* encoder_raw) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (encoder->encoder) { + x264_encoder_close(encoder->encoder); + encoder->encoder = nullptr; + } + + delete encoder; +} + +static heif_error x264_set_parameter_quality(void* encoder_raw, int quality) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (quality < 0 || quality > 100) { + return heif_error_invalid_parameter_value; + } + + encoder->add_param(heif_encoder_parameter_name_quality, quality); + + return heif_error_ok; +} + +static heif_error x264_get_parameter_quality(void* encoder_raw, int* quality) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + parameter p = encoder->get_param(heif_encoder_parameter_name_quality); + *quality = p.value_int; + + return heif_error_ok; +} + +static heif_error x264_set_parameter_lossless(void* encoder_raw, int enable) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + encoder->add_param(heif_encoder_parameter_name_lossless, (bool) enable); + + return heif_error_ok; +} + +static heif_error x264_get_parameter_lossless(void* encoder_raw, int* enable) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + parameter p = encoder->get_param(heif_encoder_parameter_name_lossless); + *enable = p.value_int; + + return heif_error_ok; +} + +static heif_error x264_set_parameter_logging_level(void* encoder_raw, int logging) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (logging < 0 || logging > 4) { + return heif_error_invalid_parameter_value; + } + + encoder->logLevel = logging; + + return heif_error_ok; +} + +static heif_error x264_get_parameter_logging_level(void* encoder_raw, int* loglevel) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + *loglevel = encoder->logLevel; + + return heif_error_ok; +} + + +static heif_error x264_set_parameter_integer(void* encoder_raw, const char* name, int value) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { + return x264_set_parameter_quality(encoder, value); + } + else if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { + return x264_set_parameter_lossless(encoder, value); + } + else if (strcmp(name, kParam_TU_intra_depth) == 0) { + if (value < 1 || value > 4) { + return heif_error_invalid_parameter_value; + } + + encoder->add_param(name, value); + return heif_error_ok; + } + else if (strcmp(name, kParam_complexity) == 0) { + if (value < 0 || value > 100) { + return heif_error_invalid_parameter_value; + } + + encoder->add_param(name, value); + return heif_error_ok; + } + + return heif_error_unsupported_parameter; +} + +static heif_error x264_get_parameter_integer(void* encoder_raw, const char* name, int* value) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { + return x264_get_parameter_quality(encoder, value); + } + else if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { + return x264_get_parameter_lossless(encoder, value); + } + else if (strcmp(name, kParam_TU_intra_depth) == 0) { + *value = encoder->get_param(name).value_int; + return heif_error_ok; + } + else if (strcmp(name, kParam_complexity) == 0) { + *value = encoder->get_param(name).value_int; + return heif_error_ok; + } + + return heif_error_unsupported_parameter; +} + + +static heif_error x264_set_parameter_boolean(void* encoder, const char* name, int value) +{ + if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { + return x264_set_parameter_lossless(encoder, value); + } + + return heif_error_unsupported_parameter; +} + +// Unused, will use "x264_get_parameter_integer" instead. +/* +static struct heif_error x264_get_parameter_boolean(void* encoder, const char* name, int* value) +{ + if (strcmp(name, heif_encoder_parameter_name_lossless)==0) { + return x264_get_parameter_lossless(encoder,value); + } + + return heif_error_unsupported_parameter; +} +*/ + + +static bool string_list_contains(const char* const* values_list, const char* value) +{ + for (int i = 0; values_list[i]; i++) { + if (strcmp(values_list[i], value) == 0) { + return true; + } + } + + return false; +} + + +static heif_error x264_set_parameter_string(void* encoder_raw, const char* name, const char* value) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (strcmp(name, kParam_preset) == 0) { + if (!string_list_contains(kParam_preset_valid_values, value)) { + return heif_error_invalid_parameter_value; + } + + encoder->preset = value; + return heif_error_ok; + } + else if (strcmp(name, kParam_tune) == 0) { + if (!string_list_contains(kParam_tune_valid_values, value)) { + return heif_error_invalid_parameter_value; + } + + encoder->tune = value; + return heif_error_ok; + } + else if (strncmp(name, "x264:", 5) == 0) { + encoder->add_param(name, std::string(value)); + return heif_error_ok; + } + else if (strcmp(name, kParam_chroma) == 0) { + if (strcmp(value, "420") == 0) { + encoder->chroma = heif_chroma_420; + return heif_error_ok; + } + else if (strcmp(value, "422") == 0) { + encoder->chroma = heif_chroma_422; + return heif_error_ok; + } + else if (strcmp(value, "444") == 0) { + encoder->chroma = heif_chroma_444; + return heif_error_ok; + } + else { + return heif_error_invalid_parameter_value; + } + } + + return heif_error_unsupported_parameter; +} + +static void save_strcpy(char* dst, int dst_size, const char* src) +{ + strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = 0; +} + +static heif_error x264_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + if (strcmp(name, kParam_preset) == 0) { + save_strcpy(value, value_size, encoder->preset.c_str()); + return heif_error_ok; + } + else if (strcmp(name, kParam_tune) == 0) { + save_strcpy(value, value_size, encoder->tune.c_str()); + return heif_error_ok; + } + else if (strcmp(name, kParam_chroma) == 0) { + switch (encoder->chroma) { + case heif_chroma_420: + save_strcpy(value, value_size, "420"); + break; + case heif_chroma_422: + save_strcpy(value, value_size, "422"); + break; + case heif_chroma_444: + save_strcpy(value, value_size, "444"); + break; + default: + assert(false); + return heif_error_invalid_parameter_value; + } + return heif_error_ok; + } + + return heif_error_unsupported_parameter; +} + + +static void x264_set_default_parameters(void* encoder) +{ + for (const heif_encoder_parameter** p = x264_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; + + if (param->has_default) { + switch (param->type) { + case heif_encoder_parameter_type_integer: + x264_set_parameter_integer(encoder, param->name, param->integer.default_value); + break; + case heif_encoder_parameter_type_boolean: + x264_set_parameter_boolean(encoder, param->name, param->boolean.default_value); + break; + case heif_encoder_parameter_type_string: + x264_set_parameter_string(encoder, param->name, param->string.default_value); + break; + } + } + } +} + + +static void x264_query_input_colorspace(heif_colorspace* colorspace, heif_chroma* chroma) +{ + if (*colorspace == heif_colorspace_monochrome) { + *colorspace = heif_colorspace_monochrome; + *chroma = heif_chroma_monochrome; + } + else { + *colorspace = heif_colorspace_YCbCr; + *chroma = heif_chroma_420; + } +} + + +static void x264_query_input_colorspace2(void* encoder_raw, heif_colorspace* colorspace, heif_chroma* chroma) +{ + auto* encoder = (encoder_struct_x264*) encoder_raw; + + if (*colorspace == heif_colorspace_monochrome) { + *colorspace = heif_colorspace_monochrome; + *chroma = heif_chroma_monochrome; + } + else { + *colorspace = heif_colorspace_YCbCr; + *chroma = encoder->chroma; + } +} + + +static int rounded_size(int s) +{ + s = (s + 1) & ~1; + + if (s < 64) { + s = 64; + } + + return s; +} + + +static heif_error x264_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + + + // close previous encoder if there is still one hanging around + if (encoder->encoder) { + x264_encoder_close(encoder->encoder); + encoder->encoder = nullptr; + } + + + int bit_depth = heif_image_get_bits_per_pixel_range(image, heif_channel_Y); + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + heif_chroma chroma = heif_image_get_chroma_format(image); + + + x264_param_t& param = encoder->param; + + x264_param_default_preset(¶m, encoder->preset.c_str(), encoder->tune.c_str()); + + // x264 encodes 8 and 10 bits, and nothing else. Which of the two a build offers + // is decided by param.i_bitdepth, which exists since x264 build 153; older + // builds are fixed to whatever they were compiled for and have no 10 bit mode + // we could ask for. x264_encoder_open() below fails if the depth is not there. + if (bit_depth != 8 && bit_depth != 10) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + kError_unsupported_bit_depth + }; + } + +#if X264_BUILD >= 153 + param.i_bitdepth = bit_depth; +#else + if (bit_depth != 8) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_bit_depth, + kError_unsupported_bit_depth + }; + } +#endif + + // Applied before param.i_csp is set below, so it constrains the coding tools + // and the bit depth only. x264 still picks the profile that the final chroma + // format needs. + x264_param_apply_profile(¶m, bit_depth > 8 ? "high10" : "main"); + + + param.i_fps_num = framerate_num; + param.i_fps_den = framerate_denom; + + if (options) { + if (options->keyframe_distance_min) { + param.i_keyint_min = options->keyframe_distance_min; + } + + if (options->keyframe_distance_max) { + param.i_keyint_max = options->keyframe_distance_max; + } + + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + param.i_bframe = 0; + param.i_keyint_min=1; + param.i_keyint_max=1; + break; + case heif_sequence_gop_structure_lowdelay: + param.i_bframe = 0; + break; + case heif_sequence_gop_structure_unrestricted: + //param.i_bframe = 1; + break; + } + } + + // BPG uses CQP. It does not seem to be better though. + // param->rc.rateControlMode = X264_RC_CQP; + // param->rc.qp = (100 - encoder->quality)/2; + param.i_frame_total = image_sequence ? 0 : 1; // TODO + + if (isGreyscale) { + param.i_csp = X264_CSP_I400; + } + else if (chroma == heif_chroma_420) { + param.i_csp = X264_CSP_I420; + } + else if (chroma == heif_chroma_422) { + param.i_csp = X264_CSP_I422; + } + else if (chroma == heif_chroma_444) { + param.i_csp = X264_CSP_I444; + } + + // Tells x264 that the input planes hold 16 bit samples. HeifPixelImage stores + // anything above 8 bits at two bytes per sample, so this has to follow the bit + // depth of the image, not the depth we encode at. + if (bit_depth > 8) { + param.i_csp |= X264_CSP_HIGH_DEPTH; + } + + if (chroma != heif_chroma_monochrome) { + int w = heif_image_get_width(image, heif_channel_Y); + int h = heif_image_get_height(image, heif_channel_Y); + if (chroma != heif_chroma_444) { w = (w + 1) / 2; } + if (chroma == heif_chroma_420) { h = (h + 1) / 2; } + + assert(heif_image_get_width(image, heif_channel_Cb)==w); + assert(heif_image_get_width(image, heif_channel_Cr)==w); + assert(heif_image_get_height(image, heif_channel_Cb)==h); + assert(heif_image_get_height(image, heif_channel_Cr)==h); + (void) w; + (void) h; + } + + // TODO: which of these exist ? + x264_param_parse(¶m, "info", "0"); + x264_param_parse(¶m, "limit-modes", "0"); + x264_param_parse(¶m, "limit-refs", "0"); + x264_param_parse(¶m, "rskip", "0"); + + x264_param_parse(¶m, "rect", "1"); + x264_param_parse(¶m, "amp", "1"); + x264_param_parse(¶m, "aq-mode", "1"); + x264_param_parse(¶m, "psy-rd", "1.0"); + x264_param_parse(¶m, "psy-rdoq", "1.0"); + + heif_color_profile_nclx* nclx = nullptr; + heif_error err = heif_image_get_nclx_color_profile(image, &nclx); + if (err.code != heif_error_Ok) { + assert(nclx == nullptr); + } + + // make sure NCLX profile is deleted at end of function + auto nclx_deleter = std::unique_ptr(nclx, heif_nclx_color_profile_free); + + // Set the VUI fields directly instead of going through x264_param_parse(), + // which only accepts symbolic names ("bt709", ...), not the numeric code points. + // The VUI enums are the same CICP code points that nclx uses. + + if (nclx) { + param.vui.b_fullrange = nclx->full_range_flag ? 1 : 0; + } + else { + param.vui.b_fullrange = 1; + } + + if (nclx && + (input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail)) { + param.vui.i_colorprim = static_cast(nclx->color_primaries); + param.vui.i_transfer = static_cast(nclx->transfer_characteristics); + param.vui.i_colmatrix = static_cast(nclx->matrix_coefficients); + } + + // Write the pixel aspect ratio into the VUI. Extended_SAR stores sar_width/sar_height + // as u(16), so ratios that do not fit are only signalled through the pasp property. + // A 1:1 ratio is omitted (VUI absence already means unspecified/square). + // Set before the user parameters below so that an explicit "x264:sar" takes precedence. + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + param.vui.i_sar_width = static_cast(aspect_h); + param.vui.i_sar_height = static_cast(aspect_v); + } + + for (const auto& p : encoder->parameters) { + if (p.name == heif_encoder_parameter_name_quality) { + // quality=0 -> crf=50 + // quality=50 -> crf=25 + // quality=100 -> crf=0 + + param.rc.f_rf_constant = (float)(100 - p.value_int) / 2.0f; + } + else if (p.name == heif_encoder_parameter_name_lossless) { + if (p.value_int) { + param.rc.i_qp_constant = 0; + } + } + else if (p.name == kParam_complexity) { + const int complexity = p.value_int; + + if (complexity >= 60) { + x264_param_parse(¶m, "rd-refine", "1"); // increases computation time + x264_param_parse(¶m, "rd", "6"); + } + } + else if (strncmp(p.name.c_str(), "x264:", 5) == 0) { + std::string x264p = p.name.substr(5); + if (x264_param_parse(¶m, x264p.c_str(), p.value_string.c_str()) < 0) { + encoder->last_error_message = std::string{"Unsupported x264 encoder parameter: "} + x264p; + + return { + .code = heif_error_Usage_error, + .subcode = heif_suberror_Unsupported_parameter, + .message = encoder->last_error_message.c_str() + }; + } + } + } + + param.i_log_level = encoder->logLevel; + + + param.i_width = heif_image_get_width(image, heif_channel_Y); + param.i_height = heif_image_get_height(image, heif_channel_Y); + //param->internalBitDepth = bit_depth; + + param.i_width = rounded_size(param.i_width); + param.i_height = rounded_size(param.i_height); + +#if 0 + // This enforces that x264 will not queue frames. + // Without this, it needs to be flushed 18x until it returns some NALs. + // -> we now use x264_encoder_delayed_frames(), which works fine. + + param.b_sliced_threads = 1; + param.i_slice_count_max = 1; + param.i_slice_count = 1; + param.i_slice_max_mbs = 0; + param.i_slice_max_size = 0; +#endif + + // output NALs with size, no startcodes (actually, we don't care since we skip the 4 bytes anyways) + param.b_annexb = 0; + + encoder->bit_depth = bit_depth; + + encoder->encoder = x264_encoder_open(¶m); + if (!encoder->encoder) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unspecified, + "Cannot create x264 encoder" + }; + } + + if (image_sequence) { + x264_nal_t* nals = nullptr; + int num_nals = 0; + + x264_encoder_headers(encoder->encoder, + &nals, + &num_nals); + + if (num_nals) { + encoder->append_nals(nals, num_nals, 0); + } + } + + return heif_error_ok; +} + + +static heif_error x264_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return x264_start_sequence_encoding_intern(encoder_raw, image, input_class, + framerate_num, framerate_denom, options, true); +} + + +static heif_error x264_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t frame_nr) +{ + // H.264 can signal different luma and chroma bit depths, but x264 has a + // single bit depth and cannot produce such a stream. x264 has 8 and 10 bit + // modes and no others. + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {8, 10}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + x264_param_t& param = encoder->param; + + if (!encoder->encoder) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "called plugin encode_sequence_frame() without start_sequence_encoding()" + }; + } + + // pic.img.i_csp below carries the X264_CSP_HIGH_DEPTH flag of the first frame + // of the sequence, while the plane pointers are this frame's. + input_error = check_sequence_frame_bit_depth(image, encoder->bit_depth); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + heif_error err; + + // Note: it is ok to cast away the const, as the image content is not changed. + // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. + err = heif_image_extend_padding_to_size(const_cast(image), + param.i_width, + param.i_height); + if (err.code) { + return err; + } + + x264_picture_t pic; + x264_picture_init(&pic); + + pic.img.i_csp = param.i_csp; + + // TODO: check that all input images use the same color format + + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); + + if (isGreyscale) { + pic.img.plane[0] = const_cast(heif_image_get_plane_readonly(image, heif_channel_Y, &pic.img.i_stride[0])); + } + else { + pic.img.plane[0] = const_cast(heif_image_get_plane_readonly(image, heif_channel_Y, &pic.img.i_stride[0])); + pic.img.plane[1] = const_cast(heif_image_get_plane_readonly(image, heif_channel_Cb, &pic.img.i_stride[1])); + pic.img.plane[2] = const_cast(heif_image_get_plane_readonly(image, heif_channel_Cr, &pic.img.i_stride[2])); + } + + // pic->bitDepth = encoder->bit_depth; + pic.i_pts = frame_nr; + + x264_nal_t* nals = nullptr; + int num_nals = 0; + + x264_picture_t out_pic; + int result = x264_encoder_encode(encoder->encoder, + &nals, + &num_nals, + &pic, + &out_pic); + (void)result; // TODO: check for error + + if (num_nals) { + encoder->append_nals(nals, num_nals, out_pic.i_pts); + } + + return heif_error_ok; +} + + +static heif_error x264_end_sequence_encoding(void* encoder_raw) +{ + encoder_struct_x264* encoder = (encoder_struct_x264*) encoder_raw; + +#if 0 + // TODO (hack) + if (encoder->nal_output_counter < encoder->num_nals) { + return heif_error_ok; + } +#endif + + // check that all NALs have been drained + //assert(encoder->nal_output_counter == encoder->num_nals); + //encoder->nal_output_counter = 0; + + x264_nal_t* nals = nullptr; + int num_nals = 0; + + while (x264_encoder_delayed_frames(encoder->encoder) > 0) { + x264_picture_t out_pic; + int result = x264_encoder_encode(encoder->encoder, + &nals, + &num_nals, + nullptr, + &out_pic); + (void)result; // TODO: check for error + encoder->out_frameNr = out_pic.i_pts; + + if (num_nals) { + encoder->append_nals(nals, num_nals, out_pic.i_pts); + break; + } + } + + x264_param_cleanup(&encoder->param); + + //encoder->nal_output_counter = 0; // TODO: is this needed ? + + return heif_error_ok; +} + + +static heif_error x264_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = x264_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } + + err = x264_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } + + x264_end_sequence_encoding(encoder_raw); + + return heif_error_ok; +} + + +static heif_error x264_get_compressed_data_intern(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* out_frame_nr, int* more_packets) +{ + encoder_struct_x264* encoder = ( encoder_struct_x264*) encoder_raw; + + if (encoder->encoder == nullptr) { + *data = nullptr; + *size = 0; + + return heif_error_ok; + } + + if (encoder->m_output_packets.empty()) { + *data = nullptr; + *size = 0; + + return heif_error_ok; + } + + auto& pkt = encoder->m_output_packets.front(); + encoder->m_active_output = std::move(pkt.data); + + if (out_frame_nr) { + *out_frame_nr = pkt.frameNr; + } + + if (more_packets) { + *more_packets = pkt.more_packets; + } + + *data = encoder->m_active_output.data() + 4; + *size = (int)encoder->m_active_output.size() - 4; + + encoder->m_output_packets.pop_front(); + +#if 0 + // const x264_api* api = x264_api_get(encoder->bit_depth); + + /*for (;;)*/ { + while (encoder->nal_output_counter < encoder->num_nals) { + *data = encoder->nals[encoder->nal_output_counter].p_payload; + *size = encoder->nals[encoder->nal_output_counter].i_payload; + encoder->nal_output_counter++; + + // --- skip start code --- + + // skip '0' bytes + while (**data == 0 && *size > 0) { + (*data)++; + (*size)--; + } + + // skip '1' byte + (*data)++; + (*size)--; + + const uint8_t nal_type = ((*data)[0] & 0x1f); + std::cout << "NAL type: " << ((int)nal_type) << "\n"; + std::cout << write_raw_data_as_hex(*data, *size, + "data: ", + " "); + + // --- skip NALs with irrelevant data --- + + if (*size >= 3 && (*data)[0] == 0x4e && (*data)[2] == 5) { + // skip "unregistered user data SEI" + + } + else { + // output NAL + + if (out_frame_nr) { + *out_frame_nr = encoder->out_frameNr; + } + + return heif_error_ok; + } + } + } + + *data = nullptr; + *size = 0; + + if (out_frame_nr) { + *out_frame_nr = 0; + } +#endif + + return heif_error_ok; +} + + +static heif_error x264_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return x264_get_compressed_data_intern(encoder_raw, data, size, nullptr, nullptr); +} + +static heif_error x264_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, + int* more_frame_packets) +{ + return x264_get_compressed_data_intern(encoder_raw, data, size, frame_nr, more_frame_packets); +} + + +static const heif_encoder_plugin encoder_plugin_x264 + { + /* plugin_api_version */ 4, + /* compression_format */ heif_compression_AVC, + /* id_name */ "x264", + /* priority */ X264_PLUGIN_PRIORITY, + /* supports_lossy_compression */ true, + /* supports_lossless_compression */ true, + /* get_plugin_name */ x264_plugin_name, + /* init_plugin */ x264_init_plugin, + /* cleanup_plugin */ x264_cleanup_plugin, + /* new_encoder */ x264_new_encoder, + /* free_encoder */ x264_free_encoder, + /* set_parameter_quality */ x264_set_parameter_quality, + /* get_parameter_quality */ x264_get_parameter_quality, + /* set_parameter_lossless */ x264_set_parameter_lossless, + /* get_parameter_lossless */ x264_get_parameter_lossless, + /* set_parameter_logging_level */ x264_set_parameter_logging_level, + /* get_parameter_logging_level */ x264_get_parameter_logging_level, + /* list_parameters */ x264_list_parameters, + /* set_parameter_integer */ x264_set_parameter_integer, + /* get_parameter_integer */ x264_get_parameter_integer, + /* set_parameter_boolean */ x264_set_parameter_integer, // boolean also maps to integer function + /* get_parameter_boolean */ x264_get_parameter_integer, // boolean also maps to integer function + /* set_parameter_string */ x264_set_parameter_string, + /* get_parameter_string */ x264_get_parameter_string, + /* query_input_colorspace */ x264_query_input_colorspace, + /* encode_image */ x264_encode_image, + /* get_compressed_data */ x264_get_compressed_data, + /* query_input_colorspace (v2) */ x264_query_input_colorspace2, + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ x264_start_sequence_encoding, + /* encode_sequence_frame (v4) */ x264_encode_sequence_frame, + /* end_sequence_encoding (v4) */ x264_end_sequence_encoding, + /* get_compressed_data2 (v4) */ x264_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 0 + }; + +const heif_encoder_plugin* get_encoder_plugin_x264() +{ + return &encoder_plugin_x264; +} + + +#if PLUGIN_X264 +heif_plugin_info plugin_info { + 1, + heif_plugin_type_encoder, + &encoder_plugin_x264 +}; +#endif diff -Nru libheif-1.19.8/libheif/plugins/encoder_x264.h libheif-1.23.4/libheif/plugins/encoder_x264.h --- libheif-1.19.8/libheif/plugins/encoder_x264.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_x264.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,34 @@ +/* + * HEIF codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_ENCODER_X264_H +#define LIBHEIF_ENCODER_X264_H + +#include "common_utils.h" + +const struct heif_encoder_plugin* get_encoder_plugin_x264(); + +#if PLUGIN_X264 +extern "C" { +MAYBE_UNUSED LIBHEIF_API extern heif_plugin_info plugin_info; +} +#endif + +#endif diff -Nru libheif-1.19.8/libheif/plugins/encoder_x265.cc libheif-1.23.4/libheif/plugins/encoder_x265.cc --- libheif-1.19.8/libheif/plugins/encoder_x265.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/encoder_x265.cc 2026-09-06 14:45:17.000000000 +0000 @@ -21,13 +21,16 @@ #include "libheif/heif.h" #include "libheif/heif_plugin.h" #include "encoder_x265.h" +#include "encoder_input_check.h" #include #include #include #include #include #include +#include #include +#include extern "C" { #include @@ -57,15 +60,27 @@ struct encoder_struct_x265 { x265_encoder* encoder = nullptr; + const x265_api* api = nullptr; + x265_param* param = nullptr; - x265_nal* nals = nullptr; - uint32_t num_nals = 0; - uint32_t nal_output_counter = 0; int bit_depth = 0; heif_chroma chroma; + // --- output + + struct Packet + { + std::vector data; + uintptr_t frameNr = 0; + }; + + std::deque output_data; + std::vector active_output_nal; + + void append_nal_packets(x265_nal* nals, uint32_t num_nals, uintptr_t frameNr); + // --- parameters std::vector parameters; @@ -199,13 +214,13 @@ #define MAX_NPARAMETERS 10 -static struct heif_encoder_parameter x265_encoder_params[MAX_NPARAMETERS]; -static const struct heif_encoder_parameter* x265_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; +static heif_encoder_parameter x265_encoder_params[MAX_NPARAMETERS]; +static const heif_encoder_parameter* x265_encoder_parameter_ptrs[MAX_NPARAMETERS + 1]; static void x265_init_parameters() { - struct heif_encoder_parameter* p = x265_encoder_params; - const struct heif_encoder_parameter** d = x265_encoder_parameter_ptrs; + heif_encoder_parameter* p = x265_encoder_params; + const heif_encoder_parameter** d = x265_encoder_parameter_ptrs; int i = 0; assert(i < MAX_NPARAMETERS); @@ -286,7 +301,7 @@ } -const struct heif_encoder_parameter** x265_list_parameters(void* encoder) +const heif_encoder_parameter** x265_list_parameters(void* encoder) { return x265_encoder_parameter_ptrs; } @@ -304,18 +319,15 @@ } -static struct heif_error x265_new_encoder(void** enc) +static heif_error x265_new_encoder(void** enc) { - struct encoder_struct_x265* encoder = new encoder_struct_x265(); - struct heif_error err = heif_error_ok; + encoder_struct_x265* encoder = new encoder_struct_x265(); + heif_error err = heif_error_ok; // encoder has to be allocated in x265_encode_image, because it needs to know the image size encoder->encoder = nullptr; - encoder->nals = nullptr; - encoder->num_nals = 0; - encoder->nal_output_counter = 0; encoder->bit_depth = 8; *enc = encoder; @@ -330,19 +342,23 @@ static void x265_free_encoder(void* encoder_raw) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (encoder->encoder) { const x265_api* api = x265_api_get(encoder->bit_depth); api->encoder_close(encoder->encoder); } + if (encoder->param && encoder->api) { + encoder->api->param_free(encoder->param); + } + delete encoder; } -static struct heif_error x265_set_parameter_quality(void* encoder_raw, int quality) +static heif_error x265_set_parameter_quality(void* encoder_raw, int quality) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (quality < 0 || quality > 100) { return heif_error_invalid_parameter_value; @@ -353,9 +369,9 @@ return heif_error_ok; } -static struct heif_error x265_get_parameter_quality(void* encoder_raw, int* quality) +static heif_error x265_get_parameter_quality(void* encoder_raw, int* quality) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; parameter p = encoder->get_param(heif_encoder_parameter_name_quality); *quality = p.value_int; @@ -363,18 +379,18 @@ return heif_error_ok; } -static struct heif_error x265_set_parameter_lossless(void* encoder_raw, int enable) +static heif_error x265_set_parameter_lossless(void* encoder_raw, int enable) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; encoder->add_param(heif_encoder_parameter_name_lossless, (bool) enable); return heif_error_ok; } -static struct heif_error x265_get_parameter_lossless(void* encoder_raw, int* enable) +static heif_error x265_get_parameter_lossless(void* encoder_raw, int* enable) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; parameter p = encoder->get_param(heif_encoder_parameter_name_lossless); *enable = p.value_int; @@ -382,9 +398,9 @@ return heif_error_ok; } -static struct heif_error x265_set_parameter_logging_level(void* encoder_raw, int logging) +static heif_error x265_set_parameter_logging_level(void* encoder_raw, int logging) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (logging < 0 || logging > 4) { return heif_error_invalid_parameter_value; @@ -395,9 +411,9 @@ return heif_error_ok; } -static struct heif_error x265_get_parameter_logging_level(void* encoder_raw, int* loglevel) +static heif_error x265_get_parameter_logging_level(void* encoder_raw, int* loglevel) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; *loglevel = encoder->logLevel; @@ -405,9 +421,9 @@ } -static struct heif_error x265_set_parameter_integer(void* encoder_raw, const char* name, int value) +static heif_error x265_set_parameter_integer(void* encoder_raw, const char* name, int value) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return x265_set_parameter_quality(encoder, value); @@ -435,9 +451,9 @@ return heif_error_unsupported_parameter; } -static struct heif_error x265_get_parameter_integer(void* encoder_raw, const char* name, int* value) +static heif_error x265_get_parameter_integer(void* encoder_raw, const char* name, int* value) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (strcmp(name, heif_encoder_parameter_name_quality) == 0) { return x265_get_parameter_quality(encoder, value); @@ -458,7 +474,7 @@ } -static struct heif_error x265_set_parameter_boolean(void* encoder, const char* name, int value) +static heif_error x265_set_parameter_boolean(void* encoder, const char* name, int value) { if (strcmp(name, heif_encoder_parameter_name_lossless) == 0) { return x265_set_parameter_lossless(encoder, value); @@ -492,9 +508,9 @@ } -static struct heif_error x265_set_parameter_string(void* encoder_raw, const char* name, const char* value) +static heif_error x265_set_parameter_string(void* encoder_raw, const char* name, const char* value) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (strcmp(name, kParam_preset) == 0) { if (!string_list_contains(kParam_preset_valid_values, value)) { @@ -534,7 +550,6 @@ } } - return heif_error_unsupported_parameter; } @@ -544,10 +559,10 @@ dst[dst_size - 1] = 0; } -static struct heif_error x265_get_parameter_string(void* encoder_raw, const char* name, - char* value, int value_size) +static heif_error x265_get_parameter_string(void* encoder_raw, const char* name, + char* value, int value_size) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; if (strcmp(name, kParam_preset) == 0) { save_strcpy(value, value_size, encoder->preset.c_str()); @@ -581,8 +596,8 @@ static void x265_set_default_parameters(void* encoder) { - for (const struct heif_encoder_parameter** p = x265_encoder_parameter_ptrs; *p; p++) { - const struct heif_encoder_parameter* param = *p; + for (const heif_encoder_parameter** p = x265_encoder_parameter_ptrs; *p; p++) { + const heif_encoder_parameter* param = *p; if (param->has_default) { switch (param->type) { @@ -616,7 +631,7 @@ static void x265_query_input_colorspace2(void* encoder_raw, heif_colorspace* colorspace, heif_chroma* chroma) { - auto* encoder = (struct encoder_struct_x265*) encoder_raw; + auto* encoder = (encoder_struct_x265*) encoder_raw; if (*colorspace == heif_colorspace_monochrome) { *colorspace = heif_colorspace_monochrome; @@ -640,13 +655,111 @@ return s; } +#if 0 +static const char* naltype_table[] = { + /* 0 */ "TRAIL_N", + /* 1 */ "TRAIL_R", + /* 2 */ "TSA_N", + /* 3 */ "TSA_R", + /* 4 */ "STSA_N", + /* 5 */ "STSA_R", + /* 6 */ "RADL_N", + /* 7 */ "RADL_R", + /* 8 */ "RASL_N", + /* 9 */ "RASL_R", + /* 10 */ "RSV_VCL_N10", + /* 11 */ "RSV_VCL_R11", + /* 12 */ "RSV_VCL_N12", + /* 13 */ "RSV_VCL_R13", + /* 14 */ "RSV_VCL_N14", + /* 15 */ "RSV_VCL_R15", + /* 16 */ "BLA_W_LP", + /* 17 */ "BLA_W_RADL", + /* 18 */ "BLA_N_LP", + /* 19 */ "IDR_W_RADL", + /* 20 */ "IDR_N_LP", + /* 21 */ "CRA_NUT", + /* 22 */ "RSV_IRAP_VCL22", + /* 23 */ "RSV_IRAP_VCL23", + /* 24 */ "RSV_VCL24", + /* 25 */ "RSV_VCL25", + /* 26 */ "RSV_VCL26", + /* 27 */ "RSV_VCL27", + /* 28 */ "RSV_VCL28", + /* 29 */ "RSV_VCL29", + /* 30 */ "RSV_VCL30", + /* 31 */ "RSV_VCL31", + /* 32 */ "VPS_NUT", + /* 33 */ "SPS_NUT", + /* 34 */ "PPS_NUT", + /* 35 */ "AUD_NUT", + /* 36 */ "EOS_NUT", + /* 37 */ "EOB_NUT", + /* 38 */ "FD_NUT", + /* 39 */ "PREFIX_SEI_NUT", + /* 40 */ "SUFFIX_SEI_NUT" +}; + + +static const char* naltype(uint8_t type) +{ + if (type <= 40) { + return naltype_table[type]; + } + else { + return "reserved"; + } +} +#endif -static struct heif_error x265_encode_image(void* encoder_raw, const struct heif_image* image, - heif_image_input_class input_class) +void encoder_struct_x265::append_nal_packets(x265_nal* nals, uint32_t num_nals, uintptr_t frameNr) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + for (uint32_t nal_idx=0 ; nal_idx < num_nals; nal_idx++) { + uint8_t* data = nals[nal_idx].payload; + uint32_t size = nals[nal_idx].sizeBytes; - // close previous encoder if there is still one hanging around + // --- skip start code --- + + // skip '0' bytes + while (*data == 0 && size > 0) { + data++; + size--; + } + + // skip '1' byte + data++; + size--; + + + // --- skip NALs with irrelevant data --- + + if (size >= 3 && data[0] == 0x4e && data[2] == 5) { + // skip "unregistered user data SEI" + } + else { + // output NAL + + Packet pkt; + pkt.data.resize(size); + memcpy(pkt.data.data(), data, size); + pkt.frameNr = frameNr; + + output_data.push_back(pkt); + } + } +} + + +static heif_error x265_start_sequence_encoding_intern(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options, + bool image_sequence) +{ + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; + + + // close previous encoder if there is still one hanging around if (encoder->encoder) { const x265_api* api = x265_api_get(encoder->bit_depth); api->encoder_close(encoder->encoder); @@ -659,6 +772,7 @@ heif_chroma chroma = heif_image_get_chroma_format(image); const x265_api* api = x265_api_get(bit_depth); + encoder->api = api; if (api == nullptr) { struct heif_error err = { heif_error_Encoder_plugin_error, @@ -669,9 +783,21 @@ } x265_param* param = api->param_alloc(); + if (encoder->param) { + api->param_free(encoder->param); + } + encoder->param = param; + api->param_default_preset(param, encoder->preset.c_str(), encoder->tune.c_str()); - if (bit_depth == 8) api->param_apply_profile(param, "mainstillpicture"); + if (bit_depth == 8) { + if (image_sequence) { + api->param_apply_profile(param, "main"); + } + else { + api->param_apply_profile(param, "mainstillpicture"); + } + } else if (bit_depth == 10) api->param_apply_profile(param, "main10-intra"); else if (bit_depth == 12) api->param_apply_profile(param, "main12-intra"); else { @@ -680,10 +806,6 @@ } - param->fpsNum = 1; - param->fpsDenom = 1; - - // x265 cannot encode images smaller than one CTU size // https://bitbucket.org/multicoreware/x265/issues/475/x265-does-not-allow-image-sizes-smaller // -> use smaller CTU sizes for very small images @@ -699,12 +821,11 @@ if (ctuSize < 16) { api->param_free(param); - struct heif_error err = { + return { heif_error_Encoder_plugin_error, heif_suberror_Invalid_parameter_value, kError_unsupported_image_size }; - return err; } #else // TODO: There seems to be a bug in x265 where increasing the CTU size between @@ -736,18 +857,41 @@ ctu = "16"; break; default: - struct heif_error err = { + return { heif_error_Encoder_plugin_error, heif_suberror_Invalid_parameter_value, kError_unsupported_ctu_size }; - return err; + } + + if (options) { + if (options->keyframe_distance_min) { + param->keyframeMin = options->keyframe_distance_min; + } + + if (options->keyframe_distance_max) { + param->keyframeMax = options->keyframe_distance_max; + } + + switch (options->gop_structure) { + case heif_sequence_gop_structure_intra_only: + param->bframes = 0; + param->keyframeMin=1; + param->keyframeMax=1; + break; + case heif_sequence_gop_structure_lowdelay: + param->bframes = 0; + break; + case heif_sequence_gop_structure_unrestricted: + param->bframes = 1; + break; + } } // BPG uses CQP. It does not seem to be better though. // param->rc.rateControlMode = X265_RC_CQP; // param->rc.qp = (100 - encoder->quality)/2; - param->totalFrames = 1; + param->totalFrames = image_sequence ? 0 : 1; // TODO if (isGreyscale) { param->internalCsp = X265_CSP_I400; @@ -788,7 +932,7 @@ api->param_parse(param, "psy-rd", "1.0"); api->param_parse(param, "psy-rdoq", "1.0"); - struct heif_color_profile_nclx* nclx = nullptr; + heif_color_profile_nclx* nclx = nullptr; heif_error err = heif_image_get_nclx_color_profile(image, &nclx); if (err.code != heif_error_Ok) { assert(nclx == nullptr); @@ -827,6 +971,23 @@ } } + // Write the pixel aspect ratio into the VUI. Extended_SAR stores sar_width/sar_height + // as u(16), so ratios that do not fit are only signalled through the pasp property. + // A 1:1 ratio is omitted (VUI absence already means unspecified/square). + // Set before the user parameters below so that an explicit "x265:sar" takes precedence. + + uint32_t aspect_h = 1, aspect_v = 1; + heif_image_get_pixel_aspect_ratio(image, &aspect_h, &aspect_v); + if ((input_class == heif_image_input_class_normal || + input_class == heif_image_input_class_thumbnail) && + aspect_h != aspect_v && + aspect_h > 0 && aspect_v > 0 && + aspect_h <= 0xFFFF && aspect_v <= 0xFFFF) { + std::stringstream sstr; + sstr << aspect_h << ':' << aspect_v; + api->param_parse(param, "sar", sstr.str().c_str()); + } + for (const auto& p : encoder->parameters) { if (p.name == heif_encoder_parameter_name_quality) { // quality=0 -> crf=50 @@ -882,17 +1043,112 @@ param->sourceWidth = rounded_size(param->sourceWidth); param->sourceHeight = rounded_size(param->sourceHeight); + // x265 sizes some of its internal picture buffers with 32-bit arithmetic and, up to at least + // v3.5, neither validates the picture size nor the result of these allocations. Pictures of + // roughly 700 Mpixel or more make it crash in one of its worker threads (GHSA-2c3g-p585-8rpq). + // Newer x265 versions refuse pictures above the HEVC Level 7.2 maximum of 142,606,336 luma + // samples (16384x8704) in x265_check_params() unless non-conformance is explicitly allowed. + // Apply the same limit here, so that the behavior is the same with all x265 versions and the + // encoder is never opened with a picture it cannot handle. + const uint64_t max_luma_samples = 142606336; + if (static_cast(param->sourceWidth) * static_cast(param->sourceHeight) > max_luma_samples) { + return {heif_error_Encoding_error, + heif_suberror_Encoder_encoding, + "Image too large for x265: at most 142,606,336 luma samples (HEVC Level 7.2) are supported"}; + } + + param->fpsNum = framerate_num; + param->fpsDenom = framerate_denom; + + encoder->bit_depth = bit_depth; + + encoder->encoder = api->encoder_open(param); + if (encoder->encoder == nullptr) { + // x265 rejected the parameters (e.g. newer versions refuse oversized pictures). + return {heif_error_Encoding_error, + heif_suberror_Encoder_initialization, + "x265 encoder could not be opened with the given parameters"}; + } + + if (image_sequence) { + x265_nal* nals = nullptr; + uint32_t num_nals = 0; + + api->encoder_headers(encoder->encoder, + &nals, + &num_nals); + + encoder->append_nal_packets(nals, num_nals, 0); + + for (uint32_t i = 0; i < num_nals; i++) { + //std::cout << "dequeue header NAL : " << naltype(encoder->nals[i].type) << "\n"; + } + } + + return heif_error_ok; +} + + +static heif_error x265_start_sequence_encoding(void* encoder_raw, const heif_image* image, + enum heif_image_input_class input_class, + uint32_t framerate_num, uint32_t framerate_denom, + const heif_sequence_encoding_options* options) +{ + return x265_start_sequence_encoding_intern(encoder_raw, image, input_class, + framerate_num, framerate_denom, options, true); +} + + +static heif_error x265_encode_sequence_frame(void* encoder_raw, const heif_image* image, + uintptr_t frame_nr) +{ + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; + + if (!encoder->api) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "called plugin encode_sequence_frame() without start_sequence_encoding()" + }; + } + + // HEVC can signal different luma and chroma bit depths, but x265 has a + // single internal bit depth and cannot produce such a stream. Whether this + // build of libx265 has 10 or 12 bit support is checked separately via + // x265_api_get(). + heif_error input_error = check_encoder_input_image(image, /*supports_monochrome=*/true, + {8, 10, 12}); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + // pic->bitDepth below is the depth the encoder was opened with, while the plane + // pointers are this frame's. A deeper first frame would make libx265 read the + // planes of a shallower later frame at two bytes per sample. + input_error = check_sequence_frame_bit_depth(image, encoder->bit_depth); + if (input_error.code != heif_error_Ok) { + return input_error; + } + + const x265_api* api = encoder->api; + + heif_error err; + // Note: it is ok to cast away the const, as the image content is not changed. // However, we have to guarantee that there are no plane pointers or stride values kept over calling the svt_encode_image() function. - err = heif_image_extend_padding_to_size(const_cast(image), - param->sourceWidth, - param->sourceHeight); + err = heif_image_extend_padding_to_size(const_cast(image), + encoder->param->sourceWidth, + encoder->param->sourceHeight); if (err.code) { return err; } x265_picture* pic = api->picture_alloc(); - api->picture_init(param, pic); + api->picture_init(encoder->param, pic); + + // TODO: check that all input images use the same color format + + bool isGreyscale = (heif_image_get_colorspace(image) == heif_colorspace_monochrome); if (isGreyscale) { pic->planes[0] = (void*) heif_image_get_plane_readonly(image, heif_channel_Y, &pic->stride[0]); @@ -903,117 +1159,151 @@ pic->planes[2] = (void*) heif_image_get_plane_readonly(image, heif_channel_Cr, &pic->stride[2]); } - pic->bitDepth = bit_depth; - + pic->bitDepth = encoder->bit_depth; + // The codec hands this opaque pointer back unchanged; it carries an integer, not an address. + pic->userData = reinterpret_cast(frame_nr); // NOLINT(performance-no-int-to-ptr) - encoder->bit_depth = bit_depth; - - encoder->encoder = api->encoder_open(param); + x265_nal* nals = nullptr; + uint32_t num_nals = 0; #if X265_BUILD == 212 // In x265 build version 212, the signature of the encoder_encode() function was changed. But it was changed back in version 213. // https://bitbucket.org/multicoreware/x265_git/issues/952/crash-in-libheif-tests x265_picture* out_pic = NULL; api->encoder_encode(encoder->encoder, - &encoder->nals, - &encoder->num_nals, + &nals, + &num_nals, pic, &out_pic); + if (num_nals > 0 && out_pic) { + uintptr_t frameNr = reinterpret_cast(out_pic->userData); + encoder->append_nal_packets(nals, num_nals, frameNr); + } #else + x265_picture out_pic; api->encoder_encode(encoder->encoder, - &encoder->nals, - &encoder->num_nals, + &nals, + &num_nals, pic, - NULL); + &out_pic); + + if (num_nals > 0) { + uintptr_t frameNr = reinterpret_cast(out_pic.userData); + encoder->append_nal_packets(nals, num_nals, frameNr); + } + #endif api->picture_free(pic); - api->param_free(param); - - encoder->nal_output_counter = 0; return heif_error_ok; } -static struct heif_error x265_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, - enum heif_encoded_data_type* type) +static heif_error x265_end_sequence_encoding(void* encoder_raw) { - struct encoder_struct_x265* encoder = (struct encoder_struct_x265*) encoder_raw; + encoder_struct_x265* encoder = (encoder_struct_x265*) encoder_raw; + const x265_api* api = encoder->api; - if (encoder->encoder == nullptr) { - *data = nullptr; - *size = 0; + x265_nal* nals = nullptr; + uint32_t num_nals = 0; - return heif_error_ok; +#if X265_BUILD == 212 + x265_picture* out_pic = NULL; + int result = api->encoder_encode(encoder->encoder, + &nals, + &num_nals, + NULL, + &out_pic); +#else + x265_picture out_pic; + int result = api->encoder_encode(encoder->encoder, + &nals, + &num_nals, + NULL, + &out_pic); +#endif + if (result > 0 && num_nals > 0) { +#if X265_BUILD == 212 + uintptr_t frameNr = out_pic ? reinterpret_cast(out_pic->userData) : 0; +#else + uintptr_t frameNr = reinterpret_cast(out_pic.userData); +#endif + encoder->append_nal_packets(nals, num_nals, frameNr); } - const x265_api* api = x265_api_get(encoder->bit_depth); + encoder->api->param_free(encoder->param); + encoder->param = nullptr; - for (;;) { - while (encoder->nal_output_counter < encoder->num_nals) { - *data = encoder->nals[encoder->nal_output_counter].payload; - *size = encoder->nals[encoder->nal_output_counter].sizeBytes; - encoder->nal_output_counter++; - - // --- skip start code --- - - // skip '0' bytes - while (**data == 0 && *size > 0) { - (*data)++; - (*size)--; - } + return heif_error_ok; +} - // skip '1' byte - (*data)++; - (*size)--; +static heif_error x265_encode_image(void* encoder_raw, const heif_image* image, + heif_image_input_class input_class) +{ + heif_error err; + err = x265_start_sequence_encoding_intern(encoder_raw, image, input_class, 1,25, nullptr, false); + if (err.code) { + return err; + } - // --- skip NALs with irrelevant data --- + err = x265_encode_sequence_frame(encoder_raw, image, 0); + if (err.code) { + return err; + } - if (*size >= 3 && (*data)[0] == 0x4e && (*data)[2] == 5) { - // skip "unregistered user data SEI" + x265_end_sequence_encoding(encoder_raw); - } - else { - // output NAL + return heif_error_ok; +} - return heif_error_ok; - } - } +static heif_error x265_get_compressed_data_intern(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* out_frame_nr) +{ + encoder_struct_x265* encoder = ( encoder_struct_x265*) encoder_raw; - encoder->nal_output_counter = 0; + if (encoder->output_data.empty()) { + *data = nullptr; + *size = 0; + return heif_error_ok; + } -#if X265_BUILD == 212 - x265_picture* out_pic = NULL; - int result = api->encoder_encode(encoder->encoder, - &encoder->nals, - &encoder->num_nals, - NULL, - &out_pic); -#else - int result = api->encoder_encode(encoder->encoder, - &encoder->nals, - &encoder->num_nals, - NULL, - NULL); -#endif - if (result <= 0) { - *data = nullptr; - *size = 0; + encoder->active_output_nal = std::move(encoder->output_data.front().data); - return heif_error_ok; - } + if (out_frame_nr) { + *out_frame_nr = encoder->output_data.front().frameNr; } + + encoder->output_data.pop_front(); + + *data = encoder->active_output_nal.data(); + *size = static_cast(encoder->active_output_nal.size()); + + return heif_error_ok; +} + + +static heif_error x265_get_compressed_data(void* encoder_raw, uint8_t** data, int* size, + heif_encoded_data_type* type) +{ + return x265_get_compressed_data_intern(encoder_raw, data, size, nullptr); +} + +static heif_error x265_get_compressed_data2(void* encoder_raw, uint8_t** data, int* size, + uintptr_t* frame_nr, int* is_keyframe, + int* more_frame_packets) +{ + return x265_get_compressed_data_intern(encoder_raw, data, size, frame_nr); } -static const struct heif_encoder_plugin encoder_plugin_x265 +static const heif_encoder_plugin encoder_plugin_x265 { - /* plugin_api_version */ 2, + /* plugin_api_version */ 4, /* compression_format */ heif_compression_HEVC, /* id_name */ "x265", /* priority */ X265_PLUGIN_PRIORITY, @@ -1040,10 +1330,17 @@ /* query_input_colorspace */ x265_query_input_colorspace, /* encode_image */ x265_encode_image, /* get_compressed_data */ x265_get_compressed_data, - /* query_input_colorspace (v2) */ x265_query_input_colorspace2 + /* query_input_colorspace (v2) */ x265_query_input_colorspace2, + /* query_encoded_size (v3) */ nullptr, + /* minimum_required_libheif_version */ LIBHEIF_MAKE_VERSION(1,21,0), + /* start_sequence_encoding (v4) */ x265_start_sequence_encoding, + /* encode_sequence_frame (v4) */ x265_encode_sequence_frame, + /* end_sequence_encoding (v4) */ x265_end_sequence_encoding, + /* get_compressed_data2 (v4) */ x265_get_compressed_data2, + /* does_indicate_keyframes (v4) */ 0 }; -const struct heif_encoder_plugin* get_encoder_plugin_x265() +const heif_encoder_plugin* get_encoder_plugin_x265() { return &encoder_plugin_x265; } diff -Nru libheif-1.19.8/libheif/plugins/nalu_utils.h libheif-1.23.4/libheif/plugins/nalu_utils.h --- libheif-1.19.8/libheif/plugins/nalu_utils.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins/nalu_utils.h 2026-09-06 14:45:17.000000000 +0000 @@ -22,11 +22,24 @@ #include #include "libheif/heif.h" -static const int NAL_UNIT_VPS_NUT = 32; -static const int NAL_UNIT_SPS_NUT = 33; -static const int NAL_UNIT_PPS_NUT = 34; -static const int NAL_UNIT_IDR_W_RADL = 19; -static const int NAL_UNIT_IDR_N_LP = 20; +static const int AVC_NAL_UNIT_MAX_VCL = 5; +static const int AVC_NAL_UNIT_SPS_NUT = 7; +static const int AVC_NAL_UNIT_PPS_NUT = 8; +static const int AVC_NAL_UNIT_SPS_EXT_NUT = 13; + +static const int HEVC_NAL_UNIT_MAX_VCL = 31; +static const int HEVC_NAL_UNIT_VPS_NUT = 32; +static const int HEVC_NAL_UNIT_SPS_NUT = 33; +static const int HEVC_NAL_UNIT_PPS_NUT = 34; +static const int HEVC_NAL_UNIT_IDR_W_RADL = 19; +static const int HEVC_NAL_UNIT_IDR_N_LP = 20; + +static const int VVC_NAL_UNIT_MAX_VCL = 11; +static const int VVC_NAL_UNIT_VPS_NUT = 14; +static const int VVC_NAL_UNIT_SPS_NUT = 15; +static const int VVC_NAL_UNIT_PPS_NUT = 16; +static const int VVC_NAL_UNIT_SUFFIX_SEI_NUT = 24; + class NalUnit { diff -Nru libheif-1.19.8/libheif/plugins_unix.cc libheif-1.23.4/libheif/plugins_unix.cc --- libheif-1.19.8/libheif/plugins_unix.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins_unix.cc 2026-09-06 14:45:17.000000000 +0000 @@ -57,6 +57,14 @@ { std::vector result; + // An empty directory string means there is no plugin directory to scan. This happens + // when the library is built with an empty PLUGIN_DIRECTORY and no LIBHEIF_PLUGIN_PATH + // is set. opendir("") fails anyway, but skip it explicitly to mirror the Windows path + // and avoid building bogus "/plugin.so" filenames. + if (directory == nullptr || directory[0] == '\0') { + return result; + } + DIR* dir = opendir(directory); if (dir == nullptr) { return {}; // TODO: return error_cannot_read_plugin_directory; diff -Nru libheif-1.19.8/libheif/plugins_windows.cc libheif-1.23.4/libheif/plugins_windows.cc --- libheif-1.19.8/libheif/plugins_windows.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/plugins_windows.cc 2026-09-06 14:45:17.000000000 +0000 @@ -49,6 +49,14 @@ { std::vector result; + // An empty directory string would build the search pattern "\\*.dll", which makes + // FindFirstFile() scan the root of the current drive (e.g. C:\). This happens when + // the library is built with an empty PLUGIN_DIRECTORY and no LIBHEIF_PLUGIN_PATH is + // set. Skip scanning in that case so that heif_init() does not fail on stray DLLs. + if (directory == nullptr || directory[0] == '\0') { + return result; + } + HANDLE hFind; WIN32_FIND_DATA FindFileData; diff -Nru libheif-1.19.8/libheif/region.cc libheif-1.23.4/libheif/region.cc --- libheif-1.19.8/libheif/region.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/region.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,9 +25,11 @@ #include "libheif/heif_regions.h" #include #include +#include -Error RegionItem::parse(const std::vector& data) +Error RegionItem::parse(const std::vector& data, + const heif_security_limits* limits) { if (data.size() < 8) { return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, @@ -46,16 +48,13 @@ return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, "Region data incomplete"); } - reference_width = - ((data[2] << 24) | (data[3] << 16) | (data[4] << 8) | (data[5])); - - reference_height = - ((data[6] << 24) | (data[7] << 16) | (data[8] << 8) | (data[9])); + reference_width = four_bytes_to_uint32(data[2], data[3], data[4], data[5]); + reference_height = four_bytes_to_uint32(data[6], data[7], data[8], data[9]); dataOffset = 10; } else { - reference_width = ((data[2] << 8) | (data[3])); - reference_height = ((data[4] << 8) | (data[5])); + reference_width = four_bytes_to_uint32(0, 0, data[2], data[3]); + reference_height = four_bytes_to_uint32(0, 0, data[4], data[5]); dataOffset = 6; } @@ -105,7 +104,7 @@ continue; } - Error error = region->parse(data, field_size, &dataOffset); + Error error = region->parse(data, field_size, &dataOffset, limits); if (error) { return error; } @@ -178,12 +177,14 @@ { uint32_t x; if (field_size == 32) { - x = ((data[*dataOffset] << 24) | (data[*dataOffset + 1] << 16) | - (data[*dataOffset + 2] << 8) | (data[*dataOffset + 3])); + x = four_bytes_to_uint32(data[*dataOffset + 0], + data[*dataOffset + 1], + data[*dataOffset + 2], + data[*dataOffset + 3]); *dataOffset = *dataOffset + 4; } else { - x = ((data[*dataOffset] << 8) | (data[*dataOffset + 1])); + x = four_bytes_to_uint32(0, 0, data[*dataOffset], data[*dataOffset + 1]); *dataOffset = *dataOffset + 2; } return x; @@ -202,7 +203,8 @@ Error RegionGeometry_Point::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { unsigned int bytesRequired = (field_size / 8) * 2; if (data.size() - *dataOffset < bytesRequired) { @@ -243,7 +245,8 @@ Error RegionGeometry_Rectangle::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { unsigned int bytesRequired = (field_size / 8) * 4; if (data.size() - *dataOffset < bytesRequired) { @@ -275,7 +278,8 @@ Error RegionGeometry_Ellipse::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { unsigned int bytesRequired = (field_size / 8) * 4; if (data.size() - *dataOffset < bytesRequired) { @@ -308,7 +312,8 @@ Error RegionGeometry_Polygon::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { uint32_t bytesRequired1 = (field_size / 8) * 1; if (data.size() - *dataOffset < bytesRequired1) { @@ -326,6 +331,37 @@ "Insufficient data remaining for polygon"); } + if (closed) { + if (numPoints < 3) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Region polygon with less than 3 points." + }; + } + } + else { + if (numPoints < 2) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Region polyline with less than 2 points." + }; + } + } + + if (UINT32_MAX / numPoints < sizeof(Point)) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + "Region polygon size exceeds integer range." + }; + } + + if (auto err = m_memory_handle.alloc(numPoints, sizeof(Point), limits, "region polygon")) { + return err; + } + for (uint32_t i = 0; i < numPoints; i++) { Point p; p.x = parse_signed(data, field_size, dataOffset); @@ -339,7 +375,8 @@ Error RegionGeometry_ReferencedMask::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { unsigned int bytesRequired = (field_size / 8) * 4; if (data.size() - *dataOffset < bytesRequired) { @@ -394,7 +431,8 @@ Error RegionGeometry_InlineMask::parse(const std::vector& data, int field_size, - unsigned int* dataOffset) + unsigned int* dataOffset, + const heif_security_limits* limits) { unsigned int bytesRequired = (field_size / 8) * 4 + 1; if (data.size() - *dataOffset < bytesRequired) { @@ -407,17 +445,58 @@ height = parse_unsigned(data, field_size, dataOffset); uint8_t mask_coding_method = data[*dataOffset]; *dataOffset = *dataOffset + 1; + if (mask_coding_method != 0) { return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, "Deflate compressed inline mask is not yet supported"); } - unsigned int additionalBytesRequired = width * height / 8; - if (data.size() - *dataOffset < additionalBytesRequired) { + + if (width==0 || height==0) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Zero size mask image." + }; + } + + // Mask is stored with one bit per pixel, no padding exists at the end of the line. + // Only the very last byte is padded. + + // error if: + // (width * height + 7) / 8 > UINT32_MAX + // more strict: + // (width * height + height) / 8 > UINT32_MAX + // width/8 * height + 1 > UINT32_MAX + + uint64_t bytes_for_mask = (static_cast(width) * height + 7) / 8; + if (bytes_for_mask > std::numeric_limits::max()) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Unspecified, + "Mask image size exceeds maximum integer range." + }; + } + + if (limits->max_image_size_pixels / width < height) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Inline mask image exceeds maximum image size." + }; + } + + if (data.size() - *dataOffset < bytes_for_mask) { return Error(heif_error_Invalid_input, heif_suberror_Invalid_region_data, "Insufficient data remaining for inline mask region data[]"); } - mask_data.resize(additionalBytesRequired); - std::copy(data.begin() + *dataOffset, data.begin() + *dataOffset + additionalBytesRequired, mask_data.begin()); + + if (auto err = m_memory_handle.alloc(bytes_for_mask, limits, "region mask")) { + return err; + } + + mask_data.resize(bytes_for_mask); + std::copy(data.begin() + *dataOffset, data.begin() + *dataOffset + static_cast(bytes_for_mask), mask_data.begin()); + *dataOffset += static_cast(bytes_for_mask); return Error::Ok; } @@ -435,15 +514,15 @@ } -RegionCoordinateTransform RegionCoordinateTransform::create(std::shared_ptr file, - heif_item_id item_id, - int reference_width, int reference_height) +Result RegionCoordinateTransform::create(const std::shared_ptr& file, + heif_item_id item_id, + int reference_width, int reference_height) { std::vector> properties; Error err = file->get_properties(item_id, properties); if (err) { - return {}; + return RegionCoordinateTransform{}; } uint32_t image_width = 0, image_height = 0; @@ -457,7 +536,7 @@ } if (image_width == 0 || image_height == 0) { - return {}; + return RegionCoordinateTransform{}; } RegionCoordinateTransform transform; @@ -519,10 +598,12 @@ } case fourcc("clap"): { auto clap = std::dynamic_pointer_cast(property); - int left = clap->left_rounded(image_width); - int top = clap->top_rounded(image_height); - transform.tx -= left; - transform.ty -= top; + auto crop = clap->get_crop(image_width, image_height); + if (!crop) { + return crop.error(); + } + transform.tx -= crop->left; + transform.ty -= crop->top; image_width = clap->get_width_rounded(); image_height = clap->get_height_rounded(); break; diff -Nru libheif-1.19.8/libheif/region.h libheif-1.23.4/libheif/region.h --- libheif-1.19.8/libheif/region.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/region.h 2026-09-06 14:45:17.000000000 +0000 @@ -24,7 +24,7 @@ #include #include #include -#include "pixelimage.h" +#include "image/pixelimage.h" #include "libheif/heif_regions.h" @@ -38,7 +38,7 @@ RegionItem(heif_item_id itemId, uint32_t ref_width, uint32_t ref_height) : item_id(itemId), reference_width(ref_width), reference_height(ref_height) {} - Error parse(const std::vector& data); + Error parse(const std::vector& data, const heif_security_limits*); Error encode(std::vector& result) const; @@ -67,7 +67,8 @@ virtual heif_region_type getRegionType() = 0; - virtual Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) = 0; + virtual Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) = 0; virtual bool encode_needs_32bit() const { return false; } @@ -82,7 +83,8 @@ class RegionGeometry_Point : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; bool encode_needs_32bit() const override; @@ -96,7 +98,8 @@ class RegionGeometry_Rectangle : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; bool encode_needs_32bit() const override; @@ -111,7 +114,8 @@ class RegionGeometry_Ellipse : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; bool encode_needs_32bit() const override; @@ -126,7 +130,8 @@ class RegionGeometry_Polygon : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int* dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; bool encode_needs_32bit() const override; @@ -144,12 +149,14 @@ bool closed = true; std::vector points; + MemoryHandle m_memory_handle; }; class RegionGeometry_ReferencedMask : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int *dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; void encode(StreamWriter&, int field_size_bytes) const override; @@ -163,13 +170,15 @@ class RegionGeometry_InlineMask : public RegionGeometry { public: - Error parse(const std::vector& data, int field_size, unsigned int *dataOffset) override; + Error parse(const std::vector& data, int field_size, unsigned int* dataOffset, + const heif_security_limits* limits) override; void encode(StreamWriter&, int field_size_bytes) const override; int32_t x,y; uint32_t width, height; std::vector mask_data; + MemoryHandle m_memory_handle; heif_region_type getRegionType() override { return heif_region_type_inline_mask; } }; @@ -179,9 +188,11 @@ class RegionCoordinateTransform { public: - static RegionCoordinateTransform create(std::shared_ptr file, - heif_item_id item_id, - int reference_width, int reference_height); + // Fails if a transformative property of the image cannot be applied (e.g. a 'clap' + // on an image size outside the supported range). + static Result create(const std::shared_ptr& file, + heif_item_id item_id, + int reference_width, int reference_height); struct Point { diff -Nru libheif-1.19.8/libheif/security_limits.cc libheif-1.23.4/libheif/security_limits.cc --- libheif-1.19.8/libheif/security_limits.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/security_limits.cc 2026-09-06 14:45:17.000000000 +0000 @@ -19,11 +19,14 @@ */ #include "security_limits.h" +#include #include +#include +#include -struct heif_security_limits global_security_limits { - .version = 1, +heif_security_limits global_security_limits{ + .version = 4, // --- version 1 @@ -31,25 +34,96 @@ // 32768^2 = 1.5 GB as YUV-4:2:0 or 4 GB as RGB32 .max_image_size_pixels = 32768 * 32768, .max_number_of_tiles = 4096 * 4096, - .max_bayer_pattern_pixels = 16*16, + .max_bayer_pattern_pixels = 16 * 16, .max_items = 1000, .max_color_profile_size = 100 * 1024 * 1024, // 100 MB - .max_memory_block_size = 512 * 1024 * 1024, // 512 MB + .max_memory_block_size = UINT64_C(4) * 1024 * 1024 * 1024, // 4 GB .max_components = 256, .max_iloc_extents_per_item = 32, .max_size_entity_group = 64, - .max_children_per_box = 100 + .max_children_per_box = 100, + + // --- version 2 + + .max_total_memory = UINT64_C(4) * 1024 * 1024 * 1024, // 4 GB + .max_sample_description_box_entries = 1024, + .max_sample_group_description_box_entries = 1024, + + // --- version 3 + + .max_sequence_frames = 18'000'000, // 100 hours at 50 fps + .max_number_of_file_brands = 1000, + + // --- version 4 + + .max_bad_pixels = 1000, + + .max_iso23001_17_pixel_size_bytes = 256, + + .parent = nullptr }; -struct heif_security_limits disabled_security_limits{ - .version = 1 +heif_security_limits disabled_security_limits{ + .version = 4, + .parent = nullptr }; +uint32_t max_coding_unit_size_for_codec(heif_compression_format format) +{ + switch (format) { + case heif_compression_AV1: return 128; // AV1 max superblock + case heif_compression_VVC: return 128; // VVC max CTU + case heif_compression_HEVC: return 64; // HEVC max CTU + case heif_compression_AVC: return 16; // H.264 macroblock + case heif_compression_JPEG: return 16; // JPEG MCU (4:2:0) + case heif_compression_JPEG2000: return 64; + case heif_compression_HTJ2K: return 64; + default: return 0; + } +} + + +heif_security_limits tighten_image_size_limit_for_ispe(const heif_security_limits* base, + uint32_t ispe_width, + uint32_t ispe_height, + uint32_t coding_unit_size) +{ + heif_security_limits result = *base; + + // The returned struct is a stack-local derived copy. Point parent at the + // registered context so MemoryHandle::alloc() can still find the entry in + // sMemoryUsage for total-memory accounting. If base is itself derived, walk + // to the root so we keep the parent chain at one hop. + result.parent = (base->version >= 4 && base->parent) ? base->parent : base; + result.version = 4; + + if (ispe_width == 0 || ispe_height == 0) { + return result; + } + + uint64_t padded_w = static_cast(ispe_width) + coding_unit_size; + uint64_t padded_h = static_cast(ispe_height) + coding_unit_size; + + // Skip tightening if the padded dimensions would overflow uint64_t when multiplied. + // The image is already absurdly large; check_for_valid_image_size will reject it. + if (padded_w != 0 && padded_h > std::numeric_limits::max() / padded_w) { + return result; + } + + uint64_t allowed = std::max(padded_w * padded_h, MIN_TIGHTENED_CODED_IMAGE_PIXELS); + + if (result.max_image_size_pixels == 0 || allowed < result.max_image_size_pixels) { + result.max_image_size_pixels = allowed; + } + return result; +} + + Error check_for_valid_image_size(const heif_security_limits* limits, uint32_t width, uint32_t height) { uint64_t maximum_image_size_limit = limits->max_image_size_pixels; @@ -78,3 +152,193 @@ return Error::Ok; } + + +struct memory_stats { + size_t total_memory_usage = 0; + size_t max_memory_usage = 0; +}; + +std::mutex& get_memory_usage_mutex() +{ + static std::mutex sMutex; + return sMutex; +} + +static std::map sMemoryUsage; + +TotalMemoryTracker::TotalMemoryTracker(const heif_security_limits* limits) +{ + std::lock_guard lock(get_memory_usage_mutex()); + + sMemoryUsage[limits] = {}; + m_limits_context = limits; +} + +TotalMemoryTracker::~TotalMemoryTracker() +{ + std::lock_guard lock(get_memory_usage_mutex()); + sMemoryUsage.erase(m_limits_context); +} + + +size_t TotalMemoryTracker::get_max_total_memory_used() const +{ + std::lock_guard lock(get_memory_usage_mutex()); + + auto it = sMemoryUsage.find(m_limits_context); + if (it != sMemoryUsage.end()) { + return it->second.max_memory_usage; + } + else { + assert(false); + return 0; + } +} + + +Error MemoryHandle::alloc(size_t count, size_t element_size, + const heif_security_limits* limits_context, + const char* reason_description) +{ + if (element_size != 0 && count > SIZE_MAX / element_size) { + std::stringstream sstr; + if (reason_description) { + sstr << "Allocation size overflow computing " << count << " * " << element_size + << " for " << reason_description; + } + else { + sstr << "Allocation size overflow computing " << count << " * " << element_size; + } + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + return alloc(count * element_size, limits_context, reason_description); +} + + +Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limits_context, + const char* reason_description) +{ + // --- check whether limits are exceeded + + if (!limits_context) { + return Error::Ok; + } + + // check against maximum memory block size + + if (limits_context->max_memory_block_size != 0 && + memory_amount > limits_context->max_memory_block_size) { + std::stringstream sstr; + + if (reason_description) { + sstr << "Allocating " << memory_amount << " bytes for " << reason_description <<" exceeds the security limit of " + << limits_context->max_memory_block_size << " bytes"; + } + else { + sstr << "Allocating " << memory_amount << " bytes exceeds the security limit of " + << limits_context->max_memory_block_size << " bytes"; + } + + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + + // Resolve to the registered (root) context for total-memory accounting. + // The passed-in limits may be a stack-local derived copy (e.g. tightened for + // ispe) whose `parent` points back to the registered context. + const heif_security_limits* root_limits = limits_context; + while (root_limits->version >= 4 && root_limits->parent) { + root_limits = root_limits->parent; + } + + // we allow several allocations on the same handle, but they have to be for the same registered context + if (m_limits_context) { + assert(m_limits_context == root_limits); + } + + if (root_limits == &global_security_limits || + root_limits == &disabled_security_limits) { + return Error::Ok; + } + + std::lock_guard lock(get_memory_usage_mutex()); + auto it = sMemoryUsage.find(root_limits); + if (it == sMemoryUsage.end()) { + // Unregistered limits context with no resolvable parent — total-memory + // tracking is not available, but the per-block check above still applies. + return Error::Ok; + } + + // check against maximum total memory usage + + if (limits_context->max_total_memory != 0 && + it->second.total_memory_usage + memory_amount > limits_context->max_total_memory) { + std::stringstream sstr; + + if (reason_description) { + sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount + << " bytes for " << reason_description << " exceeds the security limit of " + << limits_context->max_total_memory << " bytes of total memory usage"; + } + else { + sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount + << " bytes exceeds the security limit of " + << limits_context->max_total_memory << " bytes of total memory usage"; + } + + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + + + // --- register memory usage + + m_limits_context = root_limits; + m_memory_amount += memory_amount; + + it->second.total_memory_usage += memory_amount; + + // remember maximum memory usage (for informational purpose) + if (it->second.total_memory_usage > it->second.max_memory_usage) { + it->second.max_memory_usage = it->second.total_memory_usage; + } + + return Error::Ok; +} + + +void MemoryHandle::free() +{ + if (m_limits_context) { + std::lock_guard lock(get_memory_usage_mutex()); + + auto it = sMemoryUsage.find(m_limits_context); + if (it != sMemoryUsage.end()) { + it->second.total_memory_usage -= m_memory_amount; + } + + m_limits_context = nullptr; + m_memory_amount = 0; + } +} + + +void MemoryHandle::free(size_t memory_amount) +{ + if (m_limits_context) { + std::lock_guard lock(get_memory_usage_mutex()); + + auto it = sMemoryUsage.find(m_limits_context); + if (it != sMemoryUsage.end()) { + it->second.total_memory_usage -= memory_amount; + } + + m_memory_amount -= memory_amount; + } +} + diff -Nru libheif-1.19.8/libheif/security_limits.h libheif-1.23.4/libheif/security_limits.h --- libheif-1.19.8/libheif/security_limits.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/libheif/security_limits.h 2026-09-06 14:45:17.000000000 +0000 @@ -23,6 +23,9 @@ #include "libheif/heif.h" #include #include +#include +#include +#include #include "error.h" @@ -38,7 +41,161 @@ static const int64_t MAX_FILE_POS = 0x007FFFFFFFFFFFFFLL; // maximum file position static const int MAX_FRACTION_VALUE = 0x10000; +// Maximum number of 'iovl' overlay images that may be nested inside one another +// along a single decode path. Real files use at most one overlay per decode +// chain, so this is a structural sanity limit, not a common-case constraint. +static const uint32_t MAX_OVERLAY_NESTING_LEVEL = 3; + +// Maximum number of input images that a single 'iovl' overlay may composite. +// This is a hardcoded stopgap until a configurable security-limit field can be +// added to heif_security_limits in the next major release (that is an API/ABI +// change and cannot go into a point release). +static const uint32_t MAX_OVERLAY_IMAGES = 5; + +// Factor applied to max_items to bound the total number of sub-image decode +// operations triggered by a single top-level decode. Derived images (grid, +// iovl, iden) can reference the same base image through indirection, and the +// cycle-detection set is per-path (it forks at every branch), so a shared +// subtree can be re-decoded once per path that reaches it. Nested sharing makes +// the number of decode operations grow as branch^depth, which is bounded only +// by the recursion depth (<= number of items). Because a well-formed file +// decodes each of its (<= max_items) items a small number of times, capping the +// total at a modest multiple of max_items stops the exponential blow-up while +// leaving every realistic file untouched. (GHSA-x8xm-cm2c-cfc8, variants V1/V2.) +static const uint32_t MAX_DERIVED_IMAGE_DECODE_FACTOR = 2; + + +// Traversal state threaded through the derived-image decode recursion for one +// top-level decode. It is copied by value at every hop, which gives the two +// members opposite (and intended) sharing semantics: +// +// - processed_ids and overlay_nesting are plain values, so each recursion +// branch gets its own copy. processed_ids is a per-path cycle guard; +// overlay_nesting counts the overlays nested along the current path. +// +// - decode_count is a shared_ptr to an atomic, so every copy (including the +// copies handed to parallel tile-decode threads) shares one global counter +// that bounds the total number of decode operations. +// +// A default-constructed DecodeTraversalState (max_decodes == 0) imposes no limit; the +// budget is seeded once at the top level in HeifContext::decode_image(). +struct DecodeTraversalState +{ + std::set processed_ids; + + uint32_t overlay_nesting = 0; // number of overlays on the path to here + uint32_t max_overlay_nesting = 0; // 0 == unlimited + + std::shared_ptr> decode_count; + uint32_t max_decodes = 0; // 0 == unlimited + + // Count one sub-image decode against the shared budget. + // Returns false when the budget has been exhausted. + bool count_decode() + { + if (max_decodes == 0 || !decode_count) { + return true; + } + return decode_count->fetch_add(1, std::memory_order_relaxed) < max_decodes; + } +}; + Error check_for_valid_image_size(const heif_security_limits* limits, uint32_t width, uint32_t height); +// Maximum coding-unit size (in pixels) that the given codec may pad a coded +// frame up to. Used as the margin for tighten_image_size_limit_for_ispe. +// Returns 0 for codecs without coding-unit padding (e.g. uncompressed). +uint32_t max_coding_unit_size_for_codec(heif_compression_format format); + +// Return a copy of `base` with max_image_size_pixels lowered to a value +// just above the declared image size. This is used to bound how much memory +// a codec plugin may allocate for an image whose internal (codec-declared) +// dimensions exceed the file-declared (ispe) dimensions — without us having +// to parse the codec bitstream ourselves. +// +// `coding_unit_size` is the maximum coding-unit size of the target codec +// (e.g. 128 for AV1/VVC, 64 for HEVC, 16 for AVC). The allowed coded +// dimensions are (ispe + coding_unit_size) in each axis, since a codec may +// pad the coded frame up to a coding-unit boundary. +// +// The allowed size never drops below MIN_TIGHTENED_CODED_IMAGE_PIXELS: the +// HEVC/AVC conformance window may crop arbitrarily much, and hardware +// encoders use minimum surface sizes far above tiny image dimensions (issue +// #1856 has a conformant 2x2 HEIC coded as a 160x64 frame). The floor keeps +// the worst-case allocation for a lying bitstream small while accepting such +// padded coded frames; check_decoded_image_size() still validates the +// decoded output against 'ispe' afterwards. +static const uint64_t MIN_TIGHTENED_CODED_IMAGE_PIXELS = 65536; // 256x256 + +heif_security_limits tighten_image_size_limit_for_ispe(const heif_security_limits* base, + uint32_t ispe_width, + uint32_t ispe_height, + uint32_t coding_unit_size); + + +class TotalMemoryTracker +{ +public: + explicit TotalMemoryTracker(const heif_security_limits* limits_context); + ~TotalMemoryTracker(); + + size_t get_max_total_memory_used() const; + + void operator=(const TotalMemoryTracker&) = delete; + TotalMemoryTracker(const TotalMemoryTracker&) = delete; + +private: + const heif_security_limits* m_limits_context = nullptr; +}; + + +class MemoryHandle +{ +public: + MemoryHandle() = default; + ~MemoryHandle() { free(); } + + Error alloc(size_t memory_amount, const heif_security_limits* limits_context, const char* reason_description); + + // calloc-style overload: checks `count * element_size` for size_t overflow before allocating. + // Use this when allocating an array whose total size is count*element_size, to avoid silent + // truncation on 32-bit builds when count is near UINT32_MAX. + Error alloc(size_t count, size_t element_size, + const heif_security_limits* limits_context, const char* reason_description); + + void free(); + + void free(size_t memory_amount); + + const heif_security_limits* get_security_limits() const { return m_limits_context; } + + MemoryHandle(const MemoryHandle&) = delete; + MemoryHandle& operator=(const MemoryHandle&) = delete; + + MemoryHandle(MemoryHandle&& other) noexcept + : m_limits_context(other.m_limits_context), m_memory_amount(other.m_memory_amount) + { + other.m_limits_context = nullptr; + other.m_memory_amount = 0; + } + + MemoryHandle& operator=(MemoryHandle&& other) noexcept + { + if (this != &other) { + free(); + m_limits_context = other.m_limits_context; + m_memory_amount = other.m_memory_amount; + other.m_limits_context = nullptr; + other.m_memory_amount = 0; + } + return *this; + } + +private: + const heif_security_limits* m_limits_context = nullptr; + size_t m_memory_amount = 0; +}; + + #endif // LIBHEIF_SECURITY_LIMITS_H diff -Nru libheif-1.19.8/libheif/sequences/chunk.cc libheif-1.23.4/libheif/sequences/chunk.cc --- libheif-1.19.8/libheif/sequences/chunk.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/chunk.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,155 @@ +/* + * HEIF image base codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "chunk.h" +#include "context.h" +#include "codecs/avc_enc.h" +#include "codecs/hevc_enc.h" +#include "codecs/avif_enc.h" +#include "codecs/vvc_enc.h" +#include "codecs/jpeg2000_enc.h" +#include "codecs/jpeg_enc.h" + +#if WITH_UNCOMPRESSED_CODEC +#include "codecs/uncompressed/unc_enc.h" +#endif + + +Chunk::Chunk(HeifContext* ctx, uint32_t track_id, heif_compression_format format) + : m_ctx(ctx), + m_track_id(track_id), + m_compression_format(format) +{ + switch (format) { + case heif_compression_HEVC: + m_encoder = std::make_shared(); + break; + case heif_compression_AV1: + m_encoder = std::make_shared(); + break; + case heif_compression_VVC: + m_encoder = std::make_shared(); + break; + case heif_compression_AVC: + m_encoder = std::make_shared(); + break; + case heif_compression_JPEG2000: + m_encoder = std::make_shared(); + break; + case heif_compression_HTJ2K: + m_encoder = std::make_shared(); + break; + case heif_compression_JPEG: + m_encoder = std::make_shared(); + break; +#if WITH_UNCOMPRESSED_CODEC + case heif_compression_uncompressed: + m_encoder = std::make_shared(); + break; +#endif + case heif_compression_undefined: + default: + m_encoder = nullptr; + break; + } +} + + +std::shared_ptr Chunk::create(HeifContext* ctx, uint32_t track_id, + uint32_t first_sample, uint32_t num_samples, + uint64_t file_offset, + const std::shared_ptr& stsz) +{ + bool success; + auto chunk = std::shared_ptr(new Chunk(ctx, track_id, first_sample, + num_samples, file_offset, stsz, + success)); + if (!success) { + return nullptr; + } + return chunk; +} + + +Chunk::Chunk(HeifContext* ctx, uint32_t track_id, + uint32_t first_sample, uint32_t num_samples, uint64_t file_offset, + const std::shared_ptr& stsz, bool& success) +{ + success = false; + + m_ctx = ctx; + m_track_id = track_id; + + m_first_sample = first_sample; + m_last_sample = first_sample + num_samples - 1; + + m_next_sample_to_be_decoded = first_sample; + + // Reserve the exact final size so the running offset check below is the only + // growth concern (no geometric reallocation). The aggregate over all chunks is + // accounted against max_total_memory by the owning Track before any chunk is + // built (GHSA-xw34-mjcp-jqh8, variants V2/V3), so no per-chunk limit check here. + m_sample_ranges.reserve(num_samples); + + for (uint32_t i=0;ihas_fixed_sample_size()) { + range.size = stsz->get_fixed_sample_size(); + } + else { + assert(first_sample + i < stsz->num_samples()); + range.size = stsz->get_sample_sizes()[first_sample + i]; + } + + // Detect uint64_t wrap when advancing the running offset. Cumulative chunk + // size can exceed UINT64_MAX with a malformed stsz (uint32_t sample size × + // uint32_t sample count) starting from a large co64 chunk offset. Stop + // building the chunk; create() will discard the partially-built object. + if (file_offset > UINT64_MAX - range.size) { + return; + } + + m_sample_ranges.push_back(range); + + file_offset += range.size; + } + + success = true; +} + + +size_t Chunk::sample_range_entry_size() +{ + return sizeof(SampleFileRange); +} + + +DataExtent Chunk::get_data_extent_for_sample(uint32_t n) const +{ + assert(n>= m_first_sample); + assert(n<= m_last_sample); + + DataExtent extent; + extent.set_file_range(m_ctx->get_heif_file(), + m_sample_ranges[n - m_first_sample].offset, + m_sample_ranges[n - m_first_sample].size); + return extent; +} diff -Nru libheif-1.19.8/libheif/sequences/chunk.h libheif-1.23.4/libheif/sequences/chunk.h --- libheif-1.19.8/libheif/sequences/chunk.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/chunk.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,101 @@ +/* + * HEIF image base codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_CHUNK_H +#define LIBHEIF_CHUNK_H + +#include "codecs/decoder.h" +#include +#include +#include + + +class HeifContext; + +class Box_VisualSampleEntry; + + +class Chunk +{ +public: + Chunk(HeifContext* ctx, uint32_t track_id, heif_compression_format format); + + // Returns nullptr if the chunk cannot be constructed validly (e.g. the + // running file offset would wrap uint64_t). + static std::shared_ptr create(HeifContext* ctx, uint32_t track_id, + uint32_t first_sample, uint32_t num_samples, + uint64_t file_offset, + const std::shared_ptr& sample_sizes); + + virtual ~Chunk() = default; + + // Size in bytes of one entry in the internal sample-range table. Used by the + // owning Track to reserve the aggregate memory for all chunks up front + // (GHSA-xw34-mjcp-jqh8, variants V2/V3). + static size_t sample_range_entry_size(); + + heif_compression_format get_compression_format() const { return m_compression_format; } + + virtual std::shared_ptr get_decoder() const { return m_decoder; } + + virtual std::shared_ptr get_encoder() const { return m_encoder; } + + uint32_t first_sample_number() const { return m_first_sample; } + + uint32_t last_sample_number() const { return m_last_sample; } + + DataExtent get_data_extent_for_sample(uint32_t n) const; + + void set_decoder(std::shared_ptr dec) { m_decoder = std::move(dec); } + +private: + // Sets `success` to false if the chunk cannot be constructed validly + // (e.g. the running file offset would wrap uint64_t). Otherwise leaves it + // unchanged. + Chunk(HeifContext* ctx, uint32_t track_id, + uint32_t first_sample, uint32_t num_samples, uint64_t file_offset, + const std::shared_ptr& sample_sizes, bool& success); + + HeifContext* m_ctx = nullptr; + uint32_t m_track_id = 0; + + heif_compression_format m_compression_format = heif_compression_undefined; + + uint32_t m_first_sample = 0; + uint32_t m_last_sample = 0; + + //uint32_t m_sample_description_index = 0; + + uint32_t m_next_sample_to_be_decoded = 0; + + struct SampleFileRange + { + uint64_t offset = 0; + uint32_t size = 0; + }; + + std::vector m_sample_ranges; + + std::shared_ptr m_decoder; + std::shared_ptr m_encoder; +}; + + +#endif //LIBHEIF_CHUNK_H diff -Nru libheif-1.19.8/libheif/sequences/seq_boxes.cc libheif-1.23.4/libheif/sequences/seq_boxes.cc --- libheif-1.19.8/libheif/sequences/seq_boxes.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/seq_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,2596 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "sequences/seq_boxes.h" +#include +#include +#include +#include +#include + + +Error Box_container::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + return read_children(range, READ_CHILDREN_ALL, limits); +} + + +std::string Box_container::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + sstr << dump_children(indent); + + return sstr.str(); +} + + +double Box_mvhd::get_matrix_element(int idx) const +{ + if (idx == 8) { + return 1.0; + } + + return m_matrix[idx] / double(0x10000); +} + + +Error Box_mvhd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 1) { + return unsupported_version_error("mvhd"); + } + + if (get_version() == 1) { + m_creation_time = range.read64(); + m_modification_time = range.read64(); + m_timescale = range.read32(); + m_duration = range.read64(); + } + else { + // version==0 + m_creation_time = range.read32(); + m_modification_time = range.read32(); + m_timescale = range.read32(); + m_duration = range.read32(); + } + + m_rate = range.read32(); + m_volume = range.read16(); + range.skip(2); + range.skip(8); + for (uint32_t& m : m_matrix) { + m = range.read32(); + } + for (int i = 0; i < 6; i++) { + range.skip(4); + } + + m_next_track_ID = range.read32(); + + return range.get_error(); +} + + +std::string Box_mvhd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "creation time: " << m_creation_time << "\n" + << indent << "modification time: " << m_modification_time << "\n" + << indent << "timescale: " << m_timescale << "\n" + << indent << "duration: " << m_duration << "\n"; + sstr << indent << "rate: " << get_rate() << "\n" + << indent << "volume: " << get_volume() << "\n" + << indent << "matrix:\n"; + for (int y = 0; y < 3; y++) { + sstr << indent << " "; + for (int i = 0; i < 3; i++) { + sstr << get_matrix_element(i + 3 * y) << " "; + } + sstr << "\n"; + } + sstr << indent << "next_track_ID: " << m_next_track_ID << "\n"; + + return sstr.str(); +} + + +void Box_mvhd::derive_box_version() +{ + if (m_creation_time > 0xFFFFFFFF || + m_modification_time > 0xFFFFFFFF || + m_timescale > 0xFFFFFFFF || + m_duration > 0xFFFFFFFF) { + set_version(1); + } + else { + set_version(0); + } +} + + +Error Box_mvhd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (get_version() == 1) { + writer.write64(m_creation_time); + writer.write64(m_modification_time); + writer.write32(m_timescale); + writer.write64(m_duration); + } + else { + // version==0 + writer.write32(static_cast(m_creation_time)); + writer.write32(static_cast(m_modification_time)); + writer.write32(static_cast(m_timescale)); + writer.write32(static_cast(m_duration)); + } + + writer.write32(m_rate); + writer.write16(m_volume); + writer.write16(0); + writer.write64(0); + for (uint32_t m : m_matrix) { + writer.write32(m); + } + for (int i = 0; i < 6; i++) { + writer.write32(0); + } + + writer.write32(m_next_track_ID); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +double Box_tkhd::get_matrix_element(int idx) const +{ + if (idx == 8) { + return 1.0; + } + + return m_matrix[idx] / double(0x10000); +} + + +Error Box_tkhd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 1) { + return unsupported_version_error("tkhd"); + } + + if (get_version() == 1) { + m_creation_time = range.read64(); + m_modification_time = range.read64(); + m_track_id = range.read32(); + range.skip(4); + m_duration = range.read64(); + } + else { + // version==0 + m_creation_time = range.read32(); + m_modification_time = range.read32(); + m_track_id = range.read32(); + range.skip(4); + m_duration = range.read32(); + } + + range.skip(8); + m_layer = range.read16(); + m_alternate_group = range.read16(); + m_volume = range.read16(); + range.skip(2); + for (uint32_t& m : m_matrix) { + m = range.read32(); + } + + m_width = range.read32(); + m_height = range.read32(); + + return range.get_error(); +} + + +std::string Box_tkhd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "track enabled: " << ((get_flags() & Track_enabled) ? "yes" : "no") << "\n" + << indent << "track in movie: " << ((get_flags() & Track_in_movie) ? "yes" : "no") << "\n" + << indent << "track in preview: " << ((get_flags() & Track_in_preview) ? "yes" : "no") << "\n" + << indent << "track size is aspect ratio: " << ((get_flags() & Track_size_is_aspect_ratio) ? "yes" : "no") << "\n"; + sstr << indent << "creation time: " << m_creation_time << "\n" + << indent << "modification time: " << m_modification_time << "\n" + << indent << "track ID: " << m_track_id << "\n" + << indent << "duration: " << m_duration << "\n"; + sstr << indent << "layer: " << m_layer << "\n" + << indent << "alternate_group: " << m_alternate_group << "\n" + << indent << "volume: " << get_volume() << "\n" + << indent << "matrix:\n"; + for (int y = 0; y < 3; y++) { + sstr << indent << " "; + for (int i = 0; i < 3; i++) { + sstr << get_matrix_element(i + 3 * y) << " "; + } + sstr << "\n"; + } + + sstr << indent << "width: " << get_width() << "\n" + << indent << "height: " << get_height() << "\n"; + + return sstr.str(); +} + + +void Box_tkhd::derive_box_version() +{ + if (m_creation_time > 0xFFFFFFFF || + m_modification_time > 0xFFFFFFFF || + m_duration > 0xFFFFFFFF) { + set_version(1); + } + else { + set_version(0); + } +} + + +Error Box_tkhd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (get_version() == 1) { + writer.write64(m_creation_time); + writer.write64(m_modification_time); + writer.write32(m_track_id); + writer.write32(0); + writer.write64(m_duration); + } + else { + // version==0 + writer.write32(static_cast(m_creation_time)); + writer.write32(static_cast(m_modification_time)); + writer.write32(m_track_id); + writer.write32(0); + writer.write32(static_cast(m_duration)); + } + + writer.write64(0); + writer.write16(m_layer); + writer.write16(m_alternate_group); + writer.write16(m_volume); + writer.write16(0); + for (uint32_t m : m_matrix) { + writer.write32(m); + } + + writer.write32(m_width); + writer.write32(m_height); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_mdhd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 1) { + return unsupported_version_error("mdhd"); + } + + if (get_version() == 1) { + m_creation_time = range.read64(); + m_modification_time = range.read64(); + m_timescale = range.read32(); + m_duration = range.read64(); + } + else { + // version==0 + m_creation_time = range.read32(); + m_modification_time = range.read32(); + m_timescale = range.read32(); + m_duration = range.read32(); + } + + uint16_t language_packed = range.read16(); + m_language[0] = ((language_packed >> 10) & 0x1F) + 0x60; + m_language[1] = ((language_packed >> 5) & 0x1F) + 0x60; + m_language[2] = ((language_packed >> 0) & 0x1F) + 0x60; + m_language[3] = 0; + + range.skip(2); + + return range.get_error(); +} + + +std::string Box_mdhd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "creation time: " << m_creation_time << "\n" + << indent << "modification time: " << m_modification_time << "\n" + << indent << "timescale: " << m_timescale << "\n" + << indent << "duration: " << m_duration << "\n"; + sstr << indent << "language: " << m_language << "\n"; + + return sstr.str(); +} + + +void Box_mdhd::derive_box_version() +{ + if (m_creation_time > 0xFFFFFFFF || + m_modification_time > 0xFFFFFFFF || + m_duration > 0xFFFFFFFF) { + set_version(1); + } + else { + set_version(0); + } +} + + +Error Box_mdhd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (get_version() == 1) { + writer.write64(m_creation_time); + writer.write64(m_modification_time); + writer.write32(m_timescale); + writer.write64(m_duration); + } + else { + // version==0 + writer.write32(static_cast(m_creation_time)); + writer.write32(static_cast(m_modification_time)); + writer.write32(m_timescale); + writer.write32(static_cast(m_duration)); + } + + auto language_packed = static_cast((((m_language[0] - 0x60) & 0x1F) << 10) | + (((m_language[1] - 0x60) & 0x1F) << 5) | + (((m_language[2] - 0x60) & 0x1F) << 0)); + writer.write16(language_packed); + writer.write16(0); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + + +Error Box_vmhd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("vmhd"); + } + + m_graphics_mode = range.read16(); + for (uint16_t& c : m_op_color) { + c = range.read16(); + } + + return range.get_error(); +} + + +std::string Box_vmhd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "graphics mode: " << m_graphics_mode; + if (m_graphics_mode == 0) { + sstr << " (copy)"; + } + sstr << "\n" + << indent << "op color: " << m_op_color[0] << "; " << m_op_color[1] << "; " << m_op_color[2] << "\n"; + + return sstr.str(); +} + + +Error Box_vmhd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write16(m_graphics_mode); + for (uint16_t c : m_op_color) { + writer.write16(c); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_nmhd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("nmhd"); + } + + return range.get_error(); +} + + +std::string Box_nmhd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + + return sstr.str(); +} + + +Error Box_nmhd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_stsd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stsd"); + } + + uint32_t entry_count = range.read32(); + + if (limits->max_sample_description_box_entries && + entry_count > limits->max_sample_description_box_entries) { + std::stringstream sstr; + sstr << "Allocating " << static_cast(entry_count) << " sample description items exceeds the security limit of " + << limits->max_sample_description_box_entries << " items"; + + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + + } + + for (uint32_t i = 0; i < entry_count; i++) { + std::shared_ptr entrybox; + Error err = Box::read(range, &entrybox, limits); + if (err) { + return err; + } + +#if 0 + auto visualSampleEntry_box = std::dynamic_pointer_cast(entrybox); + if (!visualSampleEntry_box) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Invalid or unknown VisualSampleEntry in stsd box."}; + } +#endif + + m_sample_entries.push_back(entrybox); + } + + return range.get_error(); +} + + +std::string Box_stsd::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_sample_entries.size(); i++) { + sstr << indent << "[" << i << "]\n"; + indent++; + sstr << m_sample_entries[i]->dump(indent); + indent--; + } + + return sstr.str(); +} + + +Error Box_stsd::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_sample_entries.size())); + for (const auto& sample : m_sample_entries) { + sample->write(writer); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_stts::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stts"); + } + + uint32_t entry_count = range.read32(); + + if (limits->max_sequence_frames > 0 && entry_count > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + + if (auto err = m_memory_handle.alloc(entry_count, sizeof(TimeToSample), + limits, "the 'stts' table")) { + return err; + } + + m_entries.resize(entry_count); + + for (uint32_t i = 0; i < entry_count; i++) { + if (range.eof()) { + std::stringstream sstr; + sstr << "stts box should contain " << entry_count << " entries, but box only contained " + << i << " entries"; + + return { + heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str() + }; + } + + TimeToSample entry{}; + entry.sample_count = range.read32(); + entry.sample_delta = range.read32(); + m_entries[i] = entry; + } + + // The run-lengths describe the samples of the track, so their sum is the track's + // sample count: a 32-bit quantity that 'stsz' caps at max_sequence_frames. Nothing + // above bounds the sum, so an oversized table would wrap get_number_of_samples() + // and could thereby slip past the consistency check against 'stsz'. + { + uint64_t total_samples = 0; + for (const auto& entry : m_entries) { + total_samples += entry.sample_count; + } + + uint64_t max_samples = (limits->max_sequence_frames > 0 + ? limits->max_sequence_frames + : uint64_t{0xFFFFFFFF}); + + if (total_samples > max_samples) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + } + + // Precompute the prefix sum of sample_count for O(log entries) lookups in + // get_sample_duration(). Safe as uint32 because the total was just checked to + // be <= max_samples <= 0xFFFFFFFF. + { + uint32_t running = 0; + for (auto& entry : m_entries) { + running += entry.sample_count; + entry.cumulative_sample_count = running; + } + } + + return range.get_error(); +} + + +std::string Box_stts::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_entries.size(); i++) { + sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", delta=" << m_entries[i].sample_delta << "\n"; + } + + return sstr.str(); +} + + +Error Box_stts::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_entries.size())); + for (const auto& sample : m_entries) { + writer.write32(sample.sample_count); + writer.write32(sample.sample_delta); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +uint32_t Box_stts::get_sample_duration(uint32_t sample_idx) +{ + // The entries are contiguous and cumulative_sample_count is the non-decreasing + // index one past the last sample of each entry, so the entry covering sample_idx + // is the first one whose cumulative_sample_count is strictly greater than + // sample_idx. Binary search keeps this O(log entries); a linear scan would make + // the decode/raw output paths O(entries) per sample (GHSA-xw34-mjcp-jqh8, V1). + auto it = std::upper_bound(m_entries.begin(), m_entries.end(), sample_idx, + [](uint32_t idx, const TimeToSample& entry) { + return idx < entry.cumulative_sample_count; + }); + + if (it == m_entries.end()) { + // sample_idx is not covered by any entry. + return 0; + } + + return it->sample_delta; +} + + +void Box_stts::append_sample_duration(uint32_t duration) +{ + if (m_entries.empty() || m_entries.back().sample_delta != duration) { + TimeToSample entry{}; + entry.sample_delta = duration; + entry.sample_count = 1; + entry.cumulative_sample_count = (m_entries.empty() ? 0 : m_entries.back().cumulative_sample_count) + 1; + m_entries.push_back(entry); + return; + } + + m_entries.back().sample_count++; + m_entries.back().cumulative_sample_count++; +} + + +uint64_t Box_stts::get_total_duration(bool include_last_frame_duration) +{ + uint64_t total = 0; + + for (const auto& entry : m_entries) { + total += entry.sample_count * uint64_t(entry.sample_delta); + } + + if (!include_last_frame_duration && !m_entries.empty()) { + total -= m_entries.back().sample_delta; + } + + return total; +} + + +uint32_t Box_stts::get_number_of_samples() const +{ + // The number of samples in a track is a 32-bit quantity (ISO/IEC 14496-12 + // 8.7.3: the 'stsz' sample_count "gives the number of samples in the track"), + // and parse() rejects tables whose run-lengths sum beyond that. Accumulate in + // 64 bit anyway so that a box built by the encoder cannot wrap silently. + uint64_t total = 0; + for (const auto& entry : m_entries) { + total += entry.sample_count; + } + + assert(total <= 0xFFFFFFFF); + return static_cast(total); +} + + +Error Box_ctts::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + uint8_t version = get_version(); + + if (version > 1) { + return unsupported_version_error("ctts"); + } + + uint32_t entry_count = range.read32(); + + if (limits->max_sequence_frames > 0 && entry_count > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + + if (auto err = m_memory_handle.alloc(entry_count, sizeof(OffsetToSample), + limits, "the 'ctts' table")) { + return err; + } + + m_entries.resize(entry_count); + + for (uint32_t i = 0; i < entry_count; i++) { + if (range.eof()) { + std::stringstream sstr; + sstr << "ctts box should contain " << entry_count << " entries, but box only contained " + << i << " entries"; + + return { + heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str() + }; + } + + OffsetToSample entry{}; + entry.sample_count = range.read32(); + if (version == 0) { + uint32_t offset = range.read32(); +#if 0 + // TODO: I disabled this because I found several files that seem to + // have wrong data. Since we are not using the 'ctts' data anyway, + // let's not care about it now. + + if (offset > INT32_MAX) { + return { + heif_error_Unsupported_feature, + heif_suberror_Unsupported_parameter, + "We don't support offsets > 0x7fff in 'ctts' box." + }; + } +#endif + + entry.sample_offset = static_cast(offset); + } + else if (version == 1) { + entry.sample_offset = range.read32s(); + } + else { + assert(false); + } + + m_entries[i] = entry; + } + + // The run-lengths describe the samples of the track, so their sum is the track's + // sample count: a 32-bit quantity that 'stsz' caps at max_sequence_frames. Nothing + // above bounds the sum, so an oversized table would wrap get_number_of_samples() + // and could thereby slip past the consistency check against 'stsz'. + { + uint64_t total_samples = 0; + for (const auto& entry : m_entries) { + total_samples += entry.sample_count; + } + + uint64_t max_samples = (limits->max_sequence_frames > 0 + ? limits->max_sequence_frames + : uint64_t{0xFFFFFFFF}); + + if (total_samples > max_samples) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + } + + return range.get_error(); +} + + +std::string Box_ctts::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_entries.size(); i++) { + sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", offset=" << m_entries[i].sample_offset << "\n"; + } + + return sstr.str(); +} + + +int32_t Box_ctts::compute_min_offset() const +{ + int32_t min_offset = INT32_MAX; + for (const auto& entry : m_entries) { + min_offset = std::min(min_offset, entry.sample_offset); + } + + return min_offset; +} + + +uint32_t Box_ctts::get_number_of_samples() const +{ + // The number of samples in a track is a 32-bit quantity (ISO/IEC 14496-12 + // 8.7.3: the 'stsz' sample_count "gives the number of samples in the track"), + // and parse() rejects tables whose run-lengths sum beyond that. Accumulate in + // 64 bit anyway so that a box built by the encoder cannot wrap silently. + uint64_t total = 0; + for (const auto& entry : m_entries) { + total += entry.sample_count; + } + + assert(total <= 0xFFFFFFFF); + return static_cast(total); +} + + +Error Box_ctts::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + int32_t min_offset; + + if (get_version() == 0) { + // shift such that all offsets are >= 0 + min_offset = compute_min_offset(); + } + else { + // do not modify offsets + min_offset = 0; + } + + writer.write32(static_cast(m_entries.size())); + for (const auto& sample : m_entries) { + writer.write32(sample.sample_count); + writer.write32s(sample.sample_offset - min_offset); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +int32_t Box_ctts::get_sample_offset(uint32_t sample_idx) +{ + for (const auto& entry : m_entries) { + if (sample_idx < entry.sample_count) { + return entry.sample_offset; + } + sample_idx -= entry.sample_count; + } + + return 0; +} + + +void Box_ctts::append_sample_offset(int32_t offset) +{ + if (m_entries.empty() || m_entries.back().sample_offset != offset) { + OffsetToSample entry{}; + entry.sample_offset = offset; + entry.sample_count = 1; + m_entries.push_back(entry); + return; + } + + m_entries.back().sample_count++; +} + + +bool Box_ctts::is_constant_offset() const +{ + return m_entries.empty() || m_entries.size() == 1; +} + +void Box_ctts::derive_box_version() +{ + set_version(1); +} + + +Error Box_stsc::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stsc"); + } + + uint32_t entry_count = range.read32(); + if (entry_count == 0) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'stsc' box with zero entries."}; + } + + // Note: test against maximum number of frames (upper limit) since we have no limit on maximum number of chunks + if (limits->max_sequence_frames > 0 && entry_count > limits->max_sequence_frames) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of chunks in `stsc` box exceeds security limits of maximum number of frames."}; + } + + + if (auto err = m_memory_handle.alloc(entry_count, sizeof(SampleToChunk), + limits, "the 'stsc' table")) { + return err; + } + + m_entries.resize(entry_count); + + for (uint32_t i = 0; i < entry_count; i++) { + SampleToChunk entry{}; + entry.first_chunk = range.read32(); + entry.samples_per_chunk = range.read32(); + entry.sample_description_index = range.read32(); + + if (entry.samples_per_chunk == 0) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'stsc' box with zero samples per chunk entry."}; + } + + if (entry.sample_description_index == 0) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'sample_description_index' in 'stsc' must not be 0."}; + } + + if (limits->max_sequence_frames > 0 && entry.samples_per_chunk > limits->max_sequence_frames) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of chunk samples in `stsc` box exceeds security limits of maximum number of frames."}; + } + + m_entries[i] = entry; + } + + return range.get_error(); +} + + +std::string Box_stsc::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_entries.size(); i++) { + sstr << indent << "[" << i << "]\n" + << indent << " first chunk: " << m_entries[i].first_chunk << "\n" + << indent << " samples per chunk: " << m_entries[i].samples_per_chunk << "\n" + << indent << " sample description index: " << m_entries[i].sample_description_index << "\n"; + } + + return sstr.str(); +} + + +Error Box_stsc::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_entries.size())); + for (const auto& sample : m_entries) { + writer.write32(sample.first_chunk); + writer.write32(sample.samples_per_chunk); + writer.write32(sample.sample_description_index); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +const Box_stsc::SampleToChunk* Box_stsc::get_chunk(uint32_t idx) const +{ + assert(idx>=1); + for (size_t i = 0 ; i < m_entries.size();i++) { + if (idx >= m_entries[i].first_chunk && (i==m_entries.size()-1 || idx < m_entries[i+1].first_chunk)) { + return &m_entries[i]; + } + } + + return nullptr; +} + + +void Box_stsc::add_chunk(uint32_t description_index) +{ + SampleToChunk entry{}; + entry.first_chunk = 1; // TODO + entry.samples_per_chunk = 0; + entry.sample_description_index = description_index; + m_entries.push_back(entry); +} + + +void Box_stsc::increase_samples_in_chunk(uint32_t nFrames) +{ + assert(!m_entries.empty()); + + m_entries.back().samples_per_chunk += nFrames; +} + + +Error Box_stco::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stco"); + } + + uint32_t entry_count = range.read32(); + + // Note: test against maximum number of frames (upper limit) since we have no limit on maximum number of chunks + if (limits->max_sequence_frames > 0 && entry_count > limits->max_sequence_frames) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of chunks in 'stco' box exceeds security limits of maximum number of frames." + }; + } + + // check required memory + + uint64_t mem_size = static_cast(entry_count) * sizeof(uint32_t); + if (auto err = m_memory_handle.alloc(mem_size, + limits, "the 'stco' table")) { + return err; + } + + for (uint32_t i = 0; i < entry_count; i++) { + m_offsets.push_back(range.read32()); + + if (range.error()) { + return range.get_error(); + } + } + + return range.get_error(); +} + + +std::string Box_stco::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_offsets.size(); i++) { + sstr << indent << "[" << i << "] : 0x" << std::hex << m_offsets[i] << std::dec << "\n"; + } + + return sstr.str(); +} + + +Error Box_stco::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_offsets.size())); + + m_offset_start_pos = writer.get_position(); + + for (uint32_t offset : m_offsets) { + writer.write32(offset); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +void Box_stco::patch_file_pointers(StreamWriter& writer, size_t offset) +{ + size_t oldPosition = writer.get_position(); + + writer.set_position(m_offset_start_pos); + + for (uint32_t chunk_offset : m_offsets) { + if (chunk_offset + offset > std::numeric_limits::max()) { + writer.write32(0); // TODO: error + } + else { + writer.write32(static_cast(chunk_offset + offset)); + } + } + + writer.set_position(oldPosition); +} + + + +Error Box_stsz::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stsz"); + } + + m_fixed_sample_size = range.read32(); + m_sample_count = range.read32(); + + if (limits->max_sequence_frames > 0 && m_sample_count > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + + if (m_fixed_sample_size == 0) { + // check required memory + + if (auto err = m_memory_handle.alloc(m_sample_count, sizeof(uint32_t), + limits, "the 'stsz' table")) { + return err; + } + + for (uint32_t i = 0; i < m_sample_count; i++) { + if (range.eof()) { + std::stringstream sstr; + sstr << "stsz box should contain " << m_sample_count << " entries, but box only contained " + << i << " entries"; + + return { + heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str() + }; + } + + m_sample_sizes.push_back(range.read32()); + + if (range.error()) { + return range.get_error(); + } + } + } + + return range.get_error(); +} + + +std::string Box_stsz::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "sample count: " << m_sample_count << "\n"; + if (m_fixed_sample_size == 0) { + for (size_t i = 0; i < m_sample_sizes.size(); i++) { + sstr << indent << "[" << i << "] : " << m_sample_sizes[i] << "\n"; + } + } + else { + sstr << indent << "fixed sample size: " << m_fixed_sample_size << "\n"; + } + + return sstr.str(); +} + + +Error Box_stsz::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(m_fixed_sample_size); + writer.write32(m_sample_count); + if (m_fixed_sample_size == 0) { + assert(m_sample_count == m_sample_sizes.size()); + + for (uint32_t size : m_sample_sizes) { + writer.write32(size); + } + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +void Box_stsz::append_sample_size(uint32_t size) +{ + if (m_sample_count == 0 && size != 0) { + m_fixed_sample_size = size; + m_sample_count = 1; + return; + } + + if (m_fixed_sample_size == size && size != 0) { + m_sample_count++; + return; + } + + if (m_fixed_sample_size != 0) { + for (uint32_t i = 0; i < m_sample_count; i++) { + m_sample_sizes.push_back(m_fixed_sample_size); + } + + m_fixed_sample_size = 0; + } + + m_sample_sizes.push_back(size); + m_sample_count++; + + assert(m_sample_count == m_sample_sizes.size()); +} + + +Error Box_stss::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("stss"); + } + + uint32_t sample_count = range.read32(); + + // check required memory + + if (auto err = m_memory_handle.alloc(sample_count, sizeof(uint32_t), + limits, "the 'stss' table")) { + return err; + } + + for (uint32_t i = 0; i < sample_count; i++) { + m_sync_samples.push_back(range.read32()); + + if (range.error()) { + return range.get_error(); + } + } + + return range.get_error(); +} + + +std::string Box_stss::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + for (size_t i = 0; i < m_sync_samples.size(); i++) { + sstr << indent << "[" << i << "] : " << m_sync_samples[i] << "\n"; + } + + return sstr.str(); +} + + +void Box_stss::set_total_number_of_samples(uint32_t num_samples) +{ + m_all_samples_are_sync_samples = (m_sync_samples.size() == num_samples); +} + + +Error Box_stss::write(StreamWriter& writer) const +{ + // If we don't need this box, skip it. + if (m_all_samples_are_sync_samples) { + return Error::Ok; + } + + size_t box_start = reserve_box_header_space(writer); + + writer.write32(static_cast(m_sync_samples.size())); + for (uint32_t sample : m_sync_samples) { + writer.write32(sample); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error VisualSampleEntry::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + (void)limits; + + range.skip(6); + data_reference_index = range.read16(); + + pre_defined = range.read16(); + range.skip(2); + for (uint32_t& p : pre_defined2) { + p = range.read32(); + } + width = range.read16(); + height = range.read16(); + horizresolution = range.read32(); + vertresolution = range.read32(); + range.skip(4); + frame_count = range.read16(); + compressorname = range.read_fixed_string(32); + depth = range.read16(); + pre_defined3 = range.read16s(); + + // other boxes from derived specifications + //std::shared_ptr clap; // optional // TODO + //std::shared_ptr pixi; // optional // TODO + + return Error::Ok; +} + + +Error VisualSampleEntry::write(StreamWriter& writer) const +{ + writer.write32(0); + writer.write16(0); + writer.write16(data_reference_index); + + writer.write16(pre_defined); + writer.write16(0); + for (uint32_t p : pre_defined2) { + writer.write32(p); + } + + writer.write16(width); + writer.write16(height); + writer.write32(horizresolution); + writer.write32(vertresolution); + writer.write32(0); + writer.write16(frame_count); + writer.write_fixed_string(compressorname, 32); + writer.write16(depth); + writer.write16(pre_defined3); + + return Error::Ok; +} + + +std::string VisualSampleEntry::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << indent << "data reference index: " << data_reference_index << "\n" + << indent << "width: " << width << "\n" + << indent << "height: " << height << "\n" + << indent << "horiz. resolution: " << get_horizontal_resolution() << "\n" + << indent << "vert. resolution: " << get_vertical_resolution() << "\n" + << indent << "frame count: " << frame_count << "\n" + << indent << "compressorname: " << compressorname << "\n" + << indent << "depth: " << depth << "\n"; + + return sstr.str(); +} + + +Error Box_URIMetaSampleEntry::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(0); + writer.write16(0); + writer.write16(data_reference_index); + + write_children(writer); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_URIMetaSampleEntry::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << Box::dump(indent); + sstr << indent << "data reference index: " << data_reference_index << "\n"; + sstr << dump_children(indent); + return sstr.str(); +} + + +Error Box_URIMetaSampleEntry::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + range.skip(6); + data_reference_index = range.read16(); + + Error err = read_children(range, READ_CHILDREN_ALL, limits); + if (err) { + return err; + } + + return Error::Ok; +} + + +Error Box_uri::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("uri "); + } + + m_uri = range.read_string(); + + return range.get_error(); +} + + +std::string Box_uri::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "uri: " << m_uri << "\n"; + + return sstr.str(); +} + + +Error Box_uri::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write(m_uri); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + + +Error Box_ccst::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 0) { + return unsupported_version_error("ccst"); + } + + uint32_t bits = range.read32(); + + auto& constraints = m_codingConstraints; + + constraints.all_ref_pics_intra = (bits & 0x80000000) != 0; + constraints.intra_pred_used = (bits & 0x40000000) != 0; + constraints.max_ref_per_pic = (bits >> 26) & 0x0F; + + return range.get_error(); +} + + +std::string Box_ccst::dump(Indent& indent) const +{ + const auto& constraints = m_codingConstraints; + + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "all ref pics intra: " << std::boolalpha < 0) { + return unsupported_version_error("auxi"); + } + + m_aux_track_type = range.read_string(); + + return range.get_error(); +} + + +std::string Box_auxi::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "aux track info type: " << m_aux_track_type << "\n"; + + return sstr.str(); +} + + +Error Box_auxi::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write(m_aux_track_type); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_VisualSampleEntry::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + Error err = get_VisualSampleEntry_const().write(writer); + if (err) { + return err; + } + + write_children(writer); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_VisualSampleEntry::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << Box::dump(indent); + sstr << m_visualSampleEntry.dump(indent); + sstr << dump_children(indent); + return sstr.str(); +} + + +Error Box_VisualSampleEntry::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + auto err = m_visualSampleEntry.parse(range, limits); + if (err) { + return err; + } + + err = read_children(range, READ_CHILDREN_ALL, limits); + if (err) { + return err; + } + + return Error::Ok; +} + + +std::string Box_sbgp::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + sstr << indent << "grouping_type: " << fourcc_to_string(m_grouping_type) << "\n"; + + if (m_grouping_type_parameter) { + sstr << indent << "grouping_type_parameter: " << *m_grouping_type_parameter << "\n"; + } + + uint32_t total_samples = 0; + for (size_t i = 0; i < m_entries.size(); i++) { + sstr << indent << "[" << std::setw(2) << (i + 1) << "] : " << std::setw(3) << m_entries[i].sample_count << "x " << m_entries[i].group_description_index << "\n"; + total_samples += m_entries[i].sample_count; + } + sstr << indent << "total samples: " << total_samples << "\n"; + + return sstr.str(); +} + + +void Box_sbgp::derive_box_version() +{ + if (m_grouping_type_parameter) { + set_version(1); + } + else { + set_version(0); + } +} + + +Error Box_sbgp::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(m_grouping_type); + if (m_grouping_type_parameter) { + writer.write32(*m_grouping_type_parameter); + } + + if (m_entries.size() > 0xFFFFFFFF) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Too many sbgp entries."}; + } + + writer.write32(static_cast(m_entries.size())); + for (const auto& entry : m_entries) { + writer.write32(entry.sample_count); + writer.write32(entry.group_description_index); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_sbgp::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_version() > 1) { + return unsupported_version_error("sbgp"); + } + + m_grouping_type = range.read32(); + + if (get_version() == 1) { + m_grouping_type_parameter = range.read32(); + } + + uint32_t count = range.read32(); + if (auto err = m_memory_handle.alloc(count, sizeof(Entry), + limits, "the 'sample to group' table")) { + return err; + } + + for (uint32_t i = 0; i < count; i++) { + Entry e{}; + e.sample_count = range.read32(); + e.group_description_index = range.read32(); + m_entries.push_back(e); + if (range.error()) { + return range.get_error(); + } + } + + return range.get_error(); +} + + +std::string SampleGroupEntry_refs::dump() const +{ + std::stringstream sstr; + if (m_sample_id==0) { + sstr << "0 (non-ref) refs ="; + } + else { + sstr << m_sample_id << " refs ="; + } + for (uint32_t ref : m_direct_reference_sample_id) { + sstr << ' ' << ref; + } + + return sstr.str(); +} + +Error SampleGroupEntry_refs::write(StreamWriter& writer) const +{ + return {}; +} + +Error SampleGroupEntry_refs::parse(BitstreamRange& range, const heif_security_limits*) +{ + m_sample_id = range.read32(); + uint8_t cnt = range.read8(); + for (uint8_t i = 0; i < cnt; i++) { + m_direct_reference_sample_id.push_back(range.read32()); + } + + return Error::Ok; +} + + +void Box_sgpd::derive_box_version() +{ + if (m_default_length) { + set_version(1); + assert(!m_default_sample_description_index); + return; + } + + if (m_default_sample_description_index) { + set_version(2); + return; + } + + set_version(0); +} + + +std::string Box_sgpd::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "grouping_type: " << fourcc_to_string(m_grouping_type) << "\n"; + if (m_default_length) { + sstr << indent << "default_length: " << *m_default_length << "\n"; + } + if (m_default_sample_description_index) { + sstr << indent << "default_sample_description_index: " << *m_default_sample_description_index << "\n"; + } + + for (size_t i=0; idump() << "\n"; + } + else { + sstr << "empty (description_length=" << m_entries[i].description_length << ")\n"; + } + } + + return sstr.str(); +} + + +Error Box_sgpd::write(StreamWriter& writer) const +{ +return {}; +} + + +Error Box_sgpd::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + m_grouping_type = range.read32(); + + // Readers are expected to ignore sgpd boxes with grouping_types they don't + // understand. Skip parsing of unknown types to avoid allocating Entry objects + // for entries whose payload we wouldn't read anyway (and which, with + // version==1 + default_length!=0 or version>=2, would consume zero bytes per + // iteration and allow unbounded allocation from a tiny box). + if (m_grouping_type != fourcc("refs")) { + return Error::Ok; + } + + if (get_version() == 1) { + m_default_length = range.read32(); + } + + if (get_version() >= 2) { + m_default_sample_description_index = range.read32(); + } + + uint32_t entry_count = range.read32(); + + if (limits->max_sample_group_description_box_entries && + entry_count > limits->max_sample_group_description_box_entries) { + std::stringstream sstr; + sstr << "Allocating " << static_cast(entry_count) << " sample group description items exceeds the security limit of " + << limits->max_sample_group_description_box_entries << " items"; + + return {heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + + } + + if (auto err = m_memory_handle.alloc(entry_count, sizeof(Entry), + limits, "the 'sgpd' table")) { + return err; + } + + for (uint32_t i = 0; i < entry_count; i++) { + Entry entry; + + if (get_version() == 1) { + if (*m_default_length == 0) { + entry.description_length = range.read32(); + } + } + + switch (m_grouping_type) { + case fourcc("refs"): { + entry.sample_group_entry = std::make_shared(); + Error err = entry.sample_group_entry->parse(range, limits); + if (err) { + return err; + } + + break; + } + + default: + break; + } + + m_entries.emplace_back(std::move(entry)); + } + + return Error::Ok; +} + + +std::string Box_btrt::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "bufferSizeDB: " << m_bufferSizeDB << " bytes\n"; + sstr << indent << "max bitrate: " << m_maxBitrate << " bits/sec\n"; + sstr << indent << "avg bitrate: " << m_avgBitrate << " bits/sec\n"; + + return sstr.str(); +} + + +Error Box_btrt::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + writer.write32(m_bufferSizeDB); + writer.write32(m_maxBitrate); + writer.write32(m_avgBitrate); + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_btrt::parse(BitstreamRange& range, const heif_security_limits*) +{ + m_bufferSizeDB = range.read32(); + m_maxBitrate = range.read32(); + m_avgBitrate = range.read32(); + + return Error::Ok; +} + + + +void Box_saiz::set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter) +{ + m_aux_info_type = aux_info_type; + m_aux_info_type_parameter = aux_info_type_parameter; + + bool nonnull = (m_aux_info_type != 0 || m_aux_info_type_parameter != 0); + set_flags(nonnull ? 1 : 0); +} + + +void Box_saiz::add_sample_size(uint8_t s) +{ + // --- it is the first sample -> put into default size (except if it is a zero size = no sample aux info) + + if (s != 0 && m_num_samples == 0) { + m_default_sample_info_size = s; + m_num_samples = 1; + return; + } + + // --- if it's the default size, just add more to the number of default sizes + + if (s != 0 && s == m_default_sample_info_size) { + m_num_samples++; + return; + } + + // --- it is different from the default size -> add the list + + // first copy samples with the default size into the list + + if (m_default_sample_info_size != 0) { + for (uint32_t i = 0; i < m_num_samples; i++) { + m_sample_sizes.push_back(m_default_sample_info_size); + } + + m_default_sample_info_size = 0; + } + + // add the new sample size + + m_num_samples++; + m_sample_sizes.push_back(s); +} + + +uint8_t Box_saiz::get_sample_size(uint32_t idx) +{ + if (m_default_sample_info_size != 0) { + return m_default_sample_info_size; + } + + if (idx >= m_sample_sizes.size()) { + return 0; + } + + return m_sample_sizes[idx]; +} + + +std::string Box_saiz::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "aux_info_type: "; + if (m_aux_info_type == 0) { + sstr << "0\n"; + } + else { + sstr << fourcc_to_string(m_aux_info_type) << "\n"; + } + + sstr << indent << "aux_info_type_parameter: "; + if (m_aux_info_type_parameter == 0) { + sstr << "0\n"; + } + else { + sstr << fourcc_to_string(m_aux_info_type_parameter) << "\n"; + } + + sstr << indent << "default sample size: "; + if (m_default_sample_info_size == 0) { + sstr << "0 (variable)\n"; + } + else { + sstr << ((int)m_default_sample_info_size) << "\n"; + } + + if (m_default_sample_info_size == 0) { + for (size_t i = 0; i < m_sample_sizes.size(); i++) { + sstr << indent << "[" << i << "] : " << ((int) m_sample_sizes[i]) << "\n"; + } + } + + return sstr.str(); +} + + +Error Box_saiz::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (get_flags() & 1) { + writer.write32(m_aux_info_type); + writer.write32(m_aux_info_type_parameter); + } + + writer.write8(m_default_sample_info_size); + + if (m_default_sample_info_size == 0) { + assert(m_num_samples == m_sample_sizes.size()); + + uint32_t num_nonnull_samples = static_cast(m_sample_sizes.size()); + while (num_nonnull_samples > 0 && m_sample_sizes[num_nonnull_samples-1] == 0) { + num_nonnull_samples--; + } + + writer.write32(num_nonnull_samples); + + for (size_t i = 0; i < num_nonnull_samples; i++) { + writer.write8(m_sample_sizes[i]); + } + } + else { + writer.write32(m_num_samples); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_saiz::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_flags() & 1) { + m_aux_info_type = range.read32(); + m_aux_info_type_parameter = range.read32(); + } + + m_default_sample_info_size = range.read8(); + m_num_samples = range.read32(); + + if (limits && limits->max_sequence_frames > 0 && m_num_samples > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Number of 'saiz' samples exceeds the maximum number of sequence frames." + }; + } + + if (m_default_sample_info_size == 0) { + // check required memory + + uint64_t mem_size = m_num_samples; + if (auto err = m_memory_handle.alloc(mem_size, limits, "the 'sample aux info sizes' (saiz) table")) { + return err; + } + + // read whole table at once + + m_sample_sizes.resize(m_num_samples); + range.read(m_sample_sizes.data(), m_num_samples); + } + + return range.get_error(); +} + + + +void Box_saio::set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter) +{ + m_aux_info_type = aux_info_type; + m_aux_info_type_parameter = aux_info_type_parameter; + + bool nonnull = (m_aux_info_type != 0 || m_aux_info_type_parameter != 0); + set_flags(nonnull ? 1 : 0); +} + + +void Box_saio::add_chunk_offset(uint64_t s) +{ + if (s > 0xFFFFFFFF) { + m_need_64bit = true; + set_version(1); + } + + m_chunk_offset.push_back(s); +} + + +uint64_t Box_saio::get_chunk_offset(uint32_t idx) const +{ + if (idx >= m_chunk_offset.size()) { + return 0; + } + else { + return m_chunk_offset[idx]; + } +} + + +std::string Box_saio::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "aux_info_type: "; + if (m_aux_info_type == 0) { + sstr << "0\n"; + } + else { + sstr << fourcc_to_string(m_aux_info_type) << "\n"; + } + + sstr << indent << "aux_info_type_parameter: "; + if (m_aux_info_type_parameter == 0) { + sstr << "0\n"; + } + else { + sstr << fourcc_to_string(m_aux_info_type_parameter) << "\n"; + } + + for (size_t i = 0; i < m_chunk_offset.size(); i++) { + sstr << indent << "[" << i << "] : 0x" << std::hex << m_chunk_offset[i] << "\n"; + } + + return sstr.str(); +} + + +void Box_saio::patch_file_pointers(StreamWriter& writer, size_t offset) +{ + size_t oldPosition = writer.get_position(); + + writer.set_position(m_offset_start_pos); + + for (uint64_t ptr : m_chunk_offset) { + if (get_version() == 0 && ptr + offset > std::numeric_limits::max()) { + writer.write32(0); // TODO: error + } else if (get_version() == 0) { + writer.write32(static_cast(ptr + offset)); + } else { + writer.write64(ptr + offset); + } + } + + writer.set_position(oldPosition); +} + + +Error Box_saio::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (get_flags() & 1) { + writer.write32(m_aux_info_type); + writer.write32(m_aux_info_type_parameter); + } + + if (m_chunk_offset.size() > std::numeric_limits::max()) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Maximum number of chunks exceeded"}; + } + writer.write32(static_cast(m_chunk_offset.size())); + + m_offset_start_pos = writer.get_position(); + + for (uint64_t size : m_chunk_offset) { + if (m_need_64bit) { + writer.write64(size); + } else { + writer.write32(static_cast(size)); + } + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +Error Box_saio::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + if (get_flags() & 1) { + m_aux_info_type = range.read32(); + m_aux_info_type_parameter = range.read32(); + } + + uint32_t num_chunks = range.read32(); + + // We have no explicit maximum on the number of chunks. + // Use the maximum number of frames as an upper limit. + if (limits && limits->max_sequence_frames > 0 && num_chunks > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Number of 'saio' chunks exceeds the maximum number of sequence frames." + }; + } + + if (auto err = m_memory_handle.alloc(num_chunks, sizeof(uint64_t), + limits, "the 'saio' table")) { + return err; + } + + m_chunk_offset.resize(num_chunks); + + for (uint32_t i = 0; i < num_chunks; i++) { + uint64_t offset; + if (get_version() == 1) { + offset = range.read64(); + } + else { + offset = range.read32(); + } + + m_chunk_offset[i] = offset; + + if (range.error()) { + return range.get_error(); + } + } + + return Error::Ok; +} + + +std::string Box_sdtp::dump(Indent& indent) const +{ + std::stringstream sstr; + sstr << FullBox::dump(indent); + + assert(m_sample_information.size() <= UINT32_MAX); + + for (uint32_t i = 0; i < static_cast(m_sample_information.size()); i++) { + const char* spaces = " "; + int nSpaces = 6; + int k = i; + while (k >= 10 && nSpaces < 12) { + k /= 10; + nSpaces++; + } + + spaces = spaces + 12 - nSpaces; + + sstr << indent << "[" << i << "] : is_leading=" << (int) get_is_leading(i) << "\n" + << indent << spaces << "depends_on=" << (int) get_depends_on(i) << "\n" + << indent << spaces << "is_depended_on=" << (int) get_is_depended_on(i) << "\n" + << indent << spaces << "has_redundancy=" << (int) get_has_redundancy(i) << "\n"; + } + + return sstr.str(); +} + + +Error Box_sdtp::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + parse_full_box_header(range); + + // We have no easy way to get the number of samples from 'saiz' or 'stz2' as specified + // in the standard. Instead, we read until the end of the box. + size_t nSamples = range.get_remaining_bytes(); + + m_sample_information.resize(nSamples); + range.read(m_sample_information.data(), nSamples); + + return Error::Ok; +} + + +Error Box_tref::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + while (!range.eof()) { + BoxHeader header; + Error err = header.parse_header(range); + if (err != Error::Ok) { + return err; + } + + if (header.get_box_size() < header.get_header_size()) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Invalid box size (smaller than header)"}; + } + + uint64_t dataSize = (header.get_box_size() - header.get_header_size()); + + if (dataSize % 4 != 0 || dataSize < 4) { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "Input file has a 'tref' TrackReferenceTypeBox with invalid size."}; + } + + uint64_t nRefs = dataSize / 4; + + if (limits->max_items && nRefs > limits->max_items) { + std::stringstream sstr; + sstr << "Number of references in tref box (" << nRefs << ") exceeds the security limits of " << limits->max_items << " references."; + + return {heif_error_Invalid_input, + heif_suberror_Security_limit_exceeded, + sstr.str()}; + } + + Reference ref; + ref.reference_type = header.get_short_type(); + + for (uint64_t i = 0; i < nRefs; i++) { + if (range.eof()) { + std::stringstream sstr; + sstr << "tref box should contain " << nRefs << " references, but we can only read " << i << " references."; + + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str()}; + } + + ref.to_track_id.push_back(static_cast(range.read32())); + } + + m_references.push_back(ref); + } + + + // --- check for duplicate references + + if (auto error = check_for_double_references()) { + return error; + } + + return range.get_error(); +} + + +Error Box_tref::check_for_double_references() const +{ + for (const auto& ref : m_references) { + std::set to_ids; + for (const auto to_id : ref.to_track_id) { + if (to_ids.find(to_id) == to_ids.end()) { + to_ids.insert(to_id); + } + else { + return {heif_error_Invalid_input, + heif_suberror_Unspecified, + "'tref' has double references"}; + } + } + } + + return Error::Ok; +} + + +Error Box_tref::write(StreamWriter& writer) const +{ + if (auto error = check_for_double_references()) { + return error; + } + + size_t box_start = reserve_box_header_space(writer); + + for (const auto& ref : m_references) { + uint32_t box_size = 8 + uint32_t(ref.to_track_id.size() * 4); + + // we write the BoxHeader ourselves since it is very simple + writer.write32(box_size); + writer.write32(ref.reference_type); + + for (uint32_t r : ref.to_track_id) { + writer.write32(r); + } + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_tref::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << Box::dump(indent); + + for (const auto& ref : m_references) { + sstr << indent << "reference with type '" << fourcc_to_string(ref.reference_type) << "'" + << " to track IDs: "; + for (uint32_t id : ref.to_track_id) { + sstr << id << " "; + } + sstr << "\n"; + } + + return sstr.str(); +} + + +std::vector Box_tref::get_references(uint32_t ref_type) const +{ + for (const Reference& ref : m_references) { + if (ref.reference_type == ref_type) { + return ref.to_track_id; + } + } + + return {}; +} + + +size_t Box_tref::get_number_of_references_of_type(uint32_t ref_type) const +{ + for (const Reference& ref : m_references) { + if (ref.reference_type == ref_type) { + return ref.to_track_id.size(); + } + } + + return 0; +} + + +std::vector Box_tref::get_reference_types() const +{ + std::vector types; + types.reserve(m_references.size()); + for (const auto& ref : m_references) { + types.push_back(ref.reference_type); + } + + return types; +} + + +void Box_tref::add_references(uint32_t to_track_id, uint32_t type) +{ + for (auto& ref : m_references) { + if (ref.reference_type == type) { + ref.to_track_id.push_back(to_track_id); + return; + } + } + + Reference ref; + ref.reference_type = type; + ref.to_track_id = {to_track_id}; + + m_references.push_back(ref); +} + + +Error Box_elst::parse(BitstreamRange& range, const heif_security_limits* limits) +{ + Error err = parse_full_box_header(range); + if (err != Error::Ok) { + return err; + } + + if (get_version() > 1) { + return unsupported_version_error("edts"); + } + + uint32_t nEntries = range.read32(); + m_entries.clear(); + + for (uint64_t i = 0; i < nEntries; i++) { + if (range.eof()) { + std::stringstream sstr; + sstr << "edts box should contain " << nEntries << " entries, but we can only read " << i << " entries."; + + return {heif_error_Invalid_input, + heif_suberror_End_of_data, + sstr.str()}; + } + + Entry entry{}; + if (get_version() == 1) { + entry.segment_duration = range.read64(); + entry.media_time = range.read64s(); + } + else { + entry.segment_duration = range.read32(); + entry.media_time = range.read32s(); + } + + entry.media_rate_integer = range.read16s(); + entry.media_rate_fraction = range.read16s(); + + m_entries.push_back(entry); + } + + return range.get_error(); +} + + +Error Box_elst::write(StreamWriter& writer) const +{ + size_t box_start = reserve_box_header_space(writer); + + if (m_entries.size() > std::numeric_limits::max()) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Too many entries in edit list"}; + } + + writer.write32(static_cast(m_entries.size())); + + + for (const auto& entry : m_entries) { + if (get_version() == 1) { + writer.write64(entry.segment_duration); + writer.write64s(entry.media_time); + } + else { + // The cast is valid because we check in derive_box_version() whether everything + // fits into 32bit. If not, version 1 is used. + + writer.write32(static_cast(entry.segment_duration)); + writer.write32s(static_cast(entry.media_time)); + } + + writer.write16s(entry.media_rate_integer); + writer.write16s(entry.media_rate_fraction); + } + + prepend_header(writer, box_start); + + return Error::Ok; +} + + +std::string Box_elst::dump(Indent& indent) const +{ + std::ostringstream sstr; + sstr << FullBox::dump(indent); + + sstr << indent << "repeat list: " << ((get_flags() & Flags::Repeat_EditList) ? "yes" : "no") << "\n"; + + for (const auto& entry : m_entries) { + sstr << indent << "segment duration: " << entry.segment_duration << "\n"; + sstr << indent << "media time: " << entry.media_time << "\n"; + sstr << indent << "media rate integer: " << entry.media_rate_integer << "\n"; + sstr << indent << "media rate fraction: " << entry.media_rate_fraction << "\n"; + } + + return sstr.str(); +} + +void Box_elst::derive_box_version() +{ + // check whether we need 64bit values + + bool need_64bit = std::any_of(m_entries.begin(), + m_entries.end(), + [](const Entry& entry) { + return (entry.segment_duration > std::numeric_limits::max() || + entry.media_time > std::numeric_limits::max()); + }); + + if (need_64bit) { + set_version(1); + } + else { + set_version(0); + } +} + + +void Box_elst::enable_repeat_mode(bool enable) +{ + uint32_t flags = get_flags(); + if (enable) { + flags |= Flags::Repeat_EditList; + } + else { + flags &= ~Flags::Repeat_EditList; + } + + set_flags(flags); +} + + +void Box_elst::add_entry(const Entry& entry) +{ + m_entries.push_back(entry); +} diff -Nru libheif-1.19.8/libheif/sequences/seq_boxes.h libheif-1.23.4/libheif/sequences/seq_boxes.h --- libheif-1.19.8/libheif/sequences/seq_boxes.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/seq_boxes.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,1060 @@ +/* + * HEIF codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef SEQ_BOXES_H +#define SEQ_BOXES_H + +#include "box.h" +#include "security_limits.h" + +#include +#include +#include +#include +#include + + +class Box_container : public Box { +public: + Box_container(const char* type) + { + set_short_type(fourcc(type)); + } + + std::string dump(Indent&) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; +}; + + +// Movie Box +class Box_moov : public Box_container { +public: + Box_moov() : Box_container("moov") {} + + const char* debug_box_name() const override { return "Movie"; } +}; + + +// Movie Header Box +class Box_mvhd : public FullBox { +public: + Box_mvhd() + { + set_short_type(fourcc("mvhd")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Movie Header"; } + + Error write(StreamWriter& writer) const override; + + void derive_box_version() override; + + double get_rate() const { return m_rate / double(0x10000); } + + float get_volume() const { return float(m_volume) / float(0x100); } + + double get_matrix_element(int idx) const; + + uint32_t get_time_scale() const { return m_timescale; } + + uint64_t get_duration() const { return m_duration; } + + // True when the duration field carries the ISOBMFF "duration unknown / indefinite" + // sentinel (all-1s for the field width corresponding to the box version). + // Files written with such a duration alongside an editlist in repeat mode signal + // that the media should be looped indefinitely. + bool is_duration_indefinite() const + { + if (get_version() == 1) { + return m_duration == std::numeric_limits::max(); + } + return m_duration == std::numeric_limits::max(); + } + + void set_duration(uint64_t duration) { m_duration = duration; } + + void set_time_scale(uint32_t timescale) { m_timescale = timescale; } + + void set_next_track_id(uint32_t next_id) { m_next_track_ID = next_id; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint64_t m_creation_time = 0; + uint64_t m_modification_time = 0; + uint32_t m_timescale = 0; + uint64_t m_duration = 0; + + uint32_t m_rate = 0x00010000; // typically 1.0 + uint16_t m_volume = 0x0100; // typically, full volume + + uint32_t m_matrix[9] = {0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}; + uint32_t m_next_track_ID = 0; +}; + + +// Track Box +class Box_trak : public Box_container { +public: + Box_trak() : Box_container("trak") {} + + const char* debug_box_name() const override { return "Track"; } +}; + + +// Track Header Box +class Box_tkhd : public FullBox { +public: + Box_tkhd() + { + set_short_type(fourcc("tkhd")); + + // set default flags according to ISO 14496-12 + set_flags(Track_enabled | Track_in_movie | Track_in_preview); + } + + enum Flags : uint32_t { + Track_enabled = 0x01, + Track_in_movie = 0x02, + Track_in_preview = 0x04, + Track_size_is_aspect_ratio = 0x08 + }; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Track Header"; } + + Error write(StreamWriter& writer) const override; + + void derive_box_version() override; + + float get_volume() const { return float(m_volume) / float(0x100); } + + double get_matrix_element(int idx) const; + + double get_width() const { return float(m_width) / double(0x10000); } + + double get_height() const { return float(m_height) / double(0x10000); } + + uint32_t get_track_id() const { return m_track_id; } + + void set_track_id(uint32_t track_id) { m_track_id = track_id; } + + void set_resolution(double width, double height) + { + m_width = (uint32_t) (width * 0x10000); + m_height = (uint32_t) (height * 0x10000); + } + + uint64_t get_duration() const { return m_duration; } + + void set_duration(uint64_t duration) { m_duration = duration; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint64_t m_creation_time = 0; + uint64_t m_modification_time = 0; + uint32_t m_track_id = 0; + uint64_t m_duration = 0; + + uint16_t m_layer = 0; + uint16_t m_alternate_group = 0; + uint16_t m_volume = 0x0100; // typically, full volume + + uint32_t m_matrix[9] = {0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}; + + uint32_t m_width = 0; + uint32_t m_height = 0; +}; + + +// Media Box +class Box_mdia : public Box_container { +public: + Box_mdia() : Box_container("mdia") {} + + const char* debug_box_name() const override { return "Media"; } +}; + + +// Media Header Box +class Box_mdhd : public FullBox { +public: + Box_mdhd() + { + set_short_type(fourcc("mdhd")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Media Header"; } + + Error write(StreamWriter& writer) const override; + + void derive_box_version() override; + + double get_matrix_element(int idx) const; + + uint32_t get_timescale() const { return m_timescale; } + + void set_timescale(uint32_t timescale) { m_timescale = timescale; } + + uint64_t get_duration() const { return m_duration; } + + void set_duration(uint64_t duration) { m_duration = duration; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint64_t m_creation_time = 0; + uint64_t m_modification_time = 0; + uint32_t m_timescale = 0; + uint64_t m_duration = 0; + + char m_language[4] = {'u', 'n', 'k', 0}; +}; + + +// Media Information Box (container) +class Box_minf : public Box_container { +public: + Box_minf() : Box_container("minf") {} + + const char* debug_box_name() const override { return "Media Information"; } +}; + + +// Video Media Header +class Box_vmhd : public FullBox { +public: + Box_vmhd() + { + set_short_type(fourcc("vmhd")); + set_flags(1); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Video Media Header"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint16_t m_graphics_mode = 0; + uint16_t m_op_color[3] = {0, 0, 0}; +}; + + +// Null Media Header +class Box_nmhd : public FullBox { +public: + Box_nmhd() + { + set_short_type(fourcc("nmhd")); + set_flags(1); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Null Media Header"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; +}; + + +// Sample Table Box (container) +class Box_stbl : public Box_container { +public: + Box_stbl() : Box_container("stbl") {} + + const char* debug_box_name() const override { return "Sample Table"; } +}; + + +// Sample Description Box +class Box_stsd : public FullBox { +public: + Box_stsd() + { + set_short_type(fourcc("stsd")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Description"; } + + Error write(StreamWriter& writer) const override; + + std::shared_ptr get_sample_entry(size_t idx) const + { + if (idx >= m_sample_entries.size()) { + return nullptr; + } else { + return m_sample_entries[idx]; + } + } + + void add_sample_entry(const std::shared_ptr& entry) + { + m_sample_entries.push_back(entry); + } + + size_t get_num_sample_entries() const { return m_sample_entries.size(); } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector> m_sample_entries; +}; + + +// Decoding Time to Sample Box +class Box_stts : public FullBox { +public: + Box_stts() + { + set_short_type(fourcc("stts")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Decoding Time to Sample"; } + + Error write(StreamWriter& writer) const override; + + struct TimeToSample { + uint32_t sample_count; + uint32_t sample_delta; + + // Index one past the last sample described by this entry, i.e. the prefix sum + // of sample_count over the entries up to and including this one. Derived (not + // stored in the file); kept in sync by parse() and append_sample_duration() so + // get_sample_duration() can binary-search instead of scanning linearly. + uint32_t cumulative_sample_count = 0; + }; + + // O(log entries) lookup of the sample duration (delta) for a given sample index. + // Called once per output sample on the decode/raw paths, so a linear scan would + // be O(entries) per sample, i.e. O(entries * samples) overall (GHSA-xw34-mjcp-jqh8, + // variant V1). + uint32_t get_sample_duration(uint32_t sample_idx); + + void append_sample_duration(uint32_t duration); + + uint64_t get_total_duration(bool include_last_frame_duration); + + uint32_t get_number_of_samples() const; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_entries; + MemoryHandle m_memory_handle; +}; + + +// Composition Time to Sample Box +class Box_ctts : public FullBox { +public: + Box_ctts() + { + set_short_type(fourcc("ctts")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Composition Time to Sample"; } + + Error write(StreamWriter& writer) const override; + + struct OffsetToSample { + uint32_t sample_count; + int32_t sample_offset; // either uint32_t or int32_t, we assume that all uint32_t values will also fit into int32_t + }; + + int32_t get_sample_offset(uint32_t sample_idx); + + void append_sample_offset(int32_t offset); + + bool is_constant_offset() const; + + void derive_box_version() override; + + int32_t compute_min_offset() const; + + uint32_t get_number_of_samples() const; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_entries; + MemoryHandle m_memory_handle; +}; + + +// Sample to Chunk Box +class Box_stsc : public FullBox { +public: + Box_stsc() + { + set_short_type(fourcc("stsc")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample to Chunk"; } + + Error write(StreamWriter& writer) const override; + + struct SampleToChunk { + uint32_t first_chunk; + uint32_t samples_per_chunk; + uint32_t sample_description_index; + }; + + const std::vector& get_chunks() const { return m_entries; } + + // idx counting starts at 1 + const SampleToChunk* get_chunk(uint32_t idx) const; + + void add_chunk(uint32_t description_index); + + void increase_samples_in_chunk(uint32_t nFrames); + + bool last_chunk_empty() const { + assert(!m_entries.empty()); + + return m_entries.back().samples_per_chunk == 0; + } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_entries; + MemoryHandle m_memory_handle; +}; + + +// Chunk Offset Box +class Box_stco : public FullBox { +public: + Box_stco() + { + set_short_type(fourcc("stco")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Offset"; } + + Error write(StreamWriter& writer) const override; + + void add_chunk_offset(uint32_t offset) { m_offsets.push_back(offset); } + + const std::vector& get_offsets() const { return m_offsets; } + + size_t get_number_of_chunks() const { return m_offsets.size(); } + + void patch_file_pointers(StreamWriter&, size_t offset) override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_offsets; + MemoryHandle m_memory_handle; + + mutable size_t m_offset_start_pos = 0; +}; + + +// Sample Size Box +class Box_stsz : public FullBox { +public: + Box_stsz() + { + set_short_type(fourcc("stsz")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Size"; } + + Error write(StreamWriter& writer) const override; + + bool has_fixed_sample_size() const { return m_fixed_sample_size != 0; } + + uint32_t get_fixed_sample_size() const { return m_fixed_sample_size; } + + uint32_t num_samples() const { return m_sample_count; } + + const std::vector& get_sample_sizes() const { return m_sample_sizes; } + + void append_sample_size(uint32_t size); + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_fixed_sample_size = 0; + uint32_t m_sample_count = 0; + std::vector m_sample_sizes; + MemoryHandle m_memory_handle; +}; + + +// Sync Sample Box +class Box_stss : public FullBox { +public: + Box_stss() + { + set_short_type(fourcc("stss")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sync Sample"; } + + Error write(StreamWriter& writer) const override; + + void add_sync_sample(uint32_t sample_idx) { m_sync_samples.push_back(sample_idx); } + + // when this is set, the Box will compute whether it can be skipped + void set_total_number_of_samples(uint32_t num_samples); + + // bool skip_box() const override { return m_all_samples_are_sync_samples; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_sync_samples; + MemoryHandle m_memory_handle; + + bool m_all_samples_are_sync_samples = false; +}; + + +struct CodingConstraints { + bool all_ref_pics_intra = false; + bool intra_pred_used = false; + uint8_t max_ref_per_pic = 0; // 4 bit +}; + + +// Coding Constraints Box +class Box_ccst : public FullBox { +public: + Box_ccst() + { + set_short_type(fourcc("ccst")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Coding Constraints"; } + + Error write(StreamWriter& writer) const override; + + void set_coding_constraints(const CodingConstraints& c) { m_codingConstraints = c; } + + const CodingConstraints& get_coding_constraints() const { return m_codingConstraints; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + CodingConstraints m_codingConstraints; +}; + + +class Box_auxi : public FullBox { +public: + Box_auxi() + { + set_short_type(fourcc("auxi")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Auxiliary Info Type"; } + + Error write(StreamWriter& writer) const override; + + void set_aux_track_type_urn(const std::string& t) { m_aux_track_type = t; } + + std::string get_aux_track_type_urn() const { return m_aux_track_type; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::string m_aux_track_type; +}; + + +struct VisualSampleEntry { + // from SampleEntry + //const unsigned int(8)[6] reserved = 0; + uint16_t data_reference_index = 1; + + // VisualSampleEntry + + uint16_t pre_defined = 0; + //uint16_t reserved = 0; + uint32_t pre_defined2[3] = {0, 0, 0}; + uint16_t width = 0; + uint16_t height = 0; + uint32_t horizresolution = 0x00480000; // 72 dpi + uint32_t vertresolution = 0x00480000; // 72 dpi + //uint32_t reserved = 0; + uint16_t frame_count = 1; + std::string compressorname; // max 32 characters + uint16_t depth = 0x0018; + int16_t pre_defined3 = -1; + // other boxes from derived specifications + //std::shared_ptr clap; // optional + //std::shared_ptr pixi; // optional + + double get_horizontal_resolution() const { return horizresolution / double(0x10000); } + + double get_vertical_resolution() const { return vertresolution / double(0x10000); } + + Error parse(BitstreamRange& range, const heif_security_limits*); + + Error write(StreamWriter& writer) const; + + std::string dump(Indent&) const; +}; + + +class Box_VisualSampleEntry : public Box { +public: + Error write(StreamWriter& writer) const override; + + std::string dump(Indent&) const override; + + const VisualSampleEntry& get_VisualSampleEntry_const() const { return m_visualSampleEntry; } + + VisualSampleEntry& get_VisualSampleEntry() { return m_visualSampleEntry; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + +private: + VisualSampleEntry m_visualSampleEntry; +}; + + +class Box_URIMetaSampleEntry : public Box { +public: + Box_URIMetaSampleEntry() + { + set_short_type(fourcc("urim")); + } + + Error write(StreamWriter& writer) const override; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "URI Meta Sample Entry"; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + +private: + // from SampleEntry + //const unsigned int(8)[6] reserved = 0; + uint16_t data_reference_index; +}; + + +class Box_uri : public FullBox { +public: + Box_uri() + { + set_short_type(fourcc("uri ")); + } + + void set_uri(std::string uri) { m_uri = std::move(uri); } + + std::string get_uri() const { return m_uri; } + + Error write(StreamWriter& writer) const override; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "URI"; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits* limits) override; + +private: + std::string m_uri; +}; + + +// Sample to Group +class Box_sbgp : public FullBox { +public: + Box_sbgp() + { + set_short_type(fourcc("sbgp")); + } + + void derive_box_version() override; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample to Group"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_grouping_type = 0; // 4cc + std::optional m_grouping_type_parameter; + + struct Entry { + uint32_t sample_count; + uint32_t group_description_index; + }; + + std::vector m_entries; + MemoryHandle m_memory_handle; +}; + + +class SampleGroupEntry +{ +public: + virtual ~SampleGroupEntry() = default; + + virtual std::string dump() const = 0; + + virtual Error write(StreamWriter& writer) const = 0; + + virtual Error parse(BitstreamRange& range, const heif_security_limits*) = 0; +}; + + +class SampleGroupEntry_refs : public SampleGroupEntry +{ +public: + std::string dump() const override; + + Error write(StreamWriter& writer) const override; + + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_sample_id; + std::vector m_direct_reference_sample_id; +}; + + +// Sample Group Description +class Box_sgpd : public FullBox { +public: + Box_sgpd() + { + set_short_type(fourcc("sgpd")); + } + + void derive_box_version() override; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Group Description"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_grouping_type = 0; // 4cc + std::optional m_default_length; // version 1 (0 -> variable length) + std::optional m_default_sample_description_index;; // version >= 2 + + struct Entry { + uint32_t description_length = 0; // if version==1 && m_default_length == 0 + std::shared_ptr sample_group_entry; + }; + + std::vector m_entries; + MemoryHandle m_memory_handle; +}; + +// Bitrate +class Box_btrt : public FullBox { +public: + Box_btrt() + { + set_short_type(fourcc("btrt")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Bitrate"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_bufferSizeDB; + uint32_t m_maxBitrate; + uint32_t m_avgBitrate; +}; + + +class Box_saiz : public FullBox { +public: + Box_saiz() + { + set_short_type(fourcc("saiz")); + } + + void set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter = 0); + + uint32_t get_aux_info_type() const { return m_aux_info_type; } + + uint32_t get_aux_info_type_parameter() const { return m_aux_info_type_parameter; } + + void add_sample_size(uint8_t s); + + void add_nonpresent_sample() { add_sample_size(0); } + + uint8_t get_sample_size(uint32_t idx); + + bool have_samples_constant_size() const { return m_default_sample_info_size != 0; } + + uint32_t get_num_samples() const { return m_num_samples; } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Auxiliary Information Sizes"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + uint32_t m_aux_info_type = 0; + uint32_t m_aux_info_type_parameter = 0; + uint8_t m_default_sample_info_size = 0; // 0 -> variable length + uint32_t m_num_samples = 0; // needed in case we are using the default sample size + + std::vector m_sample_sizes; + MemoryHandle m_memory_handle; +}; + + +class Box_saio : public FullBox { +public: + Box_saio() + { + set_short_type(fourcc("saio")); + } + + void set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter = 0); + + uint32_t get_aux_info_type() const { return m_aux_info_type; } + + uint32_t get_aux_info_type_parameter() const { return m_aux_info_type_parameter; } + + void add_chunk_offset(uint64_t offset); + + // If this is 1, the SAI data of all samples is written contiguously in the file. + size_t get_num_chunks() const { return m_chunk_offset.size(); } + + uint64_t get_chunk_offset(uint32_t idx) const; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Sample Auxiliary Information Offsets"; } + + Error write(StreamWriter& writer) const override; + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + void patch_file_pointers(StreamWriter& writer, size_t offset) override; + +private: + uint32_t m_aux_info_type = 0; + uint32_t m_aux_info_type_parameter = 0; + + bool m_need_64bit = false; + mutable uint64_t m_offset_start_pos; + + // If |chunk_offset|==1, the SAI data of all samples is stored contiguously in the file + std::vector m_chunk_offset; + + MemoryHandle m_memory_handle; +}; + + +class Box_sdtp : public FullBox { +public: + Box_sdtp() + { + set_short_type(fourcc("sdtp")); + } + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Independent and Disposable Samples"; } + + // Error write(StreamWriter& writer) const override; + + uint8_t get_is_leading(uint32_t sampleIdx) const { return (m_sample_information[sampleIdx] >> 6) & 3; } + + uint8_t get_depends_on(uint32_t sampleIdx) const { return (m_sample_information[sampleIdx] >> 4) & 3; } + + uint8_t get_is_depended_on(uint32_t sampleIdx) const { return (m_sample_information[sampleIdx] >> 2) & 3; } + + uint8_t get_has_redundancy(uint32_t sampleIdx) const { return (m_sample_information[sampleIdx]) & 3; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + +private: + std::vector m_sample_information; +}; + + +class Box_tref : public Box +{ +public: + Box_tref() + { + set_short_type(fourcc("tref")); + } + + struct Reference { + uint32_t reference_type; + std::vector to_track_id; + }; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Track Reference"; } + + std::vector get_references(uint32_t ref_type) const; + + size_t get_number_of_references_of_type(uint32_t ref_type) const; + + size_t get_number_of_reference_types() const { return m_references.size(); } + + std::vector get_reference_types() const; + + void add_references(uint32_t to_track_id, uint32_t type); + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + Error write(StreamWriter& writer) const override; + + Error check_for_double_references() const; + +private: + std::vector m_references; +}; + + +// Edit List container +class Box_edts : public Box_container { +public: + Box_edts() : Box_container("edts") {} + + const char* debug_box_name() const override { return "Edit List Container"; } +}; + + +class Box_elst : public FullBox +{ +public: + Box_elst() + { + set_short_type(fourcc("elst")); + } + + enum Flags { + Repeat_EditList = 0x01 + }; + + std::string dump(Indent&) const override; + + const char* debug_box_name() const override { return "Edit List"; } + + void enable_repeat_mode(bool enable); + + bool is_repeat_mode() const { return get_flags() & Flags::Repeat_EditList; } + + struct Entry { + uint64_t segment_duration = 0; + int64_t media_time = 0; + int16_t media_rate_integer = 1; + int16_t media_rate_fraction = 0; + }; + + void add_entry(const Entry&); + + size_t num_entries() const { return m_entries.size(); } + + Entry get_entry(uint32_t entry) const { return m_entries[entry]; } + +protected: + Error parse(BitstreamRange& range, const heif_security_limits*) override; + + Error write(StreamWriter& writer) const override; + +public: + void derive_box_version() override; + +private: + std::vector m_entries; +}; + +#endif //SEQ_BOXES_H diff -Nru libheif-1.19.8/libheif/sequences/track.cc libheif-1.23.4/libheif/sequences/track.cc --- libheif-1.19.8/libheif/sequences/track.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,1298 @@ +/* + * HEIF image base codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include "track.h" +#include "context.h" +#include "sequences/seq_boxes.h" +#include "sequences/chunk.h" +#include "sequences/track_visual.h" +#include "sequences/track_metadata.h" +#include "api_structs.h" +#include +#include +#include +#include + + +TrackOptions& TrackOptions::operator=(const TrackOptions& src) +{ + if (&src == this) { + return *this; + } + + this->track_timescale = src.track_timescale; + this->write_sample_aux_infos_interleaved = src.write_sample_aux_infos_interleaved; + this->with_sample_tai_timestamps = src.with_sample_tai_timestamps; + + if (src.tai_clock_info) { + this->tai_clock_info = heif_tai_clock_info_alloc(); + heif_tai_clock_info_copy(this->tai_clock_info, src.tai_clock_info); + } + else { + this->tai_clock_info = nullptr; + } + + this->with_sample_content_ids = src.with_sample_content_ids; + this->gimi_track_content_id = src.gimi_track_content_id; + + return *this; +} + + +SampleAuxInfoHelper::SampleAuxInfoHelper(bool interleaved) + : m_interleaved(interleaved) +{ + m_saiz = std::make_shared(); + m_saio = std::make_shared(); +} + + +void SampleAuxInfoHelper::set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter) +{ + m_saiz->set_aux_info_type(aux_info_type, aux_info_type_parameter); + m_saio->set_aux_info_type(aux_info_type, aux_info_type_parameter); +} + +Error SampleAuxInfoHelper::add_sample_info(const std::vector& data) +{ + if (data.size() > 0xFF) { + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Encoded sample auxiliary information exceeds maximum size" + }; + } + + m_saiz->add_sample_size(static_cast(data.size())); + + m_data.insert(m_data.end(), data.begin(), data.end()); + + return Error::Ok; +} + +void SampleAuxInfoHelper::add_nonpresent_sample() +{ + m_saiz->add_nonpresent_sample(); +} + + +void SampleAuxInfoHelper::write_interleaved(const std::shared_ptr& file) +{ + if (m_interleaved && !m_data.empty()) { + // TODO: I think this does not work because the image data does not know that there is SAI in-between + uint64_t pos = file->append_mdat_data(m_data); + m_saio->add_chunk_offset(pos); + + m_data.clear(); + } +} + +void SampleAuxInfoHelper::write_all(const std::shared_ptr& parent, const std::shared_ptr& file) +{ + parent->append_child_box(m_saiz); + parent->append_child_box(m_saio); + + if (!m_data.empty()) { + uint64_t pos = file->append_mdat_data(m_data); + m_saio->add_chunk_offset(pos); + } +} + + +SampleAuxInfoReader::SampleAuxInfoReader(const std::shared_ptr& saiz, + const std::shared_ptr& saio, + const std::vector>& chunks) +{ + m_saiz = saiz; + m_saio = saio; + + bool oneChunk = (saio->get_num_chunks() == 1); + + uint32_t current_chunk = 0; + uint64_t offset = saio->get_chunk_offset(0); + uint32_t nSamples = saiz->get_num_samples(); + + m_contiguous_and_constant_size = (oneChunk && m_saiz->have_samples_constant_size()); + + if (m_contiguous_and_constant_size) { + m_singleChunk_offset = offset; + } + else { + m_sample_offsets.resize(nSamples); + + for (uint32_t i = 0; i < nSamples; i++) { + if (!oneChunk && i > chunks[current_chunk]->last_sample_number()) { + current_chunk++; + if (current_chunk >= chunks.size()) { + break; + } + offset = saio->get_chunk_offset(current_chunk); + } + + m_sample_offsets[i] = offset; + offset += saiz->get_sample_size(i); + } + } +} + + +heif_sample_aux_info_type SampleAuxInfoReader::get_type() const +{ + heif_sample_aux_info_type type; + type.type = m_saiz->get_aux_info_type(); + type.parameter = m_saiz->get_aux_info_type_parameter(); + return type; +} + + +Result > SampleAuxInfoReader::get_sample_info(const HeifFile* file, uint32_t sample_idx) +{ + uint64_t offset; + uint8_t size; + + if (m_contiguous_and_constant_size) { + size = m_saiz->get_sample_size(0); + offset = m_singleChunk_offset + uint64_t{sample_idx} * size; + } + else { + size = m_saiz->get_sample_size(sample_idx); + if (size > 0) { + if (sample_idx >= m_sample_offsets.size()) { + return {}; + } + + offset = m_sample_offsets[sample_idx]; + } + } + + std::vector data; + + if (size > 0) { + Error err = file->append_data_from_file_range(data, offset, size); + if (err) { + return err; + } + } + + return data; +} + + +std::shared_ptr Track::get_file() const +{ + return m_heif_context->get_heif_file(); +} + + +Track::Track(HeifContext* ctx) +{ + m_heif_context = ctx; +} + + +Error Track::load(const std::shared_ptr& trak_box) +{ + m_trak = trak_box; + + auto tkhd = trak_box->get_child_box(); + if (!tkhd) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'tkhd' box." + }; + } + + m_tkhd = tkhd; + + m_id = tkhd->get_track_id(); + + auto edts = trak_box->get_child_box(); + if (edts) { + m_elst = edts->get_child_box(); + } + + auto mdia = trak_box->get_child_box(); + if (!mdia) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'mdia' box." + }; + } + + m_tref = trak_box->get_child_box(); + + auto hdlr = mdia->get_child_box(); + if (!hdlr) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'hdlr' box." + }; + } + + m_handler_type = hdlr->get_handler_type(); + + m_minf = mdia->get_child_box(); + if (!m_minf) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'minf' box." + }; + } + + m_mdhd = mdia->get_child_box(); + if (!m_mdhd) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'mdhd' box." + }; + } + + auto stbl = m_minf->get_child_box(); + if (!stbl) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stbl' box." + }; + } + + m_stsd = stbl->get_child_box(); + if (!m_stsd) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stsd' box." + }; + } + + m_stsc = stbl->get_child_box(); + if (!m_stsc) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stsc' box." + }; + } + + m_stco = stbl->get_child_box(); + if (!m_stco) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stco' box." + }; + } + + m_stsz = stbl->get_child_box(); + if (!m_stsz) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stsz' box." + }; + } + + // Enforce the sequence-frame limit before allocating any per-chunk state below. + // Box_stsz::parse already applies this when parsing from a file, but check again + // here so the invariant holds even if m_stsz was built another way. + const auto* limits = m_heif_context->get_security_limits(); + if (limits->max_sequence_frames > 0 && + m_stsz->num_samples() > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Security limit for maximum number of sequence frames exceeded" + }; + } + + m_stts = stbl->get_child_box(); + if (!m_stts) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'stts' box." + }; + } + + m_ctts = stbl->get_child_box(); + + // --- check that number of samples in various boxes are consistent + + if (m_stts->get_number_of_samples() != m_stsz->num_samples()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of samples in 'stts' and 'stsz' is inconsistent." + }; + } + + if (m_ctts && m_ctts->get_number_of_samples() != m_stsz->num_samples()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of samples in 'ctts' and 'stsz' is inconsistent." + }; + } + + const std::vector& chunk_offsets = m_stco->get_offsets(); + assert(chunk_offsets.size() <= (size_t) std::numeric_limits::max()); // There cannot be more than uint32_t chunks. + + // Account the per-chunk sample-range tables (Chunk::m_sample_ranges) against + // max_total_memory before building any chunk. Their combined size across all + // chunks is num_samples * sizeof(SampleFileRange); a small track can declare + // millions of samples in one chunk, so without this the tables are untracked + // and multiple tracks can exhaust memory undetected (GHSA-xw34-mjcp-jqh8, V2/V3). + if (auto err = m_chunk_sample_ranges_memory.alloc(m_stsz->num_samples(), + Chunk::sample_range_entry_size(), + limits, "the sequence chunk sample-range tables")) { + return err; + } + + uint32_t current_sample_idx = 0; + int32_t previous_sample_description_index = -1; + + for (size_t chunk_idx = 0; chunk_idx < chunk_offsets.size(); chunk_idx++) { + auto* s2c = m_stsc->get_chunk(static_cast(chunk_idx + 1)); + if (!s2c) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'stco' box references a non-existing chunk." + }; + } + + Box_stsc::SampleToChunk sampleToChunk = *s2c; + + auto sample_description = m_stsd->get_sample_entry(sampleToChunk.sample_description_index - 1); + if (!sample_description) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track references a non-existing sample description." + }; + } + + if (auto auxi = sample_description->get_child_box()) { + m_auxiliary_info_type = auxi->get_aux_track_type_urn(); + } + + if (m_first_taic == nullptr) { + auto taic = sample_description->get_child_box(); + if (taic) { + m_first_taic = taic; + } + } + + if (static_cast(current_sample_idx) + sampleToChunk.samples_per_chunk > m_stsz->num_samples()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of samples in 'stsc' box exceeds sample sizes in 'stsz' box." + }; + } + + auto chunk = Chunk::create(m_heif_context, m_id, + current_sample_idx, sampleToChunk.samples_per_chunk, + m_stco->get_offsets()[chunk_idx], + m_stsz); + + if (!chunk) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Chunk file offset overflows 64-bit range." + }; + } + + if (auto visualSampleDescription = std::dynamic_pointer_cast(sample_description)) { + if (chunk_idx > 0 && (int32_t) sampleToChunk.sample_description_index == previous_sample_description_index) { + // reuse decoder from previous chunk if it uses the sample sample_description_index + chunk->set_decoder(m_chunks[chunk_idx - 1]->get_decoder()); + } + else { + // use a new decoder + auto decoder = Decoder::alloc_for_sequence_sample_description_box(visualSampleDescription); + if (!decoder) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Sample description has unsupported codec or is missing the codec configuration box." + }; + } + chunk->set_decoder(decoder); + } + } + + m_chunks.push_back(chunk); + + current_sample_idx += sampleToChunk.samples_per_chunk; + previous_sample_description_index = sampleToChunk.sample_description_index; + } + + if (current_sample_idx != m_stsz->num_samples()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of samples covered by 'stsc' does not match 'stsz'/'stts'." + }; + } + + // --- read sample auxiliary information boxes + + std::vector > saiz_boxes = stbl->get_child_boxes(); + std::vector > saio_boxes = stbl->get_child_boxes(); + + if (saio_boxes.size() != saiz_boxes.size()) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Boxes 'saiz' and `saio` must come in pairs." + }; + } + + for (const auto& saiz : saiz_boxes) { + uint32_t aux_info_type = saiz->get_aux_info_type(); + uint32_t aux_info_type_parameter = saiz->get_aux_info_type_parameter(); + + // find the corresponding saio box + + std::shared_ptr saio; + for (const auto& candidate : saio_boxes) { + if (candidate->get_aux_info_type() == aux_info_type && + candidate->get_aux_info_type_parameter() == aux_info_type_parameter) { + saio = candidate; + break; + } + } + + if (saio) { + if (saio->get_num_chunks() != 1 && + saio->get_num_chunks() != m_stco->get_number_of_chunks()) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Invalid number of chunks in 'saio' box." + }; + } + + if (saio->get_num_chunks() != 1 && m_chunks.empty() && saiz->get_num_samples() > 0) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'saiz' box references samples but no chunks exist." + }; + } + + if (saiz->get_num_samples() > m_stsz->num_samples()) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Number of samples in 'saiz' box exceeds actual number of samples." + }; + } + + if (aux_info_type == fourcc("suid")) { + m_aux_reader_content_ids = std::make_unique(saiz, saio, m_chunks); + } + + if (aux_info_type == fourcc("stai")) { + m_aux_reader_tai_timestamps = std::make_unique(saiz, saio, m_chunks); + } + } + else { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "'saiz' box without matching 'saio' box." + }; + } + } + + // --- read track properties + + if (auto meta = trak_box->get_child_box()) { + auto iloc = meta->get_child_box(); + auto idat = meta->get_child_box(); + + auto iinf = meta->get_child_box(); + if (iinf) { + auto infe_boxes = iinf->get_child_boxes(); + for (const auto& box : infe_boxes) { + if (box->get_item_type_4cc() == fourcc("uri ") && + box->get_item_uri_type() == "urn:uuid:15beb8e4-944d-5fc6-a3dd-cb5a7e655c73") { + heif_item_id id = box->get_item_ID(); + + if (!iloc) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track meta references item content-id but file has no 'iloc' box." + }; + } + + std::vector data; + Error err = iloc->read_data(id, m_heif_context->get_heif_file()->get_reader(), idat, &data, m_heif_context->get_security_limits()); + if (err) { + return err; + } + + Result contentIdResult = vector_to_string(data); + if (!contentIdResult) { + return contentIdResult.error(); + } + + m_track_info.gimi_track_content_id = *contentIdResult; + } + } + } + } + + + // --- initialize track tables + + Error err = init_sample_timing_table(); + if (err) { + return err; + } + + return {}; +} + + +Track::Track(HeifContext* ctx, uint32_t track_id, const TrackOptions* options, uint32_t handler_type) +{ + m_heif_context = ctx; + + m_moov = ctx->get_heif_file()->get_moov_box(); + assert(m_moov); + + // --- find next free track ID + + if (track_id == 0) { + auto idResult = ctx->get_id_creator().get_new_id(IDCreator::Namespace::track); + // Track constructor cannot return errors; assert on overflow (extremely unlikely). + assert(idResult); + track_id = *idResult; + + auto mvhd = m_moov->get_child_box(); + mvhd->set_next_track_id(track_id + 1); + + m_id = track_id; + } + + m_trak = std::make_shared(); + m_moov->append_child_box(m_trak); + + m_tkhd = std::make_shared(); + m_trak->append_child_box(m_tkhd); + m_tkhd->set_track_id(track_id); + + auto mdia = std::make_shared(); + m_trak->append_child_box(mdia); + + m_mdhd = std::make_shared(); + m_mdhd->set_timescale(options ? options->track_timescale : 90000); + mdia->append_child_box(m_mdhd); + + m_hdlr = std::make_shared(); + mdia->append_child_box(m_hdlr); + m_hdlr->set_handler_type(handler_type); + + m_minf = std::make_shared(); + mdia->append_child_box(m_minf); + + // add (unused) 'dinf' + + auto dinf = std::make_shared(); + auto dref = std::make_shared(); + auto url = std::make_shared(); + m_minf->append_child_box(dinf); + dinf->append_child_box(dref); + dref->append_child_box(url); + + // vmhd is added in Track_Visual + + m_stbl = std::make_shared(); + m_minf->append_child_box(m_stbl); + + m_stsd = std::make_shared(); + m_stbl->append_child_box(m_stsd); + + m_stts = std::make_shared(); + m_stbl->append_child_box(m_stts); + + m_ctts = std::make_shared(); + // The ctts box will be added in finalize_track(), but only there is frame-reordering. + + m_stsc = std::make_shared(); + m_stbl->append_child_box(m_stsc); + + m_stsz = std::make_shared(); + m_stbl->append_child_box(m_stsz); + + m_stco = std::make_shared(); + m_stbl->append_child_box(m_stco); + + m_stss = std::make_shared(); + m_stbl->append_child_box(m_stss); + + if (options) { + m_track_info = *options; + + if (m_track_info.with_sample_tai_timestamps != heif_sample_aux_info_presence_none) { + m_aux_helper_tai_timestamps = std::make_unique(m_track_info.write_sample_aux_infos_interleaved); + m_aux_helper_tai_timestamps->set_aux_info_type(fourcc("stai")); + } + + if (m_track_info.with_sample_content_ids != heif_sample_aux_info_presence_none) { + m_aux_helper_content_ids = std::make_unique(m_track_info.write_sample_aux_infos_interleaved); + m_aux_helper_content_ids->set_aux_info_type(fourcc("suid")); + } + + if (!options->gimi_track_content_id.empty()) { + auto hdlr_box = std::make_shared(); + hdlr_box->set_handler_type(fourcc("meta")); + + auto uuid_box = std::make_shared(); + uuid_box->set_item_type_4cc(fourcc("uri ")); + uuid_box->set_item_uri_type("urn:uuid:15beb8e4-944d-5fc6-a3dd-cb5a7e655c73"); + uuid_box->set_item_ID(1); + + auto iinf_box = std::make_shared(); + iinf_box->append_child_box(uuid_box); + + std::vector track_uuid_vector; + track_uuid_vector.insert(track_uuid_vector.begin(), + options->gimi_track_content_id.c_str(), + options->gimi_track_content_id.c_str() + options->gimi_track_content_id.length() + 1); + + auto iloc_box = std::make_shared(); + iloc_box->append_data(1, track_uuid_vector, 1); + + auto meta_box = std::make_shared(); + meta_box->append_child_box(hdlr_box); + meta_box->append_child_box(iinf_box); + meta_box->append_child_box(iloc_box); + + m_trak->append_child_box(meta_box); + } + } +} + + +Result > Track::alloc_track(HeifContext* ctx, const std::shared_ptr& trak) +{ + auto mdia = trak->get_child_box(); + if (!mdia) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'mdia' box." + }; + } + + auto hdlr = mdia->get_child_box(); + if (!hdlr) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track has no 'hdlr' box." + }; + } + + std::shared_ptr track; + + switch (hdlr->get_handler_type()) { + case fourcc("pict"): + case fourcc("vide"): + case fourcc("auxv"): + track = std::make_shared(ctx); + break; + case fourcc("meta"): + track = std::make_shared(ctx); + break; + default: { + std::stringstream sstr; + sstr << "Track with unsupported handler type '" << fourcc_to_string(hdlr->get_handler_type()) << "'."; + return Error{ + heif_error_Unsupported_feature, + heif_suberror_Unsupported_track_type, + sstr.str() + }; + } + } + + assert(track); + Error loadError = track->load(trak); + if (loadError) { + return loadError; + } + + return {track}; +} + + +bool Track::is_visual_track() const +{ + return (m_handler_type == fourcc("pict") || + m_handler_type == fourcc("vide")); +} + + +static const char* cAuxType_alpha_miaf = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; +static const char* cAuxType_alpha_hevc = "urn:mpeg:hevc:2015:auxid:1"; +static const char* cAuxType_alpha_avc = "urn:mpeg:avc:2015:auxid:1"; + +heif_auxiliary_track_info_type Track::get_auxiliary_info_type() const +{ + if (m_auxiliary_info_type == cAuxType_alpha_miaf || + m_auxiliary_info_type == cAuxType_alpha_hevc || + m_auxiliary_info_type == cAuxType_alpha_avc) { + return heif_auxiliary_track_info_type_alpha; + } + else { + return heif_auxiliary_track_info_type_unknown; + } +} + +const char* get_track_auxiliary_info_type(heif_compression_format format) +{ + switch (format) { + case heif_compression_HEVC: + return cAuxType_alpha_hevc; + case heif_compression_AVC: + return cAuxType_alpha_avc; + case heif_compression_AV1: + return cAuxType_alpha_miaf; + default: + return cAuxType_alpha_miaf; // TODO: is this correct for all remaining compression types ? + } +} + +// TODO: is this correct or should we set the aux_info_type depending on the compression format? +void Track::set_auxiliary_info_type(heif_auxiliary_track_info_type type) +{ + switch (type) { + case heif_auxiliary_track_info_type_alpha: + m_auxiliary_info_type = cAuxType_alpha_miaf; + break; + default: + m_auxiliary_info_type.clear(); + break; + } +} + + +uint32_t Track::get_first_cluster_sample_entry_type() const +{ + if (m_stsd->get_num_sample_entries() == 0) { + return 0; // TODO: error ? Or can we assume at this point that there is at least one sample entry? + } + + return m_stsd->get_sample_entry(0)->get_short_type(); +} + + +Result Track::get_first_cluster_urim_uri() const +{ + if (m_stsd->get_num_sample_entries() == 0) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "This track has no sample entries." + }; + } + + std::shared_ptr sampleEntry = m_stsd->get_sample_entry(0); + auto urim = std::dynamic_pointer_cast(sampleEntry); + if (!urim) { + return Error{ + heif_error_Usage_error, + heif_suberror_Unspecified, + "This cluster is no 'urim' sample entry." + }; + } + + std::shared_ptr uri = urim->get_child_box(); + if (!uri) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "The 'urim' box has no 'uri' child box." + }; + } + + return uri->get_uri(); +} + + +bool Track::end_of_sequence_reached() const +{ + //return (m_next_sample_to_be_processed > m_chunks.back()->last_sample_number()); + return m_next_sample_to_be_output >= m_num_output_samples; +} + + +Error Track::finalize_track() +{ + // --- write active chunk data + + size_t data_start = m_heif_context->get_heif_file()->append_mdat_data(m_chunk_data); + m_chunk_data.clear(); + + // first sample in chunk? -> write chunk offset + + if (true) { + // m_stsc->last_chunk_empty()) { + // TODO: we will have to call this at the end of a chunk to dump the current SAI queue + + // if auxiliary data is interleaved, write it between the chunks + if (m_aux_helper_tai_timestamps) m_aux_helper_tai_timestamps->write_interleaved(get_file()); + if (m_aux_helper_content_ids) m_aux_helper_content_ids->write_interleaved(get_file()); + + // TODO + assert(data_start < 0xFF000000); // add some headroom for header data + m_stco->add_chunk_offset(static_cast(data_start)); + } + + + // --- write rest of data + + if (m_aux_helper_tai_timestamps) m_aux_helper_tai_timestamps->write_all(m_stbl, get_file()); + if (m_aux_helper_content_ids) m_aux_helper_content_ids->write_all(m_stbl, get_file()); + + uint64_t duration = m_stts->get_total_duration(true); + m_mdhd->set_duration(duration); + + m_stss->set_total_number_of_samples(m_stsz->num_samples()); + + // only add ctts box if we use frame-reordering + if (!m_ctts->is_constant_offset()) { + m_stbl->append_child_box(m_ctts); + } + + return {}; +} + + +uint64_t Track::get_duration_in_media_units() const +{ + return m_mdhd->get_duration(); +} + + +uint32_t Track::get_timescale() const +{ + return m_mdhd->get_timescale(); +} + + +void Track::set_track_duration_in_movie_units(uint64_t total_duration, uint64_t segment_duration) +{ + m_tkhd->set_duration(total_duration); + + if (m_elst) { + Box_elst::Entry entry; + entry.segment_duration = segment_duration; + + m_elst->add_entry(entry); + } +} + + +void Track::enable_edit_list_repeat_mode(bool enable) +{ + if (!m_elst) { + if (!enable) { + return; + } + + auto edts = std::make_shared(); + m_trak->append_child_box(edts); + + m_elst = std::make_shared(); + edts->append_child_box(m_elst); + + m_elst->enable_repeat_mode(enable); + } +} + + +void Track::add_chunk(heif_compression_format format) +{ + auto chunk = std::make_shared(m_heif_context, m_id, format); + m_chunks.push_back(chunk); + + int chunkIdx = (uint32_t) m_chunks.size(); + m_stsc->add_chunk(chunkIdx); +} + +void Track::set_sample_description_box(const std::shared_ptr& sample_description_box) +{ + // --- add 'taic' when we store timestamps as sample auxiliary information + + if (m_track_info.with_sample_tai_timestamps != heif_sample_aux_info_presence_none) { + auto taic = std::make_shared(); + taic->set_from_tai_clock_info(m_track_info.tai_clock_info); + sample_description_box->append_child_box(taic); + } + + m_stsd->add_sample_entry(sample_description_box); +} + + +Error Track::write_sample_data(const std::vector& raw_data, uint32_t sample_duration, + int32_t composition_time_offset, + bool is_sync_sample, + const heif_tai_timestamp_packet* tai, const std::optional& gimi_contentID) +{ + m_chunk_data.insert(m_chunk_data.end(), raw_data.begin(), raw_data.end()); + + m_stsc->increase_samples_in_chunk(1); + + m_stsz->append_sample_size((uint32_t) raw_data.size()); + + if (is_sync_sample) { + m_stss->add_sync_sample(m_next_sample_to_be_output + 1); + } + + if (sample_duration == 0) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Sample duration may not be 0" + }; + } + + m_stts->append_sample_duration(sample_duration); + m_ctts->append_sample_offset(composition_time_offset); + + + // --- sample timestamp + + if (m_track_info.with_sample_tai_timestamps != heif_sample_aux_info_presence_none) { + if (tai) { + std::vector tai_data = Box_itai::encode_tai_to_bitstream(tai); + auto err = m_aux_helper_tai_timestamps->add_sample_info(tai_data); + if (err) { + return err; + } + } + else if (m_track_info.with_sample_tai_timestamps == heif_sample_aux_info_presence_optional) { + m_aux_helper_tai_timestamps->add_nonpresent_sample(); + } + else { + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Mandatory TAI timestamp missing" + }; + } + } + + // --- sample content id + + if (m_track_info.with_sample_content_ids != heif_sample_aux_info_presence_none) { + if (gimi_contentID) { + const auto& id = *gimi_contentID; + const char* id_str = id.c_str(); + std::vector id_vector; + id_vector.insert(id_vector.begin(), id_str, id_str + id.length() + 1); + auto err = m_aux_helper_content_ids->add_sample_info(id_vector); + if (err) { + return err; + } + } + else if (m_track_info.with_sample_content_ids == heif_sample_aux_info_presence_optional) { + m_aux_helper_content_ids->add_nonpresent_sample(); + } + else { + return { + heif_error_Encoding_error, + heif_suberror_Unspecified, + "Mandatory ContentID missing" + }; + } + } + + m_next_sample_to_be_output++; + + return Error::Ok; +} + + +void Track::add_reference_to_track(uint32_t referenceType, uint32_t to_track_id) +{ + if (!m_tref) { + m_tref = std::make_shared(); + m_trak->append_child_box(m_tref); + } + + m_tref->add_references(to_track_id, referenceType); +} + + +Error Track::init_sample_timing_table() +{ + m_num_samples = m_stsz->num_samples(); + + const auto* limits = m_heif_context->get_security_limits(); + + // Account the presentation timeline against max_total_memory before allocating + // it. m_num_samples is bounded by max_sequence_frames, but a single small track + // can still declare millions of samples, so this ~48-bytes/sample vector must be + // tracked to bound multi-track accumulation (GHSA-xw34-mjcp-jqh8, V2/V3). The + // media timeline built below is moved into m_presentation_timeline (not copied), + // so only one such buffer ever exists and this single reservation covers it. + if (auto err = m_presentation_timeline_memory.alloc(m_num_samples, sizeof(SampleTiming), + limits, "the sequence presentation timeline")) { + return err; + } + + // --- build media timeline + + std::vector media_timeline; + media_timeline.reserve(m_num_samples); + + uint64_t current_decoding_time = 0; + uint32_t current_chunk = 0; + uint32_t current_sample_in_chunk_idx = 0; + + for (uint32_t i = 0; i < m_num_samples; i++) { + SampleTiming timing; + timing.sampleIdx = i; + timing.sampleInChunkIdx = current_sample_in_chunk_idx; + timing.media_decoding_time = current_decoding_time; + // O(log entries) per lookup (Box_stts uses a prefix-sum binary search), so + // building the whole table is O(samples * log entries) rather than the former + // O(samples * entries) file-open CPU-DoS (GHSA-xw34-mjcp-jqh8, variant V1). + timing.sample_duration_media_time = m_stts->get_sample_duration(i); + current_decoding_time += timing.sample_duration_media_time; + current_sample_in_chunk_idx++; + + while (current_chunk < m_chunks.size() && + i > m_chunks[current_chunk]->last_sample_number()) { + current_chunk++; + current_sample_in_chunk_idx = 0; + } + + if (current_chunk >= m_chunks.size()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Sample not covered by any chunk." + }; + } + + timing.chunkIdx = current_chunk; + + media_timeline.push_back(timing); + } + + // --- build presentation timeline from editlist + + bool fallback = false; + + if (m_heif_context->get_sequence_timescale() != get_timescale()) { + fallback = true; + } + else if (m_elst && + m_elst->num_entries() == 1 && + m_elst->get_entry(0).media_time == 0 && + m_elst->get_entry(0).segment_duration == m_mdhd->get_duration() && + m_elst->is_repeat_mode()) { + + uint64_t duration_media_units = get_duration_in_media_units(); + if (duration_media_units == 0) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Track duration is zero." + }; + } + + uint64_t multiplier = m_heif_context->get_sequence_duration() / duration_media_units; + + // Logical number of output samples = physical samples x repeat multiplier. + // Compute this saturating instead of wrapping: with the indefinite-duration + // sentinel (mvhd.duration = UINT64_MAX) the multiplier is enormous, and a plain + // multiply could overflow uint64_t and wrap to a small, misleading value. + uint64_t timeline_size = media_timeline.size(); + uint64_t total_output_samples; + if (timeline_size != 0 && multiplier > std::numeric_limits::max() / timeline_size) { + total_output_samples = std::numeric_limits::max(); + } + else { + total_output_samples = multiplier * timeline_size; + } + + // The decode loop (Track_Visual::decode_next_image_sample), the raw-data path + // (get_next_sample_raw_data) and end_of_sequence_reached() all count emitted + // samples with a uint32_t (m_next_sample_to_be_output). If m_num_output_samples + // exceeds UINT32_MAX, that counter can never reach it and decoding never + // terminates (GHSA-xw34-mjcp-jqh8, variants V4/V5). The physical-sample limit + // (max_sequence_frames) is also never applied to this repeat-amplified logical + // count (variant V6). Bound the number of samples the loop will emit by the + // sequence-frame limit (or UINT32_MAX when the limit is disabled), so a + // repeat/indefinite edit list cannot inflate a single physical sample into an + // effectively infinite decode. The reported repetition count below still signals + // "infinite", so a caller wanting true unbounded playback can loop the media + // itself via heif_decoding_options::ignore_sequence_editlist. + uint64_t output_sample_cap = (limits->max_sequence_frames > 0) + ? limits->max_sequence_frames + : std::numeric_limits::max(); + + m_num_output_samples = std::min(total_output_samples, output_sample_cap); + + if (m_heif_context->is_sequence_duration_indefinite()) { + // mvhd carries the all-1s sentinel -> editlist repeats forever. + m_num_repetitions = std::numeric_limits::max(); + } + else if (multiplier >= std::numeric_limits::max()) { + // Doesn't fit in the API's uint32_t; treat as effectively infinite. + m_num_repetitions = std::numeric_limits::max(); + } + else { + m_num_repetitions = static_cast(multiplier); + } + } + else { + fallback = true; + } + + // Fallback: just play the media timeline + if (fallback) { + m_num_output_samples = media_timeline.size(); + // No editlist box at all: the media plays exactly once. + // Editlist box present but its pattern isn't one libheif interprets: report + // 0 so callers know they should not rely on a repetition count. + m_num_repetitions = m_elst ? 0 : 1; + } + + // Both branches above use media_timeline unchanged as the presentation timeline. + // Move (do not copy) it in so only one such buffer is ever allocated, matching + // the single reservation made at the top of this function. + m_presentation_timeline = std::move(media_timeline); + + return {}; +} + + +Result Track::get_next_sample_raw_data(const heif_decoding_options* options) +{ + uint64_t num_output_samples = m_num_output_samples; + if (options && options->ignore_sequence_editlist) { + num_output_samples = m_num_samples; + } + + if (m_next_sample_to_be_output >= num_output_samples) { + return Error{ + heif_error_End_of_sequence, + heif_suberror_Unspecified, + "End of sequence" + }; + } + + const auto& sampleTiming = m_presentation_timeline[m_next_sample_to_be_output % m_presentation_timeline.size()]; + uint32_t sample_idx = sampleTiming.sampleIdx; + uint32_t chunk_idx = sampleTiming.chunkIdx; + + const std::shared_ptr& chunk = m_chunks[chunk_idx]; + + DataExtent extent = chunk->get_data_extent_for_sample(sample_idx); + auto readResult = extent.read_data(); + if (!readResult) { + return readResult.error(); + } + + // Keep the sample in a unique_ptr until the single success exit below. Several + // error returns follow (sample auxiliary info driven by file bytes), and the + // Result error variant cannot carry the pointer back to the caller, so a raw + // 'new' here leaked the sample together with its full payload copy on every one + // of them (GHSA-4rv4-953r-p24q). + auto sample = std::make_unique(); + + // Move the payload out of the function-local extent instead of copying it. The + // extent is destroyed when this function returns anyway, and moving halves the + // peak memory needed per sample. + sample->data = std::move(**readResult); + + // read sample duration + + if (m_stts) { + sample->duration = m_stts->get_sample_duration(sample_idx); + } + + // --- read sample auxiliary data + + if (m_aux_reader_content_ids) { + auto readResult = m_aux_reader_content_ids->get_sample_info(get_file().get(), sample_idx); + if (!readResult) { + return readResult.error(); + } + + if (!readResult->empty()) { + Result convResult = vector_to_string(*readResult); + if (!convResult) { + return convResult.error(); + } + + sample->gimi_sample_content_id = *convResult; + } + } + + if (m_aux_reader_tai_timestamps) { + auto readResult = m_aux_reader_tai_timestamps->get_sample_info(get_file().get(), sample_idx); + if (!readResult) { + return readResult.error(); + } + + if (!readResult->empty()) { + auto resultTai = Box_itai::decode_tai_from_vector(*readResult); + if (!resultTai) { + return resultTai.error(); + } + + sample->timestamp = heif_tai_timestamp_packet_alloc(); + heif_tai_timestamp_packet_copy(sample->timestamp, &*resultTai); + } + } + + m_next_sample_to_be_output++; + + return sample.release(); +} + + +std::vector Track::get_sample_aux_info_types() const +{ + std::vector types; + + if (m_aux_reader_tai_timestamps) types.emplace_back(m_aux_reader_tai_timestamps->get_type()); + if (m_aux_reader_content_ids) types.emplace_back(m_aux_reader_content_ids->get_type()); + + return types; +} diff -Nru libheif-1.19.8/libheif/sequences/track.h libheif-1.23.4/libheif/sequences/track.h --- libheif-1.19.8/libheif/sequences/track.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,309 @@ +/* + * HEIF image base codec. + * Copyright (c) 2024 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_TRACK_H +#define LIBHEIF_TRACK_H + +#include "error.h" +#include "api_structs.h" +#include "security_limits.h" +#include "libheif/heif_plugin.h" +#include "libheif/heif_sequences.h" +#include +#include +#include +#include + +class HeifContext; + +class HeifPixelImage; + +class Chunk; + +class Box_trak; + + +class SampleAuxInfoHelper +{ +public: + SampleAuxInfoHelper(bool interleaved = false); + + void set_aux_info_type(uint32_t aux_info_type, uint32_t aux_info_type_parameter = 0); + + Error add_sample_info(const std::vector& data); + + void add_nonpresent_sample(); + + void write_interleaved(const std::shared_ptr& file); + + void write_all(const std::shared_ptr& parent, const std::shared_ptr& file); + +private: + std::shared_ptr m_saiz; + std::shared_ptr m_saio; + + std::vector m_data; + + bool m_interleaved; +}; + + +class SampleAuxInfoReader +{ +public: + SampleAuxInfoReader(const std::shared_ptr&, + const std::shared_ptr&, + const std::vector>& chunks); + + heif_sample_aux_info_type get_type() const; + + Result> get_sample_info(const HeifFile* file, uint32_t sample_idx); + +private: + std::shared_ptr m_saiz; + std::shared_ptr m_saio; + + // If there is only one chunk and the SAI data sizes are constant, we do not need an offset table. + // We just store the base offset and can directly calculate the sample offset from that. + bool m_contiguous_and_constant_size=false; + uint64_t m_singleChunk_offset=0; + + // For chunked data or non-constant sample sizes, we use a table with the offsets for all SAI samples. + std::vector m_sample_offsets; +}; + + +/** + * This structure specifies what will be written in a track and how it will be laid out in the file. + */ +struct TrackOptions +{ + ~TrackOptions() + { + heif_tai_clock_info_release(tai_clock_info); + } + + // Timescale (clock ticks per second) for this track. + uint32_t track_timescale = 90000; + + // If 'true', the aux_info data blocks will be interleaved with the compressed image. + // This has the advantage that the aux_info is localized near the image data. + // + // If 'false', all aux_info will be written as one block after the compressed image data. + // This has the advantage that no aux_info offsets have to be written. + bool write_sample_aux_infos_interleaved = false; + + + // --- TAI timestamps for samples + heif_sample_aux_info_presence with_sample_tai_timestamps = heif_sample_aux_info_presence_none; + heif_tai_clock_info* tai_clock_info = nullptr; + + // --- GIMI content IDs for samples + + heif_sample_aux_info_presence with_sample_content_ids = heif_sample_aux_info_presence_none; + + // --- GIMI content ID for the track + + std::string gimi_track_content_id; + + TrackOptions& operator=(const TrackOptions&); +}; + + +const char* get_track_auxiliary_info_type(heif_compression_format format); + + +class Track : public ErrorBuffer { +public: + //Track(HeifContext* ctx); + + Track(HeifContext* ctx, uint32_t track_id, const TrackOptions* info, uint32_t handler_type); + + Track(HeifContext* ctx); + + virtual ~Track() = default; + + // Allocate a Track of the correct sub-class (visual or metadata). + // For tracks with an unsupported handler type, heif_error_Unsupported_feature/heif_suberror_Unsupported_track_type is returned. + static Result> alloc_track(HeifContext*, const std::shared_ptr&); + + // load track from file + virtual Error load(const std::shared_ptr&); + + // This is called after creating all Track objects when reading a HEIF file. + // We can now do initializations that require access to all tracks. + [[nodiscard]] virtual Error initialize_after_parsing(HeifContext*, const std::vector>& all_tracks) { return {}; } + + heif_item_id get_id() const { return m_id; } + + std::shared_ptr get_file() const; + + uint32_t get_handler() const { return m_handler_type; } + + heif_auxiliary_track_info_type get_auxiliary_info_type() const; + + std::string get_auxiliary_info_type_urn() const { return m_auxiliary_info_type; } + + void set_auxiliary_info_type(heif_auxiliary_track_info_type); + + void set_auxiliary_info_type_urn(std::string t) { m_auxiliary_info_type = std::move(t); } + + bool is_visual_track() const; + + virtual bool has_alpha_channel() const { return false; } + + uint32_t get_first_cluster_sample_entry_type() const; + + Result get_first_cluster_urim_uri() const; + + uint64_t get_duration_in_media_units() const; + + uint32_t get_timescale() const; + + // The context will compute the duration in global movie units and set this. + void set_track_duration_in_movie_units(uint64_t total_duration, uint64_t segment_duration); + + void enable_edit_list_repeat_mode(bool enable); + + std::shared_ptr get_first_cluster_taic() { return m_first_taic; } + + bool end_of_sequence_reached() const; + + // See m_num_repetitions for the meaning of the return value. + uint32_t get_number_of_repetitions() const { return m_num_repetitions; } + + // Compute some parameters after all frames have been encoded (for example: track duration). + virtual Error finalize_track(); + + const TrackOptions& get_track_info() const { return m_track_info; } + + void add_reference_to_track(uint32_t referenceType, uint32_t to_track_id); + + std::shared_ptr get_tref_box() const { return m_tref; } + + Result get_next_sample_raw_data(const heif_decoding_options* options); + + std::vector get_sample_aux_info_types() const; + +protected: + HeifContext* m_heif_context = nullptr; + uint32_t m_id = 0; + uint32_t m_handler_type = 0; + + TrackOptions m_track_info; + + uint32_t m_num_samples = 0; + + struct SampleTiming { + uint32_t sampleIdx = 0; + uint32_t sampleInChunkIdx = 0; + uint32_t chunkIdx = 0; + uint64_t presentation_time = 0; // TODO + uint64_t media_composition_time = 0; // TODO + uint64_t media_decoding_time = 0; + uint32_t sample_duration_media_time = 0; + uint32_t sample_duration_presentation_time = 0; // TODO + }; + std::vector m_presentation_timeline; + // Accounts m_presentation_timeline against max_total_memory. A small track can + // declare millions of samples, so this vector (~48 bytes/sample) must be tracked + // to bound multi-track accumulation (GHSA-xw34-mjcp-jqh8, variants V2/V3). + MemoryHandle m_presentation_timeline_memory; + uint64_t m_num_output_samples = 0; // Can be larger than the vector. It then repeats the playback. + + // How many times the media timeline is repeated. + // 0 = editlist is present but its pattern is not understood (caller should assume a single playback). + // 1 = no editlist: media plays exactly once. + // UINT32_MAX = infinite (mvhd duration is the indefinite-sentinel and the editlist is in repeat mode). + // N = the media segment is played N times. + uint32_t m_num_repetitions = 1; + + // Continuous counting through all repetitions. You have to take the modulo operation to get the + // index into m_presentation_timeline SampleTiming table. + // (At 30 fps, this 32 bit integer will overflow in >4 years. I think this is acceptable.) + uint32_t m_next_sample_to_be_decoded = 0; + + // Total sequence output index. + uint32_t m_next_sample_to_be_output = 0; + bool m_decoder_is_flushed = false; + + Error init_sample_timing_table(); + + std::vector> m_chunks; + // Accounts the per-chunk Chunk::m_sample_ranges tables against max_total_memory. + // Their combined size over all chunks is num_samples * sizeof(SampleFileRange), + // so a single reservation here bounds them all (GHSA-xw34-mjcp-jqh8, V2/V3). + MemoryHandle m_chunk_sample_ranges_memory; + std::vector m_chunk_data; + + std::shared_ptr m_moov; + std::shared_ptr m_trak; + std::shared_ptr m_tkhd; + std::shared_ptr m_minf; + std::shared_ptr m_mdhd; + std::shared_ptr m_hdlr; + std::shared_ptr m_stbl; + std::shared_ptr m_stsd; + std::shared_ptr m_stsc; + std::shared_ptr m_stco; + std::shared_ptr m_stts; + std::shared_ptr m_ctts; // optional box, TODO: add only if needed + std::shared_ptr m_stss; + std::shared_ptr m_stsz; + std::shared_ptr m_elst; + + std::shared_ptr m_tref; // optional + + std::string m_auxiliary_info_type; // only for auxiliary tracks + + // --- sample auxiliary information + + std::unique_ptr m_aux_helper_tai_timestamps; + std::unique_ptr m_aux_helper_content_ids; + + std::unique_ptr m_aux_reader_tai_timestamps; + std::unique_ptr m_aux_reader_content_ids; + + std::shared_ptr m_first_taic; // the TAIC of the first chunk + + + // --- Helper functions for writing samples. + + // Call when we begin a new chunk of samples, e.g. because the compression format changed + void add_chunk(heif_compression_format format); + + // Call to set the sample_description_box for the last added chunk. + // Has to be called when we call add_chunk(). + // It is not merged with add_chunk() because the sample_description_box may need information from the + // first encoded frame. + void set_sample_description_box(const std::shared_ptr& sample_description_box); + + // Write the actual sample data. `tai` may be null and `gimi_contentID` may be empty. + // In these cases, no timestamp or no contentID will be written, respectively. + Error write_sample_data(const std::vector& raw_data, + uint32_t sample_duration, + int32_t composition_time_offset, + bool is_sync_sample, + const heif_tai_timestamp_packet* tai, + const std::optional& gimi_contentID); +}; + + +#endif //LIBHEIF_TRACK_H diff -Nru libheif-1.19.8/libheif/sequences/track_metadata.cc libheif-1.23.4/libheif/sequences/track_metadata.cc --- libheif-1.19.8/libheif/sequences/track_metadata.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track_metadata.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,189 @@ +/* + * HEIF image base codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "track_metadata.h" +#include "chunk.h" +#include "context.h" +#include "api_structs.h" +#include + + +Track_Metadata::Track_Metadata(HeifContext* ctx) + : Track(ctx) +{ +} + +Error Track_Metadata::load(const std::shared_ptr& trak) +{ + Error parentLoadError = Track::load(trak); + if (parentLoadError) { + return parentLoadError; + } + + const std::vector& chunk_offsets = m_stco->get_offsets(); + + // Metadata tracks are not meant for display + + m_tkhd->set_flags(m_tkhd->get_flags() & ~(Box_tkhd::Flags::Track_in_movie | + Box_tkhd::Flags::Track_in_preview)); + + // Find sequence resolution + + if (!chunk_offsets.empty()) { + auto* s2c = m_stsc->get_chunk(static_cast(1)); + if (!s2c) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Metadata track has no chunk 1" + }; + } + + Box_stsc::SampleToChunk sampleToChunk = *s2c; + + auto sample_description = m_stsd->get_sample_entry(sampleToChunk.sample_description_index - 1); + if (!sample_description) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Metadata track has no sample description" + }; + } + + // TODO: read URI + } + + return {}; +} + + +Track_Metadata::Track_Metadata(HeifContext* ctx, uint32_t track_id, std::string uri, const TrackOptions* options) + : Track(ctx, track_id, options, fourcc("meta")), + m_uri(std::move(uri)) +{ + auto nmhd = std::make_shared(); + m_minf->append_child_box(nmhd); +} + + +#if 0 +Result> Track_Metadata::read_next_metadata_sample() +{ + if (m_current_chunk > m_chunks.size()) { + return Error{heif_error_End_of_sequence, + heif_suberror_Unspecified, + "End of sequence"}; + } + + while (m_next_sample_to_be_decoded > m_chunks[m_current_chunk]->last_sample_number()) { + m_current_chunk++; + + if (m_current_chunk > m_chunks.size()) { + return Error{heif_error_End_of_sequence, + heif_suberror_Unspecified, + "End of sequence"}; + } + } + + const std::shared_ptr& chunk = m_chunks[m_current_chunk]; + + auto decoder = chunk->get_decoder(); + assert(decoder); + + decoder->set_data_extent(chunk->get_data_extent_for_sample(m_next_sample_to_be_decoded)); + + Result> decodingResult = decoder->decode_single_frame_from_compressed_data(options); + if (decodingResult.error) { + m_next_sample_to_be_decoded++; + return decodingResult.error; + } + + auto image = decodingResult.value; + + if (m_stts) { + image->set_sample_duration(m_stts->get_sample_duration(m_next_sample_to_be_decoded)); + } + + // --- read sample auxiliary data + + if (m_aux_reader_content_ids) { + auto readResult = m_aux_reader_content_ids->get_sample_info(get_file().get(), m_next_sample_to_be_decoded); + if (readResult.error) { + return readResult.error; + } + + Result convResult = vector_to_string(readResult.value); + if (convResult.error) { + return convResult.error; + } + + image->set_gimi_content_id(convResult.value); + } + + if (m_aux_reader_tai_timestamps) { + auto readResult = m_aux_reader_tai_timestamps->get_sample_info(get_file().get(), m_next_sample_to_be_decoded); + if (readResult.error) { + return readResult.error; + } + + auto resultTai = Box_itai::decode_tai_from_vector(readResult.value); + if (resultTai.error) { + return resultTai.error; + } + + image->set_tai_timestamp(&resultTai.value); + } + + m_next_sample_to_be_decoded++; + + return image; +} +#endif + + +Error Track_Metadata::write_raw_metadata(const heif_raw_sequence_sample* raw_sample) +{ + // generate new chunk for first metadata packet + + if (m_chunks.empty()) { + + // --- write URIMetaSampleEntry ('urim') + + auto sample_description_box = std::make_shared(); + auto uri = std::make_shared(); + uri->set_uri(m_uri); + sample_description_box->append_child_box(uri); + + add_chunk(heif_compression_undefined); + set_sample_description_box(sample_description_box); + } + + Error err = write_sample_data(raw_sample->data, + raw_sample->duration, + 0, + true, + raw_sample->timestamp, + raw_sample->gimi_sample_content_id); + if (err) { + return err; + } + + return Error::Ok; +} diff -Nru libheif-1.19.8/libheif/sequences/track_metadata.h libheif-1.23.4/libheif/sequences/track_metadata.h --- libheif-1.19.8/libheif/sequences/track_metadata.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track_metadata.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,49 @@ +/* + * HEIF image base codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_TRACK_METADATA_H +#define LIBHEIF_TRACK_METADATA_H + +#include "track.h" +#include +#include +#include + + +class Track_Metadata : public Track { +public: + //Track(HeifContext* ctx); + + Track_Metadata(HeifContext* ctx, uint32_t track_id, std::string uri, const TrackOptions* options); + + Track_Metadata(HeifContext* ctx); + + ~Track_Metadata() override = default; + + // load track from file + Error load(const std::shared_ptr&) override; + + Error write_raw_metadata(const heif_raw_sequence_sample*); + +private: + std::string m_uri; +}; + +#endif //LIBHEIF_TRACK_METADATA_H diff -Nru libheif-1.19.8/libheif/sequences/track_visual.cc libheif-1.23.4/libheif/sequences/track_visual.cc --- libheif-1.19.8/libheif/sequences/track_visual.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track_visual.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,780 @@ +/* + * HEIF image base codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include "track_visual.h" + +#include +#include +#include "codecs/decoder.h" +#include "codecs/encoder.h" +#include "chunk.h" +#include "image/pixelimage.h" +#include "context.h" +#include "api_structs.h" +#include "codecs/hevc_boxes.h" +#include "codecs/uncompressed/unc_boxes.h" + + +Track_Visual::Track_Visual(HeifContext* ctx) + : Track(ctx) +{ +} + + +Track_Visual::~Track_Visual() +{ + for (auto& user_data : m_frame_user_data) { + user_data.second.release(); + } +} + + +Error Track_Visual::load(const std::shared_ptr& trak) +{ + Error parentLoadError = Track::load(trak); + if (parentLoadError) { + return parentLoadError; + } + + const std::vector& chunk_offsets = m_stco->get_offsets(); + + // Find sequence resolution + + if (!chunk_offsets.empty()) { + auto* s2c = m_stsc->get_chunk(1); + if (!s2c) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Visual track has no chunk 1" + }; + } + + Box_stsc::SampleToChunk sampleToChunk = *s2c; + + auto sample_description = m_stsd->get_sample_entry(sampleToChunk.sample_description_index - 1); + if (!sample_description) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Visual track has sample description" + }; + } + + auto visual_sample_description = std::dynamic_pointer_cast(sample_description); + if (!visual_sample_description) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Visual track sample description does not match visual track." + }; + } + + m_width = visual_sample_description->get_VisualSampleEntry_const().width; + m_height = visual_sample_description->get_VisualSampleEntry_const().height; + } + + return {}; +} + + +Error Track_Visual::initialize_after_parsing(HeifContext* ctx, const std::vector >& all_tracks) +{ + // --- check whether there is an auxiliary alpha track assigned to this track + + // Only assign to image-sequence tracks (TODO: are there also alpha tracks allowed for video tracks 'heif_track_type_video'?) + + if (get_handler() == heif_track_type_image_sequence) { + for (const auto& track : all_tracks) { + // skip ourselves + if (track->get_id() != get_id()) { + // Is this an aux alpha track? + auto h = fourcc_to_string(track->get_handler()); + if (track->get_handler() == heif_track_type_auxiliary && + track->get_auxiliary_info_type() == heif_auxiliary_track_info_type_alpha) { + // Is it assigned to the current track + auto tref = track->get_tref_box(); + if (!tref) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Auxiliary track without 'tref'" + }; + } + + auto references = tref->get_references(fourcc("auxl")); + if (std::any_of(references.begin(), references.end(), [this](uint32_t id) { return id == get_id(); })) { + // Assign it + + m_aux_alpha_track = std::dynamic_pointer_cast(track); + } + } + } + } + } + + return {}; +} + + +Track_Visual::Track_Visual(HeifContext* ctx, uint32_t track_id, uint16_t width, uint16_t height, + const TrackOptions* options, uint32_t handler_type) + : Track(ctx, track_id, options, handler_type) +{ + m_tkhd->set_resolution(width, height); + //m_hdlr->set_handler_type(handler_type); already done in Track() + + auto vmhd = std::make_shared(); + m_minf->append_child_box(vmhd); +} + + +bool Track_Visual::has_alpha_channel() const +{ + if (m_aux_alpha_track != nullptr) { + return true; + } + + // --- special case: 'uncv' with alpha component +#if WITH_UNCOMPRESSED_CODEC + if (m_stsd) { + auto sampleEntry = m_stsd->get_sample_entry(0); + if (sampleEntry) { + if (auto box_uncv = std::dynamic_pointer_cast(sampleEntry)) { + if (auto cmpd = box_uncv->get_child_box()) { + if (cmpd->has_component(heif_cmpd_component_type_alpha)) { + return true; + } + } + } + } + } +#endif + + return false; +} + + +Result > Track_Visual::decode_next_image_sample(const heif_decoding_options& options) +{ + // --- If we ignore the editlist, we stop when we reached the end of the original samples. + + uint64_t num_output_samples = m_num_output_samples; + if (options.ignore_sequence_editlist) { + num_output_samples = m_num_samples; + } + + // --- Did we reach the end of the sequence? + + if (m_next_sample_to_be_output >= num_output_samples) { + return Error{ + heif_error_End_of_sequence, + heif_suberror_Unspecified, + "End of sequence" + }; + } + + + std::shared_ptr image; + + uint32_t sample_idx_in_chunk; + uintptr_t decoded_sample_idx = 0; // TODO: map this to sample idx + chunk + + for (;;) { + const SampleTiming& sampleTiming = m_presentation_timeline[m_next_sample_to_be_decoded % m_presentation_timeline.size()]; + sample_idx_in_chunk = sampleTiming.sampleIdx; + uint32_t chunk_idx = sampleTiming.chunkIdx; + + const std::shared_ptr& chunk = m_chunks[chunk_idx]; + + auto decoder = chunk->get_decoder(); + assert(decoder); + + // avoid calling get_decoded_frame() before starting the decoder. + if (m_next_sample_to_be_decoded != 0) { + Result > getFrameResult = decoder->get_decoded_frame(options, + &decoded_sample_idx, + m_heif_context->get_security_limits()); + if (getFrameResult.error()) { + return getFrameResult.error(); + } + + + // We received a decoded frame. Exit "push data" / "decode" loop. + + if (*getFrameResult != nullptr) { + image = *getFrameResult; + + // If this was the last frame in the EditList segment, reset the 'flushed' flag + // in case we have to restart the decoder for another repetition of the segment. + if ((m_next_sample_to_be_output + 1) % m_presentation_timeline.size() == 0) { + m_decoder_is_flushed = false; + } + + break; + } + + // If the sequence has ended and the decoder was flushed, but we still did not receive + // the image, we are waiting for, this is an error. + if (m_decoder_is_flushed) { + return Error(heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "Did not decode all frames"); + } + } + + + // --- Push more data into the decoder (or send end-of-sequence). + + if (m_next_sample_to_be_decoded < m_num_output_samples) { + + // --- Find the data extent that stores the compressed frame data. + + DataExtent extent = chunk->get_data_extent_for_sample(sample_idx_in_chunk); + decoder->set_data_extent(std::move(extent)); + + // std::cout << "PUSH chunk " << chunk_idx << " sample " << sample_idx << " (" << extent.m_size << " bytes)\n"; + + // advance decoding index to next in segment + m_next_sample_to_be_decoded++; + + // Send the decoder configuration when we send the first sample of the chunk. + // The configuration NALs might change for each chunk. + const bool is_first_sample = (sample_idx_in_chunk == 0); + + // --- Push data into the decoder. + Error decodingError = decoder->decode_sequence_frame_from_compressed_data(is_first_sample, + options, + sample_idx_in_chunk, // user data + m_heif_context->get_security_limits()); + if (decodingError) { + return decodingError; + } + } + else { + // --- End of sequence (Editlist segment) reached. + + // std::cout << "FLUSH\n"; + Error flushError = decoder->flush_decoder(); + if (flushError) { + return flushError; + } + + m_decoder_is_flushed = true; + } + } + + + // --- We have received a new decoded image. + // Postprocess decoded image, attach metadata. + + if (m_stts) { + image->set_sample_duration(m_stts->get_sample_duration(sample_idx_in_chunk)); + } + + // --- assign alpha if we have an assigned alpha track + + if (m_aux_alpha_track) { + auto alphaResult = m_aux_alpha_track->decode_next_image_sample(options); + if (!alphaResult) { + return alphaResult.error(); + } + + auto alphaImage = *alphaResult; + + // The alpha auxiliary track may have a different size than the main track. Nothing in the + // standard forbids this. We scale it to the main image size so that downstream consumers can + // rely on the alpha plane matching the color planes. This mirrors what the still-image decoder + // does in ImageItem (see image_item.cc). As noted there, we may later add a decoding option to + // turn this automatic scaling off, but that requires that libheif no longer assumes everywhere + // that the alpha channel has the same resolution as the color channels. + if (alphaImage->get_width() != image->get_width() || + alphaImage->get_height() != image->get_height()) { + std::shared_ptr scaled_alpha; + Error err = alphaImage->scale_nearest_neighbor(scaled_alpha, image->get_width(), image->get_height(), + m_heif_context->get_security_limits()); + if (err) { + return err; + } + alphaImage = std::move(scaled_alpha); + } + + if (Error err = image->transfer_channel_from_image_as(alphaImage, heif_channel_Y, heif_channel_Alpha)) { + return err; + } + } + + + // --- read sample auxiliary data + + if (m_aux_reader_content_ids) { + auto readResult = m_aux_reader_content_ids->get_sample_info(get_file().get(), (uint32_t)decoded_sample_idx); + if (!readResult) { + return readResult.error(); + } + + Result convResult = vector_to_string(*readResult); + if (!convResult) { + return convResult.error(); + } + + image->set_gimi_sample_content_id(*convResult); + } + + if (m_aux_reader_tai_timestamps) { + auto readResult = m_aux_reader_tai_timestamps->get_sample_info(get_file().get(), (uint32_t)decoded_sample_idx); + if (!readResult) { + return readResult.error(); + } + + std::vector& tai_data = *readResult; + if (!tai_data.empty()) { + auto resultTai = Box_itai::decode_tai_from_vector(tai_data); + if (!resultTai) { + return resultTai.error(); + } + + image->set_tai_timestamp(&*resultTai); + } + } + + m_next_sample_to_be_output++; + + return image; +} + + +Error Track_Visual::encode_end_of_sequence(heif_encoder* h_encoder) +{ + auto encoder = m_chunks.back()->get_encoder(); + + for (;;) { + Error err = encoder->encode_sequence_flush(h_encoder); + if (err) { + return err; + } + + Result processingResult = process_encoded_data(h_encoder); + if (processingResult.is_error()) { + return processingResult.error(); + } + + if (!*processingResult) { + break; + } + } + + + // --- also end alpha track + + if (m_aux_alpha_track) { + auto err = m_aux_alpha_track->encode_end_of_sequence(m_alpha_track_encoder.get()); + if (err) { + return err; + } + } + + return {}; +} + + +Error Track_Visual::encode_image(const std::shared_ptr& image, + heif_encoder* h_encoder, + const heif_sequence_encoding_options* in_options, + heif_image_input_class input_class) +{ + if (image->get_width() > 0xFFFF || + image->get_height() > 0xFFFF) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Input image resolution too high" + }; + } + + m_image_class = input_class; + + + // --- resolve the encoding options + + // Build an effective options struct so that the rest of this function never + // has to look at the caller's pointer. 'in_options' may be NULL, which the API + // documents as "use the default options", so every field has to come from a + // fully populated struct instead. This also lets libheif resolve "auto" fields + // (e.g. content_kind) to concrete values before passing them to the encoder + // plugin. heif_sequence_encoding_options_copy is version-aware and ignores a + // NULL source, so callers built against older headers won't be read past their + // actual allocation and a NULL source simply leaves the defaults in place. + + std::unique_ptr + effective_options(heif_sequence_encoding_options_alloc(), + heif_sequence_encoding_options_release); + heif_sequence_encoding_options_copy(effective_options.get(), in_options); + + // Resolve content_kind=auto based on this track's handler type. For auxiliary + // tracks (e.g. alpha) the parent track resolves the value before recursing in, + // so we never reach this branch with handler=auxv. + // TODO: in the future, "auto" could also factor in input frame rate or + // frame-to-frame similarity. + if (effective_options->content_kind == heif_sequence_content_kind_auto) { + switch (get_handler()) { + case heif_track_type_video: + effective_options->content_kind = heif_sequence_content_kind_video; + break; + case heif_track_type_image_sequence: + default: + effective_options->content_kind = heif_sequence_content_kind_image_sequence; + break; + } + } + + + // --- If input has an alpha channel, add an alpha auxiliary track. + + if (effective_options->save_alpha_channel && image->has_alpha() && !m_aux_alpha_track && + h_encoder->plugin->compression_format != heif_compression_uncompressed) { // TODO: ask plugin + if (m_active_encoder) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "Input images must all either have an alpha channel or none of them." + }; + } + else { + // alpha track uses default options, same timescale as color track + + TrackOptions alphaOptions; + alphaOptions.track_timescale = m_track_info.track_timescale; + + auto newAlphaTrackResult = m_heif_context->add_visual_sequence_track(&alphaOptions, + heif_track_type_auxiliary, + static_cast(image->get_width()), + static_cast(image->get_height())); + + if (auto err = newAlphaTrackResult.error()) { + return err; + } + + // add a reference to the color track + + m_aux_alpha_track = *newAlphaTrackResult; + m_aux_alpha_track->add_reference_to_track(fourcc("auxl"), m_id); + + // make a copy of the encoder from the color track for encoding the alpha track + + m_alpha_track_encoder = std::make_unique(h_encoder->plugin); + heif_error err = m_alpha_track_encoder->alloc(); + if (err.code) { + return {err.code, err.subcode, err.message}; + } + m_alpha_track_encoder->copy_parameters_from(*h_encoder); + } + } + + + if (!m_active_encoder) { + m_active_encoder = h_encoder; + } + else if (m_active_encoder != h_encoder) { + return { + heif_error_Usage_error, + heif_suberror_Unspecified, + "You may not switch the heif_encoder while encoding a sequence." + }; + } + + if (h_encoder->plugin->plugin_api_version < 4) { + return Error{ + heif_error_Plugin_loading_error, heif_suberror_No_matching_decoder_installed, + "Encoder plugin needs to be at least version 4." + }; + } + + // === generate compressed image bitstream + + // generate new chunk for first image or when compression formats don't match + + if (m_chunks.empty() || m_chunks.back()->get_compression_format() != h_encoder->plugin->compression_format) { + add_chunk(h_encoder->plugin->compression_format); + } + + // --- check whether we have to convert the image color space + + // The reason for doing the color conversion here is that the input might be an RGBA image and the color conversion + // will extract the alpha plane anyway. We can reuse that plane below instead of having to do a new conversion. + + auto encoder = m_chunks.back()->get_encoder(); + + const heif_color_profile_nclx* output_nclx; + heif_color_profile_nclx nclx; + + if (const auto* image_nclx = encoder->get_forced_output_nclx()) { + output_nclx = const_cast(image_nclx); + } + else if (in_options) { + output_nclx = in_options->output_nclx_profile; + } + else { + // Note: this is the one place that still distinguishes "caller passed no + // options" from "caller passed options with a NULL output_nclx_profile", + // so it reads in_options rather than effective_options. Both cases mean + // "keep the input image's parameters", but they express it differently: a + // NULL profile lets compute_target_nclx_profile() derive them, while the + // branch below builds the profile here. The two differ only for an image + // that carries no NCLX at all, where the explicit undefined profile built + // below makes nclx_profile_matches_spec() request a conversion that a NULL + // profile would skip. Unifying them would change that behaviour, so it is + // left alone here. + if (image->has_nclx_color_profile()) { + nclx_profile input_nclx = image->get_color_profile_nclx(); + + nclx.version = 1; + nclx.color_primaries = (enum heif_color_primaries) input_nclx.get_colour_primaries(); + nclx.transfer_characteristics = (enum heif_transfer_characteristics) input_nclx.get_transfer_characteristics(); + nclx.matrix_coefficients = (enum heif_matrix_coefficients) input_nclx.get_matrix_coefficients(); + nclx.full_range_flag = input_nclx.get_full_range_flag(); + + output_nclx = &nclx; + } + else { + nclx_profile undefined_nclx; + undefined_nclx.copy_to_heif_color_profile_nclx(&nclx); + + output_nclx = &nclx; + } + } + + Result > srcImageResult = encoder->convert_colorspace_for_encoding(image, + h_encoder, + output_nclx, + &effective_options->color_conversion_options, + m_heif_context->get_security_limits()); + if (!srcImageResult) { + return srcImageResult.error(); + } + + std::shared_ptr colorConvertedImage = *srcImageResult; + + // integer range is checked at beginning of function. + assert(colorConvertedImage->get_width() == image->get_width()); + assert(colorConvertedImage->get_height() == image->get_height()); + m_width = static_cast(colorConvertedImage->get_width()); + m_height = static_cast(colorConvertedImage->get_height()); + + // --- encode image + + Error encodeError = encoder->encode_sequence_frame(colorConvertedImage, h_encoder, + *effective_options, + input_class, + colorConvertedImage->get_sample_duration(), get_timescale(), + m_current_frame_nr); + + if (encodeError) { + return encodeError; + } + + m_sample_duration = colorConvertedImage->get_sample_duration(); + // TODO heif_tai_timestamp_packet* tai = image->get_tai_timestamp(); + // TODO image->has_gimi_sample_content_id() ? image->get_gimi_sample_content_id() : std::string{}); + + + // store frame user data + + FrameUserData userData; + userData.sample_duration = colorConvertedImage->get_sample_duration(); + if (image->has_gimi_sample_content_id()) { + userData.gimi_content_id = image->get_gimi_sample_content_id(); + } + + if (const auto* tai = image->get_tai_timestamp()) { + userData.tai_timestamp = heif_tai_timestamp_packet_alloc(); + heif_tai_timestamp_packet_copy(userData.tai_timestamp, tai); + } + + m_frame_user_data[m_current_frame_nr] = userData; + + m_current_frame_nr++; + + // --- get compressed data from encoder + + Result processingResult = process_encoded_data(h_encoder); + if (auto err = processingResult.error()) { + return err; + } + + + // --- encode alpha channel into auxiliary track + + if (m_aux_alpha_track) { + auto alphaImageResult = create_alpha_image_from_image_alpha_channel(colorConvertedImage, + m_heif_context->get_security_limits()); + if (auto err = alphaImageResult.error()) { + return err; + } + + (*alphaImageResult)->set_sample_duration(colorConvertedImage->get_sample_duration()); + + // Pass the resolved options (not the caller's in_options) so the alpha aux + // track inherits the color track's content_kind instead of re-resolving + // from its own 'auxv' handler. + auto err = m_aux_alpha_track->encode_image(*alphaImageResult, + m_alpha_track_encoder.get(), + effective_options.get(), + heif_image_input_class_alpha); + if (err) { + return err; + } + } + + return {}; +} + + +Result Track_Visual::process_encoded_data(heif_encoder* h_encoder) +{ + auto encoder = m_chunks.back()->get_encoder(); + + std::optional encodingResult = encoder->encode_sequence_get_data(); + if (!encodingResult) { + return {}; + } + + const Encoder::CodedImageData& data = *encodingResult; + + + if (data.bitstream.empty() && + data.properties.empty()) { + return {false}; + } + + // --- generate SampleDescriptionBox + + if (!m_generated_sample_description_box) { + auto sample_description_box = encoder->get_sample_description_box(data); + if (sample_description_box) { + VisualSampleEntry& visualSampleEntry = sample_description_box->get_VisualSampleEntry(); + visualSampleEntry.width = m_width; + visualSampleEntry.height = m_height; + + // add Coding-Constraints box (ccst) only if we are generating an image sequence + + // TODO: does the alpha track also need a ccst box? + // ComplianceWarden says so (and it makes sense), but HEIF says that 'ccst' shall be present + // if the handler is 'pict'. However, the alpha track is 'auxv'. + if (true) { // m_hdlr->get_handler_type() == heif_track_type_image_sequence) { + auto ccst = std::make_shared(); + ccst->set_coding_constraints(data.codingConstraints); + sample_description_box->append_child_box(ccst); + } + + if (m_image_class == heif_image_input_class_alpha) { + auto auxi_box = std::make_shared(); + auxi_box->set_aux_track_type_urn(get_track_auxiliary_info_type(h_encoder->plugin->compression_format)); + sample_description_box->append_child_box(auxi_box); + } + + set_sample_description_box(sample_description_box); + m_generated_sample_description_box = true; + } + } + + if (!data.bitstream.empty()) { + uintptr_t frame_number = data.frame_nr; + + auto& user_data = m_frame_user_data[frame_number]; + + int32_t decoding_time = static_cast(m_stsz->num_samples()) * m_sample_duration; + int32_t composition_time = static_cast(frame_number) * m_sample_duration; + + Error err = write_sample_data(data.bitstream, + user_data.sample_duration, + composition_time - decoding_time, + data.is_sync_frame, + user_data.tai_timestamp, + user_data.gimi_content_id); + + user_data.release(); + m_frame_user_data.erase(frame_number); + + if (err) { + return err; + } + } + + return {true}; +} + + +Error Track_Visual::finalize_track() +{ + if (m_active_encoder) { + Error err = encode_end_of_sequence(m_active_encoder); + if (err) { + return err; + } + } + + return Track::finalize_track(); +} + + +heif_brand2 Track_Visual::get_compatible_brand() const +{ + if (m_stsd->get_num_sample_entries() == 0) { + return 0; // TODO: error ? Or can we assume at this point that there is at least one sample entry? + } + + auto sampleEntry = m_stsd->get_sample_entry(0); + + uint32_t sample_entry_type = sampleEntry->get_short_type(); + switch (sample_entry_type) { + case fourcc("hvc1"): { + auto hvcC = sampleEntry->get_child_box(); + if (!hvcC) { return 0; } + + const auto& config = hvcC->get_configuration(); + if (config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_Main) || + config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_MainStillPicture)) { + return heif_brand2_hevc; + } + else { + return heif_brand2_hevx; + } + } + + case fourcc("avc1"): + return heif_brand2_avcs; + + case fourcc("av01"): + return heif_brand2_avis; + + case fourcc("j2ki"): + return heif_brand2_j2is; + + case fourcc("mjpg"): + return heif_brand2_jpgs; + + case fourcc("vvc1"): + return heif_brand2_vvis; + + default: + return 0; + } +} diff -Nru libheif-1.19.8/libheif/sequences/track_visual.h libheif-1.23.4/libheif/sequences/track_visual.h --- libheif-1.19.8/libheif/sequences/track_visual.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/sequences/track_visual.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,104 @@ +/* + * HEIF image base codec. + * Copyright (c) 2025 Dirk Farin + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_TRACK_VISUAL_H +#define LIBHEIF_TRACK_VISUAL_H + +#include "track.h" +#include +#include +#include +#include + + +class Track_Visual : public Track { +public: + //Track(HeifContext* ctx); + + Track_Visual(HeifContext* ctx, uint32_t track_id, uint16_t width, uint16_t height, + const TrackOptions* options, uint32_t handler_type); + + Track_Visual(HeifContext* ctx); + + ~Track_Visual() override; + + // load track from file + Error load(const std::shared_ptr&) override; + + [[nodiscard]] Error initialize_after_parsing(HeifContext* ctx, const std::vector>& all_tracks) override; + + uint16_t get_width() const { return m_width; } + + uint16_t get_height() const { return m_height; } + + bool has_alpha_channel() const override; + + Result> decode_next_image_sample(const heif_decoding_options& options); + + Error encode_image(const std::shared_ptr& image, + heif_encoder* encoder, + const heif_sequence_encoding_options* options, + heif_image_input_class image_class); + + Error encode_end_of_sequence(heif_encoder* encoder); + + Error finalize_track() override; + + heif_brand2 get_compatible_brand() const; + +private: + uint16_t m_width = 0; + uint16_t m_height = 0; + heif_image_input_class m_image_class; + + uintptr_t m_current_frame_nr = 0; + bool m_generated_sample_description_box = false; + + struct FrameUserData + { + int sample_duration = 0; + + std::optional gimi_content_id; + heif_tai_timestamp_packet* tai_timestamp = nullptr; + + void release() + { + heif_tai_timestamp_packet_release(tai_timestamp); + tai_timestamp = nullptr; + } + }; + + // map frame number to user data + std::map m_frame_user_data; + + int m_sample_duration = 0; // TODO: pass this through encoder or handle it correctly with frame reordering + + // If there is an alpha-channel track associated with this color track, we reference it from here + std::shared_ptr m_aux_alpha_track; + + heif_encoder* m_active_encoder = nullptr; + std::unique_ptr m_alpha_track_encoder; + + Result process_encoded_data(heif_encoder* encoder); +}; + + + +#endif //LIBHEIF_TRACK_VISUAL_H diff -Nru libheif-1.19.8/libheif/text.cc libheif-1.23.4/libheif/text.cc --- libheif-1.19.8/libheif/text.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/text.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,39 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * Copyright (c) 2025 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#include +#include "text.h" +#include "error.h" +#include + + +Result> TextItem::encode() const +{ + std::vector data; + data.assign(m_text.begin(), m_text.end()); + return data; +} + +Error TextItem::parse(const std::vector &data) +{ + m_text.assign(reinterpret_cast(data.data()), data.size()); + return Error::Ok; +} \ No newline at end of file diff -Nru libheif-1.19.8/libheif/text.h libheif-1.23.4/libheif/text.h --- libheif-1.19.8/libheif/text.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/libheif/text.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,57 @@ +/* + * HEIF codec. + * Copyright (c) 2023 Dirk Farin + * Copyright (c) 2025 Brad Hards + * + * This file is part of libheif. + * + * libheif is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * libheif is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libheif. If not, see . + */ + +#ifndef LIBHEIF_TEXT_H +#define LIBHEIF_TEXT_H + +#include +#include +#include +#include "error.h" +#include "libheif/heif_text.h" + +class TextItem +{ +public: + TextItem() = default; + + TextItem(heif_item_id itemId, const char *text) : m_item_id(itemId), m_text(text) {} + + Error parse(const std::vector &data); + + Result> encode() const; + + heif_item_id get_item_id() const { + return m_item_id; + } + + void set_item_id(heif_item_id id) { + m_item_id = id; + } + + std::string get_item_text() const { return m_text; } + +private: + heif_item_id m_item_id = 0; + std::string m_text; +}; + +#endif // LIBHEIF_TEXT_H diff -Nru libheif-1.19.8/post.js libheif-1.23.4/post.js --- libheif-1.19.8/post.js 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/post.js 2026-09-06 14:45:17.000000000 +0000 @@ -50,7 +50,15 @@ }; HeifImage.prototype.is_primary = function() { - return !!heif_image_handle_is_primary_image(this.handle); + return !!Module.heif_image_handle_is_primary_image(this.handle); +} + +HeifImage.prototype.has_alpha_channel = function() { + return !!Module.heif_image_handle_has_alpha_channel(this.handle); +} + +HeifImage.prototype.is_premultiplied_alpha = function() { + return !!Module.heif_image_handle_is_premultiplied_alpha(this.handle); } HeifImage.prototype.display = function(image_data, callback) { @@ -58,12 +66,12 @@ var w = this.get_width(); var h = this.get_height(); - setTimeout(function() { + setTimeout(async function() { // If image hasn't been loaded yet, decode the image if (!this.img) { - var img = Module.heif_js_decode_image2(this.handle, + var img = await Module.heif_js_decode_image2(this.handle, Module.heif_colorspace.heif_colorspace_RGB, Module.heif_chroma.heif_chroma_interleaved_RGBA); if (!img || img.code) { console.log("Decoding image failed", this.handle, img); diff -Nru libheif-1.19.8/scripts/build-oss-fuzz.sh libheif-1.23.4/scripts/build-oss-fuzz.sh --- libheif-1.19.8/scripts/build-oss-fuzz.sh 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/scripts/build-oss-fuzz.sh 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,374 @@ +#!/bin/bash -e +# +# HEIF codec. +# Copyright (c) 2026 struktur AG, Joachim Bauch +# +# This file is part of libheif. +# +# libheif is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libheif is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with libheif. If not, see . +# + +if [ -z "$SRC" ]; then + echo "Environment variable SRC is not defined." + exit 1 +elif [ -z "$OUT" ]; then + echo "Environment variable OUT is not defined." + exit 1 +elif [ -z "$WORK" ]; then + echo "Environment variable WORK is not defined." + exit 1 +fi + +# Install build dependencies. + +apt-get update + +apt-get install -y \ + autoconf \ + automake \ + build-essential \ + cmake \ + libbrotli-dev \ + libtool \ + make \ + mercurial \ + nasm \ + ninja-build \ + pkg-config \ + python3-pip \ + yasm \ + zlib1g-dev + +# The meson packaged for Ubuntu 20.04 (OSS-Fuzz base image) is too old for +# recent dav1d (requires >= 0.54.0), so install a current version via pip. +pip3 install --upgrade meson + +# Install and build codec dependencies. + +git clone \ + --depth 1 \ + --branch main \ + https://github.com/libjpeg-turbo/libjpeg-turbo.git \ + "$WORK/libjpeg-turbo" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/strukturag/libde265.git \ + "$WORK/libde265" + +git clone \ + --depth 1 \ + https://bitbucket.org/multicoreware/x265_git/src/stable/ \ + "$WORK/x265" + +git clone \ + --depth 1 \ + --branch master \ + https://aomedia.googlesource.com/aom \ + "$WORK/aom" + +git clone \ + --depth 1 \ + --branch master \ + https://code.videolan.org/videolan/dav1d.git \ + "$WORK/dav1d" + +git clone \ + --depth 1 \ + --single-branch \ + https://chromium.googlesource.com/webm/libwebp \ + "$WORK/libwebp" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/fraunhoferhhi/vvdec.git \ + "$WORK/vvdec" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/fraunhoferhhi/vvenc.git \ + "$WORK/vvenc" + +git clone \ + --depth 1 \ + --branch master \ + https://code.videolan.org/videolan/x264.git \ + "$WORK/x264" + +git clone \ + --depth 1 \ + --branch master \ + https://gitlab.com/AOMediaCodec/SVT-AV1.git \ + "$WORK/svt-av1" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/cisco/openh264.git \ + "$WORK/openh264" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/uclouvain/openjpeg.git \ + "$WORK/openjpeg" + +git clone \ + --depth 1 \ + --branch master \ + https://github.com/aous72/OpenJPH.git \ + "$WORK/openjph" + +export DEPS_PATH="$SRC/deps" +mkdir -p "$DEPS_PATH" + + +echo "" +echo "=== Building libjpeg-turbo ===" +mkdir -p "$WORK/libjpeg-turbo/build" +cd "$WORK/libjpeg-turbo/build" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DENABLE_SHARED=OFF \ + -DENABLE_STATIC=ON \ + -DWITH_TURBOJPEG=OFF \ + -DWITH_SIMD=OFF \ + .. +make -j"$(nproc)" +make install + +echo "" +echo "=== Building x265 ===" +if [ -d "$WORK/x265/.git" ]; then + mv "$WORK/x265/.git" "$WORK/x265/.git-unused" +fi +cd "$WORK/x265/build/linux" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DENABLE_SHARED:bool=off \ + -DENABLE_ASSEMBLY:bool=off \ + -DENABLE_CLI:bool=off \ + -DX265_LATEST_TAG=TRUE \ + ../../source +make clean +make -j"$(nproc)" x265-static +make install +if [ -d "$WORK/x265/.git-unused" ]; then + mv "$WORK/x265/.git-unused" "$WORK/x265/.git" +fi + +echo "" +echo "=== Building libde265 ===" +cd "$WORK/libde265" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_SHARED_LIBS:bool=off \ + -DENABLE_DECODER:bool=off \ + -DENABLE_ENCODER:bool=off \ + -DENABLE_SDL:bool=off \ + -DENABLE_SHERLOCK265:bool=off \ + . +make clean +make -j"$(nproc)" +make install + +echo "" +echo "=== Building aom ===" +mkdir -p "$WORK/aom/build/linux" +cd "$WORK/aom/build/linux" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DENABLE_SHARED:bool=off -DCONFIG_PIC=1 \ + -DENABLE_APPS:bool=off -DENABLE_EXAMPLES:bool=off -DENABLE_DOCS:bool=off \ + -DENABLE_TESTS:bool=off -DENABLE_TESTDATA:bool=off -DENABLE_TOOLS:bool=off \ + -DCONFIG_SIZE_LIMIT=1 \ + -DDECODE_HEIGHT_LIMIT=12288 -DDECODE_WIDTH_LIMIT=12288 \ + -DDO_RANGE_CHECK_CLAMP=1 \ + -DAOM_MAX_ALLOCABLE_MEMORY=536870912 \ + -DAOM_TARGET_CPU=generic \ + ../../ +make clean +make -j"$(nproc)" +make install + +echo "" +echo "=== Building dav1d ===" +cd "$WORK/dav1d" +meson build \ + --default-library=static \ + --buildtype release \ + --prefix "$DEPS_PATH" \ + -D enable_tools=false \ + -D enable_tests=false \ + -D enable_asm=false +ninja -C build +ninja -C build install + +echo "" +echo "=== Building libwebp (for sharpyuv) ===" +mkdir -p "$WORK/libwebp/build" +cd "$WORK/libwebp/build" +cmake -G Ninja \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + .. +ninja sharpyuv +ninja install + +echo "" +echo "=== Building vvdec ===" +cd "$WORK/vvdec" +cmake -B build/release-static \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS -fPIC" -DCMAKE_CXX_FLAGS="$CXXFLAGS -fPIC" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_SHARED_LIBS=FALSE \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL=TRUE \ + -DVVDEC_ENABLE_WERROR=OFF \ + -DVVDEC_LIBRARY_ONLY=ON \ + -DVVDEC_ENABLE_X86_SIMD=OFF \ + . +cmake --build build/release-static -j"$(nproc)" +cmake --build build/release-static --target install + +echo "" +echo "=== Building vvenc ===" +cd "$WORK/vvenc" +cmake -B build/release-static \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS -fPIC" -DCMAKE_CXX_FLAGS="$CXXFLAGS -fPIC" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_SHARED_LIBS=FALSE \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL=TRUE \ + -DVVENC_ENABLE_WERROR=OFF \ + -DVVENC_LIBRARY_ONLY=ON \ + -DVVENC_ENABLE_X86_SIMD=OFF \ + . +cmake --build build/release-static -j"$(nproc)" +cmake --build build/release-static --target install + +echo "" +echo "=== Building x264 ===" +cd "$WORK/x264" +./configure \ + --prefix="$DEPS_PATH" \ + --enable-static \ + --disable-shared \ + --disable-asm \ + --disable-cli +make -j"$(nproc)" +make install + +echo "" +echo "=== Building SVT-AV1 ===" +cd "$WORK/svt-av1/Build/linux" +./build.sh \ + release \ + static \ + no-apps \ + disable-lto \ + c-only \ + prefix="$DEPS_PATH" \ + install + +echo "" +echo "=== Building openh264 ===" +cd "$WORK/openh264" +make -j"$(nproc)" BUILDTYPE=Debug ENABLEASM=No libopenh264.a +make -j"$(nproc)" BUILDTYPE=Debug ENABLEASM=No PREFIX="$DEPS_PATH" install-static + +echo "" +echo "=== Building openjpeg ===" +cd "$WORK/openjpeg" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_CODEC=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + . +make -j"$(nproc)" +make install + +echo "" +echo "=== Building OpenJPH ===" +cd "$WORK/openjph" +cmake -G "Unix Makefiles" \ + -DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \ + -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DOJPH_ENABLE_TIFF_SUPPORT=OFF \ + -DOJPH_BUILD_EXECUTABLES=OFF \ + . +make -j"$(nproc)" +make install + +# Remove shared libraries to avoid accidental linking against them. +rm -f "$DEPS_PATH/lib"/*.so +rm -f "$DEPS_PATH/lib/"*.so.* +rm -f /usr/lib/*/libjpeg.so +rm -f /usr/lib/*/libjpeg.so.* + +echo "" +echo "=== Building libheif ===" +cd "$SRC/libheif" +mkdir build +cd build +PKG_CONFIG="pkg-config --static" PKG_CONFIG_PATH="$DEPS_PATH/lib/pkgconfig:$DEPS_PATH/lib/x86_64-linux-gnu/pkgconfig" cmake --preset=fuzzing \ + -DFUZZING_COMPILE_OPTIONS="" \ + -DFUZZING_LINKER_OPTIONS="$LIB_FUZZING_ENGINE" \ + -DFUZZING_C_COMPILER="$CC" -DFUZZING_CXX_COMPILER="$CXX" \ + -DCMAKE_INSTALL_PREFIX="$DEPS_PATH" \ + -DWITH_UNCOMPRESSED_CODEC=ON \ + -DWITH_JPEG_DECODER=ON \ + -DWITH_JPEG_ENCODER=ON \ + -DWITH_DAV1D=ON \ + -DWITH_LIBSHARPYUV=ON \ + -DWITH_VVDEC=ON \ + -DWITH_VVENC=ON \ + -DWITH_X264=ON \ + -DWITH_SvtEnc=ON \ + -DWITH_OpenH264_DECODER=ON \ + -DWITH_OpenJPEG_ENCODER=ON \ + -DWITH_OpenJPEG_DECODER=ON \ + -DWITH_OPENJPH_ENCODER=ON \ + .. + +make -j"$(nproc)" + +cp fuzzing/*_fuzzer "$OUT" +cp ../fuzzing/data/dictionary.txt "$OUT/box-fuzzer.dict" +cp ../fuzzing/data/dictionary.txt "$OUT/file-fuzzer.dict" + +zip -r "$OUT/file-fuzzer_seed_corpus.zip" ../fuzzing/data/corpus/*.heic +zip -j -r "$OUT/sequence_fuzzer_seed_corpus.zip" ../fuzzing/data/sequence_corpus/* diff -Nru libheif-1.19.8/scripts/check-api-symbols.sh libheif-1.23.4/scripts/check-api-symbols.sh --- libheif-1.19.8/scripts/check-api-symbols.sh 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/scripts/check-api-symbols.sh 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,128 @@ +#!/bin/bash +set -e +# +# HEIF codec. +# Copyright (c) 2026 Dirk Farin +# +# This file is part of libheif. +# +# libheif is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libheif is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with libheif. If not, see . +# +# Verify that every function declared in the public C API headers is actually +# defined (and exported) in the built libheif shared library. +# +# A public declaration whose definition has a mismatched name -- or no +# definition at all -- compiles cleanly and even links into libheif itself, +# because libheif never calls its own public API. Neither the C++ build nor the +# C-header check (scripts/check-c-headers.sh, which only compiles) notices. +# The breakage surfaces only downstream, as an "undefined symbol" link error in +# any program that calls the declared function. +# +# This is exactly what happened in https://github.com/strukturag/libheif/issues/1822: +# heif_properties.h declared heif_image_get_bayer_pattern_size() while the +# definition in heif_properties.cc was named heif_image_has_bayer_pattern(). +# The library built and shipped fine; Rust consumers got a link error. +# +# This script extracts every LIBHEIF_API-marked declaration from the public +# headers and checks each name against the defined, exported symbols of a built +# libheif.so (read with nm). Any declared-but-undefined symbol is reported and +# the script exits non-zero. +# +# Usage: +# check-api-symbols.sh [path/to/libheif.so] +# If no library is given, a minimal libheif (all codecs off, no plugins, no +# examples) is built into a temporary directory. That build needs only cmake +# and a C++ compiler -- no external codec libraries. +# +# heif_experimental.h is excluded: its declarations are compiled only when +# ENABLE_EXPERIMENTAL_FEATURES is on, so they are legitimately absent from a +# default build. heif_cxx.h / heif_emscripten.h are C++-only and declare no +# plain-C ABI symbols; heif_version.h is generated and declares none either. + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd)" +API_DIR="$ROOT/libheif/api/libheif" + +# Headers whose LIBHEIF_API declarations are not expected in a default build, +# or that declare no plain-C ABI symbols. See the note above. +EXCLUDE="heif_experimental.h heif_cxx.h heif_emscripten.h heif_version.h" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# --- Locate (or build) the libheif shared library --------------------------- + +LIB="${1:-}" +if [ -z "$LIB" ]; then + echo "No library given; building a minimal libheif (all codecs off) ..." + cmake -S "$ROOT" -B "$TMP/build" \ + -DBUILD_SHARED_LIBS=ON \ + -DWITH_LIBDE265=OFF -DWITH_X265=OFF \ + -DWITH_AOM_DECODER=OFF -DWITH_AOM_ENCODER=OFF \ + -DWITH_X264=OFF -DWITH_OpenH264_DECODER=OFF -DWITH_DAV1D=OFF \ + -DWITH_EXAMPLES=OFF -DWITH_GDK_PIXBUF=OFF \ + -DBUILD_TESTING=OFF -DENABLE_PLUGIN_LOADING=OFF \ + >"$TMP/cmake.log" 2>&1 \ + && cmake --build "$TMP/build" -j"$(nproc)" >>"$TMP/cmake.log" 2>&1 \ + || { echo "ERROR: minimal libheif build failed:" >&2; cat "$TMP/cmake.log" >&2; exit 1; } + LIB="$TMP/build/libheif/libheif.so" +fi + +if [ ! -e "$LIB" ]; then + echo "ERROR: libheif shared library not found: $LIB" >&2 + exit 1 +fi + +# --- Collect declared API symbols and defined library symbols --------------- + +# Declared symbols: every identifier introduced by a LIBHEIF_API declaration. +# For a function this is the identifier just before the argument list '('; +# for an exported data object (e.g. heif_error_success) it is the last +# identifier before the terminating ';'. Comments and preprocessor lines are +# stripped first so that commented-out declarations and the macro definition +# itself are ignored. +find_args=() +for h in $EXCLUDE; do find_args+=( ! -name "$h" ); done +find "$API_DIR" -maxdepth 1 -name '*.h' "${find_args[@]}" -print0 \ + | xargs -0 cat \ + | perl -0777 -ne ' + s{//[^\n]*|/\*.*?\*/}{}gs; # strip line and block comments + s/^\s*#.*$//mg; # strip preprocessor directives + while (/\bLIBHEIF_API\b\s+(.*?);/gs) { + my $d = $1; + if ($d =~ /(\w+)\s*\(/) { print "$1\n"; } # function + elsif ($d =~ /(\w+)\s*$/) { print "$1\n"; } # data object + }' \ + | sort -u > "$TMP/declared.txt" + +nm -D --defined-only "$LIB" | awk '{print $NF}' | sort -u > "$TMP/defined.txt" + +declared_count=$(wc -l < "$TMP/declared.txt") +missing=$(comm -23 "$TMP/declared.txt" "$TMP/defined.txt") + +# --- Report ------------------------------------------------------------------ + +echo "" +if [ -n "$missing" ]; then + missing_count=$(echo "$missing" | wc -l) + echo "API symbol check FAILED: $missing_count of $declared_count declared API symbols" + echo "are NOT defined in the built library ($(basename "$LIB")):" + echo "$missing" | sed 's/^/ /' + echo "" + echo "Each name above is declared (with LIBHEIF_API) in a public header but has" + echo "no matching definition. The usual cause is a definition whose name does" + echo "not match the declaration (see issue #1822)." + exit 1 +fi + +echo "API symbol check passed ($declared_count declared API symbols, all defined)." diff -Nru libheif-1.19.8/scripts/check-c-headers.sh libheif-1.23.4/scripts/check-c-headers.sh --- libheif-1.19.8/scripts/check-c-headers.sh 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/scripts/check-c-headers.sh 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,122 @@ +#!/bin/bash +set -e +# +# HEIF codec. +# Copyright (c) 2026 Dirk Farin +# +# This file is part of libheif. +# +# libheif is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libheif is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with libheif. If not, see . +# +# Verify that the public API headers are valid, plain C. +# +# The C++ build does not catch C-only mistakes such as referring to a struct +# type by its bare tag name (`heif_bad_pixel` instead of `struct heif_bad_pixel`), +# because C++ injects struct tags into the ordinary namespace while C does not. +# Such a mistake compiles fine in our C++ test suite but breaks every C consumer +# (see https://github.com/strukturag/libheif/pull/1812). +# +# This script compiles each public header on its own as strict C. Besides +# catching C++-isms, testing headers independently also verifies they are +# self-contained (pull in their own dependencies). Any header that fails to +# parse as C is reported and the script exits non-zero. + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd)" +API_DIR="$ROOT/libheif/api/libheif" + +# Headers that are intentionally C++ only and must NOT be checked as C. +CXX_ONLY="heif_cxx.h heif_emscripten.h" + +# Headers that are not (yet) self-contained and must be tested with the +# umbrella prelude instead of standalone. +NEEDS_PRELUDE="" + +# C standards to validate against. C99 is intentionally excluded: the headers +# legitimately repeat identical typedefs (e.g. heif_image_handle), which C11 +# permits but C99 forbids. +C_STANDARDS="c11 c17" + +# Collect available C compilers (gcc is enough; clang catches extra cases). +COMPILERS=() +command -v gcc >/dev/null 2>&1 && COMPILERS+=(gcc) +command -v clang >/dev/null 2>&1 && COMPILERS+=(clang) +if [ ${#COMPILERS[@]} -eq 0 ]; then + echo "ERROR: no C compiler (gcc or clang) found in PATH." >&2 + exit 1 +fi + +WARN_FLAGS="-pedantic -Wall -Wextra -Werror" + +# Build a self-contained include tree /libheif/ so that the angle-bracket +# includes used by the headers (#include ) resolve, and generate +# heif_version.h from its template (normally produced by CMake at build time). +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +mkdir -p "$TMP/libheif" +cp "$API_DIR"/*.h "$TMP/libheif/" + +VERSION=$(grep -m1 -E '^\s*project\(' "$ROOT/CMakeLists.txt" \ + | grep -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' | awk '{print $2}') +V_MAJOR=${VERSION%%.*} +V_REST=${VERSION#*.} +V_MINOR=${V_REST%%.*} +V_PATCH=${V_REST##*.} +sed -e "s/@PROJECT_VERSION_MAJOR@/${V_MAJOR:-0}/g" \ + -e "s/@PROJECT_VERSION_MINOR@/${V_MINOR:-0}/g" \ + -e "s/@PROJECT_VERSION_PATCH@/${V_PATCH:-0}/g" \ + -e "s|@PLUGIN_DIRECTORY@||g" \ + "$API_DIR/heif_version.h.in" > "$TMP/libheif/heif_version.h" + +failures=0 +checked=0 + +for header_path in "$TMP"/libheif/*.h; do + header=$(basename "$header_path") + case " $CXX_ONLY heif_version.h " in + *" $header "*) continue ;; + esac + + # Test each header on its own, so we also catch headers that are not + # self-contained. Listed exceptions get the umbrella prelude first. + src="$TMP/check_${header%.h}.c" + { + case " $NEEDS_PRELUDE " in + *" $header "*) echo "#include " ;; + esac + echo "#include " + echo "int main(void) { return 0; }" + } > "$src" + + for cc in "${COMPILERS[@]}"; do + for std in $C_STANDARDS; do + checked=$((checked + 1)) + if ! err=$("$cc" -std="$std" $WARN_FLAGS -I"$TMP" -c "$src" -o /dev/null 2>&1); then + echo "FAIL: $header is not valid C ($cc -std=$std)" + echo "$err" | sed 's/^/ /' + failures=$((failures + 1)) + fi + done + done +done + +echo "" +if [ "$failures" -ne 0 ]; then + echo "C API header check FAILED: $failures of $checked compilations failed." + echo "The public headers must be valid plain C. A common cause is using a" + echo "struct type by its bare tag (e.g. 'heif_foo*') instead of either a" + echo "typedef name or 'struct heif_foo*'." + exit 1 +fi + +echo "C API header check passed ($checked compilations, compilers: ${COMPILERS[*]})." diff -Nru libheif-1.19.8/scripts/check-emscripten-enums.sh libheif-1.23.4/scripts/check-emscripten-enums.sh --- libheif-1.19.8/scripts/check-emscripten-enums.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/check-emscripten-enums.sh 2026-09-06 14:45:17.000000000 +0000 @@ -29,16 +29,18 @@ heif_channel_ " +HEADERS="libheif/api/libheif/heif_error.h libheif/api/libheif/heif_image.h libheif/api/libheif/heif_context.h libheif/api/libheif/heif_color.h" + API_DEFINES="" for type in $DEFINE_TYPES; do - DEFINES=$(grep "^[ \t]*$type" libheif/api/libheif/heif.h | sed 's|[[:space:]]*\([^ \t=]*\)[[:space:]]*=.*|\1|g') + DEFINES=$(grep -hE "^[ \t]*${type}[A-Za-z0-9_]*[ \t]*=" $HEADERS | sed 's|[[:space:]]*\([^ \t=]*\)[[:space:]]*=.*|\1|g') if [ -z "$API_DEFINES" ]; then API_DEFINES="$DEFINES" else API_DEFINES="$API_DEFINES $DEFINES" fi - ALIASES=$(grep "^[ \t]*#define $type" libheif/api/libheif/heif.h | sed 's|[[:space:]]*#define \([^ \t]*\)[[:space:]]*.*|\1|g') + ALIASES=$(grep -h "^[ \t]*#define $type" $HEADERS | sed 's|[[:space:]]*#define \([^ \t]*\)[[:space:]]*.*|\1|g') if [ ! -z "$ALIASES" ]; then API_DEFINES="$API_DEFINES $ALIASES" diff -Nru libheif-1.19.8/scripts/check-go-enums.sh libheif-1.23.4/scripts/check-go-enums.sh --- libheif-1.19.8/scripts/check-go-enums.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/check-go-enums.sh 2026-09-06 14:45:17.000000000 +0000 @@ -29,16 +29,18 @@ heif_channel_ " +HEADERS="libheif/api/libheif/heif_error.h libheif/api/libheif/heif_image.h libheif/api/libheif/heif_context.h libheif/api/libheif/heif_color.h" + API_DEFINES="" for type in $DEFINE_TYPES; do - DEFINES=$(grep "^[ \t]*$type" libheif/api/libheif/heif.h | sed 's|[[:space:]]*\([^ \t=]*\)[[:space:]]*=.*|\1|g') + DEFINES=$(grep -hE "^[ \t]*${type}[A-Za-z0-9_]*[ \t]*=" $HEADERS | sed 's|[[:space:]]*\([^ \t=]*\)[[:space:]]*=.*|\1|g') if [ -z "$API_DEFINES" ]; then API_DEFINES="$DEFINES" else API_DEFINES="$API_DEFINES $DEFINES" fi - ALIASES=$(grep "^[ \t]*#define $type" libheif/api/libheif/heif.h | sed 's|[[:space:]]*#define \([^ \t]*\)[[:space:]]*.*|\1|g') + ALIASES=$(grep -h "^[ \t]*#define $type" $HEADERS | sed 's|[[:space:]]*#define \([^ \t]*\)[[:space:]]*.*|\1|g') if [ ! -z "$ALIASES" ]; then API_DEFINES="$API_DEFINES $ALIASES" diff -Nru libheif-1.19.8/scripts/check-licenses.sh libheif-1.23.4/scripts/check-licenses.sh --- libheif-1.19.8/scripts/check-licenses.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/check-licenses.sh 2026-09-06 14:45:17.000000000 +0000 @@ -21,7 +21,7 @@ # echo "Checking licenses..." -CHECK_RESULT=`/usr/bin/licensecheck --recursive --ignore 'emscripten|libde265|README\.md|post\.js|/.git/|clusterfuzz-testcase-.*|fuzzing/data/.*' .` +CHECK_RESULT=`/usr/bin/licensecheck --recursive --ignore 'emscripten|libde265|\.md$|post\.js|/.git/|clusterfuzz-testcase-.*|fuzzing/data/.*' .` FOUND= while read -r line; do diff -Nru libheif-1.19.8/scripts/encode-all-sequences.sh libheif-1.23.4/scripts/encode-all-sequences.sh --- libheif-1.19.8/scripts/encode-all-sequences.sh 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/scripts/encode-all-sequences.sh 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,81 @@ +#!/bin/bash +# +# HEIF codec. +# Copyright (c) 2025 Dirk Farin +# +# This file is part of libheif. +# +# libheif is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# libheif is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with libheif. If not, see . +# + +if [ "$#" -ne 1 ]; then + echo "Usage: pass name of first (PNG) image (with alpha) as only argument" + exit 5 +fi + +input_images=$1 + +for enc in "--hevc -e x265" \ + "--hevc -e kvazaar" \ + "--avc -e x264" \ + "--vvc -e vvenc" \ + "--vvc -e uvg266" \ + "--avif -e aom" \ + "--avif -e svt" \ + "--avif -e rav1e" \ + "--jpeg -e jpeg" \ + "--jpeg2000 -e openjpeg" \ + "--htj2k -e openjph" \ + "--uncompressed -e uncompressed" ; +do + format=${enc#--} # remove leading -- + format="${format%% *}" # stop at whitespace + codec="${enc##* }" # extract last word + + if [[ $format == "avif" ]] ; then + suffix="avif" + elif [[ $format == "hevc" ]] ; then + suffix="heics" + else + suffix="heif" + fi + + for alpha in "alpha" "noalpha" ; do + if [[ $alpha == "alpha" ]] ; then + alpha_arg="" + else + alpha_arg="--no-alpha" + fi + + for loop in "loop" "once" ; do + if [[ $loop == "once" ]] ; then + loop_arg="" + else + loop_arg="--repetitions infinite" + fi + + echo "----- format: ${format}, encoder: ${codec}, ${alpha}, ${loop} -----" + + if [[ $format == "hevc" || $format == "avc" || $format == "vvc" || $format == "avif" ]] ; then + for pred in "i" "p" "b" ; do + ./examples/heif-enc -S $input_images -o ${alpha}-${format}-${pred}-${codec}-${loop}.$suffix $enc $alpha_arg $loop_arg --gop-structure $pred + done + else + ./examples/heif-enc -S $input_images -o ${alpha}-${format}-${codec}-${loop}.$suffix $enc $alpha_arg $loop_arg + fi + done + + done + +done diff -Nru libheif-1.19.8/scripts/heif-modify-colr.py libheif-1.23.4/scripts/heif-modify-colr.py --- libheif-1.19.8/scripts/heif-modify-colr.py 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/scripts/heif-modify-colr.py 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +# +# MIT License +# +# Copyright (c) 2026 Dirk Farin +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Modify the NCLX colr box(es) in a HEIF/AVIF file. + +The colr box in HEIF/AVIF carries CICP colour signalling +(colour_primaries, transfer_characteristics, matrix_coefficients, +full_range_flag). Some files carry incorrect values: for example, some +Sony HIF files tag YCbCr range as "limited" in the colr box while the +HEVC VUI and the actual pixel data are full-range (libheif issue #1770). +libheif emits a warning when the colr box and the bitstream signalling +disagree; this script lets you correct the colr box. + +Modes (all NCLX colr boxes in the file are modified in either mode): + +* Default (no override flags): invoke heif-dec on the input, extract the + bitstream's colour signalling from libheif's mismatch warning, and + copy those values into the colr box(es). If heif-dec reports no + mismatch, the input is copied to the output unchanged so the script + is safe to use in batch loops. + +* Manual override: each -p / -t / -m / --full-range / --limited-range + flag overwrites that one CICP field. Unspecified fields keep their + existing colr-box values. heif-dec is not invoked in this mode. + +The NCLX variant of the colr box has a fixed size, so the file is +modified at byte level without any re-offsetting. + +Usage examples: + # Inspect (no -o => report only) + heif-modify-colr.py JAM07897.HIF + + # Auto-copy bitstream CICP into the colr box(es) + heif-modify-colr.py JAM07897.HIF -o fixed.HIF + + # Manual override of one field (e.g. force full_range_flag = 1) + heif-modify-colr.py JAM07897.HIF -o fixed.HIF --full-range + + # Manual override of all four CICP fields + heif-modify-colr.py in.heif -o out.heif -p 1 -t 13 -m 5 --full-range +""" + +import argparse +import os +import re +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + + +def parse_box_header(data, offset, end): + """Return (box_size, box_type, header_size) or None if malformed.""" + if offset + 8 > end: + return None + size = struct.unpack_from('>I', data, offset)[0] + box_type = bytes(data[offset + 4:offset + 8]) + header_size = 8 + if size == 1: + if offset + 16 > end: + return None + size = struct.unpack_from('>Q', data, offset + 8)[0] + header_size = 16 + elif size == 0: + size = end - offset + if box_type == b'uuid': + header_size += 16 + if size < header_size or offset + size > end: + return None + return size, box_type, header_size + + +def find_child_box(data, start, end, target_type): + """Find the first direct child box of the given type in [start, end). + Returns (payload_start, payload_end) or None.""" + offset = start + while offset + 8 <= end: + header = parse_box_header(data, offset, end) + if header is None: + return None + size, btype, hsize = header + if btype == target_type: + return offset + hsize, offset + size + offset += size + return None + + +def find_all_child_boxes(data, start, end, target_type): + """Find all direct child boxes of the given type in [start, end). + Returns list of (payload_start, payload_end).""" + found = [] + offset = start + while offset + 8 <= end: + header = parse_box_header(data, offset, end) + if header is None: + break + size, btype, hsize = header + if btype == target_type: + found.append((offset + hsize, offset + size)) + offset += size + return found + + +def find_colr_payloads(data): + """Walk meta > iprp > ipco > colr* and return [(payload_start, payload_end), ...]. + meta is a FullBox so its 4-byte version+flags header is skipped.""" + meta = find_child_box(data, 0, len(data), b'meta') + if meta is None: + return [] + iprp = find_child_box(data, meta[0] + 4, meta[1], b'iprp') + if iprp is None: + return [] + ipco = find_child_box(data, iprp[0], iprp[1], b'ipco') + if ipco is None: + return [] + return find_all_child_boxes(data, ipco[0], ipco[1], b'colr') + + +def read_nclx(data, payload_start, payload_end): + """Return (cp, tc, mc, full_range_flag) if this colr is NCLX, else None.""" + if payload_end - payload_start < 11: + return None + if bytes(data[payload_start:payload_start + 4]) != b'nclx': + return None + cp = struct.unpack_from('>H', data, payload_start + 4)[0] + tc = struct.unpack_from('>H', data, payload_start + 6)[0] + mc = struct.unpack_from('>H', data, payload_start + 8)[0] + frf = (data[payload_start + 10] >> 7) & 1 + return cp, tc, mc, frf + + +def write_nclx(data, payload_start, cp=None, tc=None, mc=None, frf=None): + if cp is not None: + struct.pack_into('>H', data, payload_start + 4, cp) + if tc is not None: + struct.pack_into('>H', data, payload_start + 6, tc) + if mc is not None: + struct.pack_into('>H', data, payload_start + 8, mc) + if frf is not None: + b = data[payload_start + 10] & 0x7F + data[payload_start + 10] = b | (0x80 if frf else 0x00) + + +# Matches libheif's mismatch warning. The bitstream values are in the +# second parenthesized triplet. Format: +# "...bitstream signalling (CP/TC/MC/{full|limited})..." +_BITSTREAM_WARNING_RE = re.compile( + r'bitstream signalling \((\d+)/(\d+)/(\d+)/(full|limited)\)' +) + + +def extract_bitstream_nclx(input_path, heif_dec): + """Run heif-dec and parse the NCLX-mismatch warning. Returns (cp, tc, mc, frf) + or None if no mismatch warning was found.""" + if heif_dec is None: + heif_dec = shutil.which('heif-dec') + if heif_dec is None: + raise RuntimeError( + 'heif-dec not found in PATH. Pass --heif-dec PATH or add it to PATH.') + with tempfile.TemporaryDirectory() as td: + # Write JPEG, not PNG: heif-dec picks the encoder from the + # extension, and PNG encoding adds noticeable time we don't need + # since we only care about the warning on stderr. + out = os.path.join(td, 'out.jpg') + try: + proc = subprocess.run( + [heif_dec, str(input_path), out], + capture_output=True, text=True, check=False, + ) + except FileNotFoundError as e: + raise RuntimeError(f'Failed to execute {heif_dec}: {e}') + if proc.returncode != 0: + sys.stderr.write(proc.stderr) + raise RuntimeError(f'{heif_dec} exited with status {proc.returncode}') + match = _BITSTREAM_WARNING_RE.search(proc.stderr) + if not match: + return None + cp = int(match.group(1)) + tc = int(match.group(2)) + mc = int(match.group(3)) + frf = 1 if match.group(4) == 'full' else 0 + return cp, tc, mc, frf + + +def main(): + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + ap.add_argument('input', type=Path, help='Input HEIF/AVIF file') + ap.add_argument('-o', '--output', type=Path, + help='Output file. If omitted, the script only reports ' + 'the current NCLX values and exits without modifying.') + ap.add_argument('-p', '--colour-primaries', type=int, metavar='N') + ap.add_argument('-t', '--transfer-characteristics', type=int, metavar='N') + ap.add_argument('-m', '--matrix-coefficients', type=int, metavar='N') + rg = ap.add_mutually_exclusive_group() + rg.add_argument('--full-range', dest='full_range', + action='store_const', const=1, + help='Set full_range_flag = 1') + rg.add_argument('--limited-range', dest='full_range', + action='store_const', const=0, + help='Set full_range_flag = 0') + ap.add_argument('--heif-dec', dest='heif_dec', metavar='PATH', default=None, + help='Path to the heif-dec executable used in auto mode ' + '(default: search PATH).') + args = ap.parse_args() + + data = bytearray(args.input.read_bytes()) + colr_payloads = find_colr_payloads(data) + print(f'Found {len(colr_payloads)} colr box(es) in {args.input}') + + nclx_payloads = [] + for pstart, pend in colr_payloads: + box_offset = pstart - 8 # NCLX colr has an 8-byte standard header + nclx = read_nclx(data, pstart, pend) + if nclx is None: + ctype = bytes(data[pstart:pstart + 4]).decode('ascii', errors='replace') + print(f' @ {box_offset:#x}: colour_type={ctype!r} (not nclx, skipped)') + continue + cp, tc, mc, frf = nclx + print(f' @ {box_offset:#x}: nclx cp={cp} tc={tc} mc={mc} full_range={frf}') + nclx_payloads.append(pstart) + + if args.output is None: + return 0 + + manual_given = (args.colour_primaries is not None + or args.transfer_characteristics is not None + or args.matrix_coefficients is not None + or args.full_range is not None) + + if manual_given: + cp_new, tc_new, mc_new, frf_new = ( + args.colour_primaries, + args.transfer_characteristics, + args.matrix_coefficients, + args.full_range, + ) + else: + # Auto mode: pull CICP from the bitstream via heif-dec's warning. + try: + bs = extract_bitstream_nclx(args.input, args.heif_dec) + except RuntimeError as e: + print(f'error: {e}', file=sys.stderr) + return 2 + if bs is None: + # No mismatch reported. Copy the input through so batch jobs + # always produce an output file regardless of whether a fix + # was needed. + print('No NCLX/VUI mismatch reported by heif-dec — ' + 'colr already matches the bitstream.') + if args.output.resolve() == args.input.resolve(): + print('Input and output are the same file; left unchanged.') + else: + shutil.copyfile(args.input, args.output) + print(f'Copied {args.input} -> {args.output} unchanged.') + return 0 + cp_new, tc_new, mc_new, frf_new = bs + print(f'Bitstream CICP from heif-dec: cp={cp_new} tc={tc_new} ' + f'mc={mc_new} full_range={frf_new}') + + if not nclx_payloads: + print('No NCLX colr boxes found to modify.', file=sys.stderr) + return 1 + + for pstart in nclx_payloads: + write_nclx(data, pstart, cp=cp_new, tc=tc_new, mc=mc_new, frf=frf_new) + cp, tc, mc, frf = read_nclx(data, pstart, pstart + 11) + print(f' patched @ {pstart - 8:#x}: cp={cp} tc={tc} mc={mc} full_range={frf}') + + args.output.write_bytes(bytes(data)) + print(f'Wrote {args.output} ({len(nclx_payloads)} colr box(es) modified)') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff -Nru libheif-1.19.8/scripts/install-ci-linux.sh libheif-1.23.4/scripts/install-ci-linux.sh --- libheif-1.19.8/scripts/install-ci-linux.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/install-ci-linux.sh 2026-09-06 14:45:17.000000000 +0000 @@ -22,20 +22,16 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -INSTALL_PACKAGES= +INSTALL_PACKAGES="gdb " REMOVE_PACKAGES= BUILD_ROOT=$ROOT/.. UPDATE_APT= -ADD_LIBHEIF_PPA= CURRENT_BRANCH=$TRAVIS_BRANCH if [ -z "$CURRENT_BRANCH" ]; then CURRENT_BRANCH=${GITHUB_REF##*/} fi -if [ "$WITH_LIBDE265" = "1" ]; then - echo "Adding PPA strukturag/libde265 ..." - sudo add-apt-repository -y ppa:strukturag/libde265 - UPDATE_APT=1 +if [ "$WITH_LIBDE265" = "1" ] || [ "$WITH_LIBDE265" = "3" ]; then INSTALL_PACKAGES="$INSTALL_PACKAGES \ libde265-dev \ " @@ -60,14 +56,12 @@ fi if [ "$WITH_AOM" = "1" ]; then - ADD_LIBHEIF_PPA=1 INSTALL_PACKAGES="$INSTALL_PACKAGES \ libaom-dev \ " fi if [ "$WITH_X265" = "1" ]; then - ADD_LIBHEIF_PPA=1 INSTALL_PACKAGES="$INSTALL_PACKAGES \ libx265-dev \ " @@ -75,11 +69,9 @@ if [ "$WITH_DAV1D" = "1" ]; then INSTALL_PACKAGES="$INSTALL_PACKAGES \ + meson \ nasm \ ninja-build \ - python3-pip \ - python3-setuptools \ - python3-wheel \ " fi @@ -143,12 +135,6 @@ " fi -if [ ! -z "$ADD_LIBHEIF_PPA" ]; then - echo "Adding PPA strukturag/libheif ..." - sudo add-apt-repository -y ppa:strukturag/libheif - UPDATE_APT=1 -fi - if [ ! -z "$INSTALL_PACKAGES" ]; then # The CI environment might have old package lists, so always update before installing. UPDATE_APT=1 @@ -195,9 +181,6 @@ fi if [ "$WITH_DAV1D" = "1" ]; then - pip3 install --user meson - - export PATH="$PATH:$HOME/.local/bin" cd third-party sh -e dav1d.cmd # dav1d does not support this option anymore: -Denable_avx512=false cd .. diff -Nru libheif-1.19.8/scripts/install-clang.sh libheif-1.23.4/scripts/install-clang.sh --- libheif-1.19.8/scripts/install-clang.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/install-clang.sh 2026-09-06 14:45:17.000000000 +0000 @@ -23,7 +23,7 @@ # Use script from https://chromium.googlesource.com/chromium/src/tools/clang/ # to download prebuilt version of clang. This commit defines which version of # the script should be used (and thus defines the version of clang). -COMMIT_HASH=55676aed71dd450595d83f107d24cb31c00160b3 +COMMIT_HASH=13d4d9000d7320838a4f4068751e23e909809ac0 DEST=$1 @@ -32,7 +32,7 @@ exit 1 fi -url="https://chromium.googlesource.com/chromium/src/tools/clang/+/${COMMIT_HASH}/scripts/update.py?format=TEXT" +url="https://github.com/chromium/chromium/raw/${COMMIT_HASH}/tools/clang/scripts/update.py" tmpdir=$(mktemp -d) echo "Using ${tmpdir} as temporary folder" @@ -40,17 +40,14 @@ script_folder=${tmpdir}/tools/clang/scripts mkdir -p "${script_folder}" echo "Downloading from ${url} ..." -curl -o "${script_folder}/update.py.b64" ${url} - -echo "Decoding base64 ..." -base64 --decode "${script_folder}/update.py.b64" > "${script_folder}/update.py" +curl --fail --location -o "${script_folder}/update.py" ${url} echo "Running ${script_folder}/update.py ..." -python "${script_folder}/update.py" +python3 "${script_folder}/update.py" --output-dir "${tmpdir}" echo "Copying to ${DEST} ..." mkdir -p "$DEST" -cp -rf "${tmpdir}/third_party/llvm-build/Release+Asserts/"* "${DEST}" +cp -rf "${tmpdir}/"* "${DEST}" echo "Cleaning up ..." rm -rf "${tmpdir}" diff -Nru libheif-1.19.8/scripts/run-ci.sh libheif-1.23.4/scripts/run-ci.sh --- libheif-1.19.8/scripts/run-ci.sh 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/scripts/run-ci.sh 2026-09-06 14:45:17.000000000 +0000 @@ -131,7 +131,7 @@ CMAKE_OPTIONS="--preset=testing" fi if [ -z "$FUZZER" ] && [ -z "$TESTS" ]; then - CMAKE_OPTIONS="--preset=release -DENABLE_PLUGIN_LOADING=OFF" + CMAKE_OPTIONS="--preset=release -DENABLE_PLUGIN_LOADING=OFF -DWITH_OPENJPH_ENCODER=OFF" fi if [ "$CURRENT_OS" = "osx" ] ; then @@ -153,6 +153,9 @@ # turn on warnings-as-errors CMAKE_OPTIONS="$CMAKE_OPTIONS -DCMAKE_COMPILE_WARNING_AS_ERROR=1" +# compilation mode +CMAKE_OPTIONS="$CMAKE_OPTIONS -DCMAKE_BUILD_TYPE=Release" + if [ ! -z "$FUZZER" ] && [ "$CURRENT_OS" = "linux" ]; then export ASAN_SYMBOLIZER="$BUILD_ROOT/clang/bin/llvm-symbolizer" @@ -161,7 +164,7 @@ if [ -z "$EMSCRIPTEN_VERSION" ] && [ -z "$CHECK_LICENSES" ] && [ -z "$TARBALL" ] ; then echo "Building libheif ..." cmake . $CMAKE_OPTIONS - make -j $(nproc) + make VERBOSE=1 -j $(nproc) if [ "$CURRENT_OS" = "linux" ] && [ -z "$MINGW" ] && [ -z "$FUZZER" ] && [ ! -z "$TESTS" ] ; then echo "Running tests ..." make test @@ -174,6 +177,7 @@ ${BIN_WRAPPER} ./examples/heif-dec${BIN_SUFFIX} --list-decoders echo "Dumping information of sample file ..." + #${BIN_WRAPPER} gdb -batch -ex "run" -ex "bt" --args ./examples/heif-info${BIN_SUFFIX} --dump-boxes examples/example.heic ${BIN_WRAPPER} ./examples/heif-info${BIN_SUFFIX} --dump-boxes examples/example.heic if [ ! -z "$WITH_GRAPHICS" ] && [ ! -z "$WITH_HEIF_DECODER" ]; then echo "Converting sample HEIF file to JPEG ..." @@ -251,7 +255,7 @@ if [ ! -z "$EMSCRIPTEN_VERSION" ]; then echo "Building with emscripten $EMSCRIPTEN_VERSION ..." - source ./emscripten/emsdk/emsdk_env.sh && USE_WASM=0 ./build-emscripten.sh . + source ./emscripten/emsdk/emsdk_env.sh && USE_WASM=0 USE_TYPESCRIPT=0 ./build-emscripten.sh . source ./emscripten/emsdk/emsdk_env.sh && node scripts/test-javascript.js fi @@ -271,13 +275,14 @@ mkdir build pushd build cmake .. --preset=release - make -j $(nproc) + make VERBOSE=1 -j $(nproc) popd fi if [ ! -z "$FUZZER" ] && [ "$CURRENT_OS" = "linux" ]; then ./fuzzing/color_conversion_fuzzer ./fuzzing/data/corpus/*color-conversion-fuzzer* ./fuzzing/file_fuzzer ./fuzzing/data/corpus/*.heic + ./fuzzing/tile_fuzzer ./fuzzing/data/corpus/*.heic echo "Running color conversion fuzzer ..." ./fuzzing/color_conversion_fuzzer -max_total_time=120 @@ -285,7 +290,10 @@ # Do not run encoder_fuzzer because it will just find errors in x265... #echo "Running encoder fuzzer ..." #./fuzzing/encoder_fuzzer -max_total_time=120 - + echo "Running file fuzzer ..." ./fuzzing/file_fuzzer -dict=./fuzzing/data/dictionary.txt -max_total_time=120 + + echo "Running tile fuzzer ..." + ./fuzzing/tile_fuzzer -dict=./fuzzing/data/dictionary.txt -max_total_time=120 fi diff -Nru libheif-1.19.8/tests/CMakeLists.txt libheif-1.23.4/tests/CMakeLists.txt --- libheif-1.19.8/tests/CMakeLists.txt 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 @@ -18,6 +18,12 @@ set(TEST_NAME ${TEST_FILE}) add_executable(${TEST_NAME} ${TEST_FILE}.cc) target_link_libraries(${TEST_NAME} PRIVATE heif testframework) + if (LIBSHARPYUV_FOUND) + target_compile_definitions(${TEST_NAME} PRIVATE HAVE_LIBSHARPYUV=1) + endif() + if (Brotli_FOUND) + target_compile_definitions(${TEST_NAME} PRIVATE HAVE_BROTLI=1) + endif() add_test(NAME ${TEST_NAME} COMMAND ./${TEST_NAME}) set_tests_properties(${TEST_NAME} PROPERTIES SKIP_REGULAR_EXPRESSION "[1-9][0-9]* skipped") endmacro() @@ -38,14 +44,32 @@ else() add_libheif_test(bitstream_tests) add_libheif_test(box_equals) + add_libheif_test(clap_zero_size) + add_libheif_test(fraction) + add_libheif_test(id_creator) add_libheif_test(conversion) + add_libheif_test(duplicate_alpha_channel) add_libheif_test(idat) + add_libheif_test(scale_plane_checks) + add_libheif_test(crop_plane_checks) + add_libheif_test(extract_area_plane_checks) add_libheif_test(jpeg2000) add_libheif_test(avc_box) + add_libheif_test(hevc_sps) add_libheif_test(file_layout) + add_libheif_test(image_description_metadata) + add_libheif_test(exception_guard) + + # decompression_bomb builds the bomb payload with the library-internal + # compress_brotli(), so it needs both Brotli and full symbol visibility. + if (Brotli_FOUND) + add_libheif_test(decompression_bomb) + else() + message(INFO " Disabling the decompression bomb test because Brotli was not found") + endif () endif() -if (ENABLE_EXPERIMENTAL_FEATURES AND WITH_REDUCED_VISIBILITY) +if (ENABLE_EXPERIMENTAL_FEATURES AND NOT WITH_REDUCED_VISIBILITY) add_libheif_test(pixel_data_types) endif() @@ -55,9 +79,34 @@ # --- tests that only access the public API +add_libheif_test(clap_decode) add_libheif_test(encode) +add_libheif_test(encode_grid) +add_libheif_test(entity_groups) add_libheif_test(extended_type) +add_libheif_test(grid_tile_missing) +add_libheif_test(iden_declared_size) +add_libheif_test(item_properties) +add_libheif_test(item_writing) +add_libheif_test(overlay_amplification) +add_libheif_test(error_item_decode) +add_libheif_test(alpha_cycle_deadlock) +add_libheif_test(parallel_grid_deadlock) +# The deadlock regression tests decode on a worker thread guarded by a timeout. +find_package(Threads REQUIRED) +target_link_libraries(alpha_cycle_deadlock PRIVATE Threads::Threads) +target_link_libraries(parallel_grid_deadlock PRIVATE Threads::Threads) add_libheif_test(region) +add_libheif_test(sequence_mixed_bit_depth) +add_libheif_test(sequence_no_track) +add_libheif_test(sequence_null_options) +add_libheif_test(sequence_timing_overflow) +add_libheif_test(sequence_raw_sample_errors) +add_libheif_test(tai) +add_libheif_test(text) +add_libheif_test(cxx_wrapper) +add_libheif_test(component_descriptions) +add_libheif_test(ffmpeg_decode_bitdepth) if (WITH_OPENJPH_ENCODER AND SUPPORTS_J2K_HT_ENCODING) add_libheif_test(encode_htj2k) @@ -71,12 +120,15 @@ message(INFO "Disabling JPEG 2000 encoder tests because no JPEG 2000 codec is enabled") endif() -if (ENABLE_EXPERIMENTAL_MINI_FORMAT) - if (NOT WITH_REDUCED_VISIBILITY) - add_libheif_test(mini_box) - endif() - add_libheif_test(mini_decode) +if (NOT WITH_REDUCED_VISIBILITY) + add_libheif_test(mini_box) endif() +add_libheif_test(mini_decode) + +if (NOT WITH_REDUCED_VISIBILITY) + add_libheif_test(omaf_boxes) +endif() +add_libheif_test(omaf) if (WITH_UNCOMPRESSED_CODEC) add_libheif_test(uncompressed_decode) @@ -88,10 +140,22 @@ add_libheif_test(uncompressed_decode_ycbcr) add_libheif_test(uncompressed_decode_ycbcr420) add_libheif_test(uncompressed_decode_ycbcr422) + add_libheif_test(uncompressed_sequence_odd_chroma) + add_libheif_test(uncompressed_mixed_chroma_depth_overflow) + add_libheif_test(uncompressed_mixed_chroma_depth_colorconv) + add_libheif_test(uncompressed_mixed_chroma_depth_encode) + add_libheif_test(uncompressed_wide_component_colorconv) + add_libheif_test(uncompressed_block_pixel_overpacked) + add_libheif_test(uncompressed_idat_tiled) add_libheif_test(uncompressed_encode) + if (ENABLE_EXPERIMENTAL_FEATURES) + add_libheif_test(uncompressed_encode_multicomponent) + endif() + if (ZLIB_FOUND) add_libheif_test(uncompressed_decode_generic_compression) + add_libheif_test(uncompressed_tile_range_overflow) else() message(WARNING "Generic compress tests of the 'uncompressed codec' are not compiled because zlib was not found") endif () @@ -99,7 +163,15 @@ message(WARNING "Tests of the 'uncompressed codec' are not compiled because the uncompressed codec is not enabled (WITH_UNCOMPRESSED_CODEC==OFF)") endif () -add_heifio_test(tiffdecode) +# find_package(TIFF) is called in the heifio subdirectory, but its result +# variables do not propagate to this sibling directory scope. Query again +# (the result is cached) so we only build the TIFF test when libtiff is present. +find_package(TIFF) +if (TIFF_FOUND) + add_heifio_test(tiffdecode) +else() + message(INFO " Disabling the TIFF decoder test because libtiff was not found") +endif () if (ENABLE_PLUGIN_LOADING) get_directory_property(ALL_TESTS TESTS) diff -Nru libheif-1.19.8/tests/alpha_cycle_deadlock.cc libheif-1.23.4/tests/alpha_cycle_deadlock.cc --- libheif-1.19.8/tests/alpha_cycle_deadlock.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/alpha_cycle_deadlock.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,304 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-8fmq-r4pf-7m57: a decoder deadlock (DoS) reachable +// in a default build. The cycle guard that protects derived-image ('dimg') +// edges did not cover the auxiliary alpha ('auxl') edge, because the guard's +// insert happens inside decode_compressed_image() which receives the traversal +// state by value, while the alpha edge is followed one level up in +// ImageItem::decode_image() using that frame's own (un-updated) state. Two +// items whose alpha references form a cycle therefore re-entered decode_image() +// on an item whose non-recursive m_decode_mutex was still held, deadlocking the +// decode thread forever. +// +// The images are 'mski' masks so no codec plugin is required. Because a +// regression re-introduces a permanent hang (not a slow-but-bounded decode), +// the decode is run on a worker thread guarded by a timeout: a regression fails +// the test quickly instead of hanging the whole suite. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// One item in the synthetic file. Every item is a decodable 8x8 'mski' mask +// (ispe + mskC + 64 bytes of idat). `is_alpha` items additionally carry an +// 'auxC' with the MIAF alpha aux-type; `auxl_to` lists the master images this +// item is the alpha channel of (an 'auxl' iref points aux -> master). +struct Item { + uint16_t id = 0; + bool is_alpha = false; + std::vector auxl_to; + std::vector data; +}; + +// Assemble a minimal, self-contained HEIF file from an explicit item list. +// ipco property indices (1-based, in box order): 1 = ispe(8x8), 2 = mskC(8bpp), +// 3 = auxC(alpha). +std::vector build_file(const std::vector& items, uint16_t primary_id) { + const uint32_t W = 8, H = 8; + + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, primary_id); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf + std::vector iinf_payload; + put_u16_be(iinf_payload, static_cast(items.size())); + for (const auto& it : items) { + std::vector infe_payload; + put_u16_be(infe_payload, it.id); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "mski"); + infe_payload.push_back(0); + append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iprp / ipco + std::vector ispe_payload; + put_u32_be(ispe_payload, W); + put_u32_be(ispe_payload, H); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(8); // bits_per_pixel + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector auxC_payload; + append_cstr(auxC_payload, "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"); // MIAF alpha + auto auxC = make_box("auxC", auxC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); // property 1 + append(ipco_payload, mskC); // property 2 + append(ipco_payload, auxC); // property 3 + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, static_cast(items.size())); // entry_count + for (const auto& it : items) { + put_u16_be(ipma_payload, it.id); + if (it.is_alpha) { + ipma_payload.push_back(3); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, mskC + ipma_payload.push_back(0x80 | 3); // essential, auxC + } + else { + ipma_payload.push_back(2); + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, mskC + } + } + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat + iloc + std::vector idat_payload; + struct Extent { uint16_t id; uint32_t off; uint32_t len; }; + std::vector extents; + for (const auto& it : items) { + if (!it.data.empty()) { + extents.push_back({it.id, static_cast(idat_payload.size()), + static_cast(it.data.size())}); + append(idat_payload, it.data); + } + } + auto idat = make_box("idat", idat_payload); + + std::vector iloc_payload; + iloc_payload.push_back((4 << 4) | 4); // offset_size=4, length_size=4 + iloc_payload.push_back((0 << 4) | 0); // base_offset_size=0, index_size=0 + put_u16_be(iloc_payload, static_cast(extents.size())); // item_count + for (const auto& e : extents) { + put_u16_be(iloc_payload, e.id); + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, e.off); // extent_offset (within idat) + put_u32_be(iloc_payload, e.len); // extent_length + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: one 'auxl' entry per alpha item, from the aux item to its master(s). + std::vector iref_children; + for (const auto& it : items) { + if (!it.auxl_to.empty()) { + std::vector auxl_payload; + put_u16_be(auxl_payload, it.id); + put_u16_be(auxl_payload, static_cast(it.auxl_to.size())); + for (uint16_t to : it.auxl_to) { put_u16_be(auxl_payload, to); } + append(iref_children, make_box("auxl", auxl_payload)); + } + } + auto iref = make_box("iref", iref_children, /*full=*/true); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Decode the primary image and return the resulting error. Self-contained so it +// can run on a detached worker thread without touching caller-owned state. +heif_error decode_primary_blocking(const std::vector& data) { + heif_context* ctx = heif_context_alloc(); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return err; + } + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return err; + } + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + if (img) { heif_image_release(img); } + heif_image_handle_release(handle); + heif_context_free(ctx); + return err; +} + +// Run decode_primary_blocking() with a timeout. Returns false on timeout (a +// regression: the decode deadlocked). The worker holds its shared state through +// a shared_ptr so that detaching it on timeout leaves no dangling references. +bool decode_primary_with_timeout(const std::vector& data, + std::chrono::seconds timeout, + heif_error& out_err) { + struct Job { + std::vector data; + std::promise prom; + }; + auto job = std::make_shared(); + job->data = data; + auto fut = job->prom.get_future(); + + std::thread worker([job]() { + job->prom.set_value(decode_primary_blocking(job->data)); + }); + + if (fut.wait_for(timeout) == std::future_status::timeout) { + worker.detach(); // leak the hung thread; the process reclaims it at exit + return false; + } + + worker.join(); + out_err = fut.get(); + return true; +} + +} // namespace + + +// The core DoS. Three 'mski' items: item1 (primary) has alpha=item2; item2 and +// item3 are each other's alpha (auxl item2->item3 and item3->item2), forming a +// cycle. Without the fix, decode_image() re-enters item2 while item2's +// m_decode_mutex is still held and deadlocks forever. With the fix, the alpha +// edge participates in the cycle guard, so the decode returns a cyclic-reference +// error essentially instantly. +TEST_CASE("alpha cycle: cyclic alpha auxl references do not deadlock") { + std::vector items; + const std::vector pixels(64, 0x7F); + // id is_alpha auxl_to (masters) data + items.push_back({1, false, {}, pixels}); // primary, master of item2 + items.push_back({2, true, {1, 3}, pixels}); // alpha of item1 and item3 + items.push_back({3, true, {2}, pixels}); // alpha of item2 -> cycle 2<->3 + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_primary_with_timeout(data, std::chrono::seconds(15), err); + + REQUIRE(completed); // fails fast if the deadlock regresses + REQUIRE(err.code != heif_error_Ok); // the cycle is reported as an error + REQUIRE(err.code == heif_error_Invalid_input); +} + +// Control: a normal image with a single (non-cyclic) alpha aux image still +// decodes, proving the added cycle-guard insert does not reject legitimate +// alpha channels. +TEST_CASE("alpha cycle: a normal alpha aux image still decodes") { + std::vector items; + const std::vector pixels(64, 0x7F); + items.push_back({1, false, {}, pixels}); // primary + items.push_back({2, true, {1}, pixels}); // alpha of item1, no further alpha + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_primary_with_timeout(data, std::chrono::seconds(15), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Ok); +} diff -Nru libheif-1.19.8/tests/avc_box.cc libheif-1.23.4/tests/avc_box.cc --- libheif-1.19.8/tests/avc_box.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/avc_box.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,11 +26,13 @@ #include "catch_amalgamated.hpp" #include "codecs/avc_boxes.h" +#include "codecs/decoder.h" #include "error.h" #include #include #include + TEST_CASE("avcC") { std::vector byteArray{ 0x00, 0x00, 0x00, 0x34, 0x61, 0x76, 0x63, 0x43, 0x01, 0x42, 0x80, @@ -81,3 +83,106 @@ const std::vector bytes = writer.get_data(); REQUIRE(bytes == byteArray); } + +TEST_CASE("Reject invalid chroma format in AVC SPS") { + const std::vector sps{ + 0x20, 0x2c, 0x20, 0x20, 0x00, 0x20, 0x20, 0x00, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}; + Box_avcC::configuration configuration; + uint32_t width = 0; + uint32_t height = 0; + + Error error = parse_sps_for_avcC_configuration( + sps.data(), sps.size(), &configuration, &width, &height); + + REQUIRE(error.error_code == heif_error_Invalid_input); +} + +TEST_CASE("Reject invalid code in AVC SPS scaling list") { + std::vector sps{ + 0x67, 0x64, 0x00, 0x1f, 0xad, 0x80, 0x00, 0x00, 0x00, 0x3f}; + sps.insert(sps.end(), 40, 0xff); + Box_avcC::configuration configuration; + uint32_t width = 0; + uint32_t height = 0; + + Error error = parse_sps_for_avcC_configuration( + sps.data(), sps.size(), &configuration, &width, &height); + + REQUIRE(error.error_code == heif_error_Invalid_input); +} + + +// Regression test for https://github.com/strukturag/libheif/issues/1866: +// the SPS frame cropping offsets are in chroma-dependent crop units +// (CropUnitX/CropUnitY), not in luma samples. +TEST_CASE("AVC SPS conformance window crop units") { + + SECTION("4:2:0 progressive, crop units are 2 luma samples") { + // SPS generated by x264 for a 150x150 image: coded size 160x160, + // frame_crop_right_offset = frame_crop_bottom_offset = 5 units = 10 pixels. + const std::vector sps{ + 0x67, 0x4d, 0x40, 0x0b, 0xec, 0xc1, 0x42, 0xbc, + 0xd3, 0x42, 0x00, 0x00, 0x03, 0x00, 0x32, 0x00, + 0x00, 0x03, 0x00, 0x04, 0x1e, 0x28, 0x53, 0x34}; + + Box_avcC::configuration configuration; + uint32_t width = 0; + uint32_t height = 0; + ImageSize coded_size{}; + + Error error = parse_sps_for_avcC_configuration( + sps.data(), sps.size(), &configuration, &width, &height, &coded_size); + + REQUIRE(error == Error::Ok); + REQUIRE(configuration.chroma_format == heif_chroma_420); + REQUIRE(coded_size.width == 160); + REQUIRE(coded_size.height == 160); + REQUIRE(width == 150); + REQUIRE(height == 150); + } + + SECTION("4:4:4 progressive, crop units are 1 luma sample") { + // High profile, chroma_format_idc = 3, coded size 160x160, + // frame_crop_right_offset = frame_crop_bottom_offset = 5 units = 5 pixels. + const std::vector sps{ + 0x67, 0x64, 0x00, 0x1f, 0x91, 0x96, 0x82, 0x85, 0x79, 0xa6, 0x40}; + + Box_avcC::configuration configuration; + uint32_t width = 0; + uint32_t height = 0; + ImageSize coded_size{}; + + Error error = parse_sps_for_avcC_configuration( + sps.data(), sps.size(), &configuration, &width, &height, &coded_size); + + REQUIRE(error == Error::Ok); + REQUIRE(configuration.chroma_format == heif_chroma_444); + REQUIRE(coded_size.width == 160); + REQUIRE(coded_size.height == 160); + REQUIRE(width == 155); + REQUIRE(height == 155); + } + + SECTION("4:2:0 interlaced, map units count double, vertical crop unit is 4") { + // Main profile, frame_mbs_only_flag = 0, 5 map units = 160 pixels frame height, + // frame_crop_right_offset = 5 units = 10 pixels, + // frame_crop_bottom_offset = 2 units = 8 pixels. + const std::vector sps{ + 0x67, 0x4d, 0x40, 0x1e, 0xda, 0x0a, 0x29, 0xcd, 0x68}; + + Box_avcC::configuration configuration; + uint32_t width = 0; + uint32_t height = 0; + ImageSize coded_size{}; + + Error error = parse_sps_for_avcC_configuration( + sps.data(), sps.size(), &configuration, &width, &height, &coded_size); + + REQUIRE(error == Error::Ok); + REQUIRE(coded_size.width == 160); + REQUIRE(coded_size.height == 160); + REQUIRE(width == 150); + REQUIRE(height == 152); + } +} diff -Nru libheif-1.19.8/tests/bitstream_tests.cc libheif-1.23.4/tests/bitstream_tests.cc --- libheif-1.19.8/tests/bitstream_tests.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/bitstream_tests.cc 2026-09-06 14:45:17.000000000 +0000 @@ -77,6 +77,161 @@ REQUIRE(overlap == 0b111000101000001100001111000111); } +// --- BitWriter tests --- + +TEST_CASE("bitwriter single flags") { + BitWriter writer; + writer.write_flag(true); + writer.write_flag(false); + writer.write_flag(true); + writer.write_flag(true); + writer.write_flag(false); + writer.write_flag(false); + writer.write_flag(true); + writer.write_flag(false); + auto data = writer.get_data(); + REQUIRE(data.size() == 1); + REQUIRE(data[0] == 0b10110010); +} + +TEST_CASE("bitwriter multi-bit values") { + BitWriter writer; + writer.write_bits(0b01, 2); // 01 + writer.write_bits(0b110, 3); // 110 + writer.write_bits(0b101, 3); // 101 + auto data = writer.get_data(); + REQUIRE(data.size() == 1); + REQUIRE(data[0] == 0b01110101); +} + +TEST_CASE("bitwriter cross-byte boundary") { + BitWriter writer; + writer.write_bits(0b11111, 5); // 5 bits + writer.write_bits(0b000111, 6); // 6 bits - crosses byte boundary + auto data = writer.get_data(); + REQUIRE(data.size() == 2); + REQUIRE(data[0] == 0b11111000); + REQUIRE(data[1] == 0b11100000); // remaining 3 bits + 5 zero-padded +} + +TEST_CASE("bitwriter 16-bit value") { + BitWriter writer; + writer.write_bits16(0x1234, 16); + auto data = writer.get_data(); + REQUIRE(data.size() == 2); + REQUIRE(data[0] == 0x12); + REQUIRE(data[1] == 0x34); +} + +TEST_CASE("bitwriter 32-bit value") { + BitWriter writer; + writer.write_bits32(0xDEADBEEF, 32); + auto data = writer.get_data(); + REQUIRE(data.size() == 4); + REQUIRE(data[0] == 0xDE); + REQUIRE(data[1] == 0xAD); + REQUIRE(data[2] == 0xBE); + REQUIRE(data[3] == 0xEF); +} + +TEST_CASE("bitwriter skip_to_byte_boundary") { + BitWriter writer; + writer.write_bits(0b101, 3); + REQUIRE(writer.get_bits_written() == 3); + writer.skip_to_byte_boundary(); + REQUIRE(writer.get_bits_written() == 8); + writer.write_bits8(0xFF, 8); + auto data = writer.get_data(); + REQUIRE(data.size() == 2); + REQUIRE(data[0] == 0b10100000); + REQUIRE(data[1] == 0xFF); +} + +TEST_CASE("bitwriter skip_to_byte_boundary already aligned") { + BitWriter writer; + writer.write_bits8(0xAB, 8); + writer.skip_to_byte_boundary(); // should be no-op + writer.write_bits8(0xCD, 8); + auto data = writer.get_data(); + REQUIRE(data.size() == 2); + REQUIRE(data[0] == 0xAB); + REQUIRE(data[1] == 0xCD); +} + +TEST_CASE("bitwriter write_bytes") { + BitWriter writer; + writer.write_bits8(0xAA, 8); + std::vector bytes = {0x01, 0x02, 0x03}; + writer.write_bytes(bytes); + auto data = writer.get_data(); + REQUIRE(data.size() == 4); + REQUIRE(data[0] == 0xAA); + REQUIRE(data[1] == 0x01); + REQUIRE(data[2] == 0x02); + REQUIRE(data[3] == 0x03); +} + +TEST_CASE("bitwriter round-trip with BitReader") { + // Write a sequence of mixed-width values + BitWriter writer; + writer.write_bits(2, 2); // version = 2 + writer.write_flag(false); // flag1 + writer.write_flag(true); // flag2 + writer.write_flag(true); // flag3 + writer.write_flag(false); // flag4 + writer.write_flag(true); // flag5 + writer.write_flag(false); // flag6 + writer.write_flag(true); // flag7 + writer.write_flag(false); // flag8 + writer.write_flag(true); // flag9 + writer.write_bits(1, 2); // chroma = 1 + writer.write_bits(4, 3); // orientation = 4 + writer.write_flag(true); // large_dim + writer.write_bits(255, 15); // width-1 + writer.write_bits(127, 15); // height-1 + writer.write_bits8(0xAB, 8); // some byte + + auto data = writer.get_data(); + + // Read back with BitReader + BitReader reader(data.data(), (int)data.size()); + REQUIRE(reader.get_bits(2) == 2); // version + REQUIRE(reader.get_flag() == false); // flag1 + REQUIRE(reader.get_flag() == true); // flag2 + REQUIRE(reader.get_flag() == true); // flag3 + REQUIRE(reader.get_flag() == false); // flag4 + REQUIRE(reader.get_flag() == true); // flag5 + REQUIRE(reader.get_flag() == false); // flag6 + REQUIRE(reader.get_flag() == true); // flag7 + REQUIRE(reader.get_flag() == false); // flag8 + REQUIRE(reader.get_flag() == true); // flag9 + REQUIRE(reader.get_bits(2) == 1); // chroma + REQUIRE(reader.get_bits(3) == 4); // orientation + REQUIRE(reader.get_flag() == true); // large_dim + REQUIRE(reader.get_bits(15) == 255); // width-1 + REQUIRE(reader.get_bits(15) == 127); // height-1 + REQUIRE(reader.get_bits8(8) == 0xAB); // byte +} + +TEST_CASE("bitwriter get_current_byte_index") { + BitWriter writer; + REQUIRE(writer.get_current_byte_index() == 0); + writer.write_bits8(0xFF, 8); + REQUIRE(writer.get_current_byte_index() == 1); + writer.write_bits(0, 3); // partial byte not yet flushed + REQUIRE(writer.get_current_byte_index() == 1); + writer.skip_to_byte_boundary(); + REQUIRE(writer.get_current_byte_index() == 2); +} + +TEST_CASE("bitwriter zero bits") { + BitWriter writer; + writer.write_bits(0, 0); // writing 0 bits should be a no-op + REQUIRE(writer.get_bits_written() == 0); + auto data = writer.get_data(); + REQUIRE(data.empty()); +} + TEST_CASE("read float") { std::vector byteArray{0x40, 0x00, 0x00, 0x00}; std::shared_ptr stream = std::make_shared(byteArray.data(), (int)byteArray.size(), false); diff -Nru libheif-1.19.8/tests/catch_amalgamated.cpp libheif-1.23.4/tests/catch_amalgamated.cpp --- libheif-1.19.8/tests/catch_amalgamated.cpp 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/catch_amalgamated.cpp 2026-09-06 14:45:17.000000000 +0000 @@ -6513,7 +6513,7 @@ std::string origStr = CATCH_MOVE(str); str.clear(); // There is at least one replacement, so reserve with the best guess - // we can make without actually counting the number of occurences. + // we can make without actually counting the number of occurrences. str.reserve(origStr.size() - replaceThis.size() + withThis.size()); do { str.append(origStr, copyBegin, i-copyBegin ); diff -Nru libheif-1.19.8/tests/catch_amalgamated.hpp libheif-1.23.4/tests/catch_amalgamated.hpp --- libheif-1.19.8/tests/catch_amalgamated.hpp 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/catch_amalgamated.hpp 2026-09-06 14:45:17.000000000 +0000 @@ -5173,7 +5173,7 @@ * when the compiler handles `ExprLhs == b`, it also tries to resolve * the overload set for `b == ExprLhs`. * - * To accomodate these use cases, decomposer ended up rather complex. + * To accommodate these use cases, decomposer ended up rather complex. * * 1) These types are handled by adding SFINAE overloads to our comparison * operators, checking whether `T == U` are comparable with the given @@ -5291,7 +5291,7 @@ public: constexpr auto isBinaryExpression() const -> bool { return m_isBinaryExpression; } constexpr auto getResult() const -> bool { return m_result; } - //! This function **has** to be overriden by the derived class. + //! This function **has** to be overridden by the derived class. virtual void streamReconstructedExpression( std::ostream& os ) const; constexpr ITransientExpression( bool isBinaryExpression, bool result ) diff -Nru libheif-1.19.8/tests/clap_decode.cc libheif-1.23.4/tests/clap_decode.cc --- libheif-1.19.8/tests/clap_decode.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/clap_decode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,140 @@ +/* + libheif decoding tests for images with a clean aperture (clap) crop + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +// Regression test for issue #1856. The per-decode tightened security limit +// (max_image_size_pixels clamped to just above the declared size, introduced +// for issue #1798) was computed from the post-transformation dimensions +// instead of the coded 'ispe' size. The codec has to decode the full coded +// frame before the crop is applied, so any image whose 'clap' removes more +// area than the coding-unit margin adds was rejected with "security limit +// exceeded" under the default limits. +// +// The test files carry a 256x256 coded frame with a centered 64x64 'clap'; +// one variant adds 'irot'/'imir' like the MIAF007 conformance file. + +TEST_CASE("decode cropped image within default security limits") +{ + struct TestFile { + const char* name; + heif_compression_format codec; + }; + + const TestFile files[] = { + {"clap_cropped.avif", heif_compression_AV1}, + {"clap_cropped.heic", heif_compression_HEVC}, + {"clap_cropped_irot_imir.avif", heif_compression_AV1}, + }; + + // The regression is in the codec-independent decode path, so every file + // whose codec can be decoded is checked and the others are left out. Builds + // without a HEVC decoder (issue #1900) still cover the AVIF variants. + int num_tested = 0; + + for (const TestFile& file : files) { + if (!heif_have_decoder_for_format(file.codec)) { + continue; + } + + INFO(file.name); + + heif_context* ctx = get_context_for_test_file(file.name); + heif_image_handle* handle = get_primary_image_handle(ctx); + + // The handle reports the size after all transformations. + REQUIRE(heif_image_handle_get_width(handle) == 64); + REQUIRE(heif_image_handle_get_height(handle) == 64); + + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, heif_colorspace_undefined, + heif_chroma_undefined, nullptr); + INFO((err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_image_get_primary_width(img) == 64); + REQUIRE(heif_image_get_primary_height(img) == 64); + heif_image_release(img); + + // Decoding without transformations must also fit within the limits and + // yield the full coded frame. + heif_decoding_options* options = heif_decoding_options_alloc(); + options->ignore_transformations = 1; + img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, + heif_chroma_undefined, options); + heif_decoding_options_free(options); + INFO((err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_image_get_primary_width(img) == 256); + REQUIRE(heif_image_get_primary_height(img) == 256); + heif_image_release(img); + + heif_image_handle_release(handle); + heif_context_free(ctx); + + num_tested++; + } + + if (num_tested == 0) { + SKIP("neither an AV1 nor a HEVC decoder is available, skipping test"); + } +} + + +// Second regression from issue #1856: a conformant HEVC stream may code a frame +// far larger than the 'ispe' size and crop it away with the SPS conformance +// window. The test file (reported through ImageMagick) declares a 2x2 image but +// codes a 160x64 frame, exceeding the ispe + one-CTU budget. The tightened +// limit must allow at least MIN_TIGHTENED_CODED_IMAGE_PIXELS for the coded +// frame. The file also carries a 1x1 'clap'. + +TEST_CASE("decode image with coded frame much larger than ispe") +{ + // The conformance window is a HEVC feature, so this test needs a HEVC decoder. + if (!heif_have_decoder_for_format(heif_compression_HEVC)) { + SKIP("HEVC decoder not available, skipping test"); + } + + heif_context* ctx = get_context_for_test_file("conformance_window_padding.heic"); + heif_image_handle* handle = get_primary_image_handle(ctx); + + REQUIRE(heif_image_handle_get_width(handle) == 1); + REQUIRE(heif_image_handle_get_height(handle) == 1); + + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, heif_colorspace_undefined, + heif_chroma_undefined, nullptr); + INFO((err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_image_get_primary_width(img) == 1); + REQUIRE(heif_image_get_primary_height(img) == 1); + heif_image_release(img); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/clap_zero_size.cc libheif-1.23.4/tests/clap_zero_size.cc --- libheif-1.19.8/tests/clap_zero_size.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/clap_zero_size.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,142 @@ +/* + libheif clean aperture (clap) zero-size unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "box.h" +#include "libheif/heif.h" +#include "test_utils.h" +#include + +// Regression tests for GHSA-jc8f-p23p-5hjg and GHSA-gh5q-69gg-c964. A zero image +// dimension used to underflow `image_width - 1U` to UINT32_MAX, and a dimension above +// INT32_MAX + 1 exceeded the Fraction range directly. Both tripped an assert in the +// former Fraction(uint32_t, uint32_t) constructor (abort in debug builds, corrupt crop +// in release builds). Box_clap::get_crop() must report both as an error instead. + +static std::shared_ptr make_clap() +{ + auto clap = std::make_shared(); + // clap 100x200 at the top-left corner of a 150x250 image + REQUIRE(clap->set(100, 200, 150, 250).error_code == heif_error_Ok); + return clap; +} + +TEST_CASE("clap crop for a valid image size") { + auto clap = make_clap(); + + auto crop = clap->get_crop(150, 250); + REQUIRE(crop); + REQUIRE(crop->left == 0); + REQUIRE(crop->right == 99); + REQUIRE(crop->top == 0); + REQUIRE(crop->bottom == 199); +} + +TEST_CASE("clap crop with zero image size") { + auto clap = make_clap(); + + const uint32_t sizes[][2] = {{0, 250}, {150, 0}, {0, 0}}; + for (auto& size : sizes) { + auto crop = clap->get_crop(size[0], size[1]); + REQUIRE(!crop); + REQUIRE(crop.error().error_code == heif_error_Invalid_input); + REQUIRE(crop.error().sub_error_code == heif_suberror_Invalid_clean_aperture); + } +} + +TEST_CASE("clap crop with oversized image size") { + auto clap = make_clap(); + + const uint32_t sizes[][2] = {{0xFFFFFFFF, 250}, {150, 0xFFFFFFFF}, {0x80000001, 250}, {150, 0x80000001}}; + for (auto& size : sizes) { + auto crop = clap->get_crop(size[0], size[1]); + REQUIRE(!crop); + REQUIRE(crop.error().error_code == heif_error_Invalid_input); + REQUIRE(crop.error().sub_error_code == heif_suberror_Invalid_clean_aperture); + } + + // The largest values that still fit must be computed normally: the crop is centered + // (up to the Fraction's reduced precision at this magnitude) and keeps its size. + auto crop = clap->get_crop(0x80000000, 0x80000000); + REQUIRE(crop); + REQUIRE(std::abs(crop->left - (0x40000000 - 75)) <= 1); + REQUIRE(crop->right - crop->left + 1 == 100); + REQUIRE(std::abs(crop->top - (0x40000000 - 125)) <= 1); + REQUIRE(crop->bottom - crop->top + 1 == 200); +} + +TEST_CASE("clap set() rejects a clean aperture larger than the image") { + auto clap = std::make_shared(); + REQUIRE(clap->set(200, 100, 150, 250).error_code == heif_error_Usage_error); + REQUIRE(clap->set(100, 300, 150, 250).error_code == heif_error_Usage_error); +} + + +// The same bug through the public API. The files carry an 'ispe' with one dimension of +// 0xFFFFFFFF and a 'clap' property; two of them add an 'irot' so that the oversized +// dimension arrives at the other side of the clap computation after the rotation swaps +// width and height. +TEST_CASE("image tiling with clap and oversized ispe") { + const char* files[] = { + "clap_oversized_ispe_height.avif", + "clap_oversized_ispe_width.avif", + "clap_oversized_ispe_irot180.avif", + "clap_oversized_ispe_irot90.avif", + }; + + for (const char* file : files) { + INFO(file); + + heif_context* ctx = get_context_for_test_file(file); + heif_image_handle* handle = get_primary_image_handle(ctx); + heif_image_tiling tiling{}; + + // With the default security limits, the tiling API must reject the image like the + // decoding path does, instead of handing out a size nobody can allocate. + heif_error err = heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + + // Without limits, the clap computation itself must report the error instead of + // aborting on an assert or computing a bogus crop. + heif_context_set_security_limits(ctx, heif_get_disabled_security_limits()); + err = heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Invalid_clean_aperture); + + // The crop-border query has no error return and reports "no cropping" instead. + int left = -1, top = -1, right = -1, bottom = -1; + heif_item_get_property_transform_crop_borders(ctx, heif_image_handle_get_item_id(handle), 0, + 64, -1, &left, &top, &right, &bottom); + REQUIRE(left == 0); + REQUIRE(top == 0); + REQUIRE(right == 0); + REQUIRE(bottom == 0); + + heif_image_handle_release(handle); + heif_context_free(ctx); + } +} diff -Nru libheif-1.19.8/tests/component_descriptions.cc libheif-1.23.4/tests/component_descriptions.cc --- libheif-1.19.8/tests/component_descriptions.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/component_descriptions.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,377 @@ +/* + libheif unit tests for component-description ID consistency between + heif_image_handle (pre-decode) and heif_image (post-decode). + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_uncompressed.h" +#include "test_utils.h" +#include + + +// Verifies the contract that handle-side and decoded-image-side per-component +// queries return the same component IDs and the same per-component metadata. +// The image is decoded with native colorspace/chroma so no conversion takes +// place that would replace the planes. +static void check_handle_matches_decoded_image(heif_image_handle* handle, + heif_compression_format codec) +{ + uint32_t handle_n = heif_image_handle_get_number_of_components(handle); + REQUIRE(handle_n > 0); + + std::vector handle_ids(handle_n); + heif_image_handle_get_used_component_ids(handle, handle_ids.data()); + + // Decode with native colorspace to avoid the conversion path. + heif_image* image = nullptr; + heif_decoding_options* options = heif_decoding_options_alloc(); + options->ignore_transformations = 1; // avoid mirror/rotate fixups changing dims + heif_error err = heif_decode_image(handle, &image, heif_colorspace_undefined, + heif_chroma_undefined, options); + heif_decoding_options_free(options); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(image != nullptr); + + uint32_t img_n = heif_image_get_number_of_used_components(image); + + // Handle and decoded image now agree exactly on component count, including + // alpha (which set_alpha_channel() appends to the main item's description + // list at file-load time). + REQUIRE(img_n == handle_n); + + std::vector img_ids(img_n); + heif_image_get_used_component_ids(image, img_ids.data()); + + // The first handle_n IDs in the decoded image must equal the handle's IDs + // and refer to the same channel/type/bit-depth/datatype. + for (uint32_t i = 0; i < handle_n; i++) { + INFO("component index " << i); + REQUIRE(img_ids[i] == handle_ids[i]); + + uint16_t handle_type = heif_image_handle_get_component_type(handle, handle_ids[i]); + uint16_t img_type = heif_image_get_component_type(image, img_ids[i]); + REQUIRE(handle_type == img_type); + + int handle_bpp = heif_image_handle_get_component_bits_per_pixel(handle, handle_ids[i]); + int img_bpp = heif_image_get_component_bits_per_pixel(image, img_ids[i]); + REQUIRE(handle_bpp == img_bpp); + + heif_component_datatype handle_dt = heif_image_handle_get_component_datatype(handle, handle_ids[i]); + heif_component_datatype img_dt = heif_image_get_component_datatype(image, img_ids[i]); + REQUIRE(handle_dt == img_dt); + } + + heif_image_release(image); + (void)codec; +} + + +// Build an RGGB Bayer filter-array image, encode as unci, write to a temp +// file, read it back, and verify that the populate-side cpat dedup matches +// the decoded-side IDs: +// - cmpd has 4 entries (filter_array, R, G, B); only filter_array carries +// pixel data, R/G/B are reference components. +// - cpat is a 2x2 RGGB pattern; the two G positions reference the same +// cmpd index, so populate_component_descriptions() emits ONE G +// description, not two. +// - heif_image_handle_get_number_of_components() returns 4. +// - The decoded image's m_components has the same 4 ids in the same order. +TEST_CASE("unci with cpat: handle and decoded image agree on component IDs") +{ + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("Skipping test because uncompressed codec is not compiled."); + } + if (!heif_have_encoder_for_format(heif_compression_uncompressed)) { + SKIP("Skipping test because uncompressed encoder is not compiled."); + } + + constexpr uint32_t W = 8; + constexpr uint32_t H = 8; + + // --- 1. Build the image programmatically. + heif_image* image = nullptr; + heif_error err = heif_image_create(W, H, heif_colorspace_filter_array, + heif_chroma_planar, &image); + REQUIRE(err.code == heif_error_Ok); + + uint32_t fa_id = 0; + err = heif_image_add_component(image, W, H, + heif_cmpd_component_type_filter_array, + heif_component_datatype_unsigned_integer, 8, + &fa_id); + REQUIRE(err.code == heif_error_Ok); + + // Fill the filter-array plane with arbitrary data. + size_t fa_stride = 0; + uint8_t* fa = heif_image_get_component(image, fa_id, &fa_stride); + REQUIRE(fa != nullptr); + for (uint32_t y = 0; y < H; y++) { + for (uint32_t x = 0; x < W; x++) { + fa[y * fa_stride + x] = static_cast((x * 31 + y * 17) & 0xFF); + } + } + + // Reference components for R, G, B (one each — G appears twice in the + // pattern but they share a single cmpd entry). + uint32_t r_id = 0, g_id = 0, b_id = 0; + err = heif_image_add_bayer_component(image, heif_cmpd_component_type_red, &r_id); REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_bayer_component(image, heif_cmpd_component_type_green, &g_id); REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_bayer_component(image, heif_cmpd_component_type_blue, &b_id); REQUIRE(err.code == heif_error_Ok); + + // 2x2 RGGB pattern; pixels[1] and pixels[2] both point at g_id. + heif_bayer_pattern_pixel pattern[4] = { + { r_id, 1.0f }, + { g_id, 1.0f }, + { g_id, 1.0f }, + { b_id, 1.0f }, + }; + err = heif_image_set_bayer_pattern(image, fa_id, /*pattern_w=*/2, /*pattern_h=*/2, pattern); + REQUIRE(err.code == heif_error_Ok); + + // --- 2. Encode + write. + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_encoding_options* enc_options = heif_encoding_options_alloc(); + err = heif_context_encode_image(ctx, image, encoder, enc_options, nullptr); + REQUIRE(err.code == heif_error_Ok); + heif_encoding_options_free(enc_options); + + std::string out_path = get_tests_output_file_path("cpat_rggb.heif"); + err = heif_context_write_to_file(ctx, out_path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); + + // --- 3. Read back and verify the handle's component descriptions. + heif_context* ctx2 = heif_context_alloc(); + err = heif_context_read_from_file(ctx2, out_path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx2, &handle); + REQUIRE(err.code == heif_error_Ok); + + // 4 components: filter_array, R, G, B (G shared between the two pattern + // positions — populate dedups by cmpd index). + REQUIRE(heif_image_handle_get_number_of_components(handle) == 4); + + uint32_t handle_ids[4]; + heif_image_handle_get_used_component_ids(handle, handle_ids); + + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[0]) == heif_cmpd_component_type_filter_array); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[1]) == heif_cmpd_component_type_red); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[2]) == heif_cmpd_component_type_green); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[3]) == heif_cmpd_component_type_blue); + + REQUIRE(heif_image_handle_get_component_bits_per_pixel(handle, handle_ids[0]) == 8); + + // --- 4. Decode and confirm that ids agree end-to-end. + heif_image* decoded = nullptr; + err = heif_decode_image(handle, &decoded, heif_colorspace_undefined, + heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(heif_image_get_number_of_used_components(decoded) == 4); + uint32_t img_ids[4]; + heif_image_get_used_component_ids(decoded, img_ids); + for (uint32_t i = 0; i < 4; i++) { + INFO("component index " << i); + REQUIRE(img_ids[i] == handle_ids[i]); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[i]) == + heif_image_get_component_type(decoded, img_ids[i])); + } + + heif_image_release(decoded); + heif_image_handle_release(handle); + heif_context_free(ctx2); +} + + +// The following three test cases load files in the low-overhead MINI box format +// (lightning_mini.heif, simple_osm_tile_meta.avif, simple_osm_tile_alpha.avif). + +TEST_CASE("HEVC YUV 4:2:0 component IDs match between handle and decoded image") +{ + if (!heif_have_decoder_for_format(heif_compression_HEVC)) { + SKIP("Skipping test because HEVC decoder is not available."); + } + + // lightning_mini.heif: 256x256, YCbCr 4:2:0, 8-bit, no alpha. + auto* context = get_context_for_test_file("lightning_mini.heif"); + auto* handle = get_primary_image_handle(context); + + // The handle should report exactly 3 components for a YUV 4:2:0 image. + REQUIRE(heif_image_handle_get_number_of_components(handle) == 3); + + uint32_t ids[3]; + heif_image_handle_get_used_component_ids(handle, ids); + + // Canonical ordering. + REQUIRE(heif_image_handle_get_component_type(handle, ids[0]) == heif_cmpd_component_type_Y); + REQUIRE(heif_image_handle_get_component_type(handle, ids[1]) == heif_cmpd_component_type_Cb); + REQUIRE(heif_image_handle_get_component_type(handle, ids[2]) == heif_cmpd_component_type_Cr); + + // 8 bits per channel. + for (uint32_t id : ids) { + REQUIRE(heif_image_handle_get_component_bits_per_pixel(handle, id) == 8); + REQUIRE(heif_image_handle_get_component_datatype(handle, id) == heif_component_datatype_unsigned_integer); + } + + // IDs and metadata must match what the decoded image reports. + check_handle_matches_decoded_image(handle, heif_compression_HEVC); + + heif_image_handle_release(handle); + heif_context_free(context); +} + + +TEST_CASE("AVIF YUV 4:4:4 component IDs match between handle and decoded image") +{ + if (!heif_have_decoder_for_format(heif_compression_AV1)) { + SKIP("Skipping test because AV1 decoder is not available."); + } + + // simple_osm_tile_meta.avif: 256x256, YCbCr 4:4:4, 8-bit, no alpha. + auto* context = get_context_for_test_file("simple_osm_tile_meta.avif"); + auto* handle = get_primary_image_handle(context); + + REQUIRE(heif_image_handle_get_number_of_components(handle) == 3); + + uint32_t ids[3]; + heif_image_handle_get_used_component_ids(handle, ids); + + REQUIRE(heif_image_handle_get_component_type(handle, ids[0]) == heif_cmpd_component_type_Y); + REQUIRE(heif_image_handle_get_component_type(handle, ids[1]) == heif_cmpd_component_type_Cb); + REQUIRE(heif_image_handle_get_component_type(handle, ids[2]) == heif_cmpd_component_type_Cr); + + for (uint32_t id : ids) { + REQUIRE(heif_image_handle_get_component_bits_per_pixel(handle, id) == 8); + REQUIRE(heif_image_handle_get_component_datatype(handle, id) == heif_component_datatype_unsigned_integer); + } + + check_handle_matches_decoded_image(handle, heif_compression_AV1); + + heif_image_handle_release(handle); + heif_context_free(context); +} + + +TEST_CASE("HEIC YUV 4:2:0 (rainbow-451x461) component IDs match") +{ + if (!heif_have_decoder_for_format(heif_compression_HEVC)) { + SKIP("Skipping test because HEVC decoder is not available."); + } + + // rainbow-451x461.heic: 451x461 displayed dims, ispe 452x462 (HEVC + // macroblock-aligned, with a clap producing the displayed size). + // Test the handle-side queries and confirm IDs match post-decode. + auto* context = get_context_for_test_file("rainbow-451x461.heic"); + auto* handle = get_primary_image_handle(context); + + // Displayed dims (post-clap). + REQUIRE(heif_image_handle_get_width(handle) == 451); + REQUIRE(heif_image_handle_get_height(handle) == 461); + // Coded ispe dims. + REQUIRE(heif_image_handle_get_ispe_width(handle) == 452); + REQUIRE(heif_image_handle_get_ispe_height(handle) == 462); + + REQUIRE(heif_image_handle_get_number_of_components(handle) == 3); + uint32_t ids[3]; + heif_image_handle_get_used_component_ids(handle, ids); + + REQUIRE(heif_image_handle_get_component_type(handle, ids[0]) == heif_cmpd_component_type_Y); + REQUIRE(heif_image_handle_get_component_type(handle, ids[1]) == heif_cmpd_component_type_Cb); + REQUIRE(heif_image_handle_get_component_type(handle, ids[2]) == heif_cmpd_component_type_Cr); + + for (uint32_t id : ids) { + REQUIRE(heif_image_handle_get_component_bits_per_pixel(handle, id) == 8); + REQUIRE(heif_image_handle_get_component_datatype(handle, id) == heif_component_datatype_unsigned_integer); + } + + // Decoded image's component IDs and per-id metadata must match. + check_handle_matches_decoded_image(handle, heif_compression_HEVC); + + heif_image_handle_release(handle); + heif_context_free(context); +} + + +// Common shape for "image has alpha" tests: handle reports four components +// (Y/Cb/Cr/Alpha), and the decoded image returns the same four IDs in the +// same order. The Alpha description on the handle is added by +// ImageItem::set_alpha_channel(), populated when the iref/auxl reference +// is resolved at file-load time. +static void check_alpha_handle_matches_decoded_image(const char* fixture, + heif_compression_format codec) +{ + auto* context = get_context_for_test_file(fixture); + auto* handle = get_primary_image_handle(context); + REQUIRE(heif_image_handle_has_alpha_channel(handle) == 1); + + REQUIRE(heif_image_handle_get_number_of_components(handle) == 4); + uint32_t handle_ids[4]; + heif_image_handle_get_used_component_ids(handle, handle_ids); + + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[0]) == heif_cmpd_component_type_Y); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[1]) == heif_cmpd_component_type_Cb); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[2]) == heif_cmpd_component_type_Cr); + REQUIRE(heif_image_handle_get_component_type(handle, handle_ids[3]) == heif_cmpd_component_type_alpha); + + // The whole point of this round of fixes: the post-decode image carries + // the same four IDs the handle reports, including the alpha id. + check_handle_matches_decoded_image(handle, codec); + + heif_image_handle_release(handle); + heif_context_free(context); +} + + +TEST_CASE("HEIC YUV 4:2:0 with alpha: handle exposes alpha as the 4th component") +{ + if (!heif_have_decoder_for_format(heif_compression_HEVC)) { + SKIP("Skipping test because HEVC decoder is not available."); + } + // with-alpha-512x512.heic: HEIC with alpha-from-aux item. + check_alpha_handle_matches_decoded_image("with-alpha-512x512.heic", + heif_compression_HEVC); +} + + +TEST_CASE("AVIF with alpha: handle exposes alpha as the 4th component") +{ + if (!heif_have_decoder_for_format(heif_compression_AV1)) { + SKIP("Skipping test because AV1 decoder is not available."); + } + // simple_osm_tile_alpha.avif: AVIF with alpha-from-aux item. + check_alpha_handle_matches_decoded_image("simple_osm_tile_alpha.avif", + heif_compression_AV1); +} + diff -Nru libheif-1.19.8/tests/conversion.cc libheif-1.23.4/tests/conversion.cc --- libheif-1.19.8/tests/conversion.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/conversion.cc 2026-09-06 14:45:17.000000000 +0000 @@ -27,7 +27,8 @@ #include #include "catch_amalgamated.hpp" #include "color-conversion/colorconversion.h" -#include "pixelimage.h" +#include "color-conversion/hdr_sdr.h" +#include "image/pixelimage.h" #include // Enable for more verbose test output. @@ -69,14 +70,14 @@ template std::string PrintChannel(const HeifPixelImage& image, heif_channel channel) { heif_chroma chroma = image.get_chroma_format(); - int num_interleaved = num_interleaved_pixels_per_plane(chroma); + int num_interleaved = num_interleaved_components_per_plane(chroma); bool is_interleaved = num_interleaved > 1; uint32_t max_cols = is_interleaved ? 3 : 10; uint32_t max_rows = 10; uint32_t width = std::min(image.get_width(channel), max_cols); uint32_t height = std::min(image.get_height(channel), max_rows); size_t stride; - const T* p = (T*)image.get_plane(channel, &stride); + const T* p = (T*)image.get_channel_memory(channel, &stride); stride /= (int)sizeof(T); int bpp = image.get_bits_per_pixel(channel); int digits = (int)std::ceil(std::log10(1 << bpp)) + 1; @@ -129,13 +130,13 @@ size_t orig_stride; size_t compressed_stride; - const T* orig_p = (T*)original.get_plane(channel, &orig_stride); - const T* compressed_p = (T*)compressed.get_plane(channel, &compressed_stride); + const T* orig_p = (T*)original.get_channel_memory(channel, &orig_stride); + const T* compressed_p = (T*)compressed.get_channel_memory(channel, &compressed_stride); orig_stride /= (int)sizeof(T); compressed_stride /= (int)sizeof(T); double mse = 0.0; - int num_interleaved = num_interleaved_pixels_per_plane(chroma); + int num_interleaved = num_interleaved_components_per_plane(chroma); int alpha_max = (1 << original.get_bits_per_pixel(channel)) - 1; CAPTURE(expect_alpha_max); for (uint32_t y = 0; y < h; y++) { @@ -198,7 +199,10 @@ {heif_channel_Alpha, width, height, state.bits_per_pixel}); } } else if (state.colorspace == heif_colorspace_RGB) { + // heif_chroma_planar is a synonym for heif_chroma_444 in the RGB + // colorspace (canonicalized in HeifPixelImage::create). if (state.chroma != heif_chroma_444 && + state.chroma != heif_chroma_planar && state.chroma != heif_chroma_interleaved_RGB && state.chroma != heif_chroma_interleaved_RGBA && state.chroma != heif_chroma_interleaved_RRGGBB_BE && @@ -207,7 +211,7 @@ state.chroma != heif_chroma_interleaved_RRGGBBAA_LE) { return {}; } - if (state.chroma == heif_chroma_444) { + if (state.chroma == heif_chroma_444 || state.chroma == heif_chroma_planar) { planes.push_back({heif_channel_R, width, height, state.bits_per_pixel}); planes.push_back({heif_channel_G, width, height, state.bits_per_pixel}); planes.push_back({heif_channel_B, width, height, state.bits_per_pixel}); @@ -228,8 +232,7 @@ bool MakeTestImage(const ColorState& state, int width, int height, HeifPixelImage* image) { image->create(width, height, state.colorspace, state.chroma); - auto target_nclx_profile = std::make_shared(state.nclx_profile); - image->set_color_profile_nclx(target_nclx_profile); + image->set_color_profile_nclx(state.nclx); std::vector planes = GetPlanes(state, width, height); if (planes.empty()) return false; for (size_t i = 0; i < planes.size(); ++i) { @@ -237,7 +240,7 @@ int half_max = (1 << (plane.bit_depth -1)); uint16_t value = SwapBytesIfNeeded( static_cast(half_max + i * 10 + i), state.chroma); - auto err = image->fill_new_plane(plane.channel, value, plane.width, plane.height, + auto err = image->fill_new_channel(plane.channel, value, plane.width, plane.height, plane.bit_depth, nullptr); if (err) { return false; @@ -247,8 +250,8 @@ } static bool NclxMatches(heif_colorspace colorspace, - const color_profile_nclx& src_nclx, - const color_profile_nclx& dst_nclx) { + const nclx_profile& src_nclx, + const nclx_profile& dst_nclx) { if (colorspace != heif_colorspace_YCbCr) { return true; } @@ -262,16 +265,36 @@ return true; } +void nclx_default_if_undefined(ColorState& state) +{ + if (state.nclx.get_colour_primaries() == heif_color_primaries_unspecified) { + state.nclx.set_colour_primaries(1); + } + + if (state.nclx.get_matrix_coefficients() == heif_matrix_coefficients_unspecified) { + state.nclx.set_matrix_coefficients(6); + } + + if (state.nclx.get_transfer_characteristics() == heif_transfer_characteristic_unspecified) { + state.nclx.set_transfer_characteristics(13); + } +} + + // Converts from 'input_state' to 'target_state' and checks that the resulting // image has the expected shape. If the reverse conversion is also supported, // does a round-trip back to the original state and checks the PSNR. // If 'require_supported' is true, checks that the conversion from 'input_state' // to 'target_state' is supported, otherwise, exists silently for unsupported // conversions. -void TestConversion(const std::string& test_name, const ColorState& input_state, - const ColorState& target_state, +void TestConversion(const std::string& test_name, ColorState input_state, + ColorState target_state, const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext, bool require_supported = true) { + nclx_default_if_undefined(input_state); + nclx_default_if_undefined(target_state); + INFO(test_name); INFO("downsampling=" << options.preferred_chroma_downsampling_algorithm << " upsampling=" @@ -280,7 +303,7 @@ << (bool)options.only_use_preferred_chroma_algorithm); ColorConversionPipeline pipeline; - bool supported = pipeline.construct_pipeline(input_state, target_state, options); + bool supported = pipeline.construct_pipeline(input_state, target_state, options, options_ext); if (require_supported) REQUIRE(supported); if (!supported) return; INFO("conversion pipeline: " << pipeline.debug_dump_pipeline()); @@ -302,7 +325,7 @@ for (const Plane& plane : GetPlanes(target_state, width, height)) { INFO("Channel: " << plane.channel); size_t stride; - CHECK(out_image->get_plane(plane.channel, &stride) != nullptr); + CHECK(out_image->get_channel_memory(plane.channel, &stride) != nullptr); CHECK(out_image->get_bits_per_pixel(plane.channel) == target_state.bits_per_pixel); // If an alpha plane was created from nothing, check that it's filled @@ -317,7 +340,7 @@ // Convert back in the other direction (if supported). ColorConversionPipeline reverse_pipeline; - if (reverse_pipeline.construct_pipeline(target_state, input_state, options)) { + if (reverse_pipeline.construct_pipeline(target_state, input_state, options, options_ext)) { INFO("reverse pipeline: " << reverse_pipeline.debug_dump_pipeline()); auto recovered_image_result =reverse_pipeline.convert_image(out_image, heif_get_disabled_security_limits()); REQUIRE(recovered_image_result); @@ -333,8 +356,8 @@ input_state.chroma != heif_chroma_422 && target_state.chroma != heif_chroma_420 && target_state.chroma != heif_chroma_422)) && - NclxMatches(input_state.colorspace, input_state.nclx_profile, - target_state.nclx_profile); + NclxMatches(input_state.colorspace, input_state.nclx, + target_state.nclx); double expected_psnr = expect_lossless ? 100. : 38.; for (const Plane& plane : GetPlanes(input_state, width, height)) { @@ -362,7 +385,8 @@ void TestFailingConversion(const std::string& test_name, const ColorState& input_state, const ColorState& target_state, - const heif_color_conversion_options& options = {}) { + const heif_color_conversion_options& options, + const heif_color_conversion_options_ext& options_ext) { INFO(test_name); INFO("downsampling=" << options.preferred_chroma_downsampling_algorithm << " upsampling=" @@ -371,7 +395,7 @@ << (bool)options.only_use_preferred_chroma_algorithm); ColorConversionPipeline pipeline; bool construct_pipeline_res = - pipeline.construct_pipeline(input_state, target_state, options); + pipeline.construct_pipeline(input_state, target_state, options, options_ext); INFO("conversion pipeline: " << pipeline.debug_dump_pipeline()); REQUIRE_FALSE(construct_pipeline_res); } @@ -440,6 +464,11 @@ std::vector color_states; for (heif_colorspace colorspace : {heif_colorspace_YCbCr, heif_colorspace_RGB, heif_colorspace_monochrome}) { for (heif_chroma chroma : get_valid_chroma_values_for_colorspace(colorspace)) { + // heif_chroma_planar is a synonym for heif_chroma_444 under + // heif_colorspace_RGB; the canonical-form case is already iterated. + if (colorspace == heif_colorspace_RGB && chroma == heif_chroma_planar) { + continue; + } for (bool has_alpha : GetValidHasAlpha(chroma)) { for (int bits_per_pixel : GetValidBitsPerPixel(chroma)) { // Without nclx. @@ -450,11 +479,11 @@ if (colorspace == heif_colorspace_YCbCr) { for (heif_matrix_coefficients matrix : matrices) { for (bool full_range : {true, false}) { - color_state.nclx_profile.set_full_range_flag(full_range); - color_state.nclx_profile.set_matrix_coefficients(matrix); - color_state.nclx_profile.set_colour_primaries( + color_state.nclx.set_full_range_flag(full_range); + color_state.nclx.set_matrix_coefficients(matrix); + color_state.nclx.set_colour_primaries( heif_color_primaries_ITU_R_BT_709_5); - color_state.nclx_profile.set_transfer_characteristics( + color_state.nclx.set_transfer_characteristics( heif_color_primaries_SMPTE_240M); color_states.push_back(color_state); } @@ -485,6 +514,10 @@ .preferred_chroma_upsampling_algorithm = upsampling, .only_use_preferred_chroma_algorithm = only_use_preferred_chroma_algorithm}; + heif_color_conversion_options_ext options_ext = { + .alpha_composition_mode = heif_alpha_composition_mode_none + }; + // Test all source and destination state combinations. ColorState src_state = GENERATE( from_range(GetAllColorStates(GetSupportedMatrices()))); @@ -510,7 +543,7 @@ std::ostringstream os; os << "from: " << src_state << "\nto: " << dst_state; - TestConversion(os.str(), src_state, dst_state, options, require_supported); + TestConversion(os.str(), src_state, dst_state, options, options_ext, require_supported); } TEST_CASE("Unsupported matrices", "[heif_image]") { @@ -531,23 +564,27 @@ .preferred_chroma_upsampling_algorithm = upsampling, .only_use_preferred_chroma_algorithm = only_use_preferred_chroma_algorithm}; + heif_color_conversion_options_ext options_ext = { + .alpha_composition_mode = heif_alpha_composition_mode_none + }; + ColorState src_state = GENERATE(from_range(GetAllColorStates(GetUnsupportedMatrices()))); ColorState dst_state = GENERATE(from_range(GetAllColorStates(GetUnsupportedMatrices()))); - color_profile_nclx default_nclx; + nclx_profile default_nclx = nclx_profile::defaults(); if (src_state == dst_state || - NclxMatches(src_state.colorspace, src_state.nclx_profile, - dst_state.nclx_profile) || - (src_state.nclx_profile.get_matrix_coefficients() == default_nclx.get_matrix_coefficients() || - dst_state.nclx_profile.get_matrix_coefficients() == default_nclx.get_matrix_coefficients())) { + NclxMatches(src_state.colorspace, src_state.nclx, + dst_state.nclx) || + (src_state.nclx.get_matrix_coefficients() == default_nclx.get_matrix_coefficients() || + dst_state.nclx.get_matrix_coefficients() == default_nclx.get_matrix_coefficients())) { return; } std::ostringstream os; os << "from: " << src_state << "\nto: " << dst_state; - TestFailingConversion(os.str(), src_state, dst_state, options); + TestFailingConversion(os.str(), src_state, dst_state, options, options_ext); } TEST_CASE("Sharp yuv conversion", "[heif_image]") { @@ -557,63 +594,67 @@ .preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear, .only_use_preferred_chroma_algorithm = true}; + heif_color_conversion_options_ext options_ext = { + .alpha_composition_mode = heif_alpha_composition_mode_none + }; + #ifdef HAVE_LIBSHARPYUV TestConversion("### interleaved RGBA -> YCbCr 420 with sharp yuv", {heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8}, {heif_colorspace_YCbCr, heif_chroma_420, true, 8}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### interleaved RGB 10bit -> YCbCr 420 10bit with sharp yuv", {heif_colorspace_RGB, heif_chroma_interleaved_RGB, false, 8}, {heif_colorspace_YCbCr, heif_chroma_420, false, 8}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### interleaved RGBA 12bit big endian -> YCbCr 420 12bit with sharp yuv", {heif_colorspace_RGB, heif_chroma_interleaved_RRGGBBAA_BE, true, 12}, {heif_colorspace_YCbCr, heif_chroma_420, true, 12}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### interleaved RGBA 12bit little endian -> YCbCr 420 12bit with sharp yuv", {heif_colorspace_RGB, heif_chroma_interleaved_RRGGBBAA_LE, true, 12}, {heif_colorspace_YCbCr, heif_chroma_420, true, 12}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### planar RGB -> YCbCr 420 with sharp yuv", {heif_colorspace_RGB, heif_chroma_444, false, 8}, {heif_colorspace_YCbCr, heif_chroma_420, false, 8}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### planar RGBA -> YCbCr 420 with sharp yuv", {heif_colorspace_RGB, heif_chroma_444, true, 8}, {heif_colorspace_YCbCr, heif_chroma_420, true, 8}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### planar RGB 10bit -> YCbCr 420 10bit with sharp yuv", {heif_colorspace_RGB, heif_chroma_444, false, 10}, {heif_colorspace_YCbCr, heif_chroma_420, false, 10}, - sharp_yuv_options); + sharp_yuv_options, options_ext); TestConversion("### planar RGBA 10bit -> YCbCr 420 10bit with sharp yuv", {heif_colorspace_RGB, heif_chroma_444, true, 10}, {heif_colorspace_YCbCr, heif_chroma_420, true, 10}, - sharp_yuv_options); + sharp_yuv_options, options_ext); #else // Should fail if libsharpyuv is not compiled in. TestFailingConversion( "### interleaved RGBA -> YCbCr 420 with sharp yuv NOT COMPILED IN", {heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8}, - {heif_colorspace_YCbCr, heif_chroma_420, false, 8}, sharp_yuv_options); + {heif_colorspace_YCbCr, heif_chroma_420, false, 8}, sharp_yuv_options, options_ext); WARN("Tests built without sharp yuv"); #endif TestFailingConversion( "### interleaved RGBA -> YCbCr 422 with sharp yuv (not supported!)", {heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8}, - {heif_colorspace_YCbCr, heif_chroma_422, false, 8}, sharp_yuv_options); + {heif_colorspace_YCbCr, heif_chroma_422, false, 8}, sharp_yuv_options, options_ext); } static void fill_plane(std::shared_ptr& img, heif_channel channel, int w, int h, const std::vector& pixels) { - auto error = img->add_plane(channel, w, h, 8, nullptr); + auto error = img->add_channel(channel, w, h, 8, nullptr); REQUIRE(!error); size_t stride; - uint8_t* p = img->get_plane(channel, &stride); + uint8_t* p = img->get_channel_memory(channel, &stride); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { @@ -630,7 +671,7 @@ uint32_t h = img->get_height(channel); size_t stride; - uint8_t* p = img->get_plane(channel, &stride); + uint8_t* p = img->get_channel_memory(channel, &stride); for (uint32_t y = 0; y < h; y++) { INFO("row: " << y); @@ -651,7 +692,7 @@ std::shared_ptr img = std::make_shared(); img->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420); - auto error = img->fill_new_plane(heif_channel_Y, 128, 4,4, 8, nullptr); + auto error = img->fill_new_channel(heif_channel_Y, 128, 4,4, 8, nullptr); REQUIRE(!error); fill_plane(img, heif_channel_Cb, 2,2, @@ -661,7 +702,8 @@ {255, 200, 50, 0}); - auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, nullptr, 8, options, heif_get_disabled_security_limits()); + auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile::defaults(), 8, options, nullptr, heif_get_disabled_security_limits()); REQUIRE(conversionResult); std::shared_ptr out = *conversionResult; @@ -692,20 +734,20 @@ const uint32_t height = 2; img->create(width, height, heif_colorspace_RGB, heif_chroma_444); Error err; - err = img->add_plane(heif_channel_R, width, height, 5, heif_get_disabled_security_limits()); + err = img->add_channel(heif_channel_R, width, height, 5, heif_get_disabled_security_limits()); REQUIRE(!err); REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 5); - err = img->add_plane(heif_channel_G, width, height, 6, heif_get_disabled_security_limits()); + err = img->add_channel(heif_channel_G, width, height, 6, heif_get_disabled_security_limits()); REQUIRE(!err); REQUIRE(img->get_bits_per_pixel(heif_channel_G) == 6); - err = img->add_plane(heif_channel_B, width, height, 5, heif_get_disabled_security_limits()); + err = img->add_channel(heif_channel_B, width, height, 5, heif_get_disabled_security_limits()); REQUIRE(!err); REQUIRE(img->get_bits_per_pixel(heif_channel_B) == 5); uint8_t v = 1; for (heif_channel plane: {heif_channel_R, heif_channel_G, heif_channel_B}) { size_t dst_stride = 0; - uint8_t *dst = img->get_plane(plane, &dst_stride); + uint8_t *dst = img->get_channel_memory(plane, &dst_stride); for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < width; x++) { dst[y * dst_stride + x] = v; @@ -714,7 +756,8 @@ } } - auto conversionResult = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_444, nullptr, 8, options, heif_get_disabled_security_limits()); + auto conversionResult = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_444, + nclx_profile::defaults(), 8, options, nullptr, heif_get_disabled_security_limits()); REQUIRE(conversionResult); std::shared_ptr out = *conversionResult; @@ -722,3 +765,328 @@ assert_plane(out, heif_channel_G, {28, 32, 36, 40, 44, 48}); assert_plane(out, heif_channel_B, {107, 115, 123, 132, 140, 148}); } + + +TEST_CASE("Mismatched alpha bit depth - pipeline construction") { + heif_color_conversion_options options{}; + options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average; + options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + + std::unique_ptr + options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free); + + SECTION("10-bit color, 8-bit alpha -> interleaved RGBA 8-bit") { + ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 10); + input_state.alpha_bits_per_pixel = 8; + nclx_default_if_undefined(input_state); + + ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8); + + ColorConversionPipeline pipeline; + bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext); + INFO("pipeline: " << pipeline.debug_dump_pipeline()); + REQUIRE(supported); + } + + SECTION("8-bit color, 10-bit alpha -> interleaved RGBA 8-bit") { + ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 8); + input_state.alpha_bits_per_pixel = 10; + nclx_default_if_undefined(input_state); + + ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8); + + ColorConversionPipeline pipeline; + bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext); + INFO("pipeline: " << pipeline.debug_dump_pipeline()); + REQUIRE(supported); + } + + SECTION("10-bit color, 8-bit alpha -> planar RGB 10-bit") { + ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 10); + input_state.alpha_bits_per_pixel = 8; + nclx_default_if_undefined(input_state); + + ColorState target_state(heif_colorspace_RGB, heif_chroma_444, true, 10); + target_state.alpha_bits_per_pixel = 10; + + ColorConversionPipeline pipeline; + bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext); + INFO("pipeline: " << pipeline.debug_dump_pipeline()); + REQUIRE(supported); + } +} + + +TEST_CASE("Mismatched alpha bit depth - conversion correctness") { + heif_color_conversion_options options{}; + + SECTION("10-bit RGB color with 8-bit alpha -> interleaved RGBA 8-bit") { + const uint32_t width = 4; + const uint32_t height = 2; + + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_RGB, heif_chroma_444); + + // Create 10-bit color planes filled with a known value (512 = mid-range for 10-bit) + img->fill_new_channel(heif_channel_R, 512, width, height, 10, nullptr); + img->fill_new_channel(heif_channel_G, 256, width, height, 10, nullptr); + img->fill_new_channel(heif_channel_B, 768, width, height, 10, nullptr); + // Create 8-bit alpha plane filled with 200 + img->fill_new_channel(heif_channel_Alpha, 200, width, height, 8, nullptr); + + // Verify the mismatch + REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 10); + REQUIRE(img->get_bits_per_pixel(heif_channel_Alpha) == 8); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, + nclx_profile::defaults(), 8, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_colorspace() == heif_colorspace_RGB); + CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGBA); + + // Verify the output has correct interleaved pixel values + size_t stride; + const uint8_t* p = out->get_channel_memory(heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + // Check first pixel: R=512>>2=128, G=256>>2=64, B=768>>2=192, A=200 + CHECK(p[0] == 128); // R + CHECK(p[1] == 64); // G + CHECK(p[2] == 192); // B + CHECK(p[3] == 200); // A (preserved from 8-bit alpha) + } + + SECTION("8-bit RGB color with 10-bit alpha -> interleaved RGBA 8-bit") { + const uint32_t width = 4; + const uint32_t height = 2; + + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_RGB, heif_chroma_444); + + // Create 8-bit color planes + img->fill_new_channel(heif_channel_R, 128, width, height, 8, nullptr); + img->fill_new_channel(heif_channel_G, 64, width, height, 8, nullptr); + img->fill_new_channel(heif_channel_B, 192, width, height, 8, nullptr); + // Create 10-bit alpha plane (value 800 -> 800>>2 = 200 when converted to 8-bit) + img->fill_new_channel(heif_channel_Alpha, 800, width, height, 10, nullptr); + + REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 8); + REQUIRE(img->get_bits_per_pixel(heif_channel_Alpha) == 10); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, + nclx_profile::defaults(), 8, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_colorspace() == heif_colorspace_RGB); + CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGBA); + + size_t stride; + const uint8_t* p = out->get_channel_memory(heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + CHECK(p[0] == 128); // R (unchanged) + CHECK(p[1] == 64); // G (unchanged) + CHECK(p[2] == 192); // B (unchanged) + CHECK(p[3] == 200); // A (10-bit 800 >> 2 = 200) + } + +#ifdef HAVE_LIBSHARPYUV + // Regression test for OSS-Fuzz 6503781601443840 (file_fuzzer, ASan + // heap-buffer-overflow READ in Op_Any_RGB_to_YCbCr_420_Sharp). + // + // Op_YCbCr_to_RGB copies the alpha plane through at its own bit depth while + // converting the color channels, so a decoded HEIC with 10-bit color and an 8-bit + // alpha auxiliary image produces exactly this planar RGB state. The sharp-yuv + // operator then read the alpha plane with the sample width and step taken from the + // color channels: it walked a 1-byte-per-sample plane with a step of 2 and read two + // bytes per sample, running off the end of the plane. Every other RGB operator + // declines a mismatched alpha depth, which lets Op_adjust_alpha_bit_depth normalize + // the plane first; the sharp operator was missing that guard. + // + // The image must be big enough that the doubled indexing leaves the allocation + // rather than landing in the stride padding: 64x64 (the size of the original PoC) + // over-reads, a small image like the 4x2 above would not. + SECTION("10-bit RGB color with 8-bit alpha -> YCbCr 420 with sharp yuv") { + const uint32_t width = 64; + const uint32_t height = 64; + + heif_color_conversion_options sharp_options{}; + sharp_options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_sharp_yuv; + sharp_options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear; + sharp_options.only_use_preferred_chroma_algorithm = true; + + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_RGB, heif_chroma_444); + + img->fill_new_channel(heif_channel_R, 512, width, height, 10, nullptr); + img->fill_new_channel(heif_channel_G, 256, width, height, 10, nullptr); + img->fill_new_channel(heif_channel_B, 768, width, height, 10, nullptr); + img->fill_new_channel(heif_channel_Alpha, 200, width, height, 8, nullptr); + + REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 10); + REQUIRE(img->get_bits_per_pixel(heif_channel_Alpha) == 8); + + nclx_profile target_nclx = nclx_profile::defaults(); + target_nclx.set_matrix_coefficients(heif_matrix_coefficients_ITU_R_BT_601_6); + + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + target_nclx, 10, sharp_options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_colorspace() == heif_colorspace_YCbCr); + CHECK(out->get_chroma_format() == heif_chroma_420); + REQUIRE(out->has_channel(heif_channel_Alpha)); + CHECK(out->get_bits_per_pixel(heif_channel_Alpha) == 10); + + // The 8-bit alpha is widened to 10 bits by bit replication before the sharp + // conversion runs: 200 -> (200 << 2) | (200 >> 6) = 803. + size_t stride; + const uint8_t* p_a = out->get_channel_memory(heif_channel_Alpha, &stride); + REQUIRE(p_a != nullptr); + const uint16_t* a16 = reinterpret_cast(p_a); + CHECK(a16[0] == 803); + CHECK(a16[(height - 1) * (stride / 2) + (width - 1)] == 803); + } +#endif +} + + +// Regression test for GHSA-8857-r8x5-7499. Op_to_hdr_planes widens an 8-bit input to a higher +// bit depth with out = (in << (m-8)) | (in >> (16-m)). For m > 16 the right shift exponent +// (16-m) becomes negative, which is undefined behavior (UBSan: "shift exponent is negative"), +// and m > 16 also does not fit the uint16_t output plane. The operation must only offer/perform +// the conversion for 8 < m <= 16. +TEST_CASE("Op_to_hdr_planes rejects out-of-range output bit depth", "[heif_image]") +{ + heif_color_conversion_options options{}; + std::unique_ptr + options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free); + + ColorState input_state(heif_colorspace_YCbCr, heif_chroma_444, false, 8); + nclx_default_if_undefined(input_state); + + const uint32_t width = 4; + const uint32_t height = 4; + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_YCbCr, heif_chroma_444); + img->fill_new_channel(heif_channel_Y, 0xAB, width, height, 8, nullptr); + img->fill_new_channel(heif_channel_Cb, 0xAB, width, height, 8, nullptr); + img->fill_new_channel(heif_channel_Cr, 0xAB, width, height, 8, nullptr); + + Op_to_hdr_planes op; + + SECTION("supported target bit depths widen correctly") { + for (int out_bits : {9, 10, 16}) { + ColorState target_state(heif_colorspace_YCbCr, heif_chroma_444, false, out_bits); + nclx_default_if_undefined(target_state); + + auto states = op.state_after_conversion(input_state, target_state, options, *options_ext); + REQUIRE(states.size() == 1); + + auto result = op.convert_colorspace(img, input_state, target_state, options, *options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + size_t stride; + const uint16_t* p = (const uint16_t*) (*result)->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p != nullptr); + const uint16_t expected = (uint16_t) ((0xAB << (out_bits - 8)) | (0xAB >> (16 - out_bits))); + CHECK(p[0] == expected); + } + } + + SECTION("out-of-range target bit depths are rejected without UB") { + for (int out_bits : {17, 20, 24, 32}) { + ColorState target_state(heif_colorspace_YCbCr, heif_chroma_444, false, out_bits); + nclx_default_if_undefined(target_state); + + // The pipeline must not select this operation for an unsupported target. + auto states = op.state_after_conversion(input_state, target_state, options, *options_ext); + CHECK(states.empty()); + + // A direct call must return an error instead of performing the negative shift. + auto result = op.convert_colorspace(img, input_state, target_state, options, *options_ext, + heif_get_disabled_security_limits()); + CHECK_FALSE(result); + } + } +} + + +// Regression test for GHSA-2c3g-p585-8rpq. Op_RGB24_32_to_YCbCr (like the other conversion +// operations) used to compute the row offset y * stride in 32-bit int, which overflows as soon +// as a plane exceeds 2 GB and made the conversion read from a wild address. All internal +// strides are size_t since v1.19.6. Reproducing the overflow inherently needs a plane of more +// than 2 GB (about 5 GB in total for this test), so the test is hidden from the default run. +// Run it explicitly with: +// ./tests/conversion "[large-memory]" +TEST_CASE("RGB24 to YCbCr conversion with planes larger than 2 GB", "[.large-memory]") +{ + // Same size as the advisory PoC. The row stride is about 98 KB, so y * stride exceeds + // INT32_MAX for every row beyond roughly 21850. + const uint32_t w = 32767; + const uint32_t h = 32767; + + // Creates an interleaved RGB image with a red first row and a blue last row. + auto make_image = [](uint32_t width, uint32_t height) { + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_RGB, heif_chroma_interleaved_RGB); + auto err = img->add_channel(heif_channel_interleaved, width, height, 8, nullptr); + REQUIRE(!err); + + size_t stride; + uint8_t* first = img->get_channel_memory(heif_channel_interleaved, &stride); + uint8_t* last = first + static_cast(height - 1) * stride; + for (uint32_t x = 0; x < width; x++) { + first[3 * x + 0] = 255; + first[3 * x + 1] = 0; + first[3 * x + 2] = 0; + last[3 * x + 0] = 0; + last[3 * x + 1] = 0; + last[3 * x + 2] = 255; + } + return img; + }; + + // Force nearest-neighbor chroma downsampling: this selects the single-step, per-pixel + // Op_RGB24_32_to_YCbCr from the advisory. The default (sharp yuv, if libsharpyuv is + // available) adjusts luma based on the neighboring rows, which are left uninitialized here. + heif_color_conversion_options options; + heif_color_conversion_options_set_defaults(&options); + options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_nearest_neighbor; + options.only_use_preferred_chroma_algorithm = true; + + auto convert = [&](const std::shared_ptr& img) { + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + nclx_profile::defaults(), 8, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + return *result; + }; + + auto big = make_image(w, h); + size_t in_stride; + big->get_channel_memory(heif_channel_interleaved, &in_stride); + REQUIRE(static_cast(in_stride) * (h - 1) > INT32_MAX); // the last row is only reachable with 64-bit offsets + + auto big_out = convert(big); + big.reset(); + auto small_out = convert(make_image(2, 2)); + + // The first and the last row of the large image must convert to the same luma values as + // the two rows of the small reference image. + size_t big_stride, small_stride; + const uint8_t* big_y = big_out->get_channel_memory(heif_channel_Y, &big_stride); + const uint8_t* small_y = small_out->get_channel_memory(heif_channel_Y, &small_stride); + REQUIRE(big_y[0] == small_y[0]); + REQUIRE(big_y[w - 1] == small_y[0]); + REQUIRE(big_y[static_cast(h - 1) * big_stride] == small_y[small_stride]); + REQUIRE(big_y[static_cast(h - 1) * big_stride + (w - 1)] == small_y[small_stride]); + REQUIRE(small_y[0] != small_y[small_stride]); +} diff -Nru libheif-1.19.8/tests/crop_plane_checks.cc libheif-1.23.4/tests/crop_plane_checks.cc --- libheif-1.19.8/tests/crop_plane_checks.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/crop_plane_checks.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,107 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "image/pixelimage.h" +#include "catch_amalgamated.hpp" + +// Regression tests for HeifPixelImage::crop(). +// +// crop() validates the requested rectangle against the image's declared +// m_width/m_height, maps it into per-plane coordinates via +// get_subsampled_size_h/v(), and hands it to ComponentStorage::crop(), which +// does a row-wise bulk memcpy. None of that checks that a plane's actual +// stored size matches what m_width/m_height (or, for Cb/Cr, their +// chroma-subsampled size) implies -- a plane can be added at any size through +// add_channel()/copy_new_channel_from()/transfer_channel_from_image_as(). A +// plane smaller than the image claims is read past its end. crop() now calls +// has_standard_plane_sizes() up front to reject that instead. + +TEST_CASE("crop rejects a plane smaller than the image's declared size") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(8, 8, heif_colorspace_monochrome, heif_chroma_monochrome); + + // Y plane smaller than the image's declared 8x8 -- add_channel() doesn't + // check this against m_width/m_height. + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + auto result = image->crop(0, 3, 0, 3, limits); + REQUIRE(result.is_error()); + REQUIRE(result.error().error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("crop crops a well-formed monochrome image correctly") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + size_t stride; + uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride); + for (uint32_t row = 0; row < 4; row++) { + for (uint32_t col = 0; col < 4; col++) { + y[row * stride + col] = static_cast(row * 4 + col); + } + } + + // Keep the inner 2x2 region: columns/rows 1..2. + auto result = image->crop(1, 2, 1, 2, limits); + REQUIRE(!result.is_error()); + + auto cropped = *result; + REQUIRE(cropped->get_width(heif_channel_Y) == 2); + REQUIRE(cropped->get_height(heif_channel_Y) == 2); + + const uint8_t* out = cropped->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(out[0 * stride + 0] == 5); // (1,1) + REQUIRE(out[0 * stride + 1] == 6); // (2,1) + REQUIRE(out[1 * stride + 0] == 9); // (1,2) + REQUIRE(out[1 * stride + 1] == 10); // (2,2) +} + +TEST_CASE("crop crops a well-formed YCbCr 4:2:0 image correctly") { + auto* limits = heif_get_global_security_limits(); + + // Cb/Cr are legitimately half-size in 4:2:0 -- has_standard_plane_sizes() + // must not reject that. + auto image = std::make_shared(); + image->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 2, 2, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 2, 2, 8, limits).error_code == heif_error_Ok); + + // Crop to an even-aligned 2x2 region so no 4:4:4 upconversion is triggered. + auto result = image->crop(0, 1, 0, 1, limits); + REQUIRE(!result.is_error()); + + auto cropped = *result; + REQUIRE(cropped->get_width(heif_channel_Y) == 2); + REQUIRE(cropped->get_height(heif_channel_Y) == 2); + REQUIRE(cropped->get_width(heif_channel_Cb) == 1); + REQUIRE(cropped->get_height(heif_channel_Cb) == 1); +} diff -Nru libheif-1.19.8/tests/cxx_wrapper.cc libheif-1.23.4/tests/cxx_wrapper.cc --- libheif-1.19.8/tests/cxx_wrapper.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/cxx_wrapper.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,46 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2025 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "test_utils.h" +#include +#include + +TEST_CASE( "C++ object copy (ColorProfile_nclx)" ) +{ + // This is the reproducer from issue #1641 + + heif::Image img; + + img.create(16,16, heif_colorspace_RGB, heif_chroma_interleaved_RGBA); + + heif::ColorProfile_nclx nclx; + nclx.set_matrix_coefficients(heif_matrix_coefficients_SMPTE_240M); + img.set_nclx_color_profile(nclx); + + heif::ColorProfile_nclx a = img.get_nclx_color_profile(); + heif::ColorProfile_nclx b(a); +} \ No newline at end of file Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_cropped.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_cropped.heic and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped.heic differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_cropped_irot_imir.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_cropped_irot_imir.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_oversized_ispe_height.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_height.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_oversized_ispe_irot180.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_irot180.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_oversized_ispe_irot90.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_irot90.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/clap_oversized_ispe_width.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/clap_oversized_ispe_width.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/conformance_window_padding.heic and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/conformance_window_padding.heic differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/lightning_mini.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/lightning_mini.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/meta_size_zero.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/meta_size_zero.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/mini_size_zero.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/mini_size_zero.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/moov_size_zero.heif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/moov_size_zero.heif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/rainbow-451x461.heic and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/rainbow-451x461.heic differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/simple_osm_tile_alpha.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/simple_osm_tile_alpha.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/simple_osm_tile_meta.avif and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/simple_osm_tile_meta.avif differ Binary files /srv/release.debian.org/tmp/IThaNObMdm/libheif-1.19.8/tests/data/with-alpha-512x512.heic and /srv/release.debian.org/tmp/cRfkxi0GAZ/libheif-1.23.4/tests/data/with-alpha-512x512.heic differ diff -Nru libheif-1.19.8/tests/decompression_bomb.cc libheif-1.23.4/tests/decompression_bomb.cc --- libheif-1.19.8/tests/decompression_bomb.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/decompression_bomb.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,226 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-24wx-9w62-c96w: a compressed ('mime', content_encoding +// "br") metadata item is a decompression bomb. Opening the file decompresses the +// item in HeifContext::interpret_heif_file_images(), which used to grow an output +// vector with no size accounting at all, bypassing the configured security limits +// and allowing OOM from a few hundred bytes of input. +// +// The test builds a tiny HEIF whose 'mime' item is a real brotli payload that +// expands to tens of MB, then opens it once with a tight total-memory limit +// (expecting a security-limit error) and once with the default limits (expecting +// success, proving the file itself is valid and it is the limit that is enforced). + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" +#include "compression.h" + +#include +#include +#include + +namespace { + +// Build a minimal HEIF file with two items: +// item 1 ('mski', 8x8, 8bpp): the primary image. The mask codec needs no +// bitstream (its "compressed" data is just the raw pixel bytes), so no +// codec plugin is required and the file opens without decoding. +// item 2 ('mime', content_encoding "br"): a brotli-compressed metadata item, +// 'cdsc'-referencing item 1, carrying the supplied bomb payload. +std::vector build_heif_with_brotli_mime(const std::vector& brotli_payload) { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + // pitm: primary item = item 1 (the 'mski' image). + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'mski', item 2 = 'mime'. + std::vector infe1_payload; + put_u16_be(infe1_payload, 1); + put_u16_be(infe1_payload, 0); + append_fourcc(infe1_payload, "mski"); + infe1_payload.push_back(0); // item_name (empty) + auto infe1 = make_box("infe", infe1_payload, /*full=*/true, /*version=*/2); + + std::vector infe2_payload; + put_u16_be(infe2_payload, 2); + put_u16_be(infe2_payload, 0); + append_fourcc(infe2_payload, "mime"); + append_cstr(infe2_payload, ""); // item_name + append_cstr(infe2_payload, "application/octet-stream"); // content_type + append_cstr(infe2_payload, "br"); // content_encoding + auto infe2 = make_box("infe", infe2_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 2); + append(iinf_payload, infe1); + append(iinf_payload, infe2); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iprp / ipco: prop 1 = ispe(8,8), prop 2 = mskC(8bpp), both for item 1. + std::vector ispe_payload; + put_u32_be(ispe_payload, 8); + put_u32_be(ispe_payload, 8); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(8); // bits_per_pixel + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, mskC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(2); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, mskC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat: 64 bytes of raw 8x8 8bpp mask data, followed by the brotli bomb payload. + std::vector idat_payload; + idat_payload.reserve(64 + brotli_payload.size()); + idat_payload.resize(64, 0x7F); + const uint32_t bomb_offset = static_cast(idat_payload.size()); + const uint32_t bomb_length = static_cast(brotli_payload.size()); + append(idat_payload, brotli_payload); + auto idat = make_box("idat", idat_payload); + + // iloc (version 1): both items store their data in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 2); // item_count + // item 1: mask pixels + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, 64); // extent_length + // item 2: brotli bomb + put_u16_be(iloc_payload, 2); // item_ID + put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, bomb_offset);// extent_offset (within idat) + put_u32_be(iloc_payload, bomb_length);// extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: item 2 ('mime') 'cdsc'-references item 1 (metadata describes the image). + std::vector cdsc_payload; + put_u16_be(cdsc_payload, 2); // from_item_ID + put_u16_be(cdsc_payload, 1); // reference_count + put_u16_be(cdsc_payload, 1); // to_item_ID + auto cdsc = make_box("cdsc", cdsc_payload); + auto iref = make_box("iref", cdsc, /*full=*/true); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + + +TEST_CASE("brotli mime metadata decompression bomb is bounded by security limits") { +#if HAVE_BROTLI + // A brotli payload that expands to ~40 MB but is only a few hundred bytes on disk. + const size_t decompressed_size = 40u * 1024 * 1024; + std::vector zeros(decompressed_size, 0); + std::vector bomb = compress_brotli(zeros.data(), zeros.size()); + REQUIRE(!bomb.empty()); + REQUIRE(bomb.size() < decompressed_size); // it really is highly compressible + + std::vector file = build_heif_with_brotli_mime(bomb); + + // --- with a tight total-memory limit, opening must fail rather than allocate the + // whole decompressed payload (previously it ignored the limit entirely). + { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_security_limits* limits = heif_context_get_security_limits(ctx); + REQUIRE(limits != nullptr); + limits->max_total_memory = 8u * 1024 * 1024; // 8 MB << 40 MB bomb + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + + heif_context_free(ctx); + } + + // --- with the default (generous) limits, the very same file opens successfully, + // proving it is structurally valid and that the limit is what is enforced. + { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_context_free(ctx); + } +#else + SUCCEED("brotli support not compiled in - skipping decompression bomb test"); +#endif +} diff -Nru libheif-1.19.8/tests/duplicate_alpha_channel.cc libheif-1.23.4/tests/duplicate_alpha_channel.cc --- libheif-1.19.8/tests/duplicate_alpha_channel.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/duplicate_alpha_channel.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,109 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "image/pixelimage.h" +#include "catch_amalgamated.hpp" + +// Regression tests for GHSA-g89c-p67h-r497. + +TEST_CASE("transfer_channel_from_image_as rejects a duplicate destination channel") +{ + auto* limits = heif_get_global_security_limits(); + + auto dst = std::make_shared(); + dst->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(dst->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + // Attaching a channel the destination doesn't have yet succeeds. + auto alpha1 = std::make_shared(); + alpha1->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(alpha1->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + Error err1 = dst->transfer_channel_from_image_as(alpha1, heif_channel_Y, heif_channel_Alpha); + REQUIRE(err1.error_code == heif_error_Ok); + REQUIRE(dst->has_channel(heif_channel_Alpha)); + REQUIRE(dst->get_bits_per_pixel(heif_channel_Alpha) == 8); + + // A second attach to the same, already-occupied destination channel must be + // rejected instead of silently appending a duplicate plane. + auto alpha2 = std::make_shared(); + alpha2->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(alpha2->add_channel(heif_channel_Y, 4, 4, 10, limits).error_code == heif_error_Ok); + + Error err2 = dst->transfer_channel_from_image_as(alpha2, heif_channel_Y, heif_channel_Alpha); + REQUIRE(err2.error_code == heif_error_Invalid_input); + + // The destination must still describe only the original 8-bit Alpha plane. + REQUIRE(dst->get_bits_per_pixel(heif_channel_Alpha) == 8); + + // The rejected source must be left untouched: the duplicate check must run + // before any plane is moved out of it, not after. + REQUIRE(alpha2->has_channel(heif_channel_Y)); +} + +TEST_CASE("overlay rejects an Alpha plane whose size does not match the other channels") +{ + auto* limits = heif_get_global_security_limits(); + + auto base = std::make_shared(); + base->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(base->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + // An overlay image whose Alpha plane is smaller than its own color plane + // must be rejected up front, since the blend loop indexes the Alpha plane + // using the color channel's extent. + auto overlay_img = std::make_shared(); + overlay_img->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(overlay_img->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + REQUIRE(overlay_img->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok); + + Error err = base->overlay(overlay_img, 0, 0); + REQUIRE(err.error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("overlay blends normally when the Alpha plane size matches") +{ + auto* limits = heif_get_global_security_limits(); + + auto base = std::make_shared(); + base->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(base->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok); + base->fill_channel(heif_channel_Y, 0); + + auto overlay_img = std::make_shared(); + overlay_img->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(overlay_img->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok); + REQUIRE(overlay_img->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok); + overlay_img->fill_channel(heif_channel_Y, 200); + overlay_img->fill_channel(heif_channel_Alpha, 255); // fully opaque + + Error err = base->overlay(overlay_img, 0, 0); + REQUIRE(err.error_code == heif_error_Ok); + + size_t stride; + const uint8_t* data = base->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(data[0] == 200); +} diff -Nru libheif-1.19.8/tests/encode.cc libheif-1.23.4/tests/encode.cc --- libheif-1.19.8/tests/encode.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/encode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,16 +26,16 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "pixelimage.h" -#include "libheif/api_structs.h" +#include "image/pixelimage.h" +#include "api_structs.h" #include "test_utils.h" #include -struct heif_image* createImage_RRGGBB_BE() { - struct heif_image* image; - struct heif_error err; +heif_image* createImage_RRGGBB_BE() { + heif_image* image; + heif_error err; err = heif_image_create(256,256, heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_BE, @@ -56,15 +56,15 @@ } -struct heif_error encode_image(struct heif_image* img) { - struct heif_context* ctx = heif_context_alloc(); +heif_error encode_image(heif_image* img) { + heif_context* ctx = heif_context_alloc(); - struct heif_encoder* enc = nullptr; - struct heif_error err { heif_error_Ok }; + heif_encoder* enc = nullptr; + heif_error err { heif_error_Ok }; enc = get_encoder_or_skip_test(heif_compression_HEVC); - struct heif_image_handle* hdl; + heif_image_handle* hdl; err = heif_context_encode_image(ctx, img, enc, @@ -117,7 +117,7 @@ heif_context* ctx = heif_context_alloc(); heif_encoder* enc = get_encoder_or_skip_test(compression); - struct heif_encoding_options* options; + heif_encoding_options* options; options = heif_encoding_options_alloc(); options->macOS_compatibility_workaround = false; options->macOS_compatibility_workaround_no_nclx_profile = false; @@ -156,3 +156,44 @@ test_ispe_size(heif_compression_AV1, heif_orientation_rotate_90_cw, 121,99, 121,99); test_ispe_size(heif_compression_AV1, heif_orientation_rotate_90_cw, 120,100, 120,100); } + + +TEST_CASE("x265 rejects oversized images", "[heif_encoder]") { + // Regression test for GHSA-2c3g-p585-8rpq. x265 (up to at least v3.5) neither checks the + // picture size nor its internal allocation results and crashes in a worker thread for + // pictures of roughly 700 Mpixel or more. The x265 plugin therefore rejects pictures above + // the HEVC Level 7.2 maximum of 142,606,336 luma samples (16384x8704), which is the same + // limit newer x265 versions enforce themselves. + + const heif_encoder_descriptor* descriptor = nullptr; + int n = heif_get_encoder_descriptors(heif_compression_HEVC, "x265", &descriptor, 1); + if (n == 0) { + SKIP("x265 encoder not available, skipping test"); + } + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc = nullptr; + heif_error err = heif_context_get_encoder(ctx, descriptor, &enc); + REQUIRE(err.code == heif_error_Ok); + + // One row more than Level 7.2 allows. A single 8-bit luma plane keeps this at about 142 MB. + const int w = 16384; + const int h = 8705; + heif_image* img = nullptr; + err = heif_image_create(w, h, heif_colorspace_monochrome, heif_chroma_monochrome, &img); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(img, heif_channel_Y, w, h, 8); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Encoding_error); + REQUIRE(err.subcode == heif_suberror_Encoder_encoding); + + if (handle) { + heif_image_handle_release(handle); + } + heif_image_release(img); + heif_encoder_release(enc); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/encode_grid.cc libheif-1.23.4/tests/encode_grid.cc --- libheif-1.19.8/tests/encode_grid.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/encode_grid.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,487 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_tiling.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include + +// Regression coverage for heif_context_encode_grid(). +// +// Two distinct bugs are exercised by this test: +// +// 1) Encoder reuse across tiles. The grid encoder feeds every tile to the +// same plugin encoder instance. Six plugins (aom, kvazaar, rav1e, svt, +// uvg266, vvenc) did not reset their internal codec context between +// tiles, so the second tile asserted/aborted (issue #1827; fixed by +// PR #1732's *_start_sequence_encoding_intern destroy-on-reuse pattern). +// +// 2) HeifPixelImage geometry drift across resize. heif_image_extract_area() +// builds an edge tile by cloning at the truncated (minW,minH) size and +// then calling extend_to_size_with_zero(w,h) to pad back to a full tile. +// Both create_clone_image_at_new_size and extend_to_size_with_zero must +// keep the ComponentDescription geometry in sync with the plane sizes; +// otherwise the uncompressed encoder mixes two dimension sources +// (compute uses get_width(), the copy loop uses get_component_width()) +// and emits a truncated bitstream for edge tiles, decoding to padding +// (visible as green stripes on the bottom row / right column). +// +// We use a 451x461 source so that cutting into 128x128 tiles produces a +// 4x4 grid in which the rightmost column (width 67) and bottom row +// (height 77) hit the extend_to_size_with_zero() padding path. The image +// is created as interleaved RGB because heif_context_encode_grid's +// implementation reads tile_width via tiles[0]->get_width(heif_channel_interleaved) +// — that grid-encoder assumption is unrelated to the bugs under test here. + +namespace { + +constexpr uint32_t kSourceWidth = 451; +constexpr uint32_t kSourceHeight = 461; +constexpr uint32_t kTileSize = 128; +constexpr uint16_t kCols = (kSourceWidth + kTileSize - 1) / kTileSize; // 4 +constexpr uint16_t kRows = (kSourceHeight + kTileSize - 1) / kTileSize; // 4 +constexpr int kChannels = 3; // RGB interleaved + +// Smooth gradients — easy for lossy codecs to roundtrip with small error, +// yet still clearly distinct from the zero/grey padding fill that the +// extend_to_size_with_zero() path would produce on a regression. +uint8_t pattern_R(uint32_t x, uint32_t /*y*/) { return static_cast(30u + x * 200u / kSourceWidth); } +uint8_t pattern_G(uint32_t /*x*/, uint32_t y) { return static_cast(30u + y * 200u / kSourceHeight); } +uint8_t pattern_B(uint32_t /*x*/, uint32_t /*y*/) { return 128u; } + + +heif_image* create_source_image() +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(kSourceWidth, kSourceHeight, + heif_colorspace_RGB, + heif_chroma_interleaved_RGB, &img); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(heif_image_add_plane(img, heif_channel_interleaved, + kSourceWidth, kSourceHeight, 8).code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, heif_channel_interleaved, &stride); + for (uint32_t y = 0; y < kSourceHeight; ++y) { + uint8_t* row = p + y * stride; + for (uint32_t x = 0; x < kSourceWidth; ++x) { + row[kChannels * x + 0] = pattern_R(x, y); + row[kChannels * x + 1] = pattern_G(x, y); + row[kChannels * x + 2] = pattern_B(x, y); + } + } + + return img; +} + + +// Extract the source into a kRows x kCols grid of kTileSize tiles via the +// public heif_image_extract_area API. Tiles on the right column / bottom row +// are partial and exercise the extend_to_size_with_zero() padding path. +std::vector extract_tiles(const heif_image* src) +{ + std::vector tiles; + tiles.reserve(kRows * kCols); + for (uint32_t ty = 0; ty < kRows; ++ty) { + for (uint32_t tx = 0; tx < kCols; ++tx) { + heif_image* tile = nullptr; + heif_error err = heif_image_extract_area(src, + tx * kTileSize, ty * kTileSize, + kTileSize, kTileSize, + nullptr, &tile); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(tile != nullptr); + tiles.push_back(tile); + } + } + return tiles; +} + + +void check_pixel(const uint8_t* plane, size_t stride, int n_channels, + uint32_t x, uint32_t y, int channel, + uint8_t expected, int tolerance) +{ + int got = plane[y * stride + n_channels * x + channel]; + int diff = std::abs(got - static_cast(expected)); + INFO("at (" << x << "," << y << ") ch=" << channel + << ": expected=" << static_cast(expected) << " got=" << got); + REQUIRE(diff <= tolerance); +} + + +void run_encode_grid_roundtrip(heif_compression_format format, + int tolerance, + const char* output_filename) +{ + heif_encoder* encoder = get_encoder_or_skip_test(format); + REQUIRE(encoder != nullptr); + + // Higher quality keeps lossy spread within `tolerance`. For uncompressed + // this returns an unsupported-parameter error which we ignore — uncompressed + // is bit-exact regardless of the quality setting. + heif_encoder_set_lossy_quality(encoder, 90); + + heif_image* src = create_source_image(); + REQUIRE(src != nullptr); + + std::vector tiles = extract_tiles(src); + REQUIRE(tiles.size() == static_cast(kRows) * kCols); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // The grid input here is symmetric (kRows == kCols == 4) so the rows/cols + // argument-order ambiguity between the public header and the .cc impl + // does not affect this test. + heif_image_handle* grid_handle = nullptr; + heif_error err = heif_context_encode_grid(ctx, tiles.data(), + kRows, kCols, + encoder, nullptr, &grid_handle); + // Regression for #1827: pre-fix, the AOM/Kvazaar/Rav1e/SVT/uvg266/vvenc + // plugins aborted (SIGABRT) on the second tile, so this line never returned. + REQUIRE(err.code == heif_error_Ok); + REQUIRE(grid_handle != nullptr); + + std::string out_path = get_tests_output_file_path(output_filename); + REQUIRE(heif_context_write_to_file(ctx, out_path.c_str()).code == heif_error_Ok); + + heif_image_handle_release(grid_handle); + for (heif_image* t : tiles) { + heif_image_release(t); + } + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(src); + + // --- read back & decode + + heif_context* rctx = heif_context_alloc(); + REQUIRE(heif_context_read_from_file(rctx, out_path.c_str(), nullptr).code == heif_error_Ok); + + heif_image_handle* rhandle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(rctx, &rhandle).code == heif_error_Ok); + + heif_image* dec = nullptr; + REQUIRE(heif_decode_image(rhandle, &dec, + heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nullptr).code == heif_error_Ok); + REQUIRE(dec != nullptr); + + size_t dec_stride = 0; + const uint8_t* dec_plane = heif_image_get_plane_readonly2(dec, heif_channel_interleaved, &dec_stride); + REQUIRE(dec_plane != nullptr); + + // Sample points: interior, right-column edge, bottom-row edge, corner. + // Pre-fix the edge points decoded to padding (zero) for the uncompressed + // codec because the per-tile bitstream was truncated. + const std::pair sample_points[] = { + { 10u, 10u}, // first tile, interior + {200u, 200u}, // middle tile, interior + {448u, 100u}, // right column tile + {448u, 200u}, // right column tile + {100u, 458u}, // bottom row tile + {200u, 458u}, // bottom row tile + {448u, 458u}, // bottom-right corner tile + }; + + for (const auto& pt : sample_points) { + uint32_t x = pt.first; + uint32_t y = pt.second; + REQUIRE(x < kSourceWidth); + REQUIRE(y < kSourceHeight); + + check_pixel(dec_plane, dec_stride, kChannels, x, y, /*ch=*/0, pattern_R(x, y), tolerance); + check_pixel(dec_plane, dec_stride, kChannels, x, y, /*ch=*/1, pattern_G(x, y), tolerance); + check_pixel(dec_plane, dec_stride, kChannels, x, y, /*ch=*/2, pattern_B(x, y), tolerance); + } + + heif_image_release(dec); + heif_image_handle_release(rhandle); + heif_context_free(rctx); +} + +} // namespace + + +TEST_CASE("heif_context_encode_grid roundtrip - uncompressed", + "[heif_context_encode_grid]") +{ + // Uncompressed is bit-exact: edge-tile pixels must match the source. + // Pre-fix (extend_to_size_with_zero not syncing ComponentDescriptions), + // the bottom-row / right-column tiles decoded to padding values instead + // of the source gradient — these REQUIREs would fail. + run_encode_grid_roundtrip(heif_compression_uncompressed, /*tolerance=*/0, + "encode_grid_uncompressed.heif"); +} + + +TEST_CASE("heif_context_encode_grid roundtrip - HEVC", + "[heif_context_encode_grid]") +{ + // Lossy codec: primarily catches "second tile aborts" reuse regressions + // for x265/kvazaar. Pixel tolerance is generous since RGB→YCbCr→RGB at + // lossy quality naturally spreads. + run_encode_grid_roundtrip(heif_compression_HEVC, /*tolerance=*/15, + "encode_grid_hevc.heif"); +} + + +TEST_CASE("heif_context_encode_grid roundtrip - AV1", + "[heif_context_encode_grid]") +{ + // Lossy codec: regression check for the aom/svt/rav1e reuse abort + // (issue #1827). Pre-fix, the encode call never returned for the second + // tile, so reaching the post-encode REQUIRE was impossible. + run_encode_grid_roundtrip(heif_compression_AV1, /*tolerance=*/15, + "encode_grid_av1.heif"); +} + + +// ----------------------------------------------------------------------------- +// Tile-by-tile grid encoding via heif_context_add_grid_image() + +// heif_context_add_image_tile() — the API path heif-enc uses for --cut-tiles. +// Uses YCbCr 4:2:0 planar tiles produced by heif_image_extract_area(); this +// exercises the unc encoder's component_interleave variant, which sizes its +// output buffer from get_width() but copies via get_component_width() — the +// exact pattern that regressed when extend_to_size_with_zero() failed to +// keep ComponentDescriptions in sync with extended planes. +// ----------------------------------------------------------------------------- + +namespace { + +heif_image* create_source_image_YCbCr_420() +{ + heif_image* img = nullptr; + REQUIRE(heif_image_create(kSourceWidth, kSourceHeight, + heif_colorspace_YCbCr, heif_chroma_420, &img).code == heif_error_Ok); + + REQUIRE(heif_image_add_plane(img, heif_channel_Y, + kSourceWidth, kSourceHeight, 8).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cb, + (kSourceWidth + 1) / 2, (kSourceHeight + 1) / 2, 8).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cr, + (kSourceWidth + 1) / 2, (kSourceHeight + 1) / 2, 8).code == heif_error_Ok); + + // Smooth gradients in Y and constant chroma — bit-exact through the + // uncompressed encoder, and clearly distinct from the zero-fill (Y=0, + // Cb=Cr=128) that extend_to_size_with_zero() would expose on regression. + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, heif_channel_Y, &stride); + for (uint32_t y = 0; y < kSourceHeight; ++y) { + for (uint32_t x = 0; x < kSourceWidth; ++x) { + p[y * stride + x] = static_cast(30u + x * 200u / kSourceWidth); + } + } + p = heif_image_get_plane2(img, heif_channel_Cb, &stride); + for (uint32_t y = 0; y < (kSourceHeight + 1) / 2; ++y) { + for (uint32_t x = 0; x < (kSourceWidth + 1) / 2; ++x) { + p[y * stride + x] = 80u; // distinctly not the neutral 128 fill + } + } + p = heif_image_get_plane2(img, heif_channel_Cr, &stride); + for (uint32_t y = 0; y < (kSourceHeight + 1) / 2; ++y) { + for (uint32_t x = 0; x < (kSourceWidth + 1) / 2; ++x) { + p[y * stride + x] = 200u; // distinctly not the neutral 128 fill + } + } + + return img; +} + +} // namespace + +TEST_CASE("heif_context_encode_grid roundtrip - uncompressed YCbCr 4:2:0 planar", + "[heif_context_encode_grid]") +{ + // Regression for heif_context_encode_grid()'s output-size computation, + // which read tile dimensions via tiles[0]->get_width(heif_channel_interleaved) + // — that channel does not exist on planar YCbCr tiles, so the call returned + // 0 and the produced grid had a 0x0 ispe ("Zero image width or height" on + // decode). The fix uses HeifPixelImage::get_width() / get_height() (the + // logical image dims) which work for any channel layout. + + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_uncompressed); + REQUIRE(encoder != nullptr); + + heif_image* src = create_source_image_YCbCr_420(); + REQUIRE(src != nullptr); + + std::vector tiles = extract_tiles(src); + REQUIRE(tiles.size() == static_cast(kRows) * kCols); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_image_handle* grid_handle = nullptr; + REQUIRE(heif_context_encode_grid(ctx, tiles.data(), + kRows, kCols, + encoder, nullptr, &grid_handle).code == heif_error_Ok); + REQUIRE(grid_handle != nullptr); + + std::string out_path = get_tests_output_file_path("encode_grid_ycbcr_planar.heif"); + REQUIRE(heif_context_write_to_file(ctx, out_path.c_str()).code == heif_error_Ok); + + heif_image_handle_release(grid_handle); + for (heif_image* t : tiles) { + heif_image_release(t); + } + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(src); + + // Pre-fix the grid was written with ispe=0x0; read_from_file would parse + // it but heif_decode_image would reject it as invalid input. + heif_context* rctx = heif_context_alloc(); + REQUIRE(heif_context_read_from_file(rctx, out_path.c_str(), nullptr).code == heif_error_Ok); + + heif_image_handle* rhandle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(rctx, &rhandle).code == heif_error_Ok); + + // The decoded grid must have the correct dimensions (kCols*kTileSize x kRows*kTileSize). + REQUIRE(heif_image_handle_get_width(rhandle) == static_cast(kCols * kTileSize)); + REQUIRE(heif_image_handle_get_height(rhandle) == static_cast(kRows * kTileSize)); + + heif_image_handle_release(rhandle); + heif_context_free(rctx); +} + + +TEST_CASE("grid encoding tile-by-tile - uncompressed YCbCr 4:2:0 (extract_area edge padding)", + "[heif_context_encode_grid][heif_context_add_image_tile]") +{ + // This test specifically targets bug #2 (extend_to_size_with_zero failing + // to update ComponentDescriptions). When the bug is present, the unc + // encoder's component_interleave variant produces a truncated bitstream + // for edge tiles and the bottom-row / right-column tiles decode to padding + // values (Y=0, Cb=Cr=128 grey) instead of the source content. + + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_uncompressed); + REQUIRE(encoder != nullptr); + + heif_image* src = create_source_image_YCbCr_420(); + REQUIRE(src != nullptr); + + std::vector tiles = extract_tiles(src); + REQUIRE(tiles.size() == static_cast(kRows) * kCols); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // Build the grid item with an explicit overall image size. Pass nullptr + // for encoding_options to also regression-check that heif_context_add_grid_image + // falls back to default options rather than dereferencing a null pointer. + heif_image_handle* grid_handle = nullptr; + REQUIRE(heif_context_add_grid_image(ctx, + kCols * kTileSize, kRows * kTileSize, + kCols, kRows, + nullptr, &grid_handle).code == heif_error_Ok); + REQUIRE(grid_handle != nullptr); + + for (uint32_t ty = 0; ty < kRows; ++ty) { + for (uint32_t tx = 0; tx < kCols; ++tx) { + heif_image* tile = tiles[ty * kCols + tx]; + heif_error err = heif_context_add_image_tile(ctx, grid_handle, tx, ty, tile, encoder); + REQUIRE(err.code == heif_error_Ok); + } + } + + std::string out_path = get_tests_output_file_path("encode_grid_tile_by_tile.heif"); + REQUIRE(heif_context_write_to_file(ctx, out_path.c_str()).code == heif_error_Ok); + + heif_image_handle_release(grid_handle); + for (heif_image* t : tiles) { + heif_image_release(t); + } + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(src); + + // --- read back & decode + + heif_context* rctx = heif_context_alloc(); + REQUIRE(heif_context_read_from_file(rctx, out_path.c_str(), nullptr).code == heif_error_Ok); + + heif_image_handle* rhandle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(rctx, &rhandle).code == heif_error_Ok); + + heif_image* dec = nullptr; + REQUIRE(heif_decode_image(rhandle, &dec, + heif_colorspace_YCbCr, heif_chroma_420, + nullptr).code == heif_error_Ok); + REQUIRE(dec != nullptr); + + size_t stride_y = 0, stride_cb = 0, stride_cr = 0; + const uint8_t* dec_y = heif_image_get_plane_readonly2(dec, heif_channel_Y, &stride_y); + const uint8_t* dec_cb = heif_image_get_plane_readonly2(dec, heif_channel_Cb, &stride_cb); + const uint8_t* dec_cr = heif_image_get_plane_readonly2(dec, heif_channel_Cr, &stride_cr); + REQUIRE(dec_y != nullptr); + REQUIRE(dec_cb != nullptr); + REQUIRE(dec_cr != nullptr); + + // Verify edge tiles bit-exact. Pre-fix, sampling within an edge tile + // beyond the original-source rectangle would expose the truncated- + // bitstream behaviour; even inside the source rectangle, the decoder + // would supply padding bytes once the encoded tile data ran out. + const std::pair sample_points[] = { + { 10u, 10u}, // first tile (interior) + {200u, 200u}, // middle tile (interior) + {448u, 100u}, // right-column tile (x near right edge) + {448u, 200u}, // right-column tile + {100u, 458u}, // bottom-row tile (y near bottom edge) + {200u, 458u}, // bottom-row tile + {448u, 458u}, // bottom-right corner tile + }; + + for (const auto& pt : sample_points) { + uint32_t x = pt.first; + uint32_t y = pt.second; + REQUIRE(x < kSourceWidth); + REQUIRE(y < kSourceHeight); + + uint8_t expected_Y = static_cast(30u + x * 200u / kSourceWidth); + INFO("Y at (" << x << "," << y << ")"); + REQUIRE(dec_y[y * stride_y + x] == expected_Y); + + uint32_t cx = x / 2; + uint32_t cy = y / 2; + INFO("Cb at (" << cx << "," << cy << ")"); + REQUIRE(dec_cb[cy * stride_cb + cx] == 80u); + INFO("Cr at (" << cx << "," << cy << ")"); + REQUIRE(dec_cr[cy * stride_cr + cx] == 200u); + } + + heif_image_release(dec); + heif_image_handle_release(rhandle); + heif_context_free(rctx); +} diff -Nru libheif-1.19.8/tests/encode_htj2k.cc libheif-1.23.4/tests/encode_htj2k.cc --- libheif-1.19.8/tests/encode_htj2k.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/encode_htj2k.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,8 +26,8 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" -#include "pixelimage.h" +#include "api_structs.h" +#include "image/pixelimage.h" #include "test_utils.h" @@ -48,14 +48,14 @@ heif_context *ctx = heif_context_alloc(); heif_encoder *encoder; - struct heif_error err; + heif_error err; err = heif_context_get_encoder_for_format(ctx, heif_compression_HTJ2K, &encoder); REQUIRE(err.code == heif_error_Ok); err = heif_encoder_set_lossless(encoder, lossless); REQUIRE(err.code == heif_error_Ok); - struct heif_encoding_options *options = get_encoding_options(); + heif_encoding_options *options = get_encoding_options(); heif_image_handle *output_image_handle; diff -Nru libheif-1.19.8/tests/encode_jpeg2000.cc libheif-1.23.4/tests/encode_jpeg2000.cc --- libheif-1.19.8/tests/encode_jpeg2000.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/encode_jpeg2000.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,8 +26,8 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" -#include "pixelimage.h" +#include "api_structs.h" +#include "image/pixelimage.h" #include "test_utils.h" @@ -48,14 +48,14 @@ heif_init(nullptr); heif_context *ctx = heif_context_alloc(); heif_encoder *encoder; - struct heif_error err; + heif_error err; err = heif_context_get_encoder_for_format(ctx, heif_compression_JPEG2000, &encoder); REQUIRE(err.code == heif_error_Ok); err = heif_encoder_set_lossless(encoder, lossless); REQUIRE(err.code == heif_error_Ok); - struct heif_encoding_options *options = set_encoding_options(); + heif_encoding_options *options = set_encoding_options(); heif_image_handle *output_image_handle; diff -Nru libheif-1.19.8/tests/entity_groups.cc libheif-1.23.4/tests/entity_groups.cc --- libheif-1.19.8/tests/entity_groups.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/entity_groups.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,117 @@ +/* + libheif regression tests for entity groups (grpl) parsing. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" +#include "libheif/heif_entity_groups.h" + +#include +#include +#include + +namespace { + +// Build a minimal HEIF file whose 'grpl' box contains a single child box +// whose four-cc is NOT one of the registered Box_EntityToGroup subclasses +// (pymd / altr / ster). Such children parse to Box_other and used to +// trigger a NULL-pointer dereference in heif_context_get_entity_groups(). +std::vector build_minimal_heif_with_bogus_grpl_child() { + // ftyp: heic / 0 / mif1 heic + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + // hdlr: handler type 'null' so HeifFile::has_images() returns false and + // pitm/iprp/ipco/ipma are not required for the file to parse. + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); // pre_defined + append_fourcc(hdlr_payload, "null"); // handler_type (NOT 'pict') + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); // name (empty, NUL-terminated) + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + // iinf: zero entries + std::vector iinf_payload; + put_u16_be(iinf_payload, 0); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iloc: offset_size=0, length_size=0, base_offset_size=0, reserved=0, item_count=0 + std::vector iloc_payload; + iloc_payload.push_back(0); + iloc_payload.push_back(0); + put_u16_be(iloc_payload, 0); + auto iloc = make_box("iloc", iloc_payload, /*full=*/true); + + // grpl with a single 'pict' child — 'pict' is not registered as an + // entity-to-group type, so it parses to Box_other. + auto bogus_child = make_box("pict", {}); + auto grpl = make_box("grpl", bogus_child); + + // meta = hdlr + iinf + iloc + grpl + std::vector meta_payload; + meta_payload.insert(meta_payload.end(), hdlr.begin(), hdlr.end()); + meta_payload.insert(meta_payload.end(), iinf.begin(), iinf.end()); + meta_payload.insert(meta_payload.end(), iloc.begin(), iloc.end()); + meta_payload.insert(meta_payload.end(), grpl.begin(), grpl.end()); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + file.insert(file.end(), ftyp.begin(), ftyp.end()); + file.insert(file.end(), meta.begin(), meta.end()); + return file; +} + +} // namespace + + +// Regression for PR #1806 / NULL-deref in heif_context_get_entity_groups +// when 'grpl' contains a child whose four-cc is not a registered +// Box_EntityToGroup subclass. +TEST_CASE("entity_groups: unknown grpl child does not crash") { + auto data = build_minimal_heif_with_bogus_grpl_child(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + int num_groups = -1; + heif_entity_group* groups = + heif_context_get_entity_groups(ctx, 0, 0, &num_groups); + // Unknown four-cc must be silently skipped, not dereferenced. + REQUIRE(num_groups == 0); + + heif_entity_groups_release(groups, num_groups); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/error_item_decode.cc libheif-1.23.4/tests/error_item_decode.cc --- libheif-1.19.8/tests/error_item_decode.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/error_item_decode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,232 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression tests around unsupported items, which are represented internally by +// an ImageItem_Error placeholder (here triggered with 'lhv1', layered HEVC). +// +// 1. These placeholders used to be constructed with a null HeifContext, so any +// code dereferencing their context crashed. ImageItem::decode_image() gained +// a verify_decodable() pre-pass that walks the reference graph and calls +// get_file() (-> context->get_heif_file()); with the error item as an alpha +// auxiliary, that walk dereferenced null (OSS-Fuzz / CIFuzz). Error items now +// carry their real context. +// +// 2. The depth/aux/thumbnail handle getters used not to reject an item with an +// item-error, unlike heif_context_get_image_handle(), so they handed out a +// handle that could only fail at decode. They now surface the item's error at +// handle-creation time. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +// item 1: decodable 16x16 'mski' primary. item 2: an 'lhv1' item (unsupported -> +// ImageItem_Error) attached to item 1 as an auxiliary of the given type (via +// 'auxl' + an 'auxC' of `aux_urn`). +std::vector build_file_with_error_aux(const std::string& aux_urn) { + const uint32_t W = 16, H = 16; + + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 2); + { + std::vector infe1; + put_u16_be(infe1, 1); + put_u16_be(infe1, 0); + append_fourcc(infe1, "mski"); + infe1.push_back(0); + append(iinf_payload, make_box("infe", infe1, /*full=*/true, /*version=*/2)); + + std::vector infe2; + put_u16_be(infe2, 2); + put_u16_be(infe2, 0); + append_fourcc(infe2, "lhv1"); // unsupported -> ImageItem_Error + infe2.push_back(0); + append(iinf_payload, make_box("infe", infe2, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ipco: 1=ispe, 2=mskC, 3=auxC(aux_urn) + std::vector ispe_payload; + put_u32_be(ispe_payload, W); + put_u32_be(ispe_payload, H); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(8); + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector auxC_payload; + append_cstr(auxC_payload, aux_urn.c_str()); + auto auxC = make_box("auxC", auxC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, mskC); + append(ipco_payload, auxC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 2); + put_u16_be(ipma_payload, 1); // item 1: ispe + mskC + ipma_payload.push_back(2); + ipma_payload.push_back(0x80 | 1); + ipma_payload.push_back(0x80 | 2); + put_u16_be(ipma_payload, 2); // item 2: ispe + auxC + ipma_payload.push_back(2); + ipma_payload.push_back(0x80 | 1); + ipma_payload.push_back(0x80 | 3); + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + std::vector idat_payload(W * H + 16, 0x7F); + auto idat = make_box("idat", idat_payload); + + std::vector iloc_payload; + iloc_payload.push_back((4 << 4) | 4); + iloc_payload.push_back(0); + put_u16_be(iloc_payload, 2); + put_u16_be(iloc_payload, 1); + put_u16_be(iloc_payload, 0x0001); + put_u16_be(iloc_payload, 0); + put_u16_be(iloc_payload, 1); + put_u32_be(iloc_payload, 0); + put_u32_be(iloc_payload, W * H); + put_u16_be(iloc_payload, 2); + put_u16_be(iloc_payload, 0x0001); + put_u16_be(iloc_payload, 0); + put_u16_be(iloc_payload, 1); + put_u32_be(iloc_payload, W * H); + put_u32_be(iloc_payload, 16); + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // 'auxl' from item 2 (aux) to item 1 (master) + std::vector auxl_payload; + put_u16_be(auxl_payload, 2); + put_u16_be(auxl_payload, 1); + put_u16_be(auxl_payload, 1); + auto iref = make_box("iref", make_box("auxl", auxl_payload), /*full=*/true); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + + +// The depth-image handle getter must reject an item that has an item-error at +// handle-creation time, not hand out a handle that only fails at decode. +TEST_CASE("error item: getting the handle of an unsupported depth aux fails early") { + auto data = build_file_with_error_aux("urn:mpeg:mpegB:cicp:systems:auxiliary:depth"); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + REQUIRE(heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr).code + == heif_error_Ok); + + heif_image_handle* primary = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &primary).code == heif_error_Ok); + + REQUIRE(heif_image_handle_get_number_of_depth_images(primary) == 1); + + heif_item_id depth_id = 0; + heif_image_handle_get_list_of_depth_image_IDs(primary, &depth_id, 1); + + heif_image_handle* depth_handle = nullptr; + heif_error err = heif_image_handle_get_depth_image_handle(primary, depth_id, &depth_handle); + + REQUIRE(err.code != heif_error_Ok); // the item's error is surfaced here + REQUIRE(depth_handle == nullptr); + + heif_image_handle_release(primary); + heif_context_free(ctx); +} + +// Decoding a primary whose alpha auxiliary is an unsupported (error) item must +// not crash: verify_decodable() walks the reference graph, including the alpha +// edge into the error item, and dereferences its context. (Error items now +// carry a real context.) +TEST_CASE("error item: decoding a primary with an unsupported alpha aux does not crash") { + auto data = build_file_with_error_aux("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + REQUIRE(heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr).code + == heif_error_Ok); + + heif_image_handle* primary = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &primary).code == heif_error_Ok); + + // Reaching this call at all (no segfault in verify_decodable) is the point. + heif_image* img = nullptr; + heif_error err = heif_decode_image(primary, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + (void) err; // may succeed (ignoring the broken alpha) or fail; must not crash + if (img) { heif_image_release(img); } + + heif_image_handle_release(primary); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/exception_guard.cc libheif-1.23.4/tests/exception_guard.cc --- libheif-1.19.8/tests/exception_guard.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/exception_guard.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,85 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Tests for exception_guard(), the translation layer that keeps C++ exceptions +// from crossing the C API boundary. It converts an out-of-memory condition into +// a clean heif_error instead of letting std::bad_alloc abort the process +// (hardening for GHSA-7p2q-crf9-xm46 / GHSA-24wx-9w62-c96w). + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "api_structs.h" + +#include +#include +#include + +TEST_CASE("exception_guard passes through the body result unchanged") { + heif_error ok = exception_guard([]() -> heif_error { + return {heif_error_Ok, heif_suberror_Unspecified, "fine"}; + }); + REQUIRE(ok.code == heif_error_Ok); + + heif_error custom = exception_guard([]() -> heif_error { + return {heif_error_Invalid_input, heif_suberror_End_of_data, "boom"}; + }); + REQUIRE(custom.code == heif_error_Invalid_input); + REQUIRE(custom.subcode == heif_suberror_End_of_data); +} + +TEST_CASE("exception_guard converts std::bad_alloc to a memory-allocation error") { + heif_error err = exception_guard([]() -> heif_error { + throw std::bad_alloc(); + }); + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.message != nullptr); + // The message must be a static literal (returning it must not allocate). + REQUIRE(strlen(err.message) > 0); +} + +TEST_CASE("exception_guard converts std::length_error to a memory-allocation error") { + // A std::vector/std::string asked to exceed max_size() throws std::length_error; + // treat it as an allocation failure rather than an abort. + heif_error err = exception_guard([]() -> heif_error { + throw std::length_error("too large"); + }); + REQUIRE(err.code == heif_error_Memory_allocation_error); +} + +TEST_CASE("exception_guard converts other std::exceptions to an internal error") { + heif_error err = exception_guard([]() -> heif_error { + throw std::runtime_error("unexpected"); + }); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.message != nullptr); +} + +TEST_CASE("exception_guard converts non-std exceptions to an internal error") { + heif_error err = exception_guard([]() -> heif_error { + throw 42; + }); + REQUIRE(err.code == heif_error_Usage_error); +} diff -Nru libheif-1.19.8/tests/extended_type.cc libheif-1.23.4/tests/extended_type.cc --- libheif-1.19.8/tests/extended_type.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/extended_type.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "libheif/heif.h" #include "test-config.h" #include "test_utils.h" @@ -39,10 +39,10 @@ heif_init(nullptr); heif_context *ctx = heif_context_alloc(); heif_encoder *encoder; - struct heif_error err; + heif_error err; encoder = get_encoder_or_skip_test(heif_compression_HEVC); - struct heif_encoding_options *options = heif_encoding_options_alloc(); + heif_encoding_options *options = heif_encoding_options_alloc(); heif_image_handle *output_image_handle; diff -Nru libheif-1.19.8/tests/extract_area_plane_checks.cc libheif-1.23.4/tests/extract_area_plane_checks.cc --- libheif-1.19.8/tests/extract_area_plane_checks.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/extract_area_plane_checks.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,121 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "image/pixelimage.h" +#include "catch_amalgamated.hpp" + +// Regression tests for HeifPixelImage::extract_image_area(). +// +// The per-channel copy window is clamped against the image's declared size +// (chroma-mapped), not each plane's own actual stored size, and the offsets +// (xs/ys) it subtracts from that clamp are themselves derived from the +// declared size too. A plane smaller than the image's declared size implies +// (the same enabling condition as crop()/scale_nearest_neighbor()) is +// therefore both read past its end and, once xs/ys exceed the plane's real +// bounds, underflows the unsigned subtraction feeding the memcpy length -- +// worse than a plain OOB read. extract_image_area() now calls +// has_standard_plane_sizes() up front to reject that, the same way crop() +// does. This is a deliberate policy choice: a "requested region exceeds the +// image" is a different, legitimate case this function already handles by +// clamping and zero-padding (extend_to_size_with_zero()), and that behavior +// is unaffected -- only a plane that disagrees with the image's own declared +// geometry is now rejected. + +TEST_CASE("extract_image_area rejects a plane smaller than the image's declared size") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(8, 8, heif_colorspace_monochrome, heif_chroma_monochrome); + + // Y plane smaller than the image's declared 8x8 -- add_channel() doesn't + // check this against m_width/m_height. + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + auto result = image->extract_image_area(0, 0, 4, 4, limits); + REQUIRE(result.is_error()); + REQUIRE(result.error().error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("extract_image_area extracts a region from a well-formed image correctly") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + size_t stride; + uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride); + for (uint32_t row = 0; row < 4; row++) { + for (uint32_t col = 0; col < 4; col++) { + y[row * stride + col] = static_cast(row * 4 + col); + } + } + + // Extract the 2x2 region starting at (1,1) -- fully inside the image. + auto result = image->extract_image_area(1, 1, 2, 2, limits); + REQUIRE(!result.is_error()); + + auto area = *result; + REQUIRE(area->get_width(heif_channel_Y) == 2); + REQUIRE(area->get_height(heif_channel_Y) == 2); + + const uint8_t* out = area->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(out[0 * stride + 0] == 5); // (1,1) + REQUIRE(out[0 * stride + 1] == 6); // (2,1) + REQUIRE(out[1 * stride + 0] == 9); // (1,2) + REQUIRE(out[1 * stride + 1] == 10); // (2,2) +} + +TEST_CASE("extract_image_area zero-pads a well-formed image when the requested region exceeds it") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok); + + size_t stride; + uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride); + y[0 * stride + 0] = 1; + y[0 * stride + 1] = 2; + y[1 * stride + 0] = 3; + y[1 * stride + 1] = 4; + + // Request a 4x4 area starting at (1,1) -- extends past the 2x2 image. This + // is unrelated to plane-size correctness (the plane matches the image's + // declared size); has_standard_plane_sizes() must not reject it. + auto result = image->extract_image_area(1, 1, 4, 4, limits); + REQUIRE(!result.is_error()); + + auto area = *result; + REQUIRE(area->get_width(heif_channel_Y) == 4); + REQUIRE(area->get_height(heif_channel_Y) == 4); + + const uint8_t* out = area->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(out[0 * stride + 0] == 4); // source pixel (1,1) + REQUIRE(out[0 * stride + 1] == 0); // past the source edge -> zero pad + REQUIRE(out[1 * stride + 0] == 0); + REQUIRE(out[3 * stride + 3] == 0); +} diff -Nru libheif-1.19.8/tests/ffmpeg_decode_bitdepth.cc libheif-1.23.4/tests/ffmpeg_decode_bitdepth.cc --- libheif-1.19.8/tests/ffmpeg_decode_bitdepth.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/ffmpeg_decode_bitdepth.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,236 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// FFmpeg's JPEG 2000 (and MJPEG) decoders have no exact-depth pixel format for every +// coded bit depth. A 10- or 12-bit grayscale codestream is returned as GRAY16 with the +// samples shifted left to fill the 16-bit container. The FFmpeg decoder plugin has to +// undo this so that the decoded image has the bit depth declared in the file and the +// original sample values. These tests encode high bit depth JPEG 2000 images with the +// OpenJPEG encoder and decode them again explicitly with the FFmpeg decoder. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include + +static const int W = 64; +static const int H = 48; + +static bool have_decoder(heif_compression_format format, const char* id) +{ + int n = heif_get_decoder_descriptors(format, nullptr, 0); + std::vector descs(n); + n = heif_get_decoder_descriptors(format, descs.data(), n); + for (int i = 0; i < n; i++) { + const char* name = heif_decoder_descriptor_get_id_name(descs[i]); + if (name && strcmp(name, id) == 0) { + return true; + } + } + return false; +} + + +// Deterministic test pattern that covers the full value range of the bit depth. +static uint16_t pattern(int x, int y, int bpp) +{ + uint32_t maxval = (1u << bpp) - 1; + if (x == 0 && y == 0) return static_cast(maxval); + if (x == 1 && y == 0) return 0; + return static_cast((static_cast(x) * 37 + static_cast(y) * 101 + x * y) & maxval); +} + + +static void fill_plane(heif_image* img, heif_channel channel, int w, int h, int bpp) +{ + heif_error err = heif_image_add_plane(img, channel, w, h, bpp); + REQUIRE(err.code == heif_error_Ok); + + size_t stride; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + uint16_t v = pattern(x, y, bpp); + if (bpp > 8) { + reinterpret_cast(p + y * stride)[x] = v; + } + else { + p[y * stride + x] = static_cast(v); + } + } + } +} + + +static void check_plane(const heif_image* img, heif_channel channel, int w, int h, int bpp) +{ + REQUIRE(heif_image_has_channel(img, channel)); + CHECK(heif_image_get_bits_per_pixel_range(img, channel) == bpp); + CHECK(heif_image_get_width(img, channel) == w); + CHECK(heif_image_get_height(img, channel) == h); + + size_t stride; + const uint8_t* p = heif_image_get_plane_readonly2(img, channel, &stride); + REQUIRE(p != nullptr); + + int mismatches = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + uint16_t expected = pattern(x, y, bpp); + uint16_t actual; + if (bpp > 8) { + actual = reinterpret_cast(p + y * stride)[x]; + } + else { + actual = p[y * stride + x]; + } + if (actual != expected) { + if (mismatches < 3) { + INFO("channel " << channel << " at (" << x << "," << y << "): expected " << expected << ", got " << actual); + CHECK(actual == expected); + } + mismatches++; + } + } + } + CHECK(mismatches == 0); +} + + +static std::string encode_j2k_lossless(heif_image* img, const char* filename) +{ + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_JPEG2000); + + heif_context* ctx = heif_context_alloc(); + + heif_error err = heif_encoder_set_lossless(encoder, 1); + REQUIRE(err.code == heif_error_Ok); + + heif_encoding_options* options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround = false; + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, encoder, options, &handle); + REQUIRE(err.code == heif_error_Ok); + heif_image_handle_release(handle); + + std::string path = get_tests_output_file_path(filename); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return path; +} + + +static heif_image* decode_with_ffmpeg(const std::string& path, heif_colorspace colorspace, heif_chroma chroma, int expected_bpp) +{ + heif_context* ctx = get_context_for_local_file(path); + heif_image_handle* handle = get_primary_image_handle(ctx); + + // The declared bit depth comes from the codestream header (SIZ), independent of the decoder. + CHECK(heif_image_handle_get_luma_bits_per_pixel(handle) == expected_bpp); + + heif_decoding_options* options = heif_decoding_options_alloc(); + options->decoder_id = "ffmpeg"; + + heif_image* img; + heif_error err = heif_decode_image(handle, &img, colorspace, chroma, options); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + heif_decoding_options_free(options); + heif_image_handle_release(handle); + heif_context_free(ctx); + + return img; +} + + +TEST_CASE("ffmpeg decodes high bit depth monochrome JPEG 2000 at the coded bit depth") +{ + if (!have_decoder(heif_compression_JPEG2000, "ffmpeg")) { + SKIP("FFmpeg decoder not available, skipping test"); + } + + // FFmpeg returns 10- and 12-bit grayscale as GRAY16 with left-shifted samples; 8- and 16-bit + // have exact formats (GRAY8 / GRAY16) and must be passed through unchanged. + int bpp = GENERATE(8, 10, 12, 16); + INFO("bit depth " << bpp); + + heif_image* input; + heif_error err = heif_image_create(W, H, heif_colorspace_monochrome, heif_chroma_monochrome, &input); + REQUIRE(err.code == heif_error_Ok); + fill_plane(input, heif_channel_Y, W, H, bpp); + + std::string filename = "ffmpeg_j2k_mono" + std::to_string(bpp) + ".heif"; + std::string path = encode_j2k_lossless(input, filename.c_str()); + heif_image_release(input); + + heif_image* decoded = decode_with_ffmpeg(path, heif_colorspace_monochrome, heif_chroma_monochrome, bpp); + check_plane(decoded, heif_channel_Y, W, H, bpp); + heif_image_release(decoded); +} + + +TEST_CASE("ffmpeg decodes high bit depth YCbCr 4:4:4 JPEG 2000 at the coded bit depth") +{ + if (!have_decoder(heif_compression_JPEG2000, "ffmpeg")) { + SKIP("FFmpeg decoder not available, skipping test"); + } + + // FFmpeg returns a 3-component 4:4:4 codestream with more than 8 bits as packed RGB48 + // with left-shifted samples (8 bit as packed RGB24). The libheif OpenJPEG encoder always + // writes 4:4:4, so subsampled codestreams cannot be generated here. + int bpp = GENERATE(8, 10, 12, 16); + INFO("bit depth " << bpp); + + heif_image* input; + heif_error err = heif_image_create(W, H, heif_colorspace_YCbCr, heif_chroma_444, &input); + REQUIRE(err.code == heif_error_Ok); + fill_plane(input, heif_channel_Y, W, H, bpp); + fill_plane(input, heif_channel_Cb, W, H, bpp); + fill_plane(input, heif_channel_Cr, W, H, bpp); + + std::string filename = "ffmpeg_j2k_yuv444_" + std::to_string(bpp) + ".heif"; + std::string path = encode_j2k_lossless(input, filename.c_str()); + heif_image_release(input); + + heif_image* decoded = decode_with_ffmpeg(path, heif_colorspace_YCbCr, heif_chroma_444, bpp); + check_plane(decoded, heif_channel_Y, W, H, bpp); + check_plane(decoded, heif_channel_Cb, W, H, bpp); + check_plane(decoded, heif_channel_Cr, W, H, bpp); + heif_image_release(decoded); +} diff -Nru libheif-1.19.8/tests/file_layout.cc libheif-1.23.4/tests/file_layout.cc --- libheif-1.19.8/tests/file_layout.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/file_layout.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,8 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "libheif/heif_sequences.h" +#include "api_structs.h" #include #include #include "test_utils.h" @@ -44,6 +45,107 @@ Error err = file.read(reader, heif_get_global_security_limits()); REQUIRE(err.error_code == heif_error_Ok); +} + + +TEST_CASE("ftyp box parse error is returned, not masked as missing ftyp") { + // A failure while reading the 'ftyp' box content (here: provoked through a + // brands security limit smaller than the file's brand list) must be returned + // from FileLayout::read(). It used to be swallowed, leaving the ftyp box + // unset, so that the file was later rejected with a misleading + // "No 'ftyp' box" error (issue #1872). + auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/uncompressed_comp_ABGR.heif", std::ios::binary)); + auto reader = std::make_shared(std::move(istr)); + + heif_security_limits limits = *heif_get_global_security_limits(); + limits.max_number_of_file_brands = 1; // the file has two compatible brands + + FileLayout file; + Error err = file.read(reader, &limits); + + REQUIRE(err.error_code == heif_error_Memory_allocation_error); + REQUIRE(err.sub_error_code == heif_suberror_Security_limit_exceeded); +} + + +TEST_CASE("disabled security limits accept any number of ftyp brands") { + // With disabled security limits, all limit fields are zero, which means + // "no limit". In v1.21.x, the zero was taken literally and every file with + // a non-empty compatible-brands list was rejected (issue #1872). + auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/uncompressed_comp_ABGR.heif", std::ios::binary)); + auto reader = std::make_shared(std::move(istr)); + + FileLayout file; + Error err = file.read(reader, heif_get_disabled_security_limits()); + + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(file.get_ftyp_box() != nullptr); +} + + +TEST_CASE("meta box with size 0 (extends to end of file)") { + // The 'meta' box uses a box size of 0, which per ISO/IEC 14496-12 clause 4.2 + // means it extends to the end of the file (legal for the last box). In this + // file 'meta' also appears after the media data rather than first. It must + // parse successfully (libavif / Chromium read and render this file). + auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/meta_size_zero.avif", std::ios::binary)); + auto reader = std::make_shared(std::move(istr)); + + FileLayout file; + Error err = file.read(reader, heif_get_global_security_limits()); + + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(file.get_meta_box() != nullptr); + + // The resolved 'meta' box must yield the correct image metadata. Read the + // file through the high-level API and check the primary image dimensions. + // These come from the 'ispe' box, so no AV1 decoder is required. + heif_context* context = get_context_for_test_file("meta_size_zero.avif"); + heif_image_handle* handle = get_primary_image_handle(context); + REQUIRE(heif_image_handle_get_ispe_width(handle) == 33); + REQUIRE(heif_image_handle_get_ispe_height(handle) == 11); + heif_image_handle_release(handle); + heif_context_free(context); +} + + +TEST_CASE("mini box with size 0 (extends to end of file)") { + // A top-level 'mini' box (MIAF low-overhead AVIF) with a box size of 0, which + // per ISO/IEC 14496-12 clause 4.2 means it extends to the end of the file. + auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/mini_size_zero.avif", std::ios::binary)); + auto reader = std::make_shared(std::move(istr)); + + FileLayout file; + Error err = file.read(reader, heif_get_global_security_limits()); + + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(file.get_mini_box() != nullptr); + + heif_context* context = get_context_for_test_file("mini_size_zero.avif"); + heif_image_handle* handle = get_primary_image_handle(context); + REQUIRE(heif_image_handle_get_ispe_width(handle) == 256); + REQUIRE(heif_image_handle_get_ispe_height(handle) == 256); + heif_image_handle_release(handle); + heif_context_free(context); +} + + +TEST_CASE("moov box with size 0 (extends to end of file)") { + // A sequence file laid out as 'ftyp' + 'mdat' + 'moov', where the trailing + // 'moov' box uses a box size of 0 (extends to end of file, per ISO/IEC + // 14496-12 clause 4.2). This is the common non-fast-start ordering. + auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/moov_size_zero.heif", std::ios::binary)); + auto reader = std::make_shared(std::move(istr)); + + FileLayout file; + Error err = file.read(reader, heif_get_global_security_limits()); + + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(file.get_moov_box() != nullptr); - // TODO: read file where 'meta' box is not the first one after 'ftyp' + // The resolved 'moov' box must yield a readable sequence track. + heif_context* context = get_context_for_test_file("moov_size_zero.heif"); + REQUIRE(heif_context_has_sequence(context) == 1); + REQUIRE(heif_context_number_of_sequence_tracks(context) == 1); + heif_context_free(context); } diff -Nru libheif-1.19.8/tests/fraction.cc libheif-1.23.4/tests/fraction.cc --- libheif-1.19.8/tests/fraction.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/fraction.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,71 @@ +/* + libheif clean aperture (clap) zero-size unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "box.h" +#include +#include + +// The checked Fraction factories replace the former Fraction(uint32_t, uint32_t) +// constructor, which only assert()ed its value range (GHSA-gh5q-69gg-c964). + +TEST_CASE("Fraction::from_unsigned") { + auto f = Fraction::from_unsigned(0x7FFFFFFF, 2); + REQUIRE(f); + REQUIRE(std::abs(f->to_double() - 0x7FFFFFFF / 2.0) < 1.0); + + REQUIRE(Fraction::from_unsigned(0, 1)); + REQUIRE(Fraction::from_unsigned(0x7FFFFFFF, 0x7FFFFFFF)); + + REQUIRE(!Fraction::from_unsigned(0x80000000, 1)); + REQUIRE(!Fraction::from_unsigned(1, 0x80000000)); + REQUIRE(!Fraction::from_unsigned(0xFFFFFFFF, 2)); + REQUIRE(!Fraction::from_unsigned(1, 0)); +} + +TEST_CASE("Fraction::from_signed") { + const int64_t max = std::numeric_limits::max(); + const int64_t min = std::numeric_limits::min(); + + auto f = Fraction::from_signed(-5, 3); + REQUIRE(f); + REQUIRE(f->numerator == -5); + REQUIRE(f->denominator == 3); + + REQUIRE(Fraction::from_signed(max, 1)); + REQUIRE(Fraction::from_signed(min, 1)); + REQUIRE(Fraction::from_signed(1, max)); + + REQUIRE(!Fraction::from_signed(max + 1, 1)); + REQUIRE(!Fraction::from_signed(min - 1, 1)); + REQUIRE(!Fraction::from_signed(1, max + 1)); + REQUIRE(!Fraction::from_signed(1, min - 1)); + + auto zero_den = Fraction::from_signed(1, 0); + REQUIRE(!zero_den); + REQUIRE(zero_den.error().error_code == heif_error_Invalid_input); + REQUIRE(zero_den.error().sub_error_code == heif_suberror_Invalid_fractional_number); +} diff -Nru libheif-1.19.8/tests/grid_tile_missing.cc libheif-1.23.4/tests/grid_tile_missing.cc --- libheif-1.19.8/tests/grid_tile_missing.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/grid_tile_missing.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,606 @@ +/* + libheif regression tests for grid image decoding edge cases. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" +#include "libheif/heif_tiling.h" + +#include +#include +#include + +namespace { + +// Build a minimal HEIF with one 'grid' item (id=1, 1x1, 64x64) whose 'dimg' +// iref references a tile item id (99) that has no corresponding 'infe' entry. +// Used to crash ImageItem_Grid::decode_grid_tile() with a NULL deref because +// HeifContext::get_image() returns nullptr for the unknown id. +std::vector build_heif_with_missing_grid_tile() { + // ftyp: heic / 0 / [mif1, heic] + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + // hdlr: handler type 'pict' (so the file is treated as an image collection + // and HeifContext discovers items) + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + // pitm v0: primary item = id 1 (the grid) + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf v0 containing one infe v2 entry: id=1, type='grid' + std::vector infe_payload; + put_u16_be(infe_payload, 1); // item_ID + put_u16_be(infe_payload, 0); // item_protection_index + append_fourcc(infe_payload, "grid"); // item_type + infe_payload.push_back(0); // item_name (empty, NUL-terminated) + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); // entry_count + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iprp / ipco { ispe(64x64) } / ipma associating ispe (prop index 1) with item 1 + std::vector ispe_payload; + put_u32_be(ispe_payload, 64); + put_u32_be(ispe_payload, 64); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + auto ipco = make_box("ipco", ispe); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID + ipma_payload.push_back(1); // association_count + ipma_payload.push_back(0x80 | 1); // essential=1, property_index=1 + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // iloc v1: item 1, construction_method=1 (idat), extent offset=0 length=8 + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // sizes + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 + put_u16_be(iloc_payload, 0); // data_reference_index + // base_offset omitted (base_offset_size=0) + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, 8); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref v0 with a single 'dimg' subbox: from item 1 to item 99 (which does + // NOT exist as an infe entry). + std::vector dimg_payload; + put_u16_be(dimg_payload, 1); // from_item_ID + put_u16_be(dimg_payload, 1); // reference_count + put_u16_be(dimg_payload, 99); // to_item_ID + auto dimg = make_box("dimg", dimg_payload); + auto iref = make_box("iref", dimg, /*full=*/true); + + // idat: 8-byte ImageGrid header (v=0, flags=0, rows-1=0, cols-1=0, w=64, h=64) + std::vector idat_payload = {0, 0, 0, 0}; + put_u16_be(idat_payload, 64); + put_u16_be(idat_payload, 64); + auto idat = make_box("idat", idat_payload); + + // meta = hdlr + pitm + iinf + iprp + iloc + iref + idat + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Build a minimal HEIF with a 2x1 'grid' item (id=1) that has an associated +// 'irot' rotation of 270° CCW. In the displayed image the grid is logically +// 1 column by 2 rows. The grid references a missing tile (id 99), but the +// test never gets that far: it passes (tile_x=1, tile_y=0) which is valid in +// the file's 2x1 grid but out-of-range in the displayed 1x2 grid. The old +// transform code did `num_rows - 1 - tile_x = 1 - 1 - 1` → unsigned underflow +// (UBSan). +std::vector build_heif_grid_with_irot270() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "grid"); + infe_payload.push_back(0); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe (prop 1): 128 (w) x 64 (h) -- the file/grid dimensions. + std::vector ispe_payload; + put_u32_be(ispe_payload, 128); + put_u32_be(ispe_payload, 64); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // irot (prop 2): rotation = 3 * 90° = 270° CCW. + std::vector irot_payload; + irot_payload.push_back(3); + auto irot = make_box("irot", irot_payload); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, irot); + auto ipco = make_box("ipco", ipco_payload); + + // ipma: item 1 associated with prop 1 (ispe) and prop 2 (irot). + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); + put_u16_be(ipma_payload, 1); + ipma_payload.push_back(2); + ipma_payload.push_back(0x80 | 1); + ipma_payload.push_back(0x80 | 2); + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // iloc v1: item 1 in idat, 8-byte ImageGrid header. + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); + put_u16_be(iloc_payload, 1); + put_u16_be(iloc_payload, 1); + put_u16_be(iloc_payload, 0x0001); + put_u16_be(iloc_payload, 0); + put_u16_be(iloc_payload, 1); + put_u32_be(iloc_payload, 0); + put_u32_be(iloc_payload, 8); + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: grid (item 1) references 2 missing tiles (ids 99 and 100). Two + // distinct ids are required because iref rejects duplicate references. + std::vector dimg_payload; + put_u16_be(dimg_payload, 1); + put_u16_be(dimg_payload, 2); + put_u16_be(dimg_payload, 99); + put_u16_be(dimg_payload, 100); + auto dimg = make_box("dimg", dimg_payload); + auto iref = make_box("iref", dimg, /*full=*/true); + + // idat: ImageGrid header — v=0, flags=0, rows-1=0, cols-1=1, w=128, h=64. + std::vector idat_payload = {0, 0, 0, 1}; + put_u16_be(idat_payload, 128); + put_u16_be(idat_payload, 64); + auto idat = make_box("idat", idat_payload); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Build a minimal AVIF with one 'grid' item (id=1, 1x1, 64x64) whose single +// tile (id=2, 'av01') is well-formed at the box level (valid infe, ispe, av1C) +// but whose iloc extent points far beyond the end of the file, so reading the +// compressed tile data fails at decode time. This models a file where every +// tile fails to decode (issue #1876: e.g. no decoder plugin installed, or all +// tile data unreadable). The tile is AV1 rather than HEVC so that the test also +// runs on builds that leave out the HEVC codecs (issue #1900). +std::vector build_heif_grid_with_undecodable_tile() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "avif"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "avif"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf with two items: id=1 'grid', id=2 'av01' + std::vector infe1_payload; + put_u16_be(infe1_payload, 1); + put_u16_be(infe1_payload, 0); + append_fourcc(infe1_payload, "grid"); + infe1_payload.push_back(0); + auto infe1 = make_box("infe", infe1_payload, /*full=*/true, /*version=*/2); + + std::vector infe2_payload; + put_u16_be(infe2_payload, 2); + put_u16_be(infe2_payload, 0); + append_fourcc(infe2_payload, "av01"); + infe2_payload.push_back(0); + auto infe2 = make_box("infe", infe2_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 2); + append(iinf_payload, infe1); + append(iinf_payload, infe2); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ipco: ispe(64x64) as prop 1, minimal av1C (no configOBUs) as prop 2 + std::vector ispe_payload; + put_u32_be(ispe_payload, 64); + put_u32_be(ispe_payload, 64); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector av1C_payload; + av1C_payload.push_back(0x81); // marker=1, version=1 + av1C_payload.push_back(0x00); // seq_profile=0 (Main), seq_level_idx_0=0 + av1C_payload.push_back(0x0C); // 8 bit, color, chroma_subsampling_x/y=1 (4:2:0) + av1C_payload.push_back(0x00); // no initial_presentation_delay + auto av1C = make_box("av1C", av1C_payload); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, av1C); + auto ipco = make_box("ipco", ipco_payload); + + // ipma: item 1 -> ispe; item 2 -> ispe + essential av1C + std::vector ipma_payload; + put_u32_be(ipma_payload, 2); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(1); // association_count + ipma_payload.push_back(0x80 | 1); // essential ispe + put_u16_be(ipma_payload, 2); // item_ID 2 + ipma_payload.push_back(2); // association_count + ipma_payload.push_back(1); // non-essential ispe + ipma_payload.push_back(0x80 | 2); // essential av1C + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // iloc v1: + // item 1: grid header via idat (construction_method 1) + // item 2: file range far beyond EOF (construction_method 0), so the tile + // data read fails only when the tile is actually decoded + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // sizes + put_u16_be(iloc_payload, 2); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, 8); // extent_length + put_u16_be(iloc_payload, 2); // item_ID + put_u16_be(iloc_payload, 0x0000); // construction_method=0 (file offset) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0x00FF0000); // extent_offset (beyond EOF) + put_u32_be(iloc_payload, 100); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: grid (item 1) references tile item 2 + std::vector dimg_payload; + put_u16_be(dimg_payload, 1); + put_u16_be(dimg_payload, 1); + put_u16_be(dimg_payload, 2); + auto dimg = make_box("dimg", dimg_payload); + auto iref = make_box("iref", dimg, /*full=*/true); + + // idat: ImageGrid header (v=0, flags=0, rows-1=0, cols-1=0, w=64, h=64) + std::vector idat_payload = {0, 0, 0, 0}; + put_u16_be(idat_payload, 64); + put_u16_be(idat_payload, 64); + auto idat = make_box("idat", idat_payload); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + + +// Regression for PR #1807 / NULL deref in ImageItem_Grid::decode_grid_tile +// when a grid's 'dimg' iref references an item id with no matching infe. +TEST_CASE("grid: missing tile reference returns error instead of crashing") { + auto data = build_heif_with_missing_grid_tile(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image* tile = nullptr; + err = heif_image_handle_decode_image_tile(handle, &tile, + heif_colorspace_YCbCr, + heif_chroma_420, + nullptr, 0, 0); + + // Must surface as a clean error, not a crash. + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Missing_grid_images); + REQUIRE(tile == nullptr); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +// Regression for PR #1808 / OOB read in ImageItem_Grid::decode_grid_tile +// when the caller passes (tile_x, tile_y) coordinates outside the grid's +// declared (columns, rows). The previous assert() was a no-op in Release +// builds and the indexing then read past m_grid_tile_ids. +TEST_CASE("grid: out-of-range tile coordinate returns error instead of OOB") { + // Reuses the same 1x1 grid built for the prior test. Coordinates (1, 0) + // are outside its (cols=1, rows=1) bounds. + auto data = build_heif_with_missing_grid_tile(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image* tile = nullptr; + err = heif_image_handle_decode_image_tile(handle, &tile, + heif_colorspace_YCbCr, + heif_chroma_420, + nullptr, /*tile_x=*/1, /*tile_y=*/0); + + // The out-of-range coordinate is now rejected up-front by the tile-position + // transform (see transform_requested_tile_position_to_original_tile_position). + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(tile == nullptr); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +// Regression for the UBSan crash in +// ImageItem::transform_requested_tile_position_to_original_tile_position: +// `tiling.num_rows - 1 - tile_x` underflowed when a 90°/270° irot was present +// and the caller passed a tile coordinate that was valid in the file's grid +// but out-of-range in the displayed (rotated) grid. +TEST_CASE("grid: rotated grid rejects tile coord beyond displayed bounds") { + auto data = build_heif_grid_with_irot270(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + // Displayed tiling should reflect the 270° rotation: 1 column, 2 rows. + heif_image_tiling tiling = {}; + err = heif_image_handle_get_image_tiling(handle, /*xform=*/1, &tiling); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 2); + + // (tile_x=1, tile_y=0) is in-range for the file's 2x1 grid but out-of-range + // for the displayed 1x2 grid. Pre-fix this hit unsigned underflow inside + // the inverse 270° transform. It must now be rejected as a usage error + // instead of crashing under UBSan. + heif_item_id tile_id = 0; + err = heif_image_handle_get_grid_image_tile_id( + handle, /*xform=*/1, /*tile_x=*/1, /*tile_y=*/0, &tile_id); + REQUIRE(err.code == heif_error_Usage_error); + + heif_image* tile = nullptr; + err = heif_image_handle_decode_image_tile(handle, &tile, + heif_colorspace_YCbCr, + heif_chroma_420, + nullptr, /*tile_x=*/1, /*tile_y=*/0); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(tile == nullptr); + + // Sanity: an in-range displayed coordinate is *not* rejected by the bounds + // check (it reaches the missing-tile path). + err = heif_image_handle_get_grid_image_tile_id( + handle, /*xform=*/1, /*tile_x=*/0, /*tile_y=*/1, &tile_id); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +// Regression for issue #1876: when *every* tile of a grid fails to decode, +// non-strict decoding used to skip each tile with a warning, never create the +// output canvas, and return a null image. The per-tile errors were dropped +// together with the warnings and the caller only saw the meaningless +// "Decoder plugin generated an error: Unspecified". The real per-tile error +// must be returned instead. +TEST_CASE("grid: real error is reported when all tiles fail to decode") { + // The expected error comes from reading the tile data, which only happens + // once a decoder for the tile has been found. + if (!heif_have_decoder_for_format(heif_compression_AV1)) { + SKIP("AV1 decoder not available, skipping test"); + } + + auto data = build_heif_grid_with_undecodable_tile(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + nullptr); + + INFO("error message: " << (err.message ? err.message : "(null)")); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(img == nullptr); + + // The tile's iloc extent points beyond EOF, so the underlying error is the + // failed data read, not a generic decoder error. + CHECK(err.code == heif_error_Invalid_input); + CHECK(err.subcode == heif_suberror_End_of_data); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +// Same file as above, but decoded with a decoder_id that does not exist. +// This models the issue #1876 situation where no matching decoder (plugin) +// is available for any tile: the plugin-lookup error must reach the caller +// instead of being dropped with the null grid image. +TEST_CASE("grid: missing decoder for tiles is reported instead of generic error") { + auto data = build_heif_grid_with_undecodable_tile(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + options->decoder_id = "nonexistent-decoder"; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + options); + heif_decoding_options_free(options); + + INFO("error message: " << (err.message ? err.message : "(null)")); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(img == nullptr); + + CHECK(err.code == heif_error_Plugin_loading_error); + CHECK(std::string(err.message).find("No decoder with that ID found") + != std::string::npos); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/hevc_sps.cc libheif-1.23.4/tests/hevc_sps.cc --- libheif-1.19.8/tests/hevc_sps.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/hevc_sps.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,84 @@ +/* + libheif HEVC SPS parsing unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "codecs/hevc_boxes.h" +#include "codecs/decoder.h" +#include "error.h" +#include +#include + +// SPS NAL unit (with emulation prevention bytes) taken from the hvcC box of +// tests/data/rainbow-451x461.heic. x265 padded the 451x461 input to a coded +// size of 456x464 and signals a conformance window that crops it to 452x462. +static const std::vector rainbow_sps{ + 0x42, 0x01, 0x01, 0x03, 0x70, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0xa0, 0x0e, 0x48, 0x07, 0x47, 0x75, + 0x96, 0xea, 0x49, 0x29, 0xae, 0x6e, 0x02, 0x1a, 0x0c, 0x08, 0x00, 0x00, + 0x03, 0x00, 0xc8, 0x00, 0x00, 0x03, 0x00, 0x08, 0x40}; + + +TEST_CASE("SPS conformance window yields visible and coded size") +{ + HEVCDecoderConfigurationRecord config; + uint32_t width = 0, height = 0; + ImageSize coded{}; + + Error err = parse_sps_for_hvcC_configuration(rainbow_sps.data(), rainbow_sps.size(), + &config, &width, &height, &coded); + REQUIRE(!err); + + CHECK(config.chroma_format == 1); + CHECK(config.bit_depth_luma == 8); + CHECK(config.bit_depth_chroma == 8); + + CHECK(width == 452); + CHECK(height == 462); + CHECK(coded.width == 456); + CHECK(coded.height == 464); +} + + +TEST_CASE("SPS chroma_format_idc out of range is rejected") +{ + // Same SPS with chroma_format_idc changed from 1 (uvlc '010') to 4 + // (uvlc '00101'). The byte at index 18 holds sps_seq_parameter_set_id and + // the start of chroma_format_idc: '1 010 ....' becomes '1 00101 ..'. + std::vector sps = rainbow_sps; + REQUIRE((sps[18] & 0xF0) == 0xA0); + sps[18] = 0x94 | (sps[18] & 0x03); + + HEVCDecoderConfigurationRecord config; + uint32_t width = 0, height = 0; + ImageSize coded{}; + + Error err = parse_sps_for_hvcC_configuration(sps.data(), sps.size(), + &config, &width, &height, &coded); + REQUIRE(err); + CHECK(err.error_code == heif_error_Invalid_input); + CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value); + CHECK(err.message.find("chroma_format_idc") != std::string::npos); +} diff -Nru libheif-1.19.8/tests/id_creator.cc libheif-1.23.4/tests/id_creator.cc --- libheif-1.19.8/tests/id_creator.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/id_creator.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,138 @@ +/* + libheif unit tests for IDCreator. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "id_creator.h" + +#include +#include + +using NS = IDCreator::Namespace; + +TEST_CASE("IDCreator hands out unique increasing ids") { + IDCreator idc; + std::set seen; + for (int i = 0; i < 1000; i++) { + auto r = idc.get_new_id(NS::item); + REQUIRE(r); + REQUIRE(seen.insert(*r).second); // no duplicate + } +} + +TEST_CASE("IDCreator never re-hands a marked id") { + IDCreator idc; + + // Reserve a scattering of ids, including some below and some above the + // starting counter (1). + const uint32_t marked[] = {1, 5, 6, 7, 100, 99, 42}; + for (uint32_t id : marked) { + idc.mark_id_used(NS::item, id); + } + + std::set reserved(std::begin(marked), std::end(marked)); + + for (int i = 0; i < 1000; i++) { + auto r = idc.get_new_id(NS::item); + REQUIRE(r); + // Must never collide with a reserved id, and must be strictly increasing + // past the highest reserved id. + REQUIRE(reserved.find(*r) == reserved.end()); + REQUIRE(*r > 100); + } +} + +TEST_CASE("IDCreator namespaces are independent") { + IDCreator idc; + idc.mark_id_used(NS::item, 500); + + // track / entity_group counters are unaffected by the item reservation. + auto t = idc.get_new_id(NS::track); + REQUIRE(t); + REQUIRE(*t == 1); + + auto g = idc.get_new_id(NS::entity_group); + REQUIRE(g); + REQUIRE(*g == 1); + + auto it = idc.get_new_id(NS::item); + REQUIRE(it); + REQUIRE(*it == 501); +} + +TEST_CASE("IDCreator exhausts cleanly at UINT32_MAX-1") { + IDCreator idc; + idc.mark_id_used(NS::item, UINT32_MAX - 1); + + // Exactly one id (UINT32_MAX) is left. + auto last = idc.get_new_id(NS::item); + REQUIRE(last); + REQUIRE(*last == UINT32_MAX); + + // Now exhausted: no further id, and definitely no wrap-around reuse. + auto overflow = idc.get_new_id(NS::item); + REQUIRE_FALSE(overflow); + REQUIRE(overflow.is_error()); +} + +TEST_CASE("IDCreator marking UINT32_MAX exhausts the namespace (no overflow)") { + // This is the value that made mark_id_used compute 'id + 1' and wrap to 0, + // aborting under -fsanitize=integer. It must exhaust the namespace, never + // hand out an id, and never reuse 0xFFFFFFFF. + IDCreator idc; + idc.mark_id_used(NS::item, UINT32_MAX); + + auto r = idc.get_new_id(NS::item); + REQUIRE_FALSE(r); + REQUIRE(r.is_error()); + + // Marking a smaller id afterwards must not resurrect the exhausted counter. + idc.mark_id_used(NS::item, 3); + auto r2 = idc.get_new_id(NS::item); + REQUIRE_FALSE(r2); +} + +TEST_CASE("IDCreator unif mode shares one id space across namespaces") { + IDCreator idc; + idc.set_unif(true); + + std::set seen; + auto a = idc.get_new_id(NS::item); + auto b = idc.get_new_id(NS::track); + auto c = idc.get_new_id(NS::entity_group); + REQUIRE(a); + REQUIRE(b); + REQUIRE(c); + REQUIRE(seen.insert(*a).second); + REQUIRE(seen.insert(*b).second); // different namespace, still unique + REQUIRE(seen.insert(*c).second); + + // Reserving the max in unif mode exhausts the single shared counter. + IDCreator idc2; + idc2.set_unif(true); + idc2.mark_id_used(NS::track, UINT32_MAX); + auto r = idc2.get_new_id(NS::item); + REQUIRE_FALSE(r); +} diff -Nru libheif-1.19.8/tests/iden_declared_size.cc libheif-1.23.4/tests/iden_declared_size.cc --- libheif-1.19.8/tests/iden_declared_size.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/iden_declared_size.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,226 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// Build a minimal HEIF file with two items: +// item 1 ('mski', 8x8, 8bpp): real, decodable content (the mask codec needs +// no bitstream -- its "compressed" data is just the raw pixel bytes). +// item 2 ('iden', primary): 'dimg'-references item 1, and carries its own +// 'ispe' declaring (decl_w x decl_h), which may or may not match +// item 1's actual 8x8. +// No codec plugin is required for either item. +std::vector build_heif_with_iden(uint32_t decl_w, uint32_t decl_h) { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + // pitm: primary item = item 2 (the 'iden'). + std::vector pitm_payload; + put_u16_be(pitm_payload, 2); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'mski', item 2 = 'iden'. + std::vector infe1_payload; + put_u16_be(infe1_payload, 1); + put_u16_be(infe1_payload, 0); + append_fourcc(infe1_payload, "mski"); + infe1_payload.push_back(0); + auto infe1 = make_box("infe", infe1_payload, /*full=*/true, /*version=*/2); + + std::vector infe2_payload; + put_u16_be(infe2_payload, 2); + put_u16_be(infe2_payload, 0); + append_fourcc(infe2_payload, "iden"); + infe2_payload.push_back(0); + auto infe2 = make_box("infe", infe2_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 2); + append(iinf_payload, infe1); + append(iinf_payload, infe2); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iprp / ipco: prop 1 = ispe(8,8) for item 1; prop 2 = mskC(8bpp) for item 1; + // prop 3 = ispe(decl_w, decl_h) for item 2 (the iden's own declared size). + std::vector ispe1_payload; + put_u32_be(ispe1_payload, 8); + put_u32_be(ispe1_payload, 8); + auto ispe1 = make_box("ispe", ispe1_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(8); // bits_per_pixel + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector ispe2_payload; + put_u32_be(ispe2_payload, decl_w); + put_u32_be(ispe2_payload, decl_h); + auto ispe2 = make_box("ispe", ispe2_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe1); + append(ipco_payload, mskC); + append(ipco_payload, ispe2); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 2); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(2); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe1 + ipma_payload.push_back(0x80 | 2); // essential, mskC + put_u16_be(ipma_payload, 2); // item_ID 2 + ipma_payload.push_back(1); // association_count + ipma_payload.push_back(0x80 | 3); // essential, ispe2 + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // iloc: only item 1 has data (raw 8x8 8bpp mask, in idat). The 'iden' item + // (item 2) has no bitstream of its own and needs no entry. + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, 64); // extent_length (8x8x1 byte) + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: item 2 ('iden') 'dimg'-references item 1. + std::vector dimg_payload; + put_u16_be(dimg_payload, 2); // from_item_ID + put_u16_be(dimg_payload, 1); // reference_count + put_u16_be(dimg_payload, 1); // to_item_ID + auto dimg = make_box("dimg", dimg_payload); + auto iref = make_box("iref", dimg, /*full=*/true); + + // idat: 8x8 = 64 bytes of raw 8bpp mask pixel data. + std::vector idat_payload(64, 0x7F); + auto idat = make_box("idat", idat_payload); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + + +// Regression test for the 'iden' hardening in ImageItem_iden::check_decoded_image_size(): +// an 'iden' item's own declared 'ispe' is now validated against what it actually +// forwards to, instead of being skipped unconditionally. +TEST_CASE("iden: declared ispe not matching the referenced item's decoded size is rejected") { + // iden (item 2) declares 16x16, but 'dimg'-references item 1, which is really 8x8. + auto data = build_heif_with_iden(16, 16); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Invalid_image_size); + REQUIRE(img == nullptr); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} + +// Control: same file, but the iden's declared ispe honestly matches item 1's +// actual size. Proves the new check doesn't reject legitimate content. +TEST_CASE("iden: declared ispe matching the referenced item's decoded size decodes normally") { + auto data = build_heif_with_iden(8, 8); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(heif_image_get_width(img, heif_channel_Y) == 8); + REQUIRE(heif_image_get_height(img, heif_channel_Y) == 8); + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/image_description_metadata.cc libheif-1.23.4/tests/image_description_metadata.cc --- libheif-1.19.8/tests/image_description_metadata.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/image_description_metadata.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,285 @@ +/* + libheif unit tests for ImageDescription metadata preservation across + HeifPixelImage transforms (rotation, crop, clone). + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "image/pixelimage.h" +#include "image/image_description.h" +#include "libheif/heif.h" +#include "libheif/heif_uncompressed.h" +#include +#include + + +// Holds the non-default values for every ImageDescription metadata field so +// that the same values can be applied to a fresh image and then checked on +// transform outputs. +struct MetadataFixture +{ + nclx_profile nclx; + uint32_t pixel_ratio_h = 16; + uint32_t pixel_ratio_v = 9; + heif_content_light_level clli{1000, 400}; + heif_mastering_display_colour_volume mdcv{}; + heif_tai_timestamp_packet tai{}; + std::string sample_gimi = "urn:uuid:sample-id"; + std::string comp1_gimi = "urn:uuid:comp1"; + std::string comp2_gimi = "urn:uuid:comp2"; + BayerPattern bayer; + PolarizationPattern polar; + SensorBadPixelsMap badpix; + SensorNonUniformityCorrection nuc; + uint8_t chroma_location = 2; + uint32_t sample_duration = 33; // sequence frame duration + heif_omaf_image_projection omaf = heif_omaf_image_projection_equirectangular; + + MetadataFixture() + { + nclx.set_colour_primaries(heif_color_primaries_ITU_R_BT_2020_2_and_2100_0); + nclx.set_transfer_characteristics(heif_transfer_characteristic_ITU_R_BT_2100_0_PQ); + nclx.set_matrix_coefficients(heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance); + nclx.set_full_range_flag(false); + + mdcv.display_primaries_x[0] = 100; mdcv.display_primaries_y[0] = 200; + mdcv.display_primaries_x[1] = 300; mdcv.display_primaries_y[1] = 400; + mdcv.display_primaries_x[2] = 500; mdcv.display_primaries_y[2] = 600; + mdcv.white_point_x = 700; mdcv.white_point_y = 800; + mdcv.max_display_mastering_luminance = 90000; + mdcv.min_display_mastering_luminance = 100; + + tai.version = 1; + tai.tai_timestamp = 1234567890ULL; + tai.synchronization_state = 1; + tai.timestamp_generation_failure = 0; + tai.timestamp_is_modified = 0; + + polar.pattern_width = 2; + polar.pattern_height = 2; + polar.polarization_angles = {0.0f, 45.0f, 90.0f, 135.0f}; + + badpix.correction_applied = false; + badpix.bad_rows = {2}; + badpix.bad_columns = {3}; + badpix.bad_pixels.push_back({1, 1}); + + nuc.nuc_is_applied = true; + nuc.image_width = 4; + nuc.image_height = 2; + nuc.nuc_gains.assign(8, 1.5f); + nuc.nuc_offsets.assign(8, 0.25f); + } +}; + + +// Builds an 8x4 monochrome image with two components added via +// add_component(). Sets every available ImageDescription metadata field. +// Returns the image and the two minted component ids. +static std::shared_ptr +build_image(MetadataFixture& fix, uint32_t& comp1, uint32_t& comp2) +{ + auto img = std::make_shared(); + img->create(8, 4, heif_colorspace_monochrome, heif_chroma_planar); + + const heif_security_limits* limits = heif_get_global_security_limits(); + + auto r1 = img->add_component(8, 4, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, 8, limits); + REQUIRE(r1); + comp1 = *r1; + + auto r2 = img->add_component(8, 4, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, 8, limits); + REQUIRE(r2); + comp2 = *r2; + + // Per-component gimi content IDs + img->find_component_description(comp1)->gimi_content_id = fix.comp1_gimi; + img->find_component_description(comp2)->gimi_content_id = fix.comp2_gimi; + + // Image-level metadata + img->set_premultiplied_alpha(true); + img->set_color_profile_nclx(fix.nclx); + img->set_pixel_ratio(fix.pixel_ratio_h, fix.pixel_ratio_v); + img->set_clli(fix.clli); + img->set_mdcv(fix.mdcv); + img->set_tai_timestamp(&fix.tai); + img->set_gimi_sample_content_id(fix.sample_gimi); + img->set_sample_duration(fix.sample_duration); + + // Bayer pattern referencing the two component IDs (2x2 GBRG-style). + fix.bayer.pattern_width = 2; + fix.bayer.pattern_height = 2; + fix.bayer.pixels.resize(4); + fix.bayer.pixels[0].component_id = comp1; fix.bayer.pixels[0].component_gain = 1.0f; + fix.bayer.pixels[1].component_id = comp2; fix.bayer.pixels[1].component_gain = 1.0f; + fix.bayer.pixels[2].component_id = comp2; fix.bayer.pixels[2].component_gain = 1.0f; + fix.bayer.pixels[3].component_id = comp1; fix.bayer.pixels[3].component_gain = 1.0f; + img->set_bayer_pattern(fix.bayer); + + fix.polar.component_ids = {comp1}; + img->add_polarization_pattern(fix.polar); + + fix.badpix.component_ids = {comp1}; + img->add_sensor_bad_pixels_map(fix.badpix); + + fix.nuc.component_ids = {comp2}; + img->add_sensor_nuc(fix.nuc); + + img->set_chroma_location(fix.chroma_location); + + img->set_omaf_image_projection(fix.omaf); + + return img; +} + + +// Asserts that every metadata field from `fix` is present on `img` and that +// the two monochrome components (with their gimi content IDs) survived. +static void check_metadata(const std::shared_ptr& img, + const MetadataFixture& fix) +{ + REQUIRE(img->is_premultiplied_alpha() == true); + REQUIRE(img->get_color_profile_nclx() == fix.nclx); + + uint32_t h = 0, v = 0; + img->get_pixel_ratio(&h, &v); + REQUIRE(h == fix.pixel_ratio_h); + REQUIRE(v == fix.pixel_ratio_v); + + REQUIRE(img->has_clli()); + REQUIRE(img->get_clli().max_content_light_level == fix.clli.max_content_light_level); + REQUIRE(img->get_clli().max_pic_average_light_level == fix.clli.max_pic_average_light_level); + + REQUIRE(img->has_mdcv()); + const auto& m = img->get_mdcv(); + REQUIRE(m.display_primaries_x[0] == fix.mdcv.display_primaries_x[0]); + REQUIRE(m.display_primaries_y[2] == fix.mdcv.display_primaries_y[2]); + REQUIRE(m.white_point_x == fix.mdcv.white_point_x); + REQUIRE(m.max_display_mastering_luminance == fix.mdcv.max_display_mastering_luminance); + + const heif_tai_timestamp_packet* tai = img->get_tai_timestamp(); + REQUIRE(tai != nullptr); + REQUIRE(tai->tai_timestamp == fix.tai.tai_timestamp); + REQUIRE(tai->synchronization_state == fix.tai.synchronization_state); + + REQUIRE(img->has_gimi_sample_content_id()); + REQUIRE(img->get_gimi_sample_content_id() == fix.sample_gimi); + + REQUIRE(img->has_any_bayer_pattern()); + const BayerPattern& bp = img->get_any_bayer_pattern(); + REQUIRE(bp.pattern_width == fix.bayer.pattern_width); + REQUIRE(bp.pattern_height == fix.bayer.pattern_height); + REQUIRE(bp.pixels.size() == fix.bayer.pixels.size()); + + REQUIRE(img->has_polarization_patterns()); + REQUIRE(img->get_polarization_patterns().size() == 1); + REQUIRE(img->get_polarization_patterns()[0].pattern_width == fix.polar.pattern_width); + REQUIRE(img->get_polarization_patterns()[0].polarization_angles == fix.polar.polarization_angles); + + REQUIRE(img->has_sensor_bad_pixels_maps()); + REQUIRE(img->get_sensor_bad_pixels_maps().size() == 1); + REQUIRE(img->get_sensor_bad_pixels_maps()[0].bad_rows == fix.badpix.bad_rows); + REQUIRE(img->get_sensor_bad_pixels_maps()[0].bad_columns == fix.badpix.bad_columns); + + REQUIRE(img->has_sensor_nuc()); + REQUIRE(img->get_sensor_nuc().size() == 1); + REQUIRE(img->get_sensor_nuc()[0].image_width == fix.nuc.image_width); + REQUIRE(img->get_sensor_nuc()[0].nuc_gains == fix.nuc.nuc_gains); + + REQUIRE(img->has_chroma_location()); + REQUIRE(img->get_chroma_location() == fix.chroma_location); + + REQUIRE(img->get_sample_duration() == fix.sample_duration); + + REQUIRE(img->get_omaf_image_projection() == fix.omaf); + + // Two monochrome components must still be there, with their gimi content IDs. + // Rotate / crop re-mint component IDs; create_clone_image_at_new_size reuses + // them. In both cases the order in m_components is preserved. + std::vector mono_gimi_ids; + for (const auto& c : img->get_component_descriptions()) { + if (c.component_type == heif_cmpd_component_type_monochrome) { + mono_gimi_ids.push_back(c.gimi_content_id); + } + } + REQUIRE(mono_gimi_ids.size() == 2); + REQUIRE(mono_gimi_ids[0] == fix.comp1_gimi); + REQUIRE(mono_gimi_ids[1] == fix.comp2_gimi); +} + + +TEST_CASE("rotate_ccw 90 preserves metadata and components", "[image_description_metadata]") +{ + MetadataFixture fix; + uint32_t c1, c2; + auto src = build_image(fix, c1, c2); + + auto rotated_r = src->rotate_ccw(90, heif_get_global_security_limits()); + REQUIRE(rotated_r); + auto rotated = *rotated_r; + + REQUIRE(rotated->get_width() == 4); + REQUIRE(rotated->get_height() == 8); + + check_metadata(rotated, fix); +} + + +TEST_CASE("crop preserves metadata and components", "[image_description_metadata]") +{ + MetadataFixture fix; + uint32_t c1, c2; + auto src = build_image(fix, c1, c2); + + // crop to a 4x2 region (left=0, right=3, top=0, bottom=1) + auto cropped_r = src->crop(0, 3, 0, 1, heif_get_global_security_limits()); + REQUIRE(cropped_r); + auto cropped = *cropped_r; + + REQUIRE(cropped->get_width() == 4); + REQUIRE(cropped->get_height() == 2); + + check_metadata(cropped, fix); +} + + +TEST_CASE("create_clone_image_at_new_size preserves metadata and components", + "[image_description_metadata]") +{ + MetadataFixture fix; + uint32_t c1, c2; + auto src = build_image(fix, c1, c2); + + auto clone = std::make_shared(); + Error err = clone->create_clone_image_at_new_size(src, 16, 8, + heif_get_global_security_limits()); + REQUIRE(!err); + + REQUIRE(clone->get_width() == 16); + REQUIRE(clone->get_height() == 8); + + check_metadata(clone, fix); +} diff -Nru libheif-1.19.8/tests/item_properties.cc libheif-1.23.4/tests/item_properties.cc --- libheif-1.19.8/tests/item_properties.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/item_properties.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,213 @@ +/* + libheif integration tests for item property ids + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_items.h" +#include "libheif/heif_properties.h" +#include "test_utils.h" +#include +#include + + +// The 'heif_property_id' returned by heif_item_add_*_property() has to be usable with the +// property getters. The property boxes of all items are stored in a single, file-wide 'ipco' +// box, while the getters address the properties of one item. As soon as the file holds more +// properties than the item we add to, the two numberings differ. Using an 'ipco' index as +// property id then reads past the end of the item's property list. + +TEST_CASE("property id round trip with several items") { + heif_init(nullptr); + heif_context* ctx = heif_context_alloc(); + + const std::vector payload1{'i', 't', 'e', 'm', '1'}; + const std::vector payload2{'i', 't', 'e', 'm', '2'}; + + heif_item_id item1, item2; + heif_error err; + + err = heif_context_add_item(ctx, "mime", payload1.data(), (int) payload1.size(), &item1); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_item(ctx, "mime", payload2.data(), (int) payload2.size(), &item2); + REQUIRE(err.code == heif_error_Ok); + + // Add a property to the second item first, so that the 'ipco' box holds a property that the + // first item does not associate. + + const std::vector body2{0x02, 0x02, 0x02, 0x02}; + heif_property_id propertyId2; + err = heif_item_add_raw_property(ctx, item2, heif_fourcc('p', 'r', 'p', '2'), nullptr, + body2.data(), body2.size(), 0, &propertyId2); + REQUIRE(err.code == heif_error_Ok); + + const std::vector body1{0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; + heif_property_id propertyId1; + err = heif_item_add_raw_property(ctx, item1, heif_fourcc('p', 'r', 'p', '1'), nullptr, + body1.data(), body1.size(), 0, &propertyId1); + REQUIRE(err.code == heif_error_Ok); + + // Both items hold exactly one property, hence both ids have to be 1. Before this was fixed, + // 'propertyId1' was 2 (the position in 'ipco'), which read past the end of item1's list. + + REQUIRE(propertyId1 == 1); + REQUIRE(propertyId2 == 1); + + // The ids the enumerator hands out have to match the ids we got when adding. + + heif_property_id enumerated[8]; + int n = heif_item_get_properties_of_type(ctx, item1, heif_item_property_type_invalid, enumerated, 8); + REQUIRE(n == 1); + REQUIRE(enumerated[0] == propertyId1); + + // Read the properties back through the ids that were returned when adding them. + + size_t size = 0; + err = heif_item_get_property_raw_size(ctx, item1, propertyId1, &size); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(size == body1.size()); + + std::vector readback(size); + err = heif_item_get_property_raw_data(ctx, item1, propertyId1, readback.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(readback == body1); + + err = heif_item_get_property_raw_size(ctx, item2, propertyId2, &size); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(size == body2.size()); + + readback.resize(size); + err = heif_item_get_property_raw_data(ctx, item2, propertyId2, readback.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(readback == body2); + + // An id past the end of the item's property list has to be rejected instead of reading + // out of bounds. + + err = heif_item_get_property_raw_size(ctx, item1, propertyId1 + 1, &size); + REQUIRE(err.code == heif_error_Usage_error); + + heif_context_free(ctx); + heif_deinit(); +} + + +// The same divergence occurs for a plain image with a thumbnail: the thumbnail contributes its +// own codec configuration and 'ispe' to the shared 'ipco' box. + +TEST_CASE("property id round trip with thumbnail") { + heif_init(nullptr); + // Query the encoder before allocating anything, so that a skipped test leaks nothing. + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC); + heif_image* input_image = createImage_RGB_planar(); + heif_context* ctx = heif_context_alloc(); + + heif_image_handle* image_handle; + heif_error err = heif_context_encode_image(ctx, input_image, encoder, nullptr, &image_handle); + UNSCOPED_INFO("heif_context_encode_image: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* thumbnail_handle = nullptr; + err = heif_context_encode_thumbnail(ctx, input_image, image_handle, encoder, nullptr, 16, + &thumbnail_handle); + UNSCOPED_INFO("heif_context_encode_thumbnail: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(thumbnail_handle != nullptr); + + heif_item_id itemId; + err = heif_context_get_primary_image_ID(ctx, &itemId); + REQUIRE(err.code == heif_error_Ok); + + int n_before = heif_item_get_properties_of_type(ctx, itemId, heif_item_property_type_invalid, + nullptr, 0); + REQUIRE(n_before > 0); + + const std::vector body{0xfa, 0xde, 0x99, 0x04}; + heif_property_id propertyId; + err = heif_item_add_raw_property(ctx, itemId, heif_fourcc('p', 'r', 'p', 'x'), nullptr, + body.data(), body.size(), 0, &propertyId); + REQUIRE(err.code == heif_error_Ok); + + // The property was appended to the item's property list, so its id is the new list length. + REQUIRE(propertyId == (heif_property_id) (n_before + 1)); + + size_t size = 0; + err = heif_item_get_property_raw_size(ctx, itemId, propertyId, &size); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(size == body.size()); + + std::vector readback(size); + err = heif_item_get_property_raw_data(ctx, itemId, propertyId, readback.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(readback == body); + + heif_image_handle_release(thumbnail_handle); + heif_image_handle_release(image_handle); + heif_encoder_release(encoder); + heif_image_release(input_image); + heif_context_free(ctx); + heif_deinit(); +} + + +// Adding properties to a context that was read from a file is not supported: the item data +// still lives in the input file and the context cannot be written out again. This has to be +// refused rather than half-succeeding. + +TEST_CASE("adding a property to a loaded context is refused") { + heif_init(nullptr); + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC); + heif_context* ctx = heif_context_alloc(); + + heif_image* input_image = createImage_RGB_planar(); + heif_image_handle* image_handle; + heif_error err = heif_context_encode_image(ctx, input_image, encoder, nullptr, &image_handle); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_write_to_file(ctx, "property_ids.heif"); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(image_handle); + heif_image_release(input_image); + heif_encoder_release(encoder); + heif_context_free(ctx); + + heif_context* read_ctx = heif_context_alloc(); + err = heif_context_read_from_file(read_ctx, "property_ids.heif", nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_item_id itemId; + err = heif_context_get_primary_image_ID(read_ctx, &itemId); + REQUIRE(err.code == heif_error_Ok); + + const std::vector body{0x01, 0x02, 0x03, 0x04}; + heif_property_id propertyId = 0; + err = heif_item_add_raw_property(read_ctx, itemId, heif_fourcc('p', 'r', 'p', 'y'), nullptr, + body.data(), body.size(), 0, &propertyId); + REQUIRE(err.code == heif_error_Unsupported_feature); + + heif_context_free(read_ctx); + heif_deinit(); +} diff -Nru libheif-1.19.8/tests/item_writing.cc libheif-1.23.4/tests/item_writing.cc --- libheif-1.19.8/tests/item_writing.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/item_writing.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,397 @@ +/* + libheif integration tests for generic item writing. + + MIT License + + Copyright (c) 2026 Greg Benz + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_items.h" +#include "test_utils.h" + +#include +#include +#include +#include + + +static std::vector get_item_ids(const heif_context* ctx) +{ + int n = heif_context_get_number_of_items(ctx); + std::vector ids(n); + REQUIRE(heif_context_get_list_of_item_IDs(ctx, ids.data(), n) == n); + return ids; +} + + +static std::vector read_item_data(const heif_context* ctx, heif_item_id id, + heif_metadata_compression* out_compression = nullptr) +{ + uint8_t* data = nullptr; + size_t size = 0; + heif_error err = heif_item_get_item_data(ctx, id, out_compression, &data, &size); + REQUIRE(err.code == heif_error_Ok); + + std::vector result(data, data + size); + heif_release_item_data(ctx, &data); + return result; +} + + +TEST_CASE("generic item writers return the allocated item IDs") +{ + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + const std::array data = {0x12, 0x34}; + heif_item_id seed_id = 0; + heif_item_id item_id = 0; + heif_item_id compressed_mime_id = 0; + heif_item_id uri_id = 0; + + // The bug this guards against (PR #1885) made the writers store the boolean "success" + // value 1 instead of the item ID. The seed item takes ID 1 first, so that a regression + // to the old behaviour cannot be mistaken for a correct ID below. + heif_error err = heif_context_add_mime_item(ctx, "application/octet-stream", + heif_metadata_compression_off, + data.data(), data.size(), &seed_id); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_add_item(ctx, "test", data.data(), data.size(), &item_id); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", "", + data.data(), data.size(), &compressed_mime_id); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_add_uri_item(ctx, "urn:example:test", data.data(), data.size(), &uri_id); + REQUIRE(err.code == heif_error_Ok); + + // IDs are handed out sequentially, starting at 1 (tests/region.cc relies on this as well). + REQUIRE(seed_id == 1); + REQUIRE(item_id == 2); + REQUIRE(compressed_mime_id == 3); + REQUIRE(uri_id == 4); + + REQUIRE(heif_item_get_item_type(ctx, seed_id) == heif_item_type_mime); + REQUIRE(heif_item_get_item_type(ctx, item_id) == heif_fourcc('t', 'e', 's', 't')); + REQUIRE(heif_item_get_item_type(ctx, compressed_mime_id) == heif_item_type_mime); + REQUIRE(heif_item_get_item_type(ctx, uri_id) == heif_item_type_uri); + + REQUIRE(std::string(heif_item_get_uri_item_uri_type(ctx, uri_id)) == "urn:example:test"); + + std::vector ids = get_item_ids(ctx); + std::sort(ids.begin(), ids.end()); + REQUIRE(ids == std::vector{1, 2, 3, 4}); + + // out_item_id may be NULL: the item is still added + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, + data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_context_get_number_of_items(ctx) == 5); + + heif_context_free(ctx); +} + + +TEST_CASE("item writers reject invalid arguments without adding an item") +{ + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + const std::array data = {1, 2, 3, 4}; + heif_item_id id = 0; + heif_error err; + + // NULL strings + err = heif_context_add_mime_item(ctx, nullptr, heif_metadata_compression_off, data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_precompressed_mime_item(ctx, nullptr, "", data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", nullptr, data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_uri_item(ctx, nullptr, data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_item(ctx, nullptr, data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_item(ctx, "toolong", data.data(), 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + + // negative size + err = heif_context_add_item(ctx, "test", data.data(), -1, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, data.data(), -1, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "", data.data(), -1, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_uri_item(ctx, "urn:example:test", data.data(), -1, &id); + REQUIRE(err.code == heif_error_Usage_error); + + // NULL data with non-zero size + err = heif_context_add_item(ctx, "test", nullptr, 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, nullptr, 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "", nullptr, 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_context_add_uri_item(ctx, "urn:example:test", nullptr, 4, &id); + REQUIRE(err.code == heif_error_Usage_error); + + REQUIRE(heif_context_get_number_of_items(ctx) == 0); + + // NULL data with size 0 is an empty item + err = heif_context_add_item(ctx, "test", nullptr, 0, &id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_context_get_number_of_items(ctx) == 1); + REQUIRE(read_item_data(ctx, id).empty()); + + heif_context_free(ctx); +} + + +TEST_CASE("item data can be read back from a context that is being written") +{ + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + const std::vector payload = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + heif_item_id id = 0; + heif_error err; + heif_metadata_compression compression; + + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, + payload.data(), (int) payload.size(), &id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(read_item_data(ctx, id, &compression) == payload); + REQUIRE(compression == heif_metadata_compression_off); + REQUIRE(std::string(heif_item_get_mime_item_content_type(ctx, id)) == "text/plain"); + + err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(read_item_data(ctx, id) == payload); + + err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(read_item_data(ctx, id) == payload); + + // "identity" is the RFC 2616 no-op content coding + err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "identity", + payload.data(), (int) payload.size(), &id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(read_item_data(ctx, id) == payload); + + // compressed mime item: either it round-trips, or (without the compressor) it is rejected cleanly + int num_items = heif_context_get_number_of_items(ctx); + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_deflate, + payload.data(), (int) payload.size(), &id); + if (heif_metadata_compression_method_supported(heif_metadata_compression_deflate)) { + REQUIRE(err.code == heif_error_Ok); + REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, id)) == "deflate"); + REQUIRE(read_item_data(ctx, id) == payload); + + std::vector compressed = read_item_data(ctx, id, &compression); + REQUIRE(compression == heif_metadata_compression_deflate); + REQUIRE(compressed != payload); + } + else { + REQUIRE(err.code != heif_error_Ok); + REQUIRE(heif_context_get_number_of_items(ctx) == num_items); + } + + heif_context_free(ctx); +} + + +TEST_CASE("items added to a loaded file get fresh IDs") +{ + heif_context* ctx = get_context_for_test_file("rainbow-451x461.heic"); + + std::vector existing_ids = get_item_ids(ctx); + REQUIRE(!existing_ids.empty()); + + std::vector existing_types; + for (heif_item_id id : existing_ids) { + existing_types.push_back(heif_item_get_item_type(ctx, id)); + } + + const std::vector payload = {'n', 'e', 'w'}; + heif_item_id mime_id = 0, uri_id = 0, generic_id = 0; + heif_error err; + + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, + payload.data(), (int) payload.size(), &mime_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &uri_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &generic_id); + REQUIRE(err.code == heif_error_Ok); + + // The new IDs must not collide with the items read from the file (the ID allocator used to + // restart at 1 and silently replaced the primary image) ... + for (heif_item_id new_id : {mime_id, uri_id, generic_id}) { + REQUIRE(std::find(existing_ids.begin(), existing_ids.end(), new_id) == existing_ids.end()); + } + REQUIRE(mime_id != uri_id); + REQUIRE(uri_id != generic_id); + REQUIRE(heif_context_get_number_of_items(ctx) == (int) existing_ids.size() + 3); + + // ... and the existing items must be untouched + for (size_t i = 0; i < existing_ids.size(); i++) { + REQUIRE(heif_item_get_item_type(ctx, existing_ids[i]) == existing_types[i]); + } + + heif_image_handle* handle = get_primary_image_handle(ctx); + REQUIRE(heif_image_handle_get_width(handle) == 451); + heif_image_handle_release(handle); + + // the new data is readable even though the context has an input file that does not contain it + REQUIRE(read_item_data(ctx, mime_id) == payload); + REQUIRE(read_item_data(ctx, uri_id) == payload); + REQUIRE(read_item_data(ctx, generic_id) == payload); + + // Writing back a context that was read from a file is not implemented (the image data + // would not be copied). This must be reported as an error, not crash or write a + // truncated file. + std::string path = get_tests_output_file_path("item_writing_loaded_context.heic"); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code != heif_error_Ok); + + heif_context_free(ctx); +} + + +TEST_CASE("items survive a round trip through a written file") +{ + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC); + + heif_image* img = createImage_RGB_planar(); + const int image_width = heif_image_get_width(img, heif_channel_R); + + heif_image_handle* handle = nullptr; + heif_error err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + heif_item_id image_id = heif_image_handle_get_item_id(handle); + heif_image_handle_release(handle); + heif_image_release(img); + heif_encoder_release(enc); + + const std::vector payload = {'n', 'e', 'w'}; + const std::vector opaque = {0xde, 0xad, 0xbe, 0xef}; + heif_item_id mime_id = 0, uri_id = 0, generic_id = 0, identity_id = 0, unknown_id = 0; + + err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, + payload.data(), (int) payload.size(), &mime_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_uri_item(ctx, "urn:example:test", payload.data(), (int) payload.size(), &uri_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_item(ctx, "test", payload.data(), (int) payload.size(), &generic_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_precompressed_mime_item(ctx, "text/plain", "identity", + payload.data(), (int) payload.size(), &identity_id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_add_precompressed_mime_item(ctx, "application/octet-stream", "x-unknown-coding", + opaque.data(), (int) opaque.size(), &unknown_id); + REQUIRE(err.code == heif_error_Ok); + + std::vector ids = {image_id, mime_id, uri_id, generic_id, identity_id, unknown_id}; + std::sort(ids.begin(), ids.end()); + REQUIRE(std::unique(ids.begin(), ids.end()) == ids.end()); + + std::string path = get_tests_output_file_path("item_writing_roundtrip.heic"); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Ok); + heif_context_free(ctx); + + // --- read back (must succeed although one item has an undecodable content_encoding) + + ctx = get_context_for_local_file(path); + REQUIRE(heif_context_get_number_of_items(ctx) == 6); + + // the item IDs are preserved + REQUIRE(heif_item_get_item_type(ctx, mime_id) == heif_item_type_mime); + REQUIRE(std::string(heif_item_get_mime_item_content_type(ctx, mime_id)) == "text/plain"); + REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, mime_id)) == ""); + REQUIRE(read_item_data(ctx, mime_id) == payload); + + REQUIRE(heif_item_get_item_type(ctx, uri_id) == heif_item_type_uri); + REQUIRE(std::string(heif_item_get_uri_item_uri_type(ctx, uri_id)) == "urn:example:test"); + REQUIRE(read_item_data(ctx, uri_id) == payload); + + REQUIRE(heif_item_get_item_type(ctx, generic_id) == heif_fourcc('t', 'e', 's', 't')); + REQUIRE(read_item_data(ctx, generic_id) == payload); + + REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, identity_id)) == "identity"); + REQUIRE(read_item_data(ctx, identity_id) == payload); + + // the undecodable item: raw data is available, decoding is refused + REQUIRE(std::string(heif_item_get_mime_item_content_encoding(ctx, unknown_id)) == "x-unknown-coding"); + heif_metadata_compression compression; + REQUIRE(read_item_data(ctx, unknown_id, &compression) == opaque); + REQUIRE(compression == heif_metadata_compression_unknown); + + uint8_t* data = nullptr; + size_t size = 0; + err = heif_item_get_item_data(ctx, unknown_id, nullptr, &data, &size); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(data == nullptr); + + // the image is intact + heif_item_id primary_id = 0; + REQUIRE(heif_context_get_primary_image_ID(ctx, &primary_id).code == heif_error_Ok); + REQUIRE(primary_id == image_id); + + handle = get_primary_image_handle(ctx); + REQUIRE(heif_image_handle_get_width(handle) == image_width); + if (heif_have_decoder_for_format(heif_compression_HEVC)) { + img = get_primary_image(handle); + REQUIRE(heif_image_get_primary_width(img) == image_width); + heif_image_release(img); + } + heif_image_handle_release(handle); + + heif_context_free(ctx); +} + + +TEST_CASE("a context without images or sequences cannot be written") +{ + const std::array data = {0x12, 0x34}; + std::string path = get_tests_output_file_path("item_writing_no_images.heic"); + + heif_context* ctx = heif_context_alloc(); + heif_item_id id = 0; + heif_error err = heif_context_add_mime_item(ctx, "text/plain", heif_metadata_compression_off, + data.data(), data.size(), &id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Usage_error); + heif_context_free(ctx); + + ctx = heif_context_alloc(); + err = heif_context_add_item(ctx, "test", data.data(), data.size(), &id); + REQUIRE(err.code == heif_error_Ok); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Usage_error); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/mini_box.cc libheif-1.23.4/tests/mini_box.cc --- libheif-1.19.8/tests/mini_box.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/mini_box.cc 2026-09-06 14:45:17.000000000 +0000 @@ -44,7 +44,7 @@ 0x00, 0x00, 0x00, 0x10, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x69, 0x66, 0x33, 0x61, 0x76, 0x69, 0x66, 0x00, 0x00, 0x00, 0x4a, 0x6d, 0x69, 0x6e, 0x69, - 0x08, 0x18, 0x00, 0xff, 0x01, 0xfe, 0xe0, 0x03, + 0x08, 0x18, 0x80, 0xff, 0x01, 0xfe, 0x20, 0x03, 0x40, 0x81, 0x20, 0x00, 0x00, 0x12, 0x00, 0x0a, 0x09, 0x38, 0x1d, 0xff, 0xff, 0xd8, 0x40, 0x43, 0x41, 0xa4, 0x32, 0x26, 0x11, 0x90, 0x01, 0x86, @@ -71,7 +71,7 @@ REQUIRE(ftyp->list_brands().size() == 0); Indent indent; std::string dumpResult = box->dump(indent); - REQUIRE(dumpResult == "Box: ftyp -----\n" + REQUIRE(dumpResult == "Box: ftyp ----- (File Type)\n" "size: 16 (header size: 8)\n" "major brand: mif3\n" "minor version: avif\n" @@ -122,6 +122,265 @@ "main_item_data offset: 37, size: 53\n"); } +TEST_CASE("mini write round-trip from scratch") +{ + // Construct a Box_mini from scratch using setters + auto mini = std::make_shared(); + mini->set_version(0); + mini->set_explicit_codec_types_flag(false); + mini->set_float_flag(false); + mini->set_full_range_flag(true); + mini->set_alpha_flag(false); + mini->set_explicit_cicp_flag(false); + mini->set_hdr_flag(false); + mini->set_icc_flag(false); + mini->set_exif_flag(false); + mini->set_xmp_flag(false); + mini->set_chroma_subsampling(3); // 4:4:4 + mini->set_orientation(1); + mini->set_width(256); + mini->set_height(256); + mini->set_bit_depth(8); + mini->set_colour_primaries(1); + mini->set_transfer_characteristics(13); + mini->set_matrix_coefficients(6); + + // Codec config (4 bytes) + mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00}); + + // Fake main item data (10 bytes) + std::vector fake_data(10, 0xAB); + mini->set_main_item_data(fake_data); + + // Write + StreamWriter writer; + Error error = mini->write(writer); + REQUIRE(error == Error::Ok); + + // Parse back + auto written_data = writer.get_data(); + auto reader = std::make_shared(written_data.data(), written_data.size(), false); + BitstreamRange range(reader, written_data.size()); + + std::shared_ptr box; + error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + auto mini2 = std::dynamic_pointer_cast(box); + REQUIRE(mini2 != nullptr); + + // Compare + REQUIRE(mini2->get_width() == 256); + REQUIRE(mini2->get_height() == 256); + REQUIRE(mini2->get_bit_depth() == 8); + REQUIRE(mini2->get_icc_flag() == false); + REQUIRE(mini2->get_exif_flag() == false); + REQUIRE(mini2->get_xmp_flag() == false); + REQUIRE(mini2->get_full_range_flag() == true); + REQUIRE(mini2->get_colour_primaries() == 1); + REQUIRE(mini2->get_transfer_characteristics() == 13); + REQUIRE(mini2->get_matrix_coefficients() == 6); + REQUIRE(mini2->get_orientation() == 1); + REQUIRE(mini2->get_main_item_codec_config().size() == 4); + REQUIRE(mini2->get_main_item_codec_config() == std::vector({0x81, 0x20, 0x00, 0x00})); + REQUIRE(mini2->get_main_item_data_size() == 10); +} + + +TEST_CASE("mini write round-trip with alpha and ICC from scratch") +{ + auto mini = std::make_shared(); + mini->set_version(0); + mini->set_explicit_codec_types_flag(false); + mini->set_float_flag(false); + mini->set_full_range_flag(true); + mini->set_alpha_flag(true); + mini->set_explicit_cicp_flag(false); + mini->set_hdr_flag(false); + mini->set_icc_flag(true); + mini->set_exif_flag(false); + mini->set_xmp_flag(false); + mini->set_chroma_subsampling(3); + mini->set_orientation(1); + mini->set_width(256); + mini->set_height(256); + mini->set_bit_depth(8); + mini->set_alpha_is_premultiplied(false); + + // CICP defaults for ICC: primaries=2, transfer=2, matrix=6 + mini->set_colour_primaries(2); + mini->set_transfer_characteristics(2); + mini->set_matrix_coefficients(6); + + mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00}); + // Alpha uses same codec config (will be zero-size in bitstream = reuse main) + mini->set_alpha_item_codec_config({0x81, 0x20, 0x00, 0x00}); + + // Fake ICC data + std::vector icc_data(100, 0xCC); + mini->set_icc_data(icc_data); + + // Fake image data + std::vector main_data(50, 0xAA); + mini->set_main_item_data(main_data); + std::vector alpha_data(30, 0xBB); + mini->set_alpha_item_data(alpha_data); + + // Write + StreamWriter writer; + Error error = mini->write(writer); + REQUIRE(error == Error::Ok); + + // Parse back + auto written_data = writer.get_data(); + auto reader = std::make_shared(written_data.data(), written_data.size(), false); + BitstreamRange range(reader, written_data.size()); + + std::shared_ptr box; + error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + auto mini2 = std::dynamic_pointer_cast(box); + REQUIRE(mini2 != nullptr); + + REQUIRE(mini2->get_width() == 256); + REQUIRE(mini2->get_height() == 256); + REQUIRE(mini2->get_bit_depth() == 8); + REQUIRE(mini2->get_icc_flag() == true); + REQUIRE(mini2->get_full_range_flag() == true); + REQUIRE(mini2->get_colour_primaries() == 2); + REQUIRE(mini2->get_transfer_characteristics() == 2); + REQUIRE(mini2->get_matrix_coefficients() == 6); + REQUIRE(mini2->get_icc_data().size() == 100); + REQUIRE(mini2->get_icc_data() == icc_data); + REQUIRE(mini2->get_main_item_codec_config() == std::vector({0x81, 0x20, 0x00, 0x00})); + REQUIRE(mini2->get_alpha_item_codec_config() == std::vector({0x81, 0x20, 0x00, 0x00})); + REQUIRE(mini2->get_main_item_data_size() == 50); + REQUIRE(mini2->get_alpha_item_data_size() == 30); +} + + +TEST_CASE("mini write round-trip with exif and xmp from scratch") +{ + auto mini = std::make_shared(); + mini->set_version(0); + mini->set_explicit_codec_types_flag(false); + mini->set_float_flag(false); + mini->set_full_range_flag(true); + mini->set_alpha_flag(false); + mini->set_explicit_cicp_flag(true); + mini->set_hdr_flag(false); + mini->set_icc_flag(true); + mini->set_exif_flag(true); + mini->set_xmp_flag(true); + mini->set_chroma_subsampling(1); // 4:2:0 + mini->set_orientation(1); + mini->set_width(320); + mini->set_height(240); + mini->set_bit_depth(10); + mini->set_chroma_is_horizontally_centered(true); + mini->set_chroma_is_vertically_centered(false); + mini->set_colour_primaries(9); + mini->set_transfer_characteristics(16); + mini->set_matrix_coefficients(9); + mini->set_exif_xmp_compressed_flag(false); + + mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00}); + + std::vector icc_data(200, 0xDD); + mini->set_icc_data(icc_data); + + std::vector main_data(100, 0xAA); + mini->set_main_item_data(main_data); + + std::vector exif_data(80, 0xEE); + mini->set_exif_data(exif_data); + + std::vector xmp_data(150, 0xFF); + mini->set_xmp_data(xmp_data); + + // Write + StreamWriter writer; + Error error = mini->write(writer); + REQUIRE(error == Error::Ok); + + // Parse back + auto written_data = writer.get_data(); + auto reader = std::make_shared(written_data.data(), written_data.size(), false); + BitstreamRange range(reader, written_data.size()); + + std::shared_ptr box; + error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + auto mini2 = std::dynamic_pointer_cast(box); + REQUIRE(mini2 != nullptr); + + REQUIRE(mini2->get_width() == 320); + REQUIRE(mini2->get_height() == 240); + REQUIRE(mini2->get_bit_depth() == 10); + REQUIRE(mini2->get_icc_flag() == true); + REQUIRE(mini2->get_exif_flag() == true); + REQUIRE(mini2->get_xmp_flag() == true); + REQUIRE(mini2->get_colour_primaries() == 9); + REQUIRE(mini2->get_transfer_characteristics() == 16); + REQUIRE(mini2->get_matrix_coefficients() == 9); + REQUIRE(mini2->get_orientation() == 1); + REQUIRE(mini2->get_icc_data().size() == 200); + REQUIRE(mini2->get_icc_data() == icc_data); + REQUIRE(mini2->get_main_item_data_size() == 100); + REQUIRE(mini2->get_exif_item_data_size() == 80); + REQUIRE(mini2->get_xmp_item_data_size() == 150); +} + + +TEST_CASE("mini write round-trip small dimensions") +{ + // Test with small dimensions (7-bit, no large_dimensions_flag) + auto mini = std::make_shared(); + mini->set_version(0); + mini->set_explicit_codec_types_flag(false); + mini->set_float_flag(false); + mini->set_full_range_flag(true); + mini->set_alpha_flag(false); + mini->set_explicit_cicp_flag(false); + mini->set_hdr_flag(false); + mini->set_icc_flag(false); + mini->set_exif_flag(false); + mini->set_xmp_flag(false); + mini->set_chroma_subsampling(1); + mini->set_orientation(3); + mini->set_width(64); + mini->set_height(48); + mini->set_bit_depth(8); + mini->set_chroma_is_horizontally_centered(true); + mini->set_chroma_is_vertically_centered(true); + mini->set_colour_primaries(1); + mini->set_transfer_characteristics(13); + mini->set_matrix_coefficients(6); + + mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00}); + mini->set_main_item_data(std::vector(20, 0x42)); + + StreamWriter writer; + Error error = mini->write(writer); + REQUIRE(error == Error::Ok); + + auto written_data = writer.get_data(); + auto reader = std::make_shared(written_data.data(), written_data.size(), false); + BitstreamRange range(reader, written_data.size()); + + std::shared_ptr box; + error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + auto mini2 = std::dynamic_pointer_cast(box); + REQUIRE(mini2 != nullptr); + + REQUIRE(mini2->get_width() == 64); + REQUIRE(mini2->get_height() == 48); + REQUIRE(mini2->get_orientation() == 3); + REQUIRE(mini2->get_bit_depth() == 8); + REQUIRE(mini2->get_main_item_data_size() == 20); +} + + TEST_CASE("check mini+alpha version") { auto istr = std::unique_ptr(new std::ifstream(tests_data_directory + "/simple_osm_tile_alpha.avif", std::ios::binary)); @@ -147,7 +406,7 @@ Indent indent; std::string dumpResult = mini->dump(indent); REQUIRE(dumpResult == "Box: mini -----\n" - "size: 788 (header size: 8)\n" + "size: 1923 (header size: 8)\n" "version: 0\n" "explicit_codec_types_flag: 0\n" "float_flag: 0\n" @@ -170,8 +429,8 @@ "alpha_item_code_config size: 4\n" "main_item_code_config size: 4\n" "icc_data size: 672\n" - "alpha_item_data offset: 717, size: 34\n" - "main_item_data offset: 751, size: 53\n"); + "alpha_item_data offset: 717, size: 219\n" + "main_item_data offset: 936, size: 1003\n"); } TEST_CASE("check mini+exif+xmp version") @@ -199,7 +458,7 @@ Indent indent; std::string dumpResult = mini->dump(indent); REQUIRE(dumpResult == "Box: mini -----\n" - "size: 4388 (header size: 8)\n" + "size: 6294 (header size: 8)\n" "version: 0\n" "explicit_codec_types_flag: 0\n" "float_flag: 0\n" @@ -220,9 +479,9 @@ "matrix_coefficients: 6\n" "main_item_code_config size: 4\n" "icc_data size: 672\n" - "main_item_data offset: 717, size: 53\n" - "exif_data offset: 770, size: 208\n" - "xmp_data offset: 978, size: 3426\n"); + "main_item_data offset: 717, size: 1003\n" + "exif_data offset: 1720, size: 314\n" + "xmp_data offset: 2034, size: 4276\n"); } @@ -241,13 +500,13 @@ REQUIRE(mini->get_colour_primaries() == 1); REQUIRE(mini->get_transfer_characteristics() == 13); REQUIRE(mini->get_matrix_coefficients() == 6); - REQUIRE(mini->get_width() == 128); - REQUIRE(mini->get_height() == 128); - REQUIRE(mini->get_main_item_codec_config().size() == 112); + REQUIRE(mini->get_width() == 256); + REQUIRE(mini->get_height() == 256); + REQUIRE(mini->get_main_item_codec_config().size() == 113); Indent indent; std::string dumpResult = mini->dump(indent); REQUIRE(dumpResult == "Box: mini -----\n" - "size: 4710 (header size: 8)\n" + "size: 19229 (header size: 8)\n" "version: 0\n" "explicit_codec_types_flag: 0\n" "float_flag: 0\n" @@ -260,15 +519,147 @@ "xmp_flag: 0\n" "chroma_subsampling: 1\n" "orientation: 1\n" - "width: 128\n" - "height: 128\n" + "width: 256\n" + "height: 256\n" "chroma_is_horizontally_centered: 0\n" "chroma_is_vertically_centered: 0\n" "bit_depth: 8\n" "colour_primaries: 1\n" "transfer_characteristics: 13\n" "matrix_coefficients: 6\n" - "main_item_code_config size: 112\n" - "main_item_data offset: 144, size: 4582\n"); + "main_item_code_config size: 113\n" + "main_item_data offset: 147, size: 19098\n"); +} + + +// --- Orientation round-trip tests --- +// +// Exercise Box_mini::compute_orientation_from_properties() — the static helper +// that walks a list of irot/imir property boxes and composes them via +// heif_orientation_concat() to recover the cumulative EXIF orientation. +// The order in ipma is not fixed across files, so the recovery must work +// regardless of whether irot or imir appears first. + +namespace { + +std::shared_ptr make_irot(int rotation_ccw) +{ + auto b = std::make_shared(); + b->set_rotation_ccw(rotation_ccw); + return b; +} + +std::shared_ptr make_imir(heif_transform_mirror_direction dir) +{ + auto b = std::make_shared(); + b->set_mirror_direction(dir); + return b; +} + +// A non-transform property to verify it's ignored. +std::shared_ptr make_ispe() +{ + auto b = std::make_shared(); + b->set_size(64, 64); + return b; +} + +} // namespace + +TEST_CASE("Box_mini::compute_orientation_from_properties — single boxes") +{ + // Empty -> normal + REQUIRE(Box_mini::compute_orientation_from_properties({}) == heif_orientation_normal); + + // Non-transform properties are ignored. + REQUIRE(Box_mini::compute_orientation_from_properties({make_ispe()}) == heif_orientation_normal); + + // Single irot at every supported angle (Box_irot stores rotation in + // counter-clockwise degrees; EXIF labels rotations clockwise). + REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(0)}) == heif_orientation_normal); + REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(180)}) == heif_orientation_rotate_180); + REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(270)}) == heif_orientation_rotate_90_cw); // 270 ccw == 90 cw + REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(90)}) == heif_orientation_rotate_270_cw); // 90 ccw == 270 cw + + // Single imir in each direction. + REQUIRE(Box_mini::compute_orientation_from_properties({make_imir(heif_transform_mirror_direction_horizontal)}) + == heif_orientation_flip_horizontally); + REQUIRE(Box_mini::compute_orientation_from_properties({make_imir(heif_transform_mirror_direction_vertical)}) + == heif_orientation_flip_vertically); } +TEST_CASE("Box_mini::compute_orientation_from_properties — irot followed by imir") +{ + // This is the canonical order emitted by HeifFile::add_orientation_properties: + // rotation first, then mirror. + + using H = std::pair>, heif_orientation>; + std::vector cases = { + // The two combinations add_orientation_properties produces (orientations + // 5 and 7 in the EXIF mapping). + { {make_irot(270), make_imir(heif_transform_mirror_direction_horizontal)}, + heif_orientation_rotate_90_cw_then_flip_horizontally }, // 5 + { {make_irot(270), make_imir(heif_transform_mirror_direction_vertical)}, + heif_orientation_rotate_90_cw_then_flip_vertically }, // 7 + + // A few more rotation+mirror pairs that exercise different table cells. + { {make_irot(180), make_imir(heif_transform_mirror_direction_horizontal)}, + heif_orientation_flip_vertically }, // 4 + { {make_irot(180), make_imir(heif_transform_mirror_direction_vertical)}, + heif_orientation_flip_horizontally }, // 2 + { {make_irot(90), make_imir(heif_transform_mirror_direction_horizontal)}, + heif_orientation_rotate_90_cw_then_flip_vertically }, // 7 + }; + + for (auto& [transforms, expected] : cases) { + INFO("expected orientation = " << expected); + REQUIRE(Box_mini::compute_orientation_from_properties(transforms) == expected); + } +} + +TEST_CASE("Box_mini::compute_orientation_from_properties — imir followed by irot") +{ + // add_orientation_properties always emits irot-then-imir, but a file + // produced by other tools could place them in the reverse order. The + // heif_orientation_concat() composition must give the right answer for + // that order too. These expected values come from the concat table in + // heif_encoding.cc: concat(imir, irot). + + using H = std::pair>, heif_orientation>; + std::vector cases = { + // imir(H) then irot(270 ccw / 90 cw) = concat(2, 6) = 7 + { {make_imir(heif_transform_mirror_direction_horizontal), make_irot(270)}, + heif_orientation_rotate_90_cw_then_flip_vertically }, // 7 + + // imir(H) then irot(180) = concat(2, 3) = 4 + { {make_imir(heif_transform_mirror_direction_horizontal), make_irot(180)}, + heif_orientation_flip_vertically }, // 4 + + // imir(V) then irot(270 ccw / 90 cw) = concat(4, 6) = 5 + { {make_imir(heif_transform_mirror_direction_vertical), make_irot(270)}, + heif_orientation_rotate_90_cw_then_flip_horizontally }, // 5 + + // imir(V) then irot(90 ccw / 270 cw) = concat(4, 8) = 7 again (different path) + { {make_imir(heif_transform_mirror_direction_vertical), make_irot(90)}, + heif_orientation_rotate_90_cw_then_flip_vertically }, // 7 + }; + + for (auto& [transforms, expected] : cases) { + INFO("expected orientation = " << expected); + REQUIRE(Box_mini::compute_orientation_from_properties(transforms) == expected); + } +} + +TEST_CASE("Box_mini::compute_orientation_from_properties — mixed with non-transform properties") +{ + // Non-transform properties between transforms must not affect the result. + std::vector> props = { + make_ispe(), + make_irot(270), + make_ispe(), + make_imir(heif_transform_mirror_direction_horizontal), + make_ispe(), + }; + REQUIRE(Box_mini::compute_orientation_from_properties(props) + == heif_orientation_rotate_90_cw_then_flip_horizontally); // 5 +} diff -Nru libheif-1.19.8/tests/mini_decode.cc libheif-1.23.4/tests/mini_decode.cc --- libheif-1.19.8/tests/mini_decode.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/mini_decode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" @@ -49,6 +49,10 @@ } TEST_CASE("check image handle size") { + if (!heif_have_decoder_for_format(heif_compression_AV1)) { + SKIP("AV1 decoder not available, skipping test"); + } + auto file = GENERATE(MINI_FILES); auto context = get_context_for_test_file(file); INFO("file name: " << file); @@ -69,21 +73,21 @@ REQUIRE(heif_image_has_channel(img, heif_channel_Alpha) == 0); REQUIRE(heif_image_has_channel(img, heif_channel_interleaved) == 0); int width = heif_image_get_primary_width(img); - REQUIRE(width == 128); + REQUIRE(width == 256); int height = heif_image_get_primary_height(img); - REQUIRE(height == 128); + REQUIRE(height == 256); width = heif_image_get_width(img, heif_channel_Y); - REQUIRE(width == 128); + REQUIRE(width == 256); height = heif_image_get_height(img, heif_channel_Y); - REQUIRE(height == 128); + REQUIRE(height == 256); width = heif_image_get_width(img, heif_channel_Cb); - REQUIRE(width == 128); + REQUIRE(width == 256); height = heif_image_get_height(img, heif_channel_Cr); - REQUIRE(height == 128); + REQUIRE(height == 256); width = heif_image_get_width(img, heif_channel_Cr); - REQUIRE(width == 128); + REQUIRE(width == 256); height = heif_image_get_height(img, heif_channel_Cr); - REQUIRE(height == 128); + REQUIRE(height == 256); int pixel_depth = heif_image_get_bits_per_pixel(img, heif_channel_Y); REQUIRE(pixel_depth == 8); @@ -103,6 +107,10 @@ } TEST_CASE("check image size HEIF mini") { + if (!heif_have_decoder_for_format(heif_compression_HEVC)) { + SKIP("HEVC decoder not available, skipping test"); + } + auto context = get_context_for_test_file("lightning_mini.heif"); check_image_size_heif_mini(context); heif_context_free(context); diff -Nru libheif-1.19.8/tests/omaf.cc libheif-1.23.4/tests/omaf.cc --- libheif-1.19.8/tests/omaf.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/omaf.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,162 @@ +/* + libheif OMAF (ISO/IEC 23090-2) unit tests + + MIT License + + Copyright (c) 2026 Brad Hards + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "api_structs.h" +#include "image/pixelimage.h" + +#include "test_utils.h" + +#include + +static heif_encoding_options * set_encoding_options() +{ + heif_encoding_options * options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround = false; + options->macOS_compatibility_workaround_no_nclx_profile = true; + options->image_orientation = heif_orientation_normal; + return options; +} + +static void do_encode(heif_image* input_image, const char* filename, heif_omaf_image_projection projection) +{ + REQUIRE(input_image != nullptr); + heif_init(nullptr); + heif_context *ctx = heif_context_alloc(); + heif_error err; + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC); + + heif_encoding_options *options = set_encoding_options(); + + heif_image_handle *output_image_handle; + + err = heif_context_encode_image(ctx, input_image, encoder, options, &output_image_handle); + REQUIRE(err.code == heif_error_Ok); + heif_image_handle_set_omaf_image_projection(output_image_handle, projection); + err = heif_context_write_to_file(ctx, filename); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(output_image_handle); + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_image_release(input_image); + + heif_context_free(ctx); + + heif_context *readbackCtx = get_context_for_local_file(filename); + heif_image_handle *readbackHandle = get_primary_image_handle(readbackCtx); + heif_omaf_image_projection readbackProjection = heif_image_handle_get_omaf_image_projection(readbackHandle); + REQUIRE(readbackProjection == projection); + + // The projection should also be reachable on the decoded heif_image, as it + // is copied from the image item into the pixel image during decoding. + heif_image* decoded = get_primary_image(readbackHandle); + REQUIRE(heif_image_get_omaf_image_projection(decoded) == projection); + heif_image_release(decoded); + + heif_image_handle_release(readbackHandle); + heif_context_free(readbackCtx); + + heif_deinit(); +} + +// Variant that sets the projection on the input heif_image rather than on the +// encoded heif_image_handle. The projection should still be persisted to the +// file's prfr property and round-trip via the handle and the decoded image. +static void do_encode_via_image(heif_image* input_image, const char* filename, heif_omaf_image_projection projection) +{ + REQUIRE(input_image != nullptr); + heif_init(nullptr); + + // A fresh heif_image starts with no projection information. + REQUIRE(heif_image_get_omaf_image_projection(input_image) == heif_omaf_image_projection_flat); + + heif_image_set_omaf_image_projection(input_image, projection); + REQUIRE(heif_image_get_omaf_image_projection(input_image) == projection); + + heif_context *ctx = heif_context_alloc(); + heif_error err; + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_HEVC); + + heif_encoding_options *options = set_encoding_options(); + + heif_image_handle *output_image_handle; + + err = heif_context_encode_image(ctx, input_image, encoder, options, &output_image_handle); + REQUIRE(err.code == heif_error_Ok); + + // The projection set on the input image should have flowed into the + // encoded image item without any explicit handle-side call. + REQUIRE(heif_image_handle_get_omaf_image_projection(output_image_handle) == projection); + + err = heif_context_write_to_file(ctx, filename); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(output_image_handle); + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_image_release(input_image); + + heif_context_free(ctx); + + heif_context *readbackCtx = get_context_for_local_file(filename); + heif_image_handle *readbackHandle = get_primary_image_handle(readbackCtx); + REQUIRE(heif_image_handle_get_omaf_image_projection(readbackHandle) == projection); + + heif_image* decoded = get_primary_image(readbackHandle); + REQUIRE(heif_image_get_omaf_image_projection(decoded) == projection); + heif_image_release(decoded); + + heif_image_handle_release(readbackHandle); + heif_context_free(readbackCtx); + + heif_deinit(); +} + +TEST_CASE("Encode OMAF HEIC") +{ + heif_image *input_image = createImage_RGB_planar(); + do_encode(input_image, "encode_omaf_equirectangular.heic", heif_omaf_image_projection::heif_omaf_image_projection_equirectangular); +} + +TEST_CASE("Encode OMAF HEIC Cubemap") +{ + heif_image *input_image = createImage_RGB_planar(); + do_encode(input_image, "encode_omaf_cubemap.heic", heif_omaf_image_projection::heif_omaf_image_projection_cube_map); +} + +TEST_CASE("Encode OMAF HEIC via heif_image") +{ + heif_image *input_image = createImage_RGB_planar(); + do_encode_via_image(input_image, "encode_omaf_equirectangular_via_image.heic", heif_omaf_image_projection::heif_omaf_image_projection_equirectangular); +} + +TEST_CASE("Encode OMAF HEIC Cubemap via heif_image") +{ + heif_image *input_image = createImage_RGB_planar(); + do_encode_via_image(input_image, "encode_omaf_cubemap_via_image.heic", heif_omaf_image_projection::heif_omaf_image_projection_cube_map); +} \ No newline at end of file diff -Nru libheif-1.19.8/tests/omaf_boxes.cc libheif-1.23.4/tests/omaf_boxes.cc --- libheif-1.19.8/tests/omaf_boxes.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/omaf_boxes.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,62 @@ +/* + libheif OMAF (ISO/IEC 23090-2) unit tests + + MIT License + + Copyright (c) 2026 Brad Hards + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "omaf_boxes.h" +#include "error.h" +#include +#include +#include + + +TEST_CASE("prfr") { + std::vector byteArray{0x00, 0x00, 0x00, 0x0d, 0x70, 0x72, 0x66, 0x72, 0x00, 0x00, 0x00, 0x00, 0x01}; + + auto reader = std::make_shared(byteArray.data(), + byteArray.size(), false); + + BitstreamRange range(reader, byteArray.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + REQUIRE(range.error() == 0); + + REQUIRE(box->get_short_type() == fourcc("prfr")); + REQUIRE(box->get_type_string() == "prfr"); + std::shared_ptr prfr = std::dynamic_pointer_cast(box); + REQUIRE(prfr->get_omaf_image_projection() == heif_omaf_image_projection_cube_map); + Indent indent; + std::string dumpResult = box->dump(indent); + REQUIRE(dumpResult == "Box: prfr ----- (Projection Format)\n" + "size: 13 (header size: 12)\n" + "projection_type: cube-map\n"); + + StreamWriter writer; + Error err = prfr->write(writer); + REQUIRE(err.error_code == heif_error_Ok); + const std::vector bytes = writer.get_data(); + REQUIRE(bytes == byteArray); +} diff -Nru libheif-1.19.8/tests/overlay_amplification.cc libheif-1.23.4/tests/overlay_amplification.cc --- libheif-1.19.8/tests/overlay_amplification.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/overlay_amplification.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,316 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression tests for GHSA-x8xm-cm2c-cfc8: derived-image (grid/iovl/iden) +// reference chains could decode a shared base image an unbounded number of +// times. Because the cycle-detection set is copied per recursion path, a shared +// subtree is re-decoded once per path that reaches it, and nested references +// make the number of decodes grow as branch^depth. A tiny file could thus pin a +// CPU for minutes (linear amplification) or effectively forever (exponential, +// via nested overlays). These tests build the amplification structures out of +// 'iovl' overlays over an 'mski' base (which needs no codec plugin) and check +// that decoding is rejected quickly instead of blowing up. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +// ImageOverlay payload (ISO/IEC 23008-12): version, flags, 4x background color, +// canvas width/height, and one (x,y) offset per composited image. flags=0 uses +// 16-bit fields. All composited images are placed at (0,0) here. +std::vector make_overlay_spec(uint16_t canvas_w, uint16_t canvas_h, size_t num_images) { + std::vector s; + s.push_back(0); // version + s.push_back(0); // flags (16-bit fields) + for (int i = 0; i < 4; i++) { put_u16_be(s, 0); } // background color RGBA + put_u16_be(s, canvas_w); + put_u16_be(s, canvas_h); + for (size_t i = 0; i < num_images; i++) { + put_u16_be(s, 0); // x offset + put_u16_be(s, 0); // y offset + } + return s; +} + +struct Item { + uint16_t id = 0; + std::string type; // "mski", "iovl", "iden" + std::vector dimg; // 'dimg' references (iovl/iden) + std::vector data; // idat payload (mski pixels / iovl spec); empty for iden +}; + +// Assemble a minimal, self-contained HEIF file from an explicit item list. Every +// item carries an 8x8 'ispe'; the single 'mski' base additionally carries a +// 'mskC'. Items with data store it in 'idat' (construction_method 1). +std::vector build_file(const std::vector& items, uint16_t primary_id) { + const uint32_t W = 8, H = 8; + + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, primary_id); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf + std::vector iinf_payload; + put_u16_be(iinf_payload, static_cast(items.size())); + for (const auto& it : items) { + std::vector infe_payload; + put_u16_be(infe_payload, it.id); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, it.type.c_str()); + infe_payload.push_back(0); + append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // iprp / ipco: one ispe(8x8) per item, plus one shared mskC for the base. + // Property indices are 1-based in the order boxes appear in ipco. + std::vector ispe_payload; + put_u32_be(ispe_payload, W); + put_u32_be(ispe_payload, H); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(8); // bits_per_pixel + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); // property 1: ispe(8x8), shared by all items + append(ipco_payload, mskC); // property 2: mskC, for the base + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, static_cast(items.size())); // entry_count + for (const auto& it : items) { + put_u16_be(ipma_payload, it.id); + if (it.type == "mski") { + ipma_payload.push_back(2); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, mskC + } + else { + ipma_payload.push_back(1); + ipma_payload.push_back(0x80 | 1); // essential, ispe + } + } + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat + iloc: concatenate the data of all items that have any. + std::vector idat_payload; + struct Extent { uint16_t id; uint32_t off; uint32_t len; }; + std::vector extents; + for (const auto& it : items) { + if (!it.data.empty()) { + extents.push_back({it.id, static_cast(idat_payload.size()), + static_cast(it.data.size())}); + append(idat_payload, it.data); + } + } + auto idat = make_box("idat", idat_payload); + + std::vector iloc_payload; + iloc_payload.push_back((4 << 4) | 4); // offset_size=4, length_size=4 + iloc_payload.push_back((0 << 4) | 0); // base_offset_size=0, index_size=0 + put_u16_be(iloc_payload, static_cast(extents.size())); // item_count + for (const auto& e : extents) { + put_u16_be(iloc_payload, e.id); + put_u16_be(iloc_payload, 0x0001); // reserved(12) + construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, e.off); // extent_offset (within idat) + put_u32_be(iloc_payload, e.len); // extent_length + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: one 'dimg' entry per item that references others. + std::vector iref_children; + for (const auto& it : items) { + if (!it.dimg.empty()) { + std::vector dimg_payload; + put_u16_be(dimg_payload, it.id); + put_u16_be(dimg_payload, static_cast(it.dimg.size())); + for (uint16_t to : it.dimg) { put_u16_be(dimg_payload, to); } + append(iref_children, make_box("dimg", dimg_payload)); + } + } + auto iref = make_box("iref", iref_children, /*full=*/true); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Build the exponential amplification gadget: a chain of `depth` overlays where +// each overlay composites the next one twice, through two distinct 'iden' items +// (a direct double reference would be rejected by Box_iref). Without the fix, +// decoding the primary overlay decodes the base 2^depth times. +// +// Item layout: id 1 = base ('mski'); then per level k = 0..depth-1: +// iovl_k = 2 + 3*k +// iden_ak = 3 + 3*k, iden_bk = 4 + 3*k (both reference the next level) +std::vector build_exponential_overlays(int depth) { + std::vector items; + items.push_back({1, "mski", {}, std::vector(64, 0x7F)}); + + for (int k = 0; k < depth; k++) { + uint16_t iovl = static_cast(2 + 3 * k); + uint16_t iden_a = static_cast(3 + 3 * k); + uint16_t iden_b = static_cast(4 + 3 * k); + uint16_t next = (k + 1 < depth) ? static_cast(2 + 3 * (k + 1)) : 1; + + items.push_back({iovl, "iovl", {iden_a, iden_b}, make_overlay_spec(8, 8, 2)}); + items.push_back({iden_a, "iden", {next}, {}}); + items.push_back({iden_b, "iden", {next}, {}}); + } + + return build_file(items, /*primary=*/2); +} + +heif_error decode_primary(const std::vector& data, bool& read_ok) { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + read_ok = (err.code == heif_error_Ok); + if (!read_ok) { + heif_context_free(ctx); + return err; + } + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return err; + } + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + if (img) { heif_image_release(img); } + heif_image_handle_release(handle); + heif_context_free(ctx); + return err; +} + +} // namespace + + +// The core DoS: without the fix this decodes the base 2^30 times and never +// returns. With the fix the overlay-nesting guard rejects it after a few levels, +// so decoding completes essentially instantly. The test therefore also proves +// (by completing at all) that the amplification is bounded. +TEST_CASE("overlay amplification: deeply nested overlays are rejected, not decoded") { + auto data = build_exponential_overlays(/*depth=*/30); + + bool read_ok = false; + heif_error err = decode_primary(data, read_ok); + + REQUIRE(read_ok); // the file parses; the blow-up is only at decode + REQUIRE(err.code != heif_error_Ok); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); +} + +// The per-overlay fan-out limit (MAX_OVERLAY_IMAGES): an overlay compositing +// more than the allowed number of input images is rejected while reading the +// overlay spec, so the item becomes undecodable. +TEST_CASE("overlay amplification: overlay with too many input images is rejected") { + std::vector items; + items.push_back({1, "mski", {}, std::vector(64, 0x7F)}); + + // 6 iden items (> MAX_OVERLAY_IMAGES == 5), all pointing at the base. + std::vector refs; + for (uint16_t k = 0; k < 6; k++) { + uint16_t iden = static_cast(3 + k); + items.push_back({iden, "iden", {1}, {}}); + refs.push_back(iden); + } + items.push_back({2, "iovl", refs, make_overlay_spec(8, 8, refs.size())}); + + auto data = build_file(items, /*primary=*/2); + + bool read_ok = false; + heif_error err = decode_primary(data, read_ok); + + REQUIRE(err.code != heif_error_Ok); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); +} + +// Control: a legitimate two-level overlay (well within all limits) still decodes +// normally, proving the guards do not reject ordinary derived images. +TEST_CASE("overlay amplification: a shallow legitimate overlay still decodes") { + std::vector items; + items.push_back({1, "mski", {}, std::vector(64, 0x7F)}); + // iovl_2 (primary) -> iovl_3 -> base. Overlay nesting depth 2 (<= 3), fan-out 1. + items.push_back({2, "iovl", {3}, make_overlay_spec(8, 8, 1)}); + items.push_back({3, "iovl", {1}, make_overlay_spec(8, 8, 1)}); + + auto data = build_file(items, /*primary=*/2); + + bool read_ok = false; + heif_error err = decode_primary(data, read_ok); + + REQUIRE(read_ok); + REQUIRE(err.code == heif_error_Ok); +} diff -Nru libheif-1.19.8/tests/parallel_grid_deadlock.cc libheif-1.23.4/tests/parallel_grid_deadlock.cc --- libheif-1.19.8/tests/parallel_grid_deadlock.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/parallel_grid_deadlock.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,468 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-prgh-72vc-3xmc: a lock-order inversion in the +// parallel grid tile-decode path deadlocks the decoder permanently (DoS) in a +// default build (ENABLE_PARALLEL_TILE_DECODING is ON, 4 threads by default). +// +// ImageItem::decode_image() held the per-item, non-recursive m_decode_mutex +// across nested decodes. Two grid tiles decoded on two worker threads that each +// recurse into the other item take the two item mutexes in opposite order and +// wedge forever. The per-path processed_ids cycle guard cannot see it because +// each worker owns its own copy of the set. There are two such nested edges: +// - the alpha ('auxl') edge, and +// - the grid-tile ('dimg') edge. +// The parse-time cycle check (HeifFile::check_for_ref_cycle) follows only +// 'dimg' and starts only from the primary item, so a mutual-'dimg' pair under a +// NON-primary top-level parent reaches decode without being rejected. +// +// The fix validates, before any decoding starts, that the decode reference +// graph reached from the requested item (both 'dimg' and 'auxl' edges) is +// acyclic (ImageItem::verify_decodable(), called from HeifContext::decode_image). +// A lock-order inversion requires a reference cycle, so rejecting cycles up +// front makes the held-across-recursion mutex deadlock-free. +// +// All items are 'mski' masks or 'grid' derived images, so no codec plugin is +// required. Because a regression re-introduces a permanent hang (not a +// slow-but-bounded decode), each decode runs on a worker thread guarded by a +// timeout: a regression fails the test quickly instead of hanging the suite. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include + +namespace { + +// One item in the synthetic file. A 'grid' item's `data` is its ImageGrid +// payload and it lists its tiles in `dimg`. An 'mski' item's `data` is its mask +// pixels; if `is_alpha` it carries an 'auxC' alpha property and `auxl` names the +// master image(s) it is the alpha channel of. +struct Item { + uint16_t id = 0; + const char* type = "mski"; // "mski" or "grid" + uint32_t w = 0, h = 0; // ispe size for this item + bool is_alpha = false; + std::vector dimg; // derived-image references (grid tiles) + std::vector auxl; // this item is the alpha of these masters + std::vector data; +}; + +// A 'grid' ImageGrid payload (version 0, 16-bit fields). +std::vector image_grid(uint8_t rows, uint8_t cols, uint16_t w, uint16_t h) { + std::vector g; + g.push_back(0); // version + g.push_back(0); // flags (16-bit output fields) + g.push_back(rows - 1); + g.push_back(cols - 1); + put_u16_be(g, w); + put_u16_be(g, h); + return g; +} + +// Assemble a minimal, self-contained HEIF file. Each item gets its own 'ispe' +// (property index = item position, 1-based); the shared 'mskC' and 'auxC' +// follow. Item payloads are stored in 'idat' and located with construction +// method 1. +std::vector build_file(const std::vector& items, uint16_t primary_id, + bool with_miaf_brand = true) { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "heic"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + if (with_miaf_brand) { + append_fourcc(ftyp_payload, "miaf"); + } + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, primary_id); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector iinf_payload; + put_u16_be(iinf_payload, static_cast(items.size())); + for (const auto& it : items) { + std::vector infe_payload; + put_u16_be(infe_payload, it.id); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, it.type); + infe_payload.push_back(0); + append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ipco: one ispe per item, then shared mskC and auxC. + const uint8_t mskC_index = static_cast(items.size() + 1); + const uint8_t auxC_index = static_cast(items.size() + 2); + + std::vector ipco_payload; + for (const auto& it : items) { + std::vector ispe_payload; + put_u32_be(ispe_payload, it.w); + put_u32_be(ispe_payload, it.h); + append(ipco_payload, make_box("ispe", ispe_payload, /*full=*/true)); + } + { + std::vector mskC_payload; + mskC_payload.push_back(8); // bits_per_pixel + append(ipco_payload, make_box("mskC", mskC_payload, /*full=*/true)); + + std::vector auxC_payload; + append_cstr(auxC_payload, "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"); + append(ipco_payload, make_box("auxC", auxC_payload, /*full=*/true)); + } + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, static_cast(items.size())); + for (size_t i = 0; i < items.size(); i++) { + const auto& it = items[i]; + const uint8_t ispe_index = static_cast(i + 1); + std::vector assoc; + assoc.push_back(0x80 | ispe_index); // ispe (essential) + bool is_grid = std::string(it.type) == "grid"; + if (!is_grid) { assoc.push_back(0x80 | mskC_index); } // mskC for masks + if (it.is_alpha) { assoc.push_back(0x80 | auxC_index); } + put_u16_be(ipma_payload, it.id); + ipma_payload.push_back(static_cast(assoc.size())); + append(ipma_payload, assoc); + } + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat + iloc + std::vector idat_payload; + struct Extent { uint16_t id; uint32_t off; uint32_t len; }; + std::vector extents; + for (const auto& it : items) { + extents.push_back({it.id, static_cast(idat_payload.size()), + static_cast(it.data.size())}); + append(idat_payload, it.data); + } + auto idat = make_box("idat", idat_payload); + + std::vector iloc_payload; + iloc_payload.push_back((4 << 4) | 4); // offset_size=4, length_size=4 + iloc_payload.push_back((0 << 4) | 0); // base_offset_size=0, index_size=0 + put_u16_be(iloc_payload, static_cast(extents.size())); + for (const auto& e : extents) { + put_u16_be(iloc_payload, e.id); + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, e.off); + put_u32_be(iloc_payload, e.len); + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector iref_children; + for (const auto& it : items) { + if (!it.dimg.empty()) { + std::vector p; + put_u16_be(p, it.id); + put_u16_be(p, static_cast(it.dimg.size())); + for (uint16_t to : it.dimg) { put_u16_be(p, to); } + append(iref_children, make_box("dimg", p)); + } + if (!it.auxl.empty()) { + std::vector p; + put_u16_be(p, it.id); + put_u16_be(p, static_cast(it.auxl.size())); + for (uint16_t to : it.auxl) { put_u16_be(p, to); } + append(iref_children, make_box("auxl", p)); + } + } + auto iref = make_box("iref", iref_children, /*full=*/true); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, iref); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Decode one item (by id) to completion and return the error. Self-contained so +// it can run on a detached worker thread without touching caller-owned state. +heif_error decode_item_blocking(const std::vector& data, uint16_t item_id) { + heif_context* ctx = heif_context_alloc(); + + heif_error err = heif_context_read_from_memory_without_copy( + ctx, data.data(), data.size(), nullptr); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return err; + } + + heif_image_handle* handle = nullptr; + err = heif_context_get_image_handle(ctx, item_id, &handle); + if (err.code != heif_error_Ok) { + heif_context_free(ctx); + return err; + } + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + if (img) { heif_image_release(img); } + heif_image_handle_release(handle); + heif_context_free(ctx); + return err; +} + +// Run decode_item_blocking() with a timeout. Returns false on timeout (a +// regression: the decode deadlocked). The worker holds its state through a +// shared_ptr so detaching it on timeout leaves no dangling references. +bool decode_item_with_timeout(const std::vector& data, uint16_t item_id, + std::chrono::seconds timeout, heif_error& out_err) { + struct Job { + std::vector data; + uint16_t item_id; + std::promise prom; + }; + auto job = std::make_shared(); + job->data = data; + job->item_id = item_id; + auto fut = job->prom.get_future(); + + std::thread worker([job]() { + job->prom.set_value(decode_item_blocking(job->data, job->item_id)); + }); + + if (fut.wait_for(timeout) == std::future_status::timeout) { + worker.detach(); // leak the hung thread; the process reclaims it at exit + return false; + } + + worker.join(); + out_err = fut.get(); + return true; +} + +} // namespace + + +// The reported DoS. A grid (primary) whose two tiles are each other's alpha: +// the two tiles decode on separate workers and the alpha ('auxl') edge takes the +// two tile mutexes in opposite order. Without the fix this deadlocks; with it +// the alpha edge is followed before the lock and the cycle guard reports it. +TEST_CASE("parallel grid: mutual alpha between grid tiles does not deadlock") { + const std::vector pixels(32 * 32, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "grid", 64, 32, false, {2, 3}, {}, image_grid(1, 2, 64, 32)}); + items.push_back({2, "mski", 32, 32, true, {}, {3}, pixels}); // alpha of tile 3 + items.push_back({3, "mski", 32, 32, true, {}, {2}, pixels}); // alpha of tile 2 + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); // fails fast if the deadlock regresses + REQUIRE(err.code == heif_error_Invalid_input); +} + +// The residual dimg-edge variant. A NON-primary parent grid P over two sub-grids +// that reference each other as their single tile. The parse-time cycle check +// runs only from the primary (a separate leaf), so this reaches the parallel +// decode. Decoding P fans the two sub-grids out across workers, which take the +// two mutexes in opposite order. The alpha-only fix does not cover this; the +// full fix (no lock held across the grid-tile recursion) does. +TEST_CASE("parallel grid: mutual dimg between sub-grids does not deadlock") { + const std::vector pixels(32 * 64, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "grid", 64, 64, false, {2, 3}, {}, image_grid(1, 2, 64, 64)}); // parent P + items.push_back({2, "grid", 32, 64, false, {3}, {}, image_grid(1, 1, 32, 64)}); // G1 -> G2 + items.push_back({3, "grid", 32, 64, false, {2}, {}, image_grid(1, 1, 32, 64)}); // G2 -> G1 + items.push_back({4, "mski", 32, 64, false, {}, {}, pixels}); // primary leaf + + auto data = build_file(items, /*primary=*/4); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); // fails fast if the deadlock regresses + REQUIRE(err.code == heif_error_Invalid_input); +} + +// An image that is (transitively) its own alpha, through several alpha ('auxl') +// hops with no 'dimg' involved: alpha(1)=2, alpha(2)=3, alpha(3)=1. The alpha +// edge is what the parse-time 'dimg'-only cycle check misses, so the validator +// must follow it. Not a direct self-reference (which the aux-image attachment +// rejects at parse), so this exercises the validator's alpha-edge walk. +TEST_CASE("parallel grid: an alpha reference cycle through indirection is rejected") { + const std::vector pixels(32 * 32, 0x7F); + std::vector items; + // auxl points aux -> master, so alpha(X)=Y is encoded as "Y is the alpha of X". + // id type w h alpha dimg auxl (masters) data + items.push_back({1, "mski", 32, 32, true, {}, {3}, pixels}); // item1 is the alpha of item3 + items.push_back({2, "mski", 32, 32, true, {}, {1}, pixels}); // item2 is the alpha of item1 + items.push_back({3, "mski", 32, 32, true, {}, {2}, pixels}); // item3 is the alpha of item2 + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Invalid_input); +} + +// A cycle that mixes the two edge kinds: item1's alpha is grid 2, and grid 2's +// single tile is item1 (item1 -alpha-> 2 -dimg-> item1). Neither a pure alpha +// cycle nor a pure dimg cycle; the validator must follow both edge kinds in one +// walk to catch it. +TEST_CASE("parallel grid: an alpha that references a grid referencing back is rejected") { + const std::vector pixels(32 * 64, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "mski", 32, 64, false, {}, {}, pixels}); // master, decoded + items.push_back({2, "grid", 32, 64, true, {1}, {1}, image_grid(1, 1, 32, 64)}); // alpha of 1; its tile is 1 + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Invalid_input); +} + +// Control: a benign grid with two independent (acyclic) mask tiles still decodes +// on the parallel path, proving the lock change does not break normal grids. +TEST_CASE("parallel grid: a normal two-tile grid still decodes") { + const std::vector pixels(32 * 32, 0x7F); + std::vector items; + items.push_back({1, "grid", 64, 32, false, {2, 3}, {}, image_grid(1, 2, 64, 32)}); + items.push_back({2, "mski", 32, 32, false, {}, {}, pixels}); + items.push_back({3, "mski", 32, 32, false, {}, {}, pixels}); + + auto data = build_file(items, /*primary=*/1); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Ok); +} + +// A reference cycle is not rejected at file load: the file still opens and its +// independently valid items still decode. Only the cyclic item itself fails, +// at decode time. Here the primary (item 1) is a grid whose two sub-grids form +// a 'dimg' cycle, while item 4 is an unrelated valid mask. +TEST_CASE("parallel grid: a cyclic primary does not make valid sibling items undecodable") { + const std::vector big(32 * 64, 0x7F); + const std::vector small(32 * 32, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "grid", 64, 64, false, {2, 3}, {}, image_grid(1, 2, 64, 64)}); // cyclic primary + items.push_back({2, "grid", 32, 64, false, {3}, {}, image_grid(1, 1, 32, 64)}); // G1 -> G2 + items.push_back({3, "grid", 32, 64, false, {2}, {}, image_grid(1, 1, 32, 64)}); // G2 -> G1 + items.push_back({4, "mski", 32, 32, false, {}, {}, small}); // valid, unrelated + + auto data = build_file(items, /*primary=*/1); + + // The valid sibling decodes. Because decode reads the file first, this also + // proves the file loaded despite the cyclic primary. + heif_error err_valid{}; + bool completed_valid = decode_item_with_timeout(data, /*item=*/4, std::chrono::seconds(20), err_valid); + REQUIRE(completed_valid); + REQUIRE(err_valid.code == heif_error_Ok); + + // The cyclic item is still rejected, at decode. + heif_error err_cyclic{}; + bool completed_cyclic = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err_cyclic); + REQUIRE(completed_cyclic); + REQUIRE(err_cyclic.code == heif_error_Invalid_input); +} + +// A nested grid (a grid whose tile is itself a grid) violates MIAF's derivation +// chain (ISO/IEC 23000-22 clause 7.3.11: a grid input must be a coded image). +// The graph is acyclic, so the cycle check passes; verify_decodable() rejects it +// only because of the MIAF derivation constraints, which apply here because the +// file carries the 'miaf' brand. +static std::vector nested_grid_items() { + const std::vector pixels(64 * 64, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "grid", 64, 64, false, {2}, {}, image_grid(1, 1, 64, 64)}); // grid over ... + items.push_back({2, "grid", 64, 64, false, {3}, {}, image_grid(1, 1, 64, 64)}); // ... a grid (invalid) + items.push_back({3, "mski", 64, 64, false, {}, {}, pixels}); // coded base + return items; +} + +TEST_CASE("MIAF: a nested grid is rejected when the file has the 'miaf' brand") { + auto data = build_file(nested_grid_items(), /*primary=*/1, /*with_miaf_brand=*/true); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Invalid_input); +} + +// The same nested grid, without the 'miaf' brand, is not subject to the MIAF +// derivation constraints. It is acyclic and structurally decodable, so it is +// not rejected by verify_decodable(). (This documents that the MIAF check is +// brand-gated; a future security-limits flag will be able to force it on.) +TEST_CASE("MIAF: without the 'miaf' brand a nested grid is not structurally rejected") { + auto data = build_file(nested_grid_items(), /*primary=*/1, /*with_miaf_brand=*/false); + + heif_error err{}; + bool completed = decode_item_with_timeout(data, /*item=*/1, std::chrono::seconds(20), err); + + REQUIRE(completed); + REQUIRE(err.code == heif_error_Ok); +} diff -Nru libheif-1.19.8/tests/pixel_data_types.cc libheif-1.23.4/tests/pixel_data_types.cc --- libheif-1.19.8/tests/pixel_data_types.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/pixel_data_types.cc 2026-09-06 14:45:17.000000000 +0000 @@ -24,25 +24,27 @@ SOFTWARE. */ -#include "pixelimage.h" +#include "image/pixelimage.h" #include "catch_amalgamated.hpp" TEST_CASE( "uint32_t" ) { HeifPixelImage image; - image.create(3,2, heif_colorspace_nonvisual, heif_chroma_undefined); - image.add_channel(heif_channel_Y, 3,2, heif_channel_datatype_unsigned_integer, 32); - - uint32_t stride; - uint32_t* data = image.get_channel(heif_channel_Y, &stride); + auto* limits = heif_get_global_security_limits(); + image.create(3,2, heif_colorspace_custom, heif_chroma_planar); + image.add_channel(heif_channel_Y, 3,2, 32, limits, heif_component_datatype_unsigned_integer); + + size_t stride; + uint32_t* data = image.get_channel_memory(heif_channel_Y, &stride); + stride /= sizeof(uint32_t); REQUIRE(stride >= 3); REQUIRE(image.get_width(heif_channel_Y) == 3); REQUIRE(image.get_height(heif_channel_Y) == 2); REQUIRE(image.get_bits_per_pixel(heif_channel_Y) == 32); REQUIRE(image.get_storage_bits_per_pixel(heif_channel_Y) == 32); - REQUIRE(image.get_datatype(heif_channel_Y) == heif_channel_datatype_unsigned_integer); + REQUIRE(image.get_datatype(heif_channel_Y) == heif_component_datatype_unsigned_integer); REQUIRE(image.get_number_of_interleaved_components(heif_channel_Y) == 1); data[0*stride + 0] = 0; @@ -57,11 +59,12 @@ // --- rotate data std::shared_ptr rot; - auto rotationResult = image.rotate_ccw(90); - REQUIRE(rotationResult.error.error_code == heif_error_Ok); - rot = rotationResult.value; + auto rotationResult = image.rotate_ccw(90, limits); + REQUIRE(rotationResult.error().error_code == heif_error_Ok); + rot = *rotationResult; - data = rot->get_channel(heif_channel_Y, &stride); + data = rot->get_channel_memory(heif_channel_Y, &stride); + stride /= sizeof(uint32_t); REQUIRE(data[0*stride + 0] == 1000); REQUIRE(data[0*stride + 1] == 2000); @@ -72,7 +75,7 @@ // --- mirror - rot->mirror_inplace(heif_transform_mirror_direction_horizontal); + rot->mirror_inplace(heif_transform_mirror_direction_horizontal, limits); REQUIRE(data[0*stride + 1] == 1000); REQUIRE(data[0*stride + 0] == 2000); @@ -81,7 +84,7 @@ REQUIRE(data[2*stride + 1] == 0); REQUIRE(data[2*stride + 0] == 0xFFFFFFFFu); - rot->mirror_inplace(heif_transform_mirror_direction_vertical); + rot->mirror_inplace(heif_transform_mirror_direction_vertical, limits); REQUIRE(data[2*stride + 1] == 1000); REQUIRE(data[2*stride + 0] == 2000); @@ -93,26 +96,28 @@ // --- crop std::shared_ptr crop; - auto cropResult = image.crop(1,2,1,1); - REQUIRE(cropResult.error.error_code == heif_error_Ok); - crop = cropResult.value; + auto cropResult = image.crop(1,2,1,1, limits); + REQUIRE(cropResult.error().error_code == heif_error_Ok); + crop = *cropResult; REQUIRE(crop->get_width(heif_channel_Y) == 2); REQUIRE(crop->get_height(heif_channel_Y) == 1); - data = crop->get_channel(heif_channel_Y, &stride); + data = crop->get_channel_memory(heif_channel_Y, &stride); + stride /= sizeof(uint32_t); REQUIRE(data[0*stride + 0] == 0); REQUIRE(data[0*stride + 1] == 2000); - cropResult = image.crop(0,1,0,1); - REQUIRE(cropResult.error.error_code == heif_error_Ok); - crop = cropResult.value; + cropResult = image.crop(0,1,0,1, limits); + REQUIRE(cropResult.error().error_code == heif_error_Ok); + crop = *cropResult; REQUIRE(crop->get_width(heif_channel_Y) == 2); REQUIRE(crop->get_height(heif_channel_Y) == 2); - data = crop->get_channel(heif_channel_Y, &stride); + data = crop->get_channel_memory(heif_channel_Y, &stride); + stride /= sizeof(uint32_t); REQUIRE(data[0*stride + 0] == 0); REQUIRE(data[0*stride + 1] == 0xFFFFFFFFu); @@ -125,18 +130,20 @@ { HeifPixelImage image; - image.create(3,2, heif_colorspace_nonvisual, heif_chroma_undefined); - image.add_channel(heif_channel_Y, 3,2, heif_channel_datatype_complex_number, 128); - - uint32_t stride; - heif_complex64* data = image.get_channel(heif_channel_Y, &stride); + auto* limits = heif_get_global_security_limits(); + image.create(3,2, heif_colorspace_custom, heif_chroma_planar); + image.add_channel(heif_channel_Y, 3,2, 128, limits, heif_component_datatype_complex_number); + + size_t stride; + heif_complex64* data = image.get_channel_memory(heif_channel_Y, &stride); + stride /= sizeof(heif_complex64); REQUIRE(stride >= 3); REQUIRE(image.get_width(heif_channel_Y) == 3); REQUIRE(image.get_height(heif_channel_Y) == 2); REQUIRE(image.get_bits_per_pixel(heif_channel_Y) == 128); REQUIRE(image.get_storage_bits_per_pixel(heif_channel_Y) == 128); - REQUIRE(image.get_datatype(heif_channel_Y) == heif_channel_datatype_complex_number); + REQUIRE(image.get_datatype(heif_channel_Y) == heif_component_datatype_complex_number); REQUIRE(image.get_number_of_interleaved_components(heif_channel_Y) == 1); data[0*stride + 0] = {0.0, -1.0}; @@ -154,18 +161,20 @@ TEST_CASE( "image datatype through public API" ) { heif_image* image; - heif_error error = heif_image_create(3,2,heif_colorspace_nonvisual, heif_chroma_undefined, &image); + heif_error error = heif_image_create(3,2,heif_colorspace_custom, heif_chroma_planar, &image); REQUIRE(!error.code); - heif_image_add_channel(image, heif_channel_Y, 3,2, heif_channel_datatype_unsigned_integer, 32); + uint32_t comp_idx; + error = heif_image_add_component(image, 3, 2, 0, heif_component_datatype_unsigned_integer, 32, &comp_idx); + REQUIRE(!error.code); - uint32_t stride; - uint32_t* data = heif_image_get_channel_uint32(image, heif_channel_Y, &stride); + size_t stride; + uint32_t* data = heif_image_get_component_uint32(image, comp_idx, &stride); REQUIRE(data != nullptr); + stride /= sizeof(uint32_t); REQUIRE(stride >= 3); - REQUIRE(heif_image_get_datatype(image, heif_channel_Y) == heif_channel_datatype_unsigned_integer); - REQUIRE(heif_image_get_bits_per_pixel_range(image, heif_channel_Y) == 32); + REQUIRE(heif_image_get_component_bits_per_pixel(image, comp_idx) == 32); data[stride*0 + 0] = 0xFFFFFFFFu; data[stride*0 + 1] = 0; @@ -174,3 +183,119 @@ data[stride*1 + 1] = 200; data[stride*1 + 2] = 5; } + + +// Verify that an interleaved RGB plane produces THREE ComponentDescription +// entries (one per cmpd component: red, green, blue) all sharing +// channel=heif_channel_interleaved, while m_storage has a single buffer. +TEST_CASE( "interleaved RGB component descriptions" ) +{ + HeifPixelImage image; + auto* limits = heif_get_global_security_limits(); + image.create(100, 100, heif_colorspace_RGB, heif_chroma_interleaved_RGB); + REQUIRE(image.add_channel(heif_channel_interleaved, 100, 100, 8, limits).error_code == heif_error_Ok); + + REQUIRE(image.get_number_of_used_components() == 3); + + auto ids = image.get_used_component_ids(); + REQUIRE(ids.size() == 3); + + // ids should be three distinct, monotonically-increasing values starting + // from m_next_component_id's initial value (1). + REQUIRE(ids[0] == 1); + REQUIRE(ids[1] == 2); + REQUIRE(ids[2] == 3); + + REQUIRE(image.get_component_type(ids[0]) == heif_cmpd_component_type_red); + REQUIRE(image.get_component_type(ids[1]) == heif_cmpd_component_type_green); + REQUIRE(image.get_component_type(ids[2]) == heif_cmpd_component_type_blue); + + // All three descriptions share channel=interleaved, identical dims/bpp. + for (uint32_t id : ids) { + REQUIRE(image.get_component_channel(id) == heif_channel_interleaved); + REQUIRE(image.get_component_width(id) == 100); + REQUIRE(image.get_component_height(id) == 100); + REQUIRE(image.get_component_bits_per_pixel(id) == 8); + REQUIRE(image.get_component_datatype(id) == heif_component_datatype_unsigned_integer); + } + + // Public C API surface: the same three ids, addressing the same single + // interleaved buffer (one plane). + REQUIRE(image.has_channel(heif_channel_interleaved)); + REQUIRE(image.get_number_of_interleaved_components(heif_channel_interleaved) == 3); +} + + +// Same but for RGBA: four ComponentDescription entries. +TEST_CASE( "interleaved RGBA component descriptions" ) +{ + HeifPixelImage image; + auto* limits = heif_get_global_security_limits(); + image.create(50, 50, heif_colorspace_RGB, heif_chroma_interleaved_RGBA); + REQUIRE(image.add_channel(heif_channel_interleaved, 50, 50, 8, limits).error_code == heif_error_Ok); + + REQUIRE(image.get_number_of_used_components() == 4); + auto ids = image.get_used_component_ids(); + REQUIRE(ids.size() == 4); + REQUIRE(image.get_component_type(ids[0]) == heif_cmpd_component_type_red); + REQUIRE(image.get_component_type(ids[1]) == heif_cmpd_component_type_green); + REQUIRE(image.get_component_type(ids[2]) == heif_cmpd_component_type_blue); + REQUIRE(image.get_component_type(ids[3]) == heif_cmpd_component_type_alpha); + + for (uint32_t id : ids) { + REQUIRE(image.get_component_channel(id) == heif_channel_interleaved); + REQUIRE(image.get_component_bits_per_pixel(id) == 8); + } +} + + +// Verify that BayerPattern.pixels reuses the same component_id for two +// pattern positions that reference the same cmpd entry. This exercises +// HeifPixelImage's data model directly (not the unci file -> ImageItem +// populate path, which would need a full HEIF round-trip and a fixture). +TEST_CASE( "Bayer pattern shares component id for same cmpd" ) +{ + HeifPixelImage image; + auto* limits = heif_get_global_security_limits(); + image.create(4, 4, heif_colorspace_filter_array, heif_chroma_planar); + + // 1) The data plane: a 14-bit filter-array component. + auto fa_result = image.add_component(4, 4, heif_cmpd_component_type_filter_array, + heif_component_datatype_unsigned_integer, 14, limits); + REQUIRE(fa_result.error().error_code == heif_error_Ok); + + // 2) Reference (no-data-plane) components for R, G, B. + uint32_t r_id = image.add_component_without_data(heif_cmpd_component_type_red); + uint32_t g_id = image.add_component_without_data(heif_cmpd_component_type_green); + uint32_t b_id = image.add_component_without_data(heif_cmpd_component_type_blue); + + REQUIRE(image.get_number_of_used_components() == 4); + + // 3) Build a 2x2 RGGB pattern: R, G, G, B with the two G positions sharing g_id. + BayerPattern pattern; + pattern.pattern_width = 2; + pattern.pattern_height = 2; + pattern.pixels = { + { r_id, 1.0f }, + { g_id, 1.0f }, + { g_id, 1.0f }, // <-- same id as the previous G position + { b_id, 1.0f }, + }; + image.set_bayer_pattern(pattern); + + REQUIRE(image.has_any_bayer_pattern()); + const auto& stored = image.get_any_bayer_pattern(); + REQUIRE(stored.pixels.size() == 4); + REQUIRE(stored.pixels[0].component_id == r_id); + REQUIRE(stored.pixels[1].component_id == g_id); + REQUIRE(stored.pixels[2].component_id == g_id); // shared + REQUIRE(stored.pixels[3].component_id == b_id); + REQUIRE(stored.pixels[1].component_id == stored.pixels[2].component_id); + + // The reference components should have has_data_plane=false on their + // descriptions and not appear as image planes. + REQUIRE(image.has_channel(heif_channel_filter_array)); + REQUIRE_FALSE(image.has_channel(heif_channel_R)); + REQUIRE_FALSE(image.has_channel(heif_channel_G)); + REQUIRE_FALSE(image.has_channel(heif_channel_B)); +} diff -Nru libheif-1.19.8/tests/region.cc libheif-1.23.4/tests/region.cc --- libheif-1.19.8/tests/region.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/region.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "libheif/heif.h" #include "test-config.h" #include "test_utils.h" @@ -450,6 +450,135 @@ } +TEST_CASE("inline mask region followed by another region") { + // Regression test: when an inline mask is not the last region in a region_item, + // the parser must advance the data offset past the mask bytes so that the next + // region is read from the correct position. Prior to the fix, the trailing + // std::copy of the mask data did not increment *dataOffset, causing the next + // region to be misparsed or skipped. + struct heif_error err; + + heif_image* img; + uint32_t input_width = 1280; + uint32_t input_height = 1024; + heif_image_create(input_width, input_height, heif_colorspace_YCbCr, + heif_chroma_420, &img); + fill_new_plane(img, heif_channel_Y, input_width, input_height); + fill_new_plane(img, heif_channel_Cb, (input_width + 1) / 2, + (input_height + 1) / 2); + fill_new_plane(img, heif_channel_Cr, (input_width + 1) / 2, + (input_height + 1) / 2); + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc; + err = heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &enc); + REQUIRE(err.code == heif_error_Ok); + + struct heif_encoding_options* options; + options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround = false; + options->macOS_compatibility_workaround_no_nclx_profile = false; + options->image_orientation = heif_orientation_normal; + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, enc, options, &handle); + REQUIRE(err.code == heif_error_Ok); + + struct heif_region_item* region_item; + err = heif_image_handle_add_region_item(handle, input_width, input_height, + ®ion_item); + REQUIRE(err.code == heif_error_Ok); + + // Inline mask region (not the last region in this item). + std::vector mask_data((64 * 3) / 8); + mask_data[0] = 0x80; + mask_data[2] = 0x7f; + mask_data[10] = 0x3e; + mask_data[18] = 0x1d; + mask_data[23] = 0x01; + err = heif_region_item_add_region_inline_mask_data( + region_item, 20, 50, 64, 3, mask_data.data(), mask_data.size(), NULL); + REQUIRE(err.code == heif_error_Ok); + + // A rectangle following the inline mask. With the bug, the parser would + // read this region's fields from inside the mask data and either misparse + // or treat the type byte as unknown and skip it. + err = heif_region_item_add_region_rectangle(region_item, 370, 420, 10, 16, NULL); + REQUIRE(err.code == heif_error_Ok); + + // A point as a third region, to detect any offset slippage that might still + // leave room for a misparse to "land" on a valid type. + err = heif_region_item_add_region_point(region_item, 11, 22, NULL); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_write_to_file(ctx, "regions_mask_inline_followed.heif"); + REQUIRE(err.code == heif_error_Ok); + + heif_region_item_release(region_item); + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_encoding_options_free(options); + heif_context_free(ctx); + heif_image_release(img); + + heif_context* readbackCtx = get_context_for_local_file("regions_mask_inline_followed.heif"); + heif_image_handle* readbackHandle = get_primary_image_handle(readbackCtx); + int num_region_items = heif_image_handle_get_number_of_region_items(readbackHandle); + REQUIRE(num_region_items == 1); + std::vector region_item_ids(num_region_items); + int num_returned = heif_image_handle_get_list_of_region_item_ids( + readbackHandle, region_item_ids.data(), num_region_items); + REQUIRE(num_returned == 1); + heif_region_item* in_region_item; + err = heif_context_get_region_item(readbackCtx, region_item_ids[0], &in_region_item); + REQUIRE(err.code == heif_error_Ok); + + int num_regions = heif_region_item_get_number_of_regions(in_region_item); + REQUIRE(num_regions == 3); + + std::vector regions(num_regions); + int num_regions_returned = heif_region_item_get_list_of_regions( + in_region_item, regions.data(), (int)(regions.size())); + REQUIRE(num_regions_returned == num_regions); + + // First region: inline mask + REQUIRE(heif_region_get_type(regions[0]) == heif_region_type_inline_mask); + int32_t x, y; + uint32_t width, height; + size_t data_len = heif_region_get_inline_mask_data_len(regions[0]); + std::vector mask_data_in(data_len); + err = heif_region_get_inline_mask_data(regions[0], &x, &y, &width, &height, mask_data_in.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(x == 20); + REQUIRE(y == 50); + REQUIRE(width == 64); + REQUIRE(height == 3); + + // Second region: rectangle — must be readable with correct values, which + // requires the inline-mask parser to have advanced *dataOffset past the + // mask bytes. + REQUIRE(heif_region_get_type(regions[1]) == heif_region_type_rectangle); + err = heif_region_get_rectangle(regions[1], &x, &y, &width, &height); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(x == 370); + REQUIRE(y == 420); + REQUIRE(width == 10); + REQUIRE(height == 16); + + // Third region: point. + REQUIRE(heif_region_get_type(regions[2]) == heif_region_type_point); + err = heif_region_get_point(regions[2], &x, &y); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(x == 11); + REQUIRE(y == 22); + + heif_region_release_many(regions.data(), (int)(regions.size())); + heif_region_item_release(in_region_item); + heif_image_handle_release(readbackHandle); + heif_context_free(readbackCtx); +} + + TEST_CASE("create inline mask region from image") { struct heif_error err; @@ -586,3 +715,361 @@ heif_image_handle_release(readbackHandle); heif_context_free(readbackCtx); } + + +TEST_CASE("inline mask region from oversized image is cropped") { + // Regression test for GHSA-5hqq-636x-r3cr: when the source mask image is + // larger than the declared region, the old code sized the destination mask + // buffer from the region dimensions but indexed writes by the source image + // dimensions, causing a heap out-of-bounds write. Per the documented API + // contract the source image must be cropped to the region instead. + struct heif_error err; + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc; + err = heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &enc); + REQUIRE(err.code == heif_error_Ok); + + uint32_t input_width = 64; + uint32_t input_height = 64; + heif_image* img; + heif_image_create(input_width, input_height, heif_colorspace_YCbCr, + heif_chroma_420, &img); + fill_new_plane(img, heif_channel_Y, input_width, input_height); + fill_new_plane(img, heif_channel_Cb, (input_width + 1) / 2, (input_height + 1) / 2); + fill_new_plane(img, heif_channel_Cr, (input_width + 1) / 2, (input_height + 1) / 2); + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + + struct heif_region_item* region_item; + err = heif_image_handle_add_region_item(handle, input_width, input_height, ®ion_item); + REQUIRE(err.code == heif_error_Ok); + + // Source mask image (64x64) much larger than the declared region (8x2). + // With the bug this writes ~4 KiB into a 2-byte buffer. + heif_image* mask_image; + heif_image_create(64, 64, heif_colorspace_monochrome, heif_chroma_monochrome, &mask_image); + err = heif_image_add_plane(mask_image, heif_channel_Y, 64, 64, 8); + REQUIRE(err.code == heif_error_Ok); + int stride; + uint8_t* p = heif_image_get_plane(mask_image, heif_channel_Y, &stride); + memset(p, 0, 64 * stride); + p[0] = 0x80; // region pixel (0,0) -> set + p[7] = 0x80; // region pixel (7,0) -> set + p[stride + 0] = 0x80; // region pixel (0,1) -> set + // Pixels outside the 8x2 region must be cropped away. With the old code these + // would corrupt the in-bounds result (p[10]) or write out of bounds (p[2*stride]): + p[10] = 0x80; // (10,0) beyond declared width + p[2 * stride + 0] = 0x80; // (0,2) beyond declared height + + heif_region* out_region = nullptr; + err = heif_region_item_add_region_inline_mask(region_item, 20, 50, 8, 2, mask_image, &out_region); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(out_region != nullptr); + + size_t data_len = heif_region_get_inline_mask_data_len(out_region); + REQUIRE(data_len == 2); // (8*2+7)/8 = 2 bytes + + std::vector mask_data_in(data_len); + int32_t x, y; + uint32_t width, height; + err = heif_region_get_inline_mask_data(out_region, &x, &y, &width, &height, mask_data_in.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(x == 20); + REQUIRE(y == 50); + REQUIRE(width == 8); + REQUIRE(height == 2); + // Destination bits, row-major over the 8x2 region: + // (0,0)=index0 -> 0x80, (7,0)=index7 -> 0x01 => byte0 = 0x81 + // (0,1)=index8 -> 0x80 => byte1 = 0x80 + // The cropped-away pixels (10,0) and (0,2) must not appear. The old code + // would instead set byte1 = 0x20 (from the source pixel at x=10) and write + // the region's second row out of bounds. + REQUIRE(mask_data_in[0] == 0x81); + REQUIRE(mask_data_in[1] == 0x80); + + heif_region_release(out_region); + heif_image_release(mask_image); + heif_region_item_release(region_item); + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_context_free(ctx); + heif_image_release(img); +} + + +TEST_CASE("inline mask region data length must match region geometry") { + // Regression test for GHSA-p58j-h3vm-3fp5: heif_region_item_add_region_inline_mask_data() + // stored the caller-supplied buffer verbatim without checking that it holds at least + // (width*height+7)/8 bytes. heif_region_get_mask_image() derives the number of mask + // bytes to read from the region geometry, so an undersized buffer led to a heap + // out-of-bounds read whose bytes were copied into the returned mask image. + struct heif_error err; + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc; + err = heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &enc); + REQUIRE(err.code == heif_error_Ok); + + uint32_t input_width = 64; + uint32_t input_height = 64; + heif_image* img; + heif_image_create(input_width, input_height, heif_colorspace_YCbCr, + heif_chroma_420, &img); + fill_new_plane(img, heif_channel_Y, input_width, input_height); + fill_new_plane(img, heif_channel_Cb, (input_width + 1) / 2, (input_height + 1) / 2); + fill_new_plane(img, heif_channel_Cr, (input_width + 1) / 2, (input_height + 1) / 2); + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + + struct heif_region_item* region_item; + err = heif_image_handle_add_region_item(handle, input_width, input_height, ®ion_item); + REQUIRE(err.code == heif_error_Ok); + + // A 64x3 region needs (64*3+7)/8 = 24 bytes of mask data. + const uint32_t region_width = 64; + const uint32_t region_height = 3; + const size_t required_len = (region_width * region_height + 7) / 8; + REQUIRE(required_len == 24); + + heif_region* out_region = nullptr; + + // Only 1 byte instead of 24. With the bug this was accepted and rendering the + // mask afterwards read 23 bytes past the end of the 1-byte allocation. + std::vector short_mask_data(1, 0xff); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + short_mask_data.data(), short_mask_data.size(), + &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + + // Off by one (23 of 24 bytes) must be rejected as well. + std::vector almost_mask_data(required_len - 1, 0xff); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + almost_mask_data.data(), almost_mask_data.size(), + &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + + // The file parser rejects zero-sized inline masks, so the writer must not produce them. + std::vector mask_data(required_len, 0x00); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, 0, region_height, + mask_data.data(), mask_data.size(), &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, 0, + mask_data.data(), mask_data.size(), &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + + // NULL mask data. + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + nullptr, required_len, &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + + // None of the rejected calls may have added a region. + REQUIRE(heif_region_item_get_number_of_regions(region_item) == 0); + + // A buffer of exactly the required size is accepted and can be rendered as a mask image. + mask_data[0] = 0x80; // pixel (0,0) + mask_data[23] = 0x01; // pixel (63,2), the very last pixel of the region + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + mask_data.data(), mask_data.size(), &out_region); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(out_region != nullptr); + REQUIRE(heif_region_get_inline_mask_data_len(out_region) == required_len); + + int32_t x, y; + uint32_t width, height; + heif_image* mask_image = nullptr; + err = heif_region_get_mask_image(out_region, &x, &y, &width, &height, &mask_image); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(mask_image != nullptr); + REQUIRE(x == 20); + REQUIRE(y == 50); + REQUIRE(width == 64); + REQUIRE(height == 3); + int stride; + const uint8_t* p = heif_image_get_plane_readonly(mask_image, heif_channel_Y, &stride); + REQUIRE(p[0] == 0xff); + REQUIRE(p[1] == 0x00); + REQUIRE(p[2 * stride + 62] == 0x00); + REQUIRE(p[2 * stride + 63] == 0xff); + heif_image_release(mask_image); + heif_region_release(out_region); + out_region = nullptr; + + // A buffer larger than required is rejected as well: a surplus indicates that the + // caller's geometry and mask data disagree. (Silently keeping only the canonical + // prefix would also risk desynchronizing the on-disk parsing of following regions, + // since the format stores exactly (width*height+7)/8 bytes.) + std::vector long_mask_data(required_len + 8, 0xaa); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + long_mask_data.data(), long_mask_data.size(), + &out_region); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(out_region == nullptr); + + // Add a second, correctly-sized inline mask so the round-trip below exercises + // parsing of multiple consecutive inline-mask regions. + std::vector mask_data2(required_len, 0xaa); + err = heif_region_item_add_region_inline_mask_data(region_item, 20, 50, region_width, region_height, + mask_data2.data(), mask_data2.size(), &out_region); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(out_region != nullptr); + REQUIRE(heif_region_get_inline_mask_data_len(out_region) == required_len); + heif_region_release(out_region); + + err = heif_region_item_add_region_point(region_item, 5, 7, nullptr); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_write_to_file(ctx, "regions_mask_inline_data_lengths.heif"); + REQUIRE(err.code == heif_error_Ok); + + heif_region_item_release(region_item); + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_context_free(ctx); + heif_image_release(img); + + // Read the file back: all three regions must still be parsable and intact. + heif_context* readbackCtx = get_context_for_local_file("regions_mask_inline_data_lengths.heif"); + heif_image_handle* readbackHandle = get_primary_image_handle(readbackCtx); + REQUIRE(heif_image_handle_get_number_of_region_items(readbackHandle) == 1); + heif_item_id region_item_id; + REQUIRE(heif_image_handle_get_list_of_region_item_ids(readbackHandle, ®ion_item_id, 1) == 1); + heif_region_item* in_region_item; + err = heif_context_get_region_item(readbackCtx, region_item_id, &in_region_item); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_region_item_get_number_of_regions(in_region_item) == 3); + heif_region* regions[3]; + REQUIRE(heif_region_item_get_list_of_regions(in_region_item, regions, 3) == 3); + REQUIRE(heif_region_get_type(regions[0]) == heif_region_type_inline_mask); + REQUIRE(heif_region_get_type(regions[1]) == heif_region_type_inline_mask); + REQUIRE(heif_region_get_type(regions[2]) == heif_region_type_point); + REQUIRE(heif_region_get_inline_mask_data_len(regions[0]) == required_len); + REQUIRE(heif_region_get_inline_mask_data_len(regions[1]) == required_len); + + std::vector mask_data_in(required_len); + err = heif_region_get_inline_mask_data(regions[1], &x, &y, &width, &height, mask_data_in.data()); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(width == 64); + REQUIRE(height == 3); + REQUIRE(mask_data_in[0] == 0xaa); + REQUIRE(mask_data_in[23] == 0xaa); + + int32_t px, py; + err = heif_region_get_point(regions[2], &px, &py); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(px == 5); + REQUIRE(py == 7); + + heif_region_release_many(regions, 3); + heif_region_item_release(in_region_item); + heif_image_handle_release(readbackHandle); + heif_context_free(readbackCtx); +} + + +TEST_CASE("polygon and polyline point arguments are validated") { + // Regression test for GHSA-prcj-g5xh-rw95. + // + // heif_region_item_add_region_polygon() and heif_region_item_add_region_polyline() + // read 2*nPoints values from the caller's array. The library has no way to check + // the real size of that array (that is the caller's responsibility, as documented + // in heif_regions.h), but it must reject the inconsistent argument combinations + // it can detect instead of crashing: a negative point count used to be converted + // to a huge size_t in std::vector::resize() and aborted the process with an + // uncaught std::length_error, and a NULL array was dereferenced. + + auto context = get_context_for_test_file("rainbow-451x461.heic"); + heif_image_handle* handle = get_primary_image_handle(context); + + heif_region_item* region_item; + heif_error err = heif_image_handle_add_region_item(handle, 451, 461, ®ion_item); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_region_item_get_number_of_regions(region_item) == 0); + + int32_t pts[6] = {10, 20, 30, 20, 30, 40}; + + // On failure, `out_region` must be reset to NULL so that callers never see a + // dangling or uninitialized pointer. Start from a non-NULL sentinel to verify this. + heif_region sentinel; + heif_region* region; + + // --- negative point count --- + + region = &sentinel; + err = heif_region_item_add_region_polygon(region_item, pts, -1, ®ion); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.subcode == heif_suberror_Invalid_parameter_value); + REQUIRE(region == nullptr); + + region = &sentinel; + err = heif_region_item_add_region_polyline(region_item, pts, -1, ®ion); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.subcode == heif_suberror_Invalid_parameter_value); + REQUIRE(region == nullptr); + + region = &sentinel; + err = heif_region_item_add_region_polygon(region_item, pts, INT32_MIN, ®ion); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.subcode == heif_suberror_Invalid_parameter_value); + REQUIRE(region == nullptr); + + // --- NULL point array with a non-zero count --- + + region = &sentinel; + err = heif_region_item_add_region_polygon(region_item, nullptr, 3, ®ion); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.subcode == heif_suberror_Null_pointer_argument); + REQUIRE(region == nullptr); + + region = &sentinel; + err = heif_region_item_add_region_polyline(region_item, nullptr, 2, ®ion); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(err.subcode == heif_suberror_Null_pointer_argument); + REQUIRE(region == nullptr); + + // The out_region argument is optional, the checks must also work without it. + err = heif_region_item_add_region_polygon(region_item, nullptr, 3, nullptr); + REQUIRE(err.code == heif_error_Usage_error); + err = heif_region_item_add_region_polygon(region_item, pts, -1, nullptr); + REQUIRE(err.code == heif_error_Usage_error); + + // None of the failed calls may have added a region. + REQUIRE(heif_region_item_get_number_of_regions(region_item) == 0); + + // --- valid calls still work and the points round-trip --- + + region = nullptr; + err = heif_region_item_add_region_polygon(region_item, pts, 3, ®ion); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(region != nullptr); + REQUIRE(heif_region_get_type(region) == heif_region_type_polygon); + REQUIRE(heif_region_get_polygon_num_points(region) == 3); + int32_t out[6] = {0}; + err = heif_region_get_polygon_points(region, out); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(memcmp(out, pts, sizeof(pts)) == 0); + heif_region_release(region); + + region = nullptr; + err = heif_region_item_add_region_polyline(region_item, pts, 2, ®ion); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(region != nullptr); + REQUIRE(heif_region_get_type(region) == heif_region_type_polyline); + REQUIRE(heif_region_get_polyline_num_points(region) == 2); + heif_region_release(region); + + REQUIRE(heif_region_item_get_number_of_regions(region_item) == 2); + + heif_region_item_release(region_item); + heif_image_handle_release(handle); + heif_context_free(context); +} diff -Nru libheif-1.19.8/tests/scale_plane_checks.cc libheif-1.23.4/tests/scale_plane_checks.cc --- libheif-1.19.8/tests/scale_plane_checks.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/scale_plane_checks.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,160 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "image/pixelimage.h" +#include "catch_amalgamated.hpp" + +// Regression tests for HeifPixelImage::scale_nearest_neighbor()'s plane checks. +// +// The per-channel loop that does the actual scaling indexes each source plane +// using coordinates derived from the image's logical size, trusting that the +// plane actually covers that geometry. Nothing about add_channel()/ +// copy_new_channel_from()/transfer_channel_from_image_as() enforces that as an +// invariant, so two independent guards are needed: +// - has_standard_plane_sizes() rejects any single plane (Y, Alpha, R, G, B, +// interleaved: must match the full image size; Cb, Cr: must match the +// chroma-subsampled size) whose actual size doesn't match what its channel +// is expected to have -- this covers a single mismatched plane of any +// channel, not just Alpha; +// - a plane-count check rejects the source having more planes of a given +// channel than were allocated for the destination (a same-channel +// duplicate, e.g. from add_channel()/copy_new_channel_from() being called +// twice for the same channel -- neither rejects duplicates the way +// transfer_channel_from_image_as() does): each stored duplicate can have +// the *correct* size and still slip past has_standard_plane_sizes(), so +// the loop would write every one of them into the single allocated +// destination plane, including whatever bit-depth mismatch it carries. + +TEST_CASE("scale_nearest_neighbor rejects a duplicate color channel") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + + // A second, differently-sized-per-sample Y plane. Neither add_channel() nor + // copy_new_channel_from() reject an already-occupied channel, so this is + // reachable without going through transfer_channel_from_image_as() (which + // does reject it). + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 16, limits).error_code == heif_error_Ok); + + std::shared_ptr scaled; + Error err = image->scale_nearest_neighbor(scaled, 8, 8, limits); + REQUIRE(err.error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("scale_nearest_neighbor rejects an Alpha plane whose size differs from the color planes") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(8, 8, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 8, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Alpha, 4, 4, 8, limits).error_code == heif_error_Ok); + + std::shared_ptr scaled; + Error err = image->scale_nearest_neighbor(scaled, 16, 16, limits); + REQUIRE(err.error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("scale_nearest_neighbor rejects a single color channel whose size doesn't match the image") { + auto* limits = heif_get_global_security_limits(); + + // Not a duplicate: exactly one Y plane, but sized smaller than the image's + // own declared 256x256 -- reachable directly through the public plane API + // (heif_image_create() + heif_image_add_plane()), with no decode involved. + auto image = std::make_shared(); + image->create(256, 256, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 8, 8, 8, limits).error_code == heif_error_Ok); + + std::shared_ptr scaled; + Error err = image->scale_nearest_neighbor(scaled, 512, 512, limits); + REQUIRE(err.error_code == heif_error_Unsupported_feature); +} + +TEST_CASE("scale_nearest_neighbor scales a well-formed YCbCr 4:2:0 image correctly") { + auto* limits = heif_get_global_security_limits(); + + // Cb/Cr are legitimately half-size in 4:2:0 -- has_standard_plane_sizes() + // must not reject that. + auto image = std::make_shared(); + image->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 4, 4, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 2, 2, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 2, 2, 8, limits).error_code == heif_error_Ok); + + std::shared_ptr scaled; + Error err = image->scale_nearest_neighbor(scaled, 8, 8, limits); + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(scaled->get_width(heif_channel_Y) == 8); + REQUIRE(scaled->get_height(heif_channel_Y) == 8); + REQUIRE(scaled->get_width(heif_channel_Cb) == 4); + REQUIRE(scaled->get_height(heif_channel_Cb) == 4); +} + +TEST_CASE("scale_nearest_neighbor scales a well-formed image with matching Alpha correctly") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(2, 2, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 2, 2, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Alpha, 2, 2, 8, limits).error_code == heif_error_Ok); + + { + size_t stride; + uint8_t* y = image->get_channel_memory(heif_channel_Y, &stride); + y[0 * stride + 0] = 10; + y[0 * stride + 1] = 20; + y[1 * stride + 0] = 30; + y[1 * stride + 1] = 40; + + uint8_t* a = image->get_channel_memory(heif_channel_Alpha, &stride); + a[0 * stride + 0] = 100; + a[0 * stride + 1] = 110; + a[1 * stride + 0] = 120; + a[1 * stride + 1] = 130; + } + + std::shared_ptr scaled; + Error err = image->scale_nearest_neighbor(scaled, 4, 4, limits); + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(scaled->get_width(heif_channel_Y) == 4); + REQUIRE(scaled->get_height(heif_channel_Y) == 4); + REQUIRE(scaled->has_channel(heif_channel_Alpha)); + REQUIRE(scaled->get_width(heif_channel_Alpha) == 4); + REQUIRE(scaled->get_height(heif_channel_Alpha) == 4); + + // Nearest-neighbor 2x2 -> 4x4: each source pixel covers a 2x2 block. + size_t stride; + const uint8_t* y = scaled->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(y[0 * stride + 0] == 10); + REQUIRE(y[0 * stride + 3] == 20); + REQUIRE(y[3 * stride + 0] == 30); + REQUIRE(y[3 * stride + 3] == 40); + + const uint8_t* a = scaled->get_channel_memory(heif_channel_Alpha, &stride); + REQUIRE(a[0 * stride + 0] == 100); + REQUIRE(a[3 * stride + 3] == 130); +} diff -Nru libheif-1.19.8/tests/sequence_file_builder.h libheif-1.23.4/tests/sequence_file_builder.h --- libheif-1.19.8/tests/sequence_file_builder.h 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_file_builder.h 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,348 @@ +/* + libheif integration tests: hand-built HEIF sequence files. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Builds minimal, structurally valid HEIF sequence files containing a single +// 'urim' metadata track, following the box tree that Track::load() requires. +// A metadata track is used so that the raw-sample API path can be exercised +// without any codec plugin. Shared by the sequence_* tests. + +#ifndef LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H +#define LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H + +#include +#include +#include +#include + +namespace seqfile { + +inline void put16(std::vector& v, uint16_t x) +{ + v.push_back(static_cast(x >> 8)); + v.push_back(static_cast(x)); +} + +inline void put32(std::vector& v, uint32_t x) +{ + v.push_back(static_cast(x >> 24)); + v.push_back(static_cast(x >> 16)); + v.push_back(static_cast(x >> 8)); + v.push_back(static_cast(x)); +} + +inline void put64(std::vector& v, uint64_t x) +{ + for (int i = 7; i >= 0; i--) { + v.push_back(static_cast(x >> (i * 8))); + } +} + +inline void put_fourcc(std::vector& v, const char* s) +{ + v.push_back(static_cast(s[0])); + v.push_back(static_cast(s[1])); + v.push_back(static_cast(s[2])); + v.push_back(static_cast(s[3])); +} + +inline void append(std::vector& dst, const std::vector& src) +{ + dst.insert(dst.end(), src.begin(), src.end()); +} + +// Wrap a payload in a box: [uint32 size][fourcc type][payload]. +inline std::vector box(const char* type, const std::vector& payload) +{ + std::vector b; + put32(b, static_cast(8 + payload.size())); + put_fourcc(b, type); + append(b, payload); + return b; +} + +inline std::vector concat(std::initializer_list> parts) +{ + std::vector out; + for (const auto& p : parts) { + append(out, p); + } + return out; +} + +struct Stts_entry +{ + uint32_t sample_count; + uint32_t sample_delta; +}; + +// One 'saiz'/'saio' pair attached to the track. Every sample carries the same +// aux-info payload (constant size, at most 255 bytes, because 'saiz' stores the +// default size in a uint8_t). The payloads are appended to 'mdat' behind the +// sample data. +struct SampleAuxInfo +{ + const char* type = "suid"; // aux_info_type: 'suid' = GIMI content ID, 'stai' = TAI timestamp + std::vector data; // per-sample payload, identical for all samples + bool offset_past_eof = false; // let 'saio' point beyond the end of the file +}; + +struct SequenceFileParams +{ + bool mvhd_v1 = true; + uint64_t mvhd_duration = std::numeric_limits::max(); // indefinite sentinel + uint32_t timescale = 1000; + + uint64_t mdhd_duration = 1; + + bool with_editlist = true; + uint64_t elst_segment_duration = 1; // must equal mdhd_duration to match the repeat pattern + + std::vector stts = {{1, 1}}; + + uint32_t num_samples = 1; + uint32_t sample_size = 1; + + std::vector aux_infos; +}; + +inline std::vector build_sequence_file(const SequenceFileParams& p) +{ + // --- ftyp + std::vector ftyp_payload; + put_fourcc(ftyp_payload, "msf1"); // major brand: HEIF image sequence + put32(ftyp_payload, 0); // minor version + put_fourcc(ftyp_payload, "msf1"); + put_fourcc(ftyp_payload, "isom"); + std::vector ftyp = box("ftyp", ftyp_payload); + + // --- mvhd + std::vector mvhd_payload; + if (p.mvhd_v1) { + put32(mvhd_payload, 0x01000000); // version 1, flags 0 + put64(mvhd_payload, 0); // creation_time + put64(mvhd_payload, 0); // modification_time + put32(mvhd_payload, p.timescale); + put64(mvhd_payload, p.mvhd_duration); + } + else { + put32(mvhd_payload, 0x00000000); // version 0, flags 0 + put32(mvhd_payload, 0); // creation_time + put32(mvhd_payload, 0); // modification_time + put32(mvhd_payload, p.timescale); + put32(mvhd_payload, static_cast(p.mvhd_duration)); + } + put32(mvhd_payload, 0x00010000); // rate 1.0 + put16(mvhd_payload, 0x0100); // volume + put16(mvhd_payload, 0); // reserved + put32(mvhd_payload, 0); // reserved + put32(mvhd_payload, 0); // reserved + for (int i = 0; i < 9; i++) put32(mvhd_payload, 0); // matrix + for (int i = 0; i < 6; i++) put32(mvhd_payload, 0); // pre_defined + put32(mvhd_payload, 2); // next_track_ID + std::vector mvhd = box("mvhd", mvhd_payload); + + // --- tkhd (version 1) + std::vector tkhd_payload; + put32(tkhd_payload, 0x01000007); // version 1, flags = enabled|in_movie|in_preview + put64(tkhd_payload, 0); // creation_time + put64(tkhd_payload, 0); // modification_time + put32(tkhd_payload, 1); // track_ID + put32(tkhd_payload, 0); // reserved + put64(tkhd_payload, 0); // duration + put64(tkhd_payload, 0); // reserved + put16(tkhd_payload, 0); // layer + put16(tkhd_payload, 0); // alternate_group + put16(tkhd_payload, 0); // volume + put16(tkhd_payload, 0); // reserved + for (int i = 0; i < 9; i++) put32(tkhd_payload, 0); // matrix + put32(tkhd_payload, 0); // width + put32(tkhd_payload, 0); // height + std::vector tkhd = box("tkhd", tkhd_payload); + + // --- edts / elst (version 1, flags = repeat) + std::vector edts; + if (p.with_editlist) { + std::vector elst_payload; + put32(elst_payload, 0x01000001); // version 1, flags = Repeat_EditList + put32(elst_payload, 1); // entry_count + put64(elst_payload, p.elst_segment_duration); + put64(elst_payload, 0); // media_time + put16(elst_payload, 1); // media_rate_integer + put16(elst_payload, 0); // media_rate_fraction + edts = box("edts", box("elst", elst_payload)); + } + + // --- mdhd (version 0) + std::vector mdhd_payload; + put32(mdhd_payload, 0x00000000); // version 0, flags 0 + put32(mdhd_payload, 0); // creation_time + put32(mdhd_payload, 0); // modification_time + put32(mdhd_payload, p.timescale); + put32(mdhd_payload, static_cast(p.mdhd_duration)); + put16(mdhd_payload, 0x55c4); // language ('und') + put16(mdhd_payload, 0); // pre_defined + std::vector mdhd = box("mdhd", mdhd_payload); + + // --- hdlr (handler type 'meta') + std::vector hdlr_payload; + put32(hdlr_payload, 0x00000000); // version 0, flags 0 + put32(hdlr_payload, 0); // pre_defined + put_fourcc(hdlr_payload, "meta"); + put32(hdlr_payload, 0); // reserved + put32(hdlr_payload, 0); // reserved + put32(hdlr_payload, 0); // reserved + hdlr_payload.push_back(0); // name (empty, null-terminated) + std::vector hdlr = box("hdlr", hdlr_payload); + + // --- nmhd (null media header for metadata tracks) + std::vector nmhd_payload; + put32(nmhd_payload, 0x00000000); // version 0, flags 0 + std::vector nmhd = box("nmhd", nmhd_payload); + + // --- stsd with a single 'urim' (URI meta) sample entry + std::vector urim_payload; + for (int i = 0; i < 6; i++) urim_payload.push_back(0); // SampleEntry reserved + put16(urim_payload, 1); // data_reference_index + std::vector urim = box("urim", urim_payload); + + std::vector stsd_payload; + put32(stsd_payload, 0x00000000); // version 0, flags 0 + put32(stsd_payload, 1); // entry_count + append(stsd_payload, urim); + std::vector stsd = box("stsd", stsd_payload); + + // --- stts + std::vector stts_payload; + put32(stts_payload, 0x00000000); // version 0, flags 0 + put32(stts_payload, static_cast(p.stts.size())); + for (const auto& e : p.stts) { + put32(stts_payload, e.sample_count); + put32(stts_payload, e.sample_delta); + } + std::vector stts = box("stts", stts_payload); + + // --- stsc: all samples in a single chunk + std::vector stsc_payload; + put32(stsc_payload, 0x00000000); // version 0, flags 0 + put32(stsc_payload, 1); // entry_count + put32(stsc_payload, 1); // first_chunk + put32(stsc_payload, p.num_samples); // samples_per_chunk + put32(stsc_payload, 1); // sample_description_index + std::vector stsc = box("stsc", stsc_payload); + + // --- stsz: fixed sample size + std::vector stsz_payload; + put32(stsz_payload, 0x00000000); // version 0, flags 0 + put32(stsz_payload, p.sample_size); // fixed sample size (non-zero -> no per-sample array) + put32(stsz_payload, p.num_samples); // sample_count + std::vector stsz = box("stsz", stsz_payload); + + // --- saiz: one per aux info, constant per-sample size + std::vector> saiz_boxes; + for (const auto& aux : p.aux_infos) { + std::vector saiz_payload; + put32(saiz_payload, 0x00000001); // version 0, flags = aux_info_type present + put_fourcc(saiz_payload, aux.type); + put32(saiz_payload, 0); // aux_info_type_parameter + saiz_payload.push_back(static_cast(aux.data.size())); // default_sample_info_size + put32(saiz_payload, p.num_samples); // sample_count + saiz_boxes.push_back(box("saiz", saiz_payload)); + } + + // --- stco / saio: offsets are patched below to point into the mdat payload + auto make_stco = [](uint32_t offset) { + std::vector stco_payload; + put32(stco_payload, 0x00000000); // version 0, flags 0 + put32(stco_payload, 1); // entry_count + put32(stco_payload, offset); // chunk offset + return box("stco", stco_payload); + }; + + auto make_saio = [](const SampleAuxInfo& aux, uint32_t offset) { + std::vector saio_payload; + put32(saio_payload, 0x00000001); // version 0, flags = aux_info_type present + put_fourcc(saio_payload, aux.type); + put32(saio_payload, 0); // aux_info_type_parameter + put32(saio_payload, 1); // entry_count (single chunk) + put32(saio_payload, offset); + return box("saio", saio_payload); + }; + + auto assemble = [&](const std::vector& stco, + const std::vector>& saio_boxes) { + std::vector stbl_payload = concat({stsd, stts, stsc, stsz, stco}); + for (const auto& b : saiz_boxes) append(stbl_payload, b); + for (const auto& b : saio_boxes) append(stbl_payload, b); + std::vector stbl = box("stbl", stbl_payload); + std::vector minf = box("minf", concat({nmhd, stbl})); + std::vector mdia = box("mdia", concat({mdhd, hdlr, minf})); + std::vector trak = box("trak", concat({tkhd, edts, mdia})); + std::vector moov = box("moov", concat({mvhd, trak})); + return moov; + }; + + // --- mdat payload: num_samples * sample_size bytes of arbitrary content, + // followed by one block of aux-info payloads per saiz/saio pair. + std::vector mdat_payload(static_cast(p.num_samples) * p.sample_size); + for (size_t i = 0; i < mdat_payload.size(); i++) { + mdat_payload[i] = static_cast(0xA0 + (i & 0x0f)); + } + + std::vector aux_block_offsets; // relative to the start of the mdat payload + for (const auto& aux : p.aux_infos) { + aux_block_offsets.push_back(mdat_payload.size()); + for (uint32_t s = 0; s < p.num_samples; s++) { + append(mdat_payload, aux.data); + } + } + + // Box sizes are independent of the offset values, so we can size the file with + // placeholders, then patch the real offsets in one pass. + std::vector> saio_placeholders; + for (const auto& aux : p.aux_infos) { + saio_placeholders.push_back(make_saio(aux, 0)); + } + std::vector moov0 = assemble(make_stco(0), saio_placeholders); + uint32_t mdat_payload_offset = static_cast(ftyp.size() + moov0.size() + 8 /* mdat header */); + uint32_t file_size = static_cast(mdat_payload_offset + mdat_payload.size()); + + std::vector> saio_boxes; + for (size_t i = 0; i < p.aux_infos.size(); i++) { + uint32_t offset = p.aux_infos[i].offset_past_eof + ? file_size + 4096 + : mdat_payload_offset + static_cast(aux_block_offsets[i]); + saio_boxes.push_back(make_saio(p.aux_infos[i], offset)); + } + std::vector moov = assemble(make_stco(mdat_payload_offset), saio_boxes); + + std::vector mdat = box("mdat", mdat_payload); + + return concat({ftyp, moov, mdat}); +} + +} // namespace seqfile + +#endif diff -Nru libheif-1.19.8/tests/sequence_mixed_bit_depth.cc libheif-1.23.4/tests/sequence_mixed_bit_depth.cc --- libheif-1.19.8/tests/sequence_mixed_bit_depth.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_mixed_bit_depth.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,237 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// An encoder is opened once per sequence and configured from the first frame: +// Encoder_HEVC / Encoder_AVC / Encoder_AVIF / Encoder_VVC::encode_sequence_frame() +// call the plugin's start_sequence_encoding() only while the encoder is not +// running yet. Every later frame goes straight to encode_sequence_frame(), and +// nothing in the sequence API requires the frames to agree on a bit depth. +// +// The plugins latch the depth at that point. x265 is the worst case: it hands +// libx265 the plane pointers of the current frame together with a pic->bitDepth +// taken from the first one, so a 10 bit first frame followed by an 8 bit frame +// made libx265 walk a one byte per sample plane at two bytes per sample. That is +// a heap out-of-bounds read, and it reproduces under valgrind on the unfixed code +// as an "Invalid read" inside x265_10bit::Encoder::encode(). aom refused such a +// frame with an error of its own that named no cause, and rav1e and SVT-AV1 +// quietly encoded the mismatch. +// +// The fix is check_sequence_frame_bit_depth() in the plugins, so every frame of +// a sequence has to carry the depth the encoder was opened with. This test pins +// both directions of the mismatch, and that a uniform sequence still encodes. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// Large enough that the over-read of the unfixed code runs past the end of the +// plane allocation instead of staying inside its stride padding. +constexpr uint32_t WIDTH = 512; +constexpr uint32_t HEIGHT = 512; + +heif_image* make_image(int bit_depth) +{ + heif_image* img = nullptr; + REQUIRE(heif_image_create(WIDTH, HEIGHT, heif_colorspace_YCbCr, heif_chroma_420, &img).code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_add_plane(img, heif_channel_Y, WIDTH, HEIGHT, bit_depth).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cb, WIDTH / 2, HEIGHT / 2, bit_depth).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cr, WIDTH / 2, HEIGHT / 2, bit_depth).code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + uint32_t h = (channel == heif_channel_Y) ? HEIGHT : HEIGHT / 2; + uint32_t w = (channel == heif_channel_Y) ? WIDTH : WIDTH / 2; + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + + // Mid grey, so that a frame read at the wrong sample width is visibly wrong + // rather than accidentally plausible. + uint16_t value = static_cast(1 << (bit_depth - 1)); + + for (uint32_t y = 0; y < h; y++) { + if (bit_depth > 8) { + auto* row = reinterpret_cast(p + y * stride); + for (uint32_t x = 0; x < w; x++) { + row[x] = value; + } + } + else { + memset(p + y * stride, static_cast(value), w); + } + } + } + + heif_image_set_duration(img, 1); + + return img; +} + +struct TwoFrameResult +{ + // False when this build cannot encode the first depth at all (a libx265 without + // high bit depth support, for instance). The sequence then never reached a state + // in which two frames could disagree, and there is nothing to assert. + bool first_frame_encoded = false; + + heif_error second_frame_error = heif_error{heif_error_Ok, heif_suberror_Unspecified, nullptr}; +}; + +// Encode 'first_depth' and then 'second_depth' into one track and report what the +// second frame returned. +TwoFrameResult encode_two_frames(heif_compression_format format, int first_depth, int second_depth) +{ + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_encoder* encoder = nullptr; + REQUIRE(heif_context_get_encoder_for_format(ctx, format, &encoder).code == heif_error_Ok); + REQUIRE(encoder != nullptr); + + heif_track* track = nullptr; + REQUIRE(heif_context_add_visual_sequence_track(ctx, static_cast(WIDTH), static_cast(HEIGHT), + heif_track_type_video, nullptr, nullptr, &track).code == heif_error_Ok); + REQUIRE(track != nullptr); + + TwoFrameResult result; + + heif_image* first = make_image(first_depth); + heif_error err = heif_track_encode_sequence_image(track, first, encoder, nullptr); + heif_image_release(first); + + result.first_frame_encoded = (err.code == heif_error_Ok); + + if (result.first_frame_encoded) { + heif_image* second = make_image(second_depth); + result.second_frame_error = heif_track_encode_sequence_image(track, second, encoder, nullptr); + heif_image_release(second); + } + + heif_encoder_release(encoder); + heif_context_free(ctx); + + return result; +} + +void require_mismatch_refused(heif_compression_format format, int first_depth, int second_depth) +{ + TwoFrameResult result = encode_two_frames(format, first_depth, second_depth); + + if (!result.first_frame_encoded) { + return; + } + + const heif_error& err = result.second_frame_error; + + INFO("second frame error (" << err.code << "/" << err.subcode << "): " + << (err.message ? err.message : "(null)")); + + // Silently succeeding is the failure mode of the unfixed x265 plugin: it read + // past the end of the second frame's planes and encoded whatever it found. + REQUIRE(err.code != heif_error_Ok); + + // An encoder may bail out for a reason of its own, but when it is our check + // that fires, it has to report the bit depth as the reason. + if (err.code == heif_error_Encoder_plugin_error) { + REQUIRE(err.subcode == heif_suberror_Unsupported_bit_depth); + } +} + +void require_uniform_accepted(heif_compression_format format, int depth) +{ + TwoFrameResult result = encode_two_frames(format, depth, depth); + + REQUIRE(result.first_frame_encoded); + + const heif_error& err = result.second_frame_error; + + INFO("second frame error (" << err.code << "/" << err.subcode << "): " + << (err.message ? err.message : "(null)")); + REQUIRE(err.code == heif_error_Ok); +} + +} // namespace + +// One section per codec, so that a failure in one does not stop the other from +// running: the AV1 encoders report the mismatch as an error of their own, while +// it is the HEVC leg that reads out of bounds without the fix. +TEST_CASE("a sequence frame must carry the bit depth of the first frame") +{ + SECTION("AV1") + { + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + SKIP("Skipping because no AV1 encoder is compiled."); + } + + // 10 bit first: this is the direction that made x265 read past the end of the + // 8 bit planes of the second frame. + require_mismatch_refused(heif_compression_AV1, 10, 8); + + // 8 bit first: the mirror case, where the second frame's planes are wider than + // what the encoder was opened for. + require_mismatch_refused(heif_compression_AV1, 8, 10); + } + + SECTION("HEVC") + { + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("Skipping because no HEVC encoder is compiled."); + } + + require_mismatch_refused(heif_compression_HEVC, 10, 8); + require_mismatch_refused(heif_compression_HEVC, 8, 10); + } +} + +TEST_CASE("a sequence of frames with one bit depth still encodes") +{ + // The check must not reject what it is supposed to let through. + SECTION("AV1") + { + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + SKIP("Skipping because no AV1 encoder is compiled."); + } + + require_uniform_accepted(heif_compression_AV1, 8); + } + + SECTION("HEVC") + { + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("Skipping because no HEVC encoder is compiled."); + } + + require_uniform_accepted(heif_compression_HEVC, 8); + } +} diff -Nru libheif-1.19.8/tests/sequence_no_track.cc libheif-1.23.4/tests/sequence_no_track.cc --- libheif-1.19.8/tests/sequence_no_track.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_no_track.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,108 @@ +/* + libheif regression test for requesting a track from a context without sequence tracks. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// Sequence API queries that must work on a context that holds no sequence +// tracks (a still image, or no image at all). heif_context_get_track() is +// documented to return nullptr on failure; it must not abort/crash. This +// formerly tripped assert(has_sequence()) in HeifContext::get_track(). +// See https://github.com/strukturag/libheif/issues/1844. +void check_no_sequence_apis(heif_context* ctx) +{ + REQUIRE(heif_context_has_sequence(ctx) == 0); + REQUIRE(heif_context_number_of_sequence_tracks(ctx) == 0); + + // Listing track IDs must work (and write nothing) when there are no tracks. + heif_context_get_track_ids(ctx, nullptr); + + heif_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track == nullptr); +} + +heif_error mem_writer(heif_context*, const void* data, size_t size, void* userdata) +{ + auto* out = static_cast*>(userdata); + const auto* p = static_cast(data); + out->insert(out->end(), p, p + size); + return heif_error{heif_error_Ok, heif_suberror_Unspecified, nullptr}; +} + +} + +TEST_CASE("get_track on context without sequence returns nullptr") +{ + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // Fresh context, nothing loaded: no sequence tracks present. + check_no_sequence_apis(ctx); + + heif_context_free(ctx); +} + +TEST_CASE("get_track on a still-image file returns nullptr") +{ + // Encode a tiny still image to an in-memory HEIF file, then read it back. + // A still image is a perfectly valid file that contains no sequence tracks. + heif_image* img = nullptr; + REQUIRE(heif_image_create(16, 16, heif_colorspace_YCbCr, heif_chroma_420, &img).code == heif_error_Ok); + fill_new_plane(img, heif_channel_Y, 16, 16); + fill_new_plane(img, heif_channel_Cb, 8, 8); + fill_new_plane(img, heif_channel_Cr, 8, 8); + + heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC); + + heif_context* enc_ctx = heif_context_alloc(); + REQUIRE(heif_context_encode_image(enc_ctx, img, enc, nullptr, nullptr).code == heif_error_Ok); + + std::vector file; + heif_writer writer{}; + writer.writer_api_version = 1; + writer.write = mem_writer; + REQUIRE(heif_context_write(enc_ctx, &writer, &file).code == heif_error_Ok); + + heif_encoder_release(enc); + heif_context_free(enc_ctx); + heif_image_release(img); + + // Read the still image back and query the sequence API on it. + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok); + + check_no_sequence_apis(ctx); + + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/sequence_null_options.cc libheif-1.23.4/tests/sequence_null_options.cc --- libheif-1.19.8/tests/sequence_null_options.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_null_options.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,184 @@ +/* + libheif regression test for encoding a sequence image with NULL options. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// heif_context_add_visual_sequence_track() and heif_track_encode_sequence_image() +// both document their options parameters as optional ("If NULL, default options +// will be used"). Track_Visual::encode_image() nevertheless read +// in_options->save_alpha_channel without a NULL check, so passing NULL crashed +// with a null pointer dereference. A second, later dereference lurked on the same +// path: the color conversion options were forwarded as a NULL pointer that +// Encoder::convert_colorspace_for_encoding() dereferences whenever a conversion +// is actually needed. +// +// The fix resolves the caller's pointer into a fully populated options struct at +// the top of the function, so a NULL argument means "the documented defaults" +// rather than "read through a null pointer". This test pins both halves: that +// NULL options encode at all, and that they behave like the documented default +// save_alpha_channel = 1 rather than silently dropping the alpha track. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 64; +constexpr uint32_t HEIGHT = 64; + +heif_error mem_writer(heif_context*, const void* data, size_t size, void* userdata) +{ + auto* out = static_cast*>(userdata); + const auto* p = static_cast(data); + out->insert(out->end(), p, p + size); + return heif_error{heif_error_Ok, heif_suberror_Unspecified, nullptr}; +} + +heif_image* make_image(bool with_alpha) +{ + heif_image* img = nullptr; + REQUIRE(heif_image_create(WIDTH, HEIGHT, heif_colorspace_YCbCr, heif_chroma_420, &img).code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_add_plane(img, heif_channel_Y, WIDTH, HEIGHT, 8).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cb, WIDTH / 2, HEIGHT / 2, 8).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(img, heif_channel_Cr, WIDTH / 2, HEIGHT / 2, 8).code == heif_error_Ok); + if (with_alpha) { + REQUIRE(heif_image_add_plane(img, heif_channel_Alpha, WIDTH, HEIGHT, 8).code == heif_error_Ok); + } + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, heif_channel_Alpha}) { + if (!heif_image_has_channel(img, channel)) { + continue; + } + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + uint32_t rows = (channel == heif_channel_Cb || channel == heif_channel_Cr) ? HEIGHT / 2 : HEIGHT; + memset(p, 0x40, stride * rows); + } + + // Without a duration the encoder is handed a zero timebase and rejects the frame. + heif_image_set_duration(img, 1000); + + return img; +} + +// Encode a one frame sequence. 'save_alpha' < 0 means "pass NULL options". +std::vector encode_sequence(bool with_alpha, int save_alpha) +{ + heif_image* img = make_image(with_alpha); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_encoder* encoder = nullptr; + REQUIRE(heif_context_get_encoder_for_format(ctx, heif_compression_AV1, &encoder).code == heif_error_Ok); + REQUIRE(encoder != nullptr); + + heif_sequence_encoding_options* options = nullptr; + if (save_alpha >= 0) { + options = heif_sequence_encoding_options_alloc(); + REQUIRE(options != nullptr); + options->save_alpha_channel = save_alpha; + } + + heif_track* track = nullptr; + heif_error err = heif_context_add_visual_sequence_track(ctx, (uint16_t) WIDTH, (uint16_t) HEIGHT, + heif_track_type_image_sequence, + nullptr, options, &track); + INFO("add_visual_sequence_track: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(track != nullptr); + + // The formerly crashing call. + err = heif_track_encode_sequence_image(track, img, encoder, options); + INFO("encode_sequence_image: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + + err = heif_track_encode_end_of_sequence(track, encoder); + REQUIRE(err.code == heif_error_Ok); + + std::vector file; + heif_writer writer{}; + writer.writer_api_version = 1; + writer.write = mem_writer; + REQUIRE(heif_context_write(ctx, &writer, &file).code == heif_error_Ok); + + if (options) { + heif_sequence_encoding_options_release(options); + } + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(img); + + return file; +} + +} // namespace + +TEST_CASE("sequence encoding accepts NULL options") +{ + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + SKIP("Skipping test because no AV1 encoder is compiled."); + } + + // Passing NULL for both options parameters must work, as documented. + std::vector file = encode_sequence(/*with_alpha=*/false, /*save_alpha=*/-1); + REQUIRE(!file.empty()); + + // The resulting file must be readable and hold one sequence track. + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok); + REQUIRE(heif_context_has_sequence(ctx) == 1); + heif_context_free(ctx); +} + +TEST_CASE("NULL sequence options mean the documented defaults, not zeroed fields") +{ + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + SKIP("Skipping test because no AV1 encoder is compiled."); + } + + // heif_sequence_encoding_options_alloc() defaults save_alpha_channel to 1, so + // an alpha input encoded with NULL options must produce the same file as one + // encoded with an explicitly enabled alpha channel, and must differ from one + // with the alpha channel explicitly disabled. Guarding the NULL dereference + // alone (treating a missing options struct as all-zero) would silently drop + // the alpha track and make these two comparisons swap places. + std::vector with_null = encode_sequence(/*with_alpha=*/true, /*save_alpha=*/-1); + std::vector alpha_on = encode_sequence(/*with_alpha=*/true, /*save_alpha=*/1); + std::vector alpha_off = encode_sequence(/*with_alpha=*/true, /*save_alpha=*/0); + + REQUIRE(!with_null.empty()); + REQUIRE(with_null == alpha_on); + REQUIRE(with_null != alpha_off); +} diff -Nru libheif-1.19.8/tests/sequence_raw_sample_errors.cc libheif-1.23.4/tests/sequence_raw_sample_errors.cc --- libheif-1.19.8/tests/sequence_raw_sample_errors.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_raw_sample_errors.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,238 @@ +/* + libheif integration tests for the error paths of the raw sequence sample API. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression tests for GHSA-4rv4-953r-p24q. +// +// Track::get_next_sample_raw_data() allocated the heif_raw_sequence_sample, holding +// a full copy of the sample payload, before reading the sample auxiliary +// information ('saiz'/'saio'). Four error returns in that aux-info stage dropped +// the pointer: the Result error variant cannot carry it, so the caller never +// received anything it could release. A file with many tracks aliasing one payload +// range leaked that payload once per track and per call. The same C entry point +// also lacked exception_guard(), so a failing allocation of the file-controlled +// sample size aborted the host process instead of returning heif_error_out_of_memory. +// +// The leak itself is only visible under LeakSanitizer. These tests pin down the +// caller-visible contract on each of the four error paths (a clean heif_error, +// *out_sample untouched, the call repeatable) and exercise the success path so the +// payload move in the fix is covered. Run this binary under ASan/LSan to check the +// allocation side: before the fix, every repetition in expect_clean_failure() +// leaked one sample object plus one payload copy. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "libheif/heif_tai_timestamps.h" +#include "sequence_file_builder.h" + +#include +#include +#include + +using namespace seqfile; + +namespace { + +// One 'urim' metadata track with a single 64-byte sample and no edit list. +SequenceFileParams base_params() +{ + SequenceFileParams p; + p.mvhd_v1 = false; + p.mvhd_duration = 10; + p.mdhd_duration = 10; + p.with_editlist = false; + p.stts = {{1, 10}}; + p.num_samples = 1; + p.sample_size = 64; + return p; +} + +struct LoadedTrack +{ + heif_context* ctx = nullptr; + heif_track* track = nullptr; + + LoadedTrack() = default; + LoadedTrack(const LoadedTrack&) = delete; + LoadedTrack& operator=(const LoadedTrack&) = delete; + + ~LoadedTrack() + { + if (track) heif_track_release(track); + if (ctx) heif_context_free(ctx); + } +}; + +// Read the file and fetch its first track. Loading must succeed: all the malformed +// aux-info variants below are only detected when the sample itself is read. +void load(const std::vector& file, LoadedTrack& out) +{ + out.ctx = heif_context_alloc(); + REQUIRE(out.ctx != nullptr); + + heif_error err = heif_context_read_from_memory(out.ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_context_has_sequence(out.ctx) == 1); + + out.track = heif_context_get_track(out.ctx, 0); + REQUIRE(out.track != nullptr); +} + +// Every call on a malformed file must fail with a clean error and leave the output +// pointer untouched. The sample index does not advance on error, so repeating the +// call exercises the same path again (and, before the fix, leaked again). +void expect_clean_failure(heif_track* track, int repetitions = 20) +{ + for (int i = 0; i < repetitions; i++) { + heif_raw_sequence_sample* sample = nullptr; + heif_error err = heif_track_get_next_raw_sequence_sample(track, &sample); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(sample == nullptr); + } +} + +} // namespace + + +TEST_CASE("raw sample with non-terminated 'suid' content ID fails cleanly") +{ + SequenceFileParams p = base_params(); + p.aux_infos = {{"suid", {'A'}}}; // utf8string without NUL terminator + + LoadedTrack t; + load(build_sequence_file(p), t); + expect_clean_failure(t.track); +} + + +TEST_CASE("raw sample with embedded NUL in 'suid' content ID fails cleanly") +{ + SequenceFileParams p = base_params(); + p.aux_infos = {{"suid", {'A', 0, 'B', 0}}}; + + LoadedTrack t; + load(build_sequence_file(p), t); + expect_clean_failure(t.track); +} + + +TEST_CASE("raw sample with 'saio' offset past end of file fails cleanly") +{ + SequenceFileParams p = base_params(); + SampleAuxInfo aux; + aux.type = "suid"; + aux.data = {'A', 0}; + aux.offset_past_eof = true; + p.aux_infos = {aux}; + + LoadedTrack t; + load(build_sequence_file(p), t); + expect_clean_failure(t.track); +} + + +TEST_CASE("raw sample with wrong-sized 'stai' TAI timestamp fails cleanly") +{ + SequenceFileParams p = base_params(); + p.aux_infos = {{"stai", {1, 2, 3, 4, 5}}}; // a TAI timestamp payload must be exactly 9 bytes + + LoadedTrack t; + load(build_sequence_file(p), t); + expect_clean_failure(t.track); +} + + +TEST_CASE("raw sample with well-formed 'suid' and 'stai' aux info succeeds") +{ + // Control: the same file structure with valid aux-info payloads must deliver the + // sample together with its content ID and TAI timestamp. This also covers the + // payload move in Track::get_next_sample_raw_data(): the sample bytes must arrive + // intact. + SequenceFileParams p = base_params(); + p.aux_infos = { + {"suid", {'u', 'r', 'n', ':', 'x', 0}}, + {"stai", {0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x00}} + }; + + LoadedTrack t; + load(build_sequence_file(p), t); + + heif_raw_sequence_sample* sample = nullptr; + heif_error err = heif_track_get_next_raw_sequence_sample(t.track, &sample); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(sample != nullptr); + + size_t size = 0; + const uint8_t* data = heif_raw_sequence_sample_get_data(sample, &size); + REQUIRE(data != nullptr); + REQUIRE(size == 64); + for (size_t i = 0; i < size; i++) { + REQUIRE(data[i] == static_cast(0xA0 + (i & 0x0f))); // pattern written by the builder + } + REQUIRE(heif_raw_sequence_sample_get_duration(sample) == 10); + + const char* content_id = heif_raw_sequence_sample_get_gimi_sample_content_id(sample); + REQUIRE(content_id != nullptr); + REQUIRE(std::string(content_id) == "urn:x"); + heif_string_release(content_id); + + REQUIRE(heif_raw_sequence_sample_has_tai_timestamp(sample) == 1); + const heif_tai_timestamp_packet* tai = heif_raw_sequence_sample_get_tai_timestamp(sample); + REQUIRE(tai != nullptr); + REQUIRE(tai->tai_timestamp == 0x12345678); + + heif_raw_sequence_sample_release(sample); + + sample = nullptr; + err = heif_track_get_next_raw_sequence_sample(t.track, &sample); + REQUIRE(err.code == heif_error_End_of_sequence); + REQUIRE(sample == nullptr); +} + + +TEST_CASE("oversized raw sample is rejected with a heif_error") +{ + // The sample buffer is sized from the file's 'stsz'. A size above + // max_memory_block_size must come back as a heif_error through the C API. (The + // entry point is additionally wrapped in exception_guard() so that a failing + // allocation below that limit is reported as heif_error_out_of_memory instead of + // aborting the process; that path needs an rlimit and is not unit-tested here.) + SequenceFileParams p = base_params(); + p.sample_size = 4096; + + LoadedTrack t; + load(build_sequence_file(p), t); + + // Tighten the limit only after loading so that the file read itself is unaffected. + heif_security_limits* limits = heif_context_get_security_limits(t.ctx); + limits->max_memory_block_size = 1024; + + heif_raw_sequence_sample* sample = nullptr; + heif_error err = heif_track_get_next_raw_sequence_sample(t.track, &sample); + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + REQUIRE(sample == nullptr); +} diff -Nru libheif-1.19.8/tests/sequence_timing_overflow.cc libheif-1.23.4/tests/sequence_timing_overflow.cc --- libheif-1.19.8/tests/sequence_timing_overflow.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/sequence_timing_overflow.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,257 @@ +/* + libheif integration tests for sequence sample-timing overflow. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression tests for GHSA-xw34-mjcp-jqh8: an ISOBMFF edit list in "repeat" +// mode combined with the movie-header duration amplifies a single physical +// sample into an astronomically large *logical* output count. Before the fix, +// Track::init_sample_timing_table() left m_num_output_samples (uint64_t) at that +// value while the decode/raw-output loops count with a uint32_t, so +// end_of_sequence_reached() could never become true and decoding never +// terminated (variants V4/V5). The physical-sample security limit +// (max_sequence_frames) was also never applied to the amplified logical count +// (variant V6). +// +// These tests use the hand-built sequence files from sequence_file_builder.h. A 'urim' metadata +// track is used so the raw-sample API path exercises the timing table without +// needing any codec plugin. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "sequence_file_builder.h" + +#include +#include +#include + +using namespace seqfile; + + +TEST_CASE("repeat edit list with indefinite duration terminates") +{ + // A single physical sample plus an indefinite-duration repeat edit list. + // Before the fix this inflated m_num_output_samples to ~UINT64_MAX, and the + // uint32_t output counter could never reach it -> non-terminating loop. + SequenceFileParams p; + p.mvhd_v1 = true; + p.mvhd_duration = std::numeric_limits::max(); // indefinite sentinel + p.mdhd_duration = 1; + p.with_editlist = true; + p.elst_segment_duration = 1; + p.stts = {{1, 1}}; + p.num_samples = 1; + + std::vector file = build_sequence_file(p); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // Lower the frame limit so the (now bounded) output count is small and the test + // runs quickly. This is exactly the limit that must cap the repeat-amplified + // logical sample count. + heif_security_limits* limits = heif_context_get_security_limits(ctx); + const uint32_t kFrameLimit = 100; + limits->max_sequence_frames = kFrameLimit; + + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_context_has_sequence(ctx) == 1); + + heif_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track != nullptr); + + // The file still advertises "infinite" repetition to the caller... + REQUIRE(heif_track_get_number_of_repetitions(track) == + heif_sequence_track_number_of_repetitions_infinite); + + // ...but iterating the raw samples must terminate. It is bounded by the + // frame limit. Cap the loop well above that so a regression fails (rather than + // hanging): with the bug, End_of_sequence is never reached. + const int kIterationCap = 100000; + int count = 0; + bool reached_end = false; + for (; count < kIterationCap; count++) { + heif_raw_sequence_sample* sample = nullptr; + heif_error serr = heif_track_get_next_raw_sequence_sample(track, &sample); + if (serr.code == heif_error_End_of_sequence) { + reached_end = true; + break; + } + REQUIRE(serr.code == heif_error_Ok); + REQUIRE(sample != nullptr); + heif_raw_sequence_sample_release(sample); + } + + REQUIRE(reached_end); + REQUIRE(count == static_cast(kFrameLimit)); + + heif_track_release(track); + heif_context_free(ctx); +} + + +TEST_CASE("repeat edit list with finite over-uint32 multiplier terminates") +{ + // Non-sentinel duration whose multiplier exceeds UINT32_MAX (variant V5): the + // repeat is finite but the logical count overflows the uint32_t output counter. + SequenceFileParams p; + p.mvhd_v1 = true; + p.mvhd_duration = (uint64_t{1} << 32) + 1; // 0x100000001, > UINT32_MAX, not the sentinel + p.mdhd_duration = 1; + p.with_editlist = true; + p.elst_segment_duration = 1; + p.stts = {{1, 1}}; + p.num_samples = 1; + + std::vector file = build_sequence_file(p); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_security_limits* limits = heif_context_get_security_limits(ctx); + const uint32_t kFrameLimit = 50; + limits->max_sequence_frames = kFrameLimit; + + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track != nullptr); + + const int kIterationCap = 100000; + int count = 0; + bool reached_end = false; + for (; count < kIterationCap; count++) { + heif_raw_sequence_sample* sample = nullptr; + heif_error serr = heif_track_get_next_raw_sequence_sample(track, &sample); + if (serr.code == heif_error_End_of_sequence) { + reached_end = true; + break; + } + REQUIRE(serr.code == heif_error_Ok); + heif_raw_sequence_sample_release(sample); + } + + REQUIRE(reached_end); + REQUIRE(count == static_cast(kFrameLimit)); + + heif_track_release(track); + heif_context_free(ctx); +} + + +TEST_CASE("multi-entry stts yields correct per-sample durations") +{ + // Guards the prefix-sum + binary-search rewrite of Box_stts::get_sample_duration(): + // the run-length coded 'stts' must resolve to the same per-sample durations as the + // original linear scan, including across entry boundaries. Uses a plain track (no + // edit list -> single playback). + SequenceFileParams p; + p.mvhd_v1 = false; + p.mvhd_duration = 6; + p.mdhd_duration = 6; + p.with_editlist = false; + p.stts = {{2, 10}, {1, 20}, {3, 30}}; // durations: 10, 10, 20, 30, 30, 30 + p.num_samples = 6; + + std::vector file = build_sequence_file(p); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track != nullptr); + + std::vector expected_durations = {10, 10, 20, 30, 30, 30}; + std::vector got_durations; + + for (int i = 0; i < 20; i++) { + heif_raw_sequence_sample* sample = nullptr; + heif_error serr = heif_track_get_next_raw_sequence_sample(track, &sample); + if (serr.code == heif_error_End_of_sequence) { + break; + } + REQUIRE(serr.code == heif_error_Ok); + got_durations.push_back(heif_raw_sequence_sample_get_duration(sample)); + heif_raw_sequence_sample_release(sample); + } + + REQUIRE(got_durations == expected_durations); + + heif_track_release(track); + heif_context_free(ctx); +} + + +TEST_CASE("per-track timeline/chunk memory is bounded by max_total_memory") +{ + // A tiny file that declares a large sample count via a single run-length 'stts' + // entry plus a fixed_sample_size 'stsz'. The per-track presentation timeline + // (~48 B/sample) and the chunk sample-range table (~16 B/sample) must be accounted + // against max_total_memory. Before the fix they bypassed MemoryHandle, so a small + // file could force hundreds of MB per track undetected (GHSA-xw34-mjcp-jqh8, V2/V3). + const uint32_t N = 500000; // ~24 MB timeline + ~8 MB chunk ranges; file stays tiny + + SequenceFileParams p; + p.mvhd_v1 = false; + p.mvhd_duration = N; + p.mdhd_duration = N; + p.with_editlist = false; + p.stts = {{N, 1}}; // one run-length entry describes all N samples + p.num_samples = N; + p.sample_size = 1; + + std::vector file = build_sequence_file(p); + + // With ample memory the file is structurally valid and reads fine. + { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + heif_context_free(ctx); + } + + // With a small max_total_memory the per-track tables must trip the limit. The + // 'stts'/'stsz'/'stsc'/'stco' tables are all tiny here, so the only allocation + // large enough to exceed 4 MB is one of the newly-tracked per-track tables. + { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_security_limits* limits = heif_context_get_security_limits(ctx); + limits->max_total_memory = 4 * 1024 * 1024; // 4 MB, far below the ~32 MB needed + + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + + heif_context_free(ctx); + } +} diff -Nru libheif-1.19.8/tests/tai.cc libheif-1.23.4/tests/tai.cc --- libheif-1.19.8/tests/tai.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/tai.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,140 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2025 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "test_utils.h" +#include +#include + +TEST_CASE( "image-tai" ) +{ + heif_error err{}; + + err = heif_init(nullptr); + REQUIRE(err.code == heif_error_Ok); + + std::string filename = get_tests_output_file_path("tai-1.heic"); + + heif_image* img = createImage_RGB_planar(); + heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC); + heif_context* ctx = heif_context_alloc(); + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + heif_item_id itemId = heif_image_handle_get_item_id(handle); + + // add TAI clock info + + heif_tai_clock_info* clock_info = heif_tai_clock_info_alloc(); + clock_info->clock_resolution = 1000; + clock_info->clock_drift_rate = 123; + clock_info->clock_type = heif_tai_clock_info_clock_type_synchronized_to_atomic_source; + clock_info->time_uncertainty = 999; + + err = heif_item_set_property_tai_clock_info(ctx, itemId, clock_info, nullptr); + REQUIRE(err.code == heif_error_Ok); + + // check that adding a second timestamp leads to an error + err = heif_item_set_property_tai_clock_info(ctx, itemId, clock_info, nullptr); + REQUIRE(err.code != heif_error_Ok); + + heif_tai_clock_info_release(clock_info); + + // add TAI timestamp + + heif_tai_timestamp_packet* timestamp = heif_tai_timestamp_packet_alloc(); + timestamp->tai_timestamp = 1234567890; + timestamp->synchronization_state = 1; + timestamp->timestamp_generation_failure = 0; + timestamp->timestamp_is_modified = 0; + + err = heif_item_set_property_tai_timestamp(ctx, itemId, timestamp, nullptr); + REQUIRE(err.code == heif_error_Ok); + + // check that adding a second timestamp leads to an error + err = heif_item_set_property_tai_timestamp(ctx, itemId, timestamp, nullptr); + REQUIRE(err.code != heif_error_Ok); + + heif_tai_timestamp_packet_release(timestamp); + + err = heif_context_write_to_file(ctx, filename.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(handle); + heif_context_free(ctx); + heif_image_release(img); + + + // --- read file + + ctx = heif_context_alloc(); + err = heif_context_read_from_file(ctx, filename.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + + itemId = heif_image_handle_get_item_id(handle); + + clock_info = nullptr; // make sure that we are not accidentally using old data + err = heif_item_get_property_tai_clock_info(ctx, itemId, &clock_info); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(clock_info != nullptr); + + timestamp = nullptr; // make sure that we are not accidentally using old data + err = heif_item_get_property_tai_timestamp(ctx, itemId, ×tamp); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(timestamp != nullptr); + + REQUIRE(clock_info->clock_resolution == 1000); + REQUIRE(clock_info->clock_drift_rate == 123); + REQUIRE(clock_info->clock_type == heif_tai_clock_info_clock_type_synchronized_to_atomic_source); + REQUIRE(clock_info->time_uncertainty == 999); + heif_tai_clock_info_release(clock_info); + + REQUIRE(timestamp->tai_timestamp == 1234567890); + REQUIRE(timestamp->synchronization_state == 1); + REQUIRE(timestamp->timestamp_generation_failure == 0); + REQUIRE(timestamp->timestamp_is_modified == 0); + heif_tai_timestamp_packet_release(timestamp); + + // check whether we can get the timestamp also from the decoded image + + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + + timestamp = nullptr; // make sure that we are not accidentally using old data + err = heif_image_get_tai_timestamp(img, ×tamp); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(timestamp != nullptr); + + REQUIRE(timestamp->tai_timestamp == 1234567890); + REQUIRE(timestamp->synchronization_state == 1); + REQUIRE(timestamp->timestamp_generation_failure == 0); + REQUIRE(timestamp->timestamp_is_modified == 0); + heif_tai_timestamp_packet_release(timestamp); +} \ No newline at end of file diff -Nru libheif-1.19.8/tests/test_utils.cc libheif-1.23.4/tests/test_utils.cc --- libheif-1.19.8/tests/test_utils.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/test_utils.cc 2026-09-06 14:45:17.000000000 +0000 @@ -30,15 +30,16 @@ #include #include "catch_amalgamated.hpp" -struct heif_context * get_context_for_test_file(std::string filename) + +heif_context * get_context_for_test_file(std::string filename) { return get_context_for_local_file(tests_data_directory + "/" + filename); } -struct heif_context * get_context_for_local_file(std::string filename) +heif_context * get_context_for_local_file(std::string filename) { - struct heif_context* context; - struct heif_error err; + heif_context* context; + heif_error err; context = heif_context_alloc(); err = heif_context_read_from_file(context, filename.c_str(), NULL); INFO(filename); @@ -46,10 +47,10 @@ return context; } -struct heif_image_handle * get_primary_image_handle(heif_context *context) +heif_image_handle * get_primary_image_handle(heif_context *context) { - struct heif_error err; - struct heif_image_handle * image_handle; + heif_error err; + heif_image_handle * image_handle; int num_images = heif_context_get_number_of_top_level_images(context); REQUIRE(num_images == 1); err = heif_context_get_primary_image_handle(context, &image_handle); @@ -57,28 +58,28 @@ return image_handle; } -struct heif_image * get_primary_image(heif_image_handle * handle) +heif_image * get_primary_image(heif_image_handle * handle) { - struct heif_error err; - struct heif_image* img; + heif_error err; + heif_image* img; err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_444, NULL); REQUIRE(err.code == heif_error_Ok); return img; } -struct heif_image * get_primary_image_mono(heif_image_handle * handle) +heif_image * get_primary_image_mono(heif_image_handle * handle) { - struct heif_error err; - struct heif_image* img; + heif_error err; + heif_image* img; err = heif_decode_image(handle, &img, heif_colorspace_monochrome, heif_chroma_monochrome, NULL); REQUIRE(err.code == heif_error_Ok); return img; } -struct heif_image * get_primary_image_ycbcr(heif_image_handle * handle, heif_chroma chroma) +heif_image * get_primary_image_ycbcr(heif_image_handle * handle, heif_chroma chroma) { - struct heif_error err; - struct heif_image* img; + heif_error err; + heif_image* img; err = heif_decode_image(handle, &img, heif_colorspace_YCbCr, chroma, NULL); REQUIRE(err.code == heif_error_Ok); return img; @@ -86,7 +87,7 @@ void fill_new_plane(heif_image* img, heif_channel channel, int w, int h) { - struct heif_error err; + heif_error err; err = heif_image_add_plane(img, channel, w, h, 8); REQUIRE(err.code == heif_error_Ok); @@ -99,10 +100,10 @@ } } -struct heif_image * createImage_RGB_planar() +heif_image * createImage_RGB_planar() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_RGB, @@ -194,3 +195,88 @@ return encoder; } + + +fs::path get_tests_output_dir() +{ + if (const char* env_p = std::getenv("LIBHEIF_TEST_OUTPUT_DIR")) { + return fs::path(env_p); + } + + static const fs::path output_dir = fs::current_path() / "libheif_test_output"; + + if (!fs::exists(output_dir)) { + fs::create_directories(output_dir); + } + + return output_dir; +} + + +std::string get_tests_output_file_path(const char* filename) +{ + fs::path dir = get_tests_output_dir(); + dir /= filename; + return dir.string(); +} + + +void put_u32_be(std::vector& out, uint32_t v) +{ + out.push_back(static_cast(v >> 24)); + out.push_back(static_cast(v >> 16)); + out.push_back(static_cast(v >> 8)); + out.push_back(static_cast(v)); +} + +void put_u16_be(std::vector& out, uint16_t v) +{ + out.push_back(static_cast(v >> 8)); + out.push_back(static_cast(v)); +} + +void append_fourcc(std::vector& out, const char fourcc[4]) +{ + // Append the bytes one at a time. A range insert from the char array makes + // GCC 13 report a bogus -Wstringop-overflow in optimized builds. + for (int i = 0; i < 4; i++) { + out.push_back(static_cast(fourcc[i])); + } +} + +void append_cstr(std::vector& out, const char* s) +{ + while (*s) { + out.push_back(static_cast(*s)); + ++s; + } + out.push_back(0); +} + +void append(std::vector& out, const std::vector& v) +{ + out.insert(out.end(), v.begin(), v.end()); +} + +std::vector make_box(const char fourcc[4], + const std::vector& payload, + bool is_full_box, + uint8_t version, + uint32_t flags) +{ + const size_t header_size = is_full_box ? 12 : 8; + + std::vector box; + box.reserve(header_size + payload.size()); + + put_u32_be(box, static_cast(header_size + payload.size())); + append_fourcc(box, fourcc); + if (is_full_box) { + box.push_back(version); + box.push_back(static_cast(flags >> 16)); + box.push_back(static_cast(flags >> 8)); + box.push_back(static_cast(flags)); + } + append(box, payload); + return box; +} diff -Nru libheif-1.19.8/tests/test_utils.h libheif-1.23.4/tests/test_utils.h --- libheif-1.19.8/tests/test_utils.h 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/test_utils.h 2026-09-06 14:45:17.000000000 +0000 @@ -24,22 +24,55 @@ SOFTWARE. */ +#include #include +#include #include "libheif/heif.h" -struct heif_context * get_context_for_test_file(std::string filename); -struct heif_context * get_context_for_local_file(std::string filename); +#include -struct heif_image_handle * get_primary_image_handle(heif_context *context); +namespace fs = std::filesystem; -struct heif_image * get_primary_image(heif_image_handle * handle); -struct heif_image * get_primary_image_mono(heif_image_handle * handle); -struct heif_image * get_primary_image_ycbcr(heif_image_handle * handle, heif_chroma chroma); +heif_context * get_context_for_test_file(std::string filename); +heif_context * get_context_for_local_file(std::string filename); + +heif_image_handle * get_primary_image_handle(heif_context *context); + +heif_image * get_primary_image(heif_image_handle * handle); +heif_image * get_primary_image_mono(heif_image_handle * handle); +heif_image * get_primary_image_ycbcr(heif_image_handle * handle, heif_chroma chroma); void fill_new_plane(heif_image* img, heif_channel channel, int w, int h); -struct heif_image * createImage_RGB_planar(); +heif_image * createImage_RGB_planar(); std::string get_path_for_heifio_test_file(std::string filename); -heif_encoder* get_encoder_or_skip_test(heif_compression_format format); \ No newline at end of file +heif_encoder* get_encoder_or_skip_test(heif_compression_format format); + +fs::path get_tests_output_dir(); + +std::string get_tests_output_file_path(const char* filename); + + +// --- Helpers to build ISOBMFF byte streams for synthetic test files --- + +// Append a big-endian integer. +void put_u32_be(std::vector& out, uint32_t v); +void put_u16_be(std::vector& out, uint16_t v); + +// Append the four characters of a box type or brand. +void append_fourcc(std::vector& out, const char fourcc[4]); + +// Append a null-terminated string, including the terminator. +void append_cstr(std::vector& out, const char* s); + +// Append the contents of another byte vector. +void append(std::vector& out, const std::vector& v); + +// Build a box, or a full box with version and flags, from its type and payload. +std::vector make_box(const char fourcc[4], + const std::vector& payload, + bool is_full_box = false, + uint8_t version = 0, + uint32_t flags = 0); diff -Nru libheif-1.19.8/tests/text.cc libheif-1.23.4/tests/text.cc --- libheif-1.19.8/tests/text.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/text.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,166 @@ +/* + libheif integration tests for text items + + MIT License + + Copyright (c) 2025 Brad Hards + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "api_structs.h" +#include "libheif/heif.h" +#include "test-config.h" +#include "test_utils.h" +#include +#include +#include +#include +#include + + +TEST_CASE("no text") { + // skip test if we do not have the uncompressed codec + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("Skipping test because uncompressed codec is not compiled."); + } + + auto context = get_context_for_test_file("uncompressed_comp_RGB.heif"); + heif_image_handle *handle = get_primary_image_handle(context); + int num_text_items = heif_image_handle_get_number_of_text_items(handle); + REQUIRE(num_text_items == 0); + heif_image_handle_release(handle); + heif_context_free(context); +} + +TEST_CASE("create text item") { + // skip test if we do not have the uncompressed codec + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("Skipping test because uncompressed codec is not compiled."); + } + struct heif_error err; + + heif_image* img; + uint32_t input_width = 1280; + uint32_t input_height = 1024; + heif_image_create(input_width,input_height, heif_colorspace_YCbCr, heif_chroma_420, &img); + fill_new_plane(img, heif_channel_Y, input_width, input_height); + fill_new_plane(img, heif_channel_Cb, (input_width+1)/2, (input_height+1)/2); + fill_new_plane(img, heif_channel_Cr, (input_width+1)/2, (input_height+1)/2); + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc; + err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &enc); + REQUIRE(err.code == heif_error_Ok); + + struct heif_encoding_options* options; + options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround = false; + options->macOS_compatibility_workaround_no_nclx_profile = false; + options->image_orientation = heif_orientation_normal; + + heif_image_handle* handle; + err = heif_context_encode_image(ctx, img, enc, options, &handle); + REQUIRE(err.code == heif_error_Ok); + + struct heif_text_item* text_item1; + std::string text_body1("first string"); + err = heif_image_handle_add_text_item(handle, "text/plain", text_body1.c_str(), &text_item1); + REQUIRE(err.code == heif_error_Ok); + err = heif_text_item_set_extended_language(text_item1, "en-AU", NULL); + REQUIRE(err.code == heif_error_Ok); + + struct heif_text_item* text_item2; + std::string text_body2("a second string"); + err = heif_image_handle_add_text_item(handle, "text/plain", text_body2.c_str(), &text_item2); + REQUIRE(err.code == heif_error_Ok); + heif_property_id elng_prop_id; + err = heif_text_item_set_extended_language(text_item2, "en-UK", &elng_prop_id); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(elng_prop_id != 0); + + err = heif_context_write_to_file(ctx, "text.heif"); + REQUIRE(err.code == heif_error_Ok); + + heif_text_item_release(text_item1); + heif_text_item_release(text_item2); + + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_encoding_options_free(options); + heif_context_free(ctx); + heif_image_release(img); + + heif_context *readbackCtx = get_context_for_local_file("text.heif"); + heif_image_handle *readbackHandle = get_primary_image_handle(readbackCtx); + int num_text_items = heif_image_handle_get_number_of_text_items(readbackHandle); + REQUIRE(num_text_items == 2); + + std::vector text_item_ids_minus(1); + int num_returned = heif_image_handle_get_list_of_text_item_ids(readbackHandle, text_item_ids_minus.data(), 1); + REQUIRE(num_returned == 1); + + std::vector text_item_ids_extra(3); + num_returned = heif_image_handle_get_list_of_text_item_ids(readbackHandle, text_item_ids_extra.data(), 3); + REQUIRE(num_returned == num_text_items); + + std::vector text_item_ids(num_text_items); + num_returned = heif_image_handle_get_list_of_text_item_ids(readbackHandle, text_item_ids.data(), num_text_items); + REQUIRE(num_returned == 2); + + heif_text_item* text0; + err = heif_context_get_text_item(readbackCtx, text_item_ids[0], &text0); + heif_item_id id0 = heif_text_item_get_id(text0); + REQUIRE(id0 == text_item_ids[0]); + + REQUIRE(heif_item_get_item_type(readbackCtx, text_item_ids[0]) == fourcc("mime")); + const char* content_type0 = heif_item_get_mime_item_content_type(readbackCtx, text_item_ids[0]); + REQUIRE(std::string(content_type0) == "text/plain"); + const char* body0 = heif_text_item_get_content(text0); + REQUIRE(std::string(body0) == text_body1); + heif_string_release(body0); + char * elng0; + err = heif_item_get_property_extended_language(readbackCtx, id0, &elng0); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(strcmp(elng0, "en-AU") == 0); + heif_string_release(elng0); + + heif_text_item* text1; + err = heif_context_get_text_item(readbackCtx, text_item_ids[1], &text1); + heif_item_id id1 = heif_text_item_get_id(text1); + REQUIRE(id1 == text_item_ids[1]); + + REQUIRE(heif_item_get_item_type(readbackCtx, text_item_ids[1]) == fourcc("mime")); + const char* content_type1 = heif_item_get_mime_item_content_type(readbackCtx, text_item_ids[0]); + REQUIRE(std::string(content_type1) == "text/plain"); + const char* body1 = heif_text_item_get_content(text1); + REQUIRE(std::string(body1) == text_body2); + heif_string_release(body1); + char * elng1; + err = heif_item_get_property_extended_language(readbackCtx, id1, &elng1); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(strcmp(elng1, "en-UK") == 0); + heif_string_release(elng1); + + heif_text_item_release(text0); + heif_text_item_release(text1); + heif_image_handle_release(readbackHandle); + heif_context_free(readbackCtx); +} diff -Nru libheif-1.19.8/tests/tiffdecode.cc libheif-1.23.4/tests/tiffdecode.cc --- libheif-1.19.8/tests/tiffdecode.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/tiffdecode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -33,11 +33,10 @@ #include "libheif/heif.h" #include "libheif/api_structs.h" -#if HAVE_LIBTIFF void checkMono(InputImage input_image) { REQUIRE(input_image.orientation == heif_orientation_normal); REQUIRE(input_image.image != nullptr); - const struct heif_image* image = input_image.image.get(); + const heif_image* image = input_image.image.get(); REQUIRE(heif_image_get_colorspace(image) == heif_colorspace_monochrome); REQUIRE(heif_image_get_chroma_format(image) == heif_chroma_monochrome); REQUIRE(heif_image_get_width(image, heif_channel_Y) == 128); @@ -56,7 +55,7 @@ TEST_CASE("mono8") { InputImage input_image; std::string path = get_path_for_heifio_test_file("mono.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkMono(input_image); } @@ -64,7 +63,7 @@ TEST_CASE("mono8planar") { InputImage input_image; std::string path = get_path_for_heifio_test_file("mono_planar.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkMono(input_image); } @@ -91,7 +90,7 @@ TEST_CASE("rgb") { InputImage input_image; std::string path = get_path_for_heifio_test_file("rgb.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkRGB(input_image); } @@ -99,7 +98,7 @@ TEST_CASE("rgb_planar") { InputImage input_image; std::string path = get_path_for_heifio_test_file("rgb_planar.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkRGB(input_image); } @@ -108,7 +107,7 @@ void checkRGBA(InputImage input_image) { REQUIRE(input_image.orientation == heif_orientation_normal); REQUIRE(input_image.image != nullptr); - const struct heif_image* image = input_image.image.get(); + const heif_image* image = input_image.image.get(); REQUIRE(heif_image_get_colorspace(image) == heif_colorspace_RGB); REQUIRE(heif_image_get_chroma_format(image) == heif_chroma_interleaved_RGBA); REQUIRE(heif_image_get_width(image, heif_channel_interleaved) == 32); @@ -127,7 +126,7 @@ TEST_CASE("rgba") { InputImage input_image; std::string path = get_path_for_heifio_test_file("rgba.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkRGBA(input_image); } @@ -135,12 +134,7 @@ TEST_CASE("rgba_planar") { InputImage input_image; std::string path = get_path_for_heifio_test_file("rgba_planar.tif"); - heif_error err = loadTIFF(path.c_str(), &input_image); + heif_error err = loadTIFF(path.c_str(), 10, &input_image); REQUIRE(err.code == heif_error_Ok); checkRGBA(input_image); -} -#else -TEST_CASE("no_tiff dummy") { - // Dummy test if we don't have the TIFF library, so that testing does not fail with "No test ran". -} -#endif // HAVE_LIBTIFF \ No newline at end of file +} \ No newline at end of file diff -Nru libheif-1.19.8/tests/uncompressed_block_pixel_overpacked.cc libheif-1.23.4/tests/uncompressed_block_pixel_overpacked.cc --- libheif-1.19.8/tests/uncompressed_block_pixel_overpacked.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_block_pixel_overpacked.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,239 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test: block-pixel interleave with components that do not fit the +// block must be rejected on the per-tile decode path. +// +// A crafted 'unci' image uses pixel interleave with block_size == pixel_size == 8 +// bytes (64 bits) and five 16-bit components (80 bits total). The block-pixel +// decoder packs all components into one 64-bit block and derives each component's +// bit shift by accumulating the component bit depths. When the depths sum to at +// least the block width, the highest shift reaches or exceeds 64, so +// `block_val >> shift` on the uint64_t block in +// unc_decoder_block_pixel_interleave::decode_tile() is undefined behaviour +// (UBSan abort; otherwise corrupted output). +// +// The full-image decode path already rejected this via check_hard_limits(), but +// the per-tile decode path (UncompressedImageCodec::decode_uncompressed_image_tile, +// reachable through the public heif_image_handle_decode_image_tile()) skipped that +// validation. The fix runs check_hard_limits() on the tile path too, and also +// teaches unc_decoder_factory_block_pixel_interleave::can_decode() to refuse a +// layout whose components overflow the block, so the decoder is safe regardless of +// which path selected it. +// +// The file is structurally valid and must open; requesting a tile must now return +// a structured heif_error_Invalid_input instead of triggering the undefined shift. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 2; +constexpr uint32_t HEIGHT = 2; +constexpr uint32_t TILE_COLS = 1; // single tile: the decoder reads the whole +constexpr uint32_t TILE_ROWS = 1; // item extent in one go (idat ranged reads + // only support a single full-extent read). +constexpr uint32_t COMPONENTS = 5; // 5 x 16 bit = 80 bits > 64-bit block +constexpr uint32_t PIXEL_SIZE = 8; // block is 8 bytes = 64 bits + +// Exactly one tile worth of raw pixel data (bytes_per_row = WIDTH * PIXEL_SIZE, +// times HEIGHT rows). The single tile spans the whole item, so before the fix +// the decoder reaches the block-unpacking loop instead of failing a short read. +std::vector make_pixel_data() { + return std::vector(WIDTH * PIXEL_SIZE * HEIGHT, 0x00); +} + +// Build a minimal HEIF file with a single 'unci' item using uncompressed +// block-pixel interleave whose components overflow the block. +std::vector build_heif_unci_block_pixel_overpacked() { + const std::vector pixel_data = make_pixel_data(); + + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'unci'. + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: COMPONENTS monochrome components (type 0). + std::vector cmpd_payload; + put_u32_be(cmpd_payload, COMPONENTS); + for (uint32_t i = 0; i < COMPONENTS; i++) { + put_u16_be(cmpd_payload, 0); + } + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): pixel interleave, 16-bit components, block_size == pixel_size == 8. + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, COMPONENTS); // component_count + for (uint32_t i = 0; i < COMPONENTS; i++) { + put_u16_be(uncC_payload, static_cast(i)); // component_index + uncC_payload.push_back(15); // component_bit_depth_minus_one -> 16 bit + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + } + uncC_payload.push_back(0); // sampling_type (no subsampling) + uncC_payload.push_back(1); // interleave_type (pixel) + uncC_payload.push_back(PIXEL_SIZE); // block_size == pixel_size + uncC_payload.push_back(0); // flags + put_u32_be(uncC_payload, PIXEL_SIZE); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, TILE_COLS - 1); + put_u32_be(uncC_payload, TILE_ROWS - 1); + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(3); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat: the raw (uncompressed) pixel data. + auto idat = make_box("idat", pixel_data); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(pixel_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci block-pixel overpacked layout is rejected on the tile path") { + std::vector file = build_heif_unci_block_pixel_overpacked(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // The file is structurally valid, so opening it must succeed. The bug is only + // reachable by then requesting a tile decode. + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image_tiling tiling; + heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(tiling.num_columns == TILE_COLS); + REQUIRE(tiling.num_rows == TILE_ROWS); + + // Request tile (0,0). Before the fix this reached + // unc_decoder_block_pixel_interleave::decode_tile() and executed an undefined + // `block_val >> shift` with shift >= 64 (UBSan abort, else corrupted output). + // It must now be rejected with a structured invalid-input error from + // check_hard_limits() on the tile path. + heif_image* img = nullptr; + err = heif_image_handle_decode_image_tile(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + nullptr, + 0, 0); + INFO("decode error code: " << err.code); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(img == nullptr); + + if (img) { + heif_image_release(img); + } + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_box.cc libheif-1.23.4/tests/uncompressed_box.cc --- libheif-1.19.8/tests/uncompressed_box.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_box.cc 2026-09-06 14:45:17.000000000 +0000 @@ -4,6 +4,7 @@ MIT License Copyright (c) 2023 Brad Hards + Copyright (c) 2026 Dirk Farin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -325,8 +326,8 @@ std::shared_ptr box; Error error = Box::read(range, &box, heif_get_global_security_limits()); REQUIRE(range.error() == 0); - REQUIRE(error.error_code == 6); - REQUIRE(error.sub_error_code == 1000); + REQUIRE(error.error_code == heif_error_Unsupported_feature); + REQUIRE(error.sub_error_code == heif_suberror_Invalid_parameter_value); } TEST_CASE("uncC_parse_excess_tile_rows") { @@ -347,8 +348,8 @@ std::shared_ptr box; Error error = Box::read(range, &box, heif_get_global_security_limits()); REQUIRE(range.error() == 0); - REQUIRE(error.error_code == 6); - REQUIRE(error.sub_error_code == 1000); + REQUIRE(error.error_code == heif_error_Unsupported_feature); + REQUIRE(error.sub_error_code == heif_suberror_Invalid_parameter_value); } TEST_CASE("cmpC_defl") { @@ -644,3 +645,277 @@ REQUIRE(error.sub_error_code == heif_suberror_Unsupported_data_version); REQUIRE(error.message == std::string("icef box data version 1 is not implemented yet")); } + + +TEST_CASE("cloc") +{ + // Construct and set field + auto cloc = std::make_shared(); + cloc->set_chroma_location(2); + REQUIRE(cloc->get_chroma_location() == 2); + + // Write + StreamWriter writer; + Error err = cloc->write(writer); + REQUIRE(err.error_code == heif_error_Ok); + const std::vector bytes = writer.get_data(); + + // FullBox header (12 bytes) + 1 byte payload = 13 bytes + std::vector expected = { + 0x00, 0x00, 0x00, 0x0D, 'c', 'l', 'o', 'c', + 0x00, 0x00, 0x00, 0x00, + 0x02 + }; + REQUIRE(bytes == expected); + + // Parse back + auto reader = std::make_shared(bytes.data(), bytes.size(), false); + BitstreamRange range(reader, bytes.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + REQUIRE(range.error() == 0); + + REQUIRE(box->get_short_type() == fourcc("cloc")); + auto parsed = std::dynamic_pointer_cast(box); + REQUIRE(parsed != nullptr); + REQUIRE(parsed->get_chroma_location() == 2); + + // Dump + Indent indent; + std::string dump_output = parsed->dump(indent); + REQUIRE(dump_output == "Box: cloc -----\nsize: 13 (header size: 12)\nversion: 0\nflags: 0\nchroma_location: 2 (h=0, v=0)\n"); +} + + +TEST_CASE("cloc_bad_version") +{ + std::vector byteArray = { + 0x00, 0x00, 0x00, 0x0D, 'c', 'l', 'o', 'c', + 0x01, 0x00, 0x00, 0x00, + 0x02 + }; + + auto reader = std::make_shared(byteArray.data(), byteArray.size(), false); + BitstreamRange range(reader, byteArray.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error.error_code == heif_error_Unsupported_feature); + REQUIRE(error.sub_error_code == heif_suberror_Unsupported_data_version); + REQUIRE(error.message == std::string("cloc box data version 1 is not implemented yet")); +} + + +TEST_CASE("cloc_out_of_range") +{ + std::vector byteArray = { + 0x00, 0x00, 0x00, 0x0D, 'c', 'l', 'o', 'c', + 0x00, 0x00, 0x00, 0x00, + 0x07 + }; + + auto reader = std::make_shared(byteArray.data(), byteArray.size(), false); + BitstreamRange range(reader, byteArray.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error.error_code == heif_error_Invalid_input); + REQUIRE(error.sub_error_code == heif_suberror_Invalid_parameter_value); +} + + +TEST_CASE("splz") +{ + // Construct: 2 component indices, 2x1 pattern, angles 45.0 and 90.0 + auto splz = std::make_shared(); + PolarizationPattern pattern; + pattern.component_ids = {0, 1}; + pattern.pattern_width = 2; + pattern.pattern_height = 1; + pattern.polarization_angles = {45.0f, 90.0f}; + splz->set_pattern(pattern); + + // Write + StreamWriter writer; + Error err = splz->write(writer); + REQUIRE(err.error_code == heif_error_Ok); + const std::vector bytes = writer.get_data(); + + // FullBox header (12) + 4 (count) + 8 (2×index) + 4 (w+h) + 8 (2×float) = 36 + std::vector expected = { + 0x00, 0x00, 0x00, 0x24, 's', 'p', 'l', 'z', + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, // component_count = 2 + 0x00, 0x00, 0x00, 0x00, // index[0] = 0 + 0x00, 0x00, 0x00, 0x01, // index[1] = 1 + 0x00, 0x02, // pattern_width = 2 + 0x00, 0x01, // pattern_height = 1 + 0x42, 0x34, 0x00, 0x00, // 45.0f + 0x42, 0xB4, 0x00, 0x00 // 90.0f + }; + REQUIRE(bytes == expected); + + // Parse back + auto reader = std::make_shared(bytes.data(), bytes.size(), false); + BitstreamRange range(reader, bytes.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + REQUIRE(range.error() == 0); + + REQUIRE(box->get_short_type() == fourcc("splz")); + auto parsed = std::dynamic_pointer_cast(box); + REQUIRE(parsed != nullptr); + + const auto& p = parsed->get_pattern(); + REQUIRE(p.component_ids.size() == 2); + REQUIRE(p.component_ids[0] == 0); + REQUIRE(p.component_ids[1] == 1); + REQUIRE(p.pattern_width == 2); + REQUIRE(p.pattern_height == 1); + REQUIRE(p.polarization_angles.size() == 2); + REQUIRE(p.polarization_angles[0] == 45.0f); + REQUIRE(p.polarization_angles[1] == 90.0f); + + // Dump + Indent indent; + std::string dump_output = parsed->dump(indent); + REQUIRE(dump_output == "Box: splz -----\n" + "size: 36 (header size: 12)\n" + "version: 0\n" + "flags: 0\n" + "component_count: 2\n" + " component_index[0]: 0\n" + " component_index[1]: 1\n" + "pattern_width: 2\n" + "pattern_height: 1\n" + " [0,0]: 45 degrees\n" + " [1,0]: 90 degrees\n"); +} + + +TEST_CASE("splz_bad_version") +{ + std::vector byteArray = { + 0x00, 0x00, 0x00, 0x24, 's', 'p', 'l', 'z', + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x02, + 0x00, 0x01, + 0x42, 0x34, 0x00, 0x00, + 0x42, 0xB4, 0x00, 0x00 + }; + + auto reader = std::make_shared(byteArray.data(), byteArray.size(), false); + BitstreamRange range(reader, byteArray.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error.error_code == heif_error_Unsupported_feature); + REQUIRE(error.sub_error_code == heif_suberror_Unsupported_data_version); + REQUIRE(error.message == std::string("splz box data version 1 is not implemented yet")); +} + + +TEST_CASE("snuc") +{ + // Construct: 1 component index, nuc_is_applied=true, 2x1 image, 2 gains + 2 offsets + auto snuc = std::make_shared(); + SensorNonUniformityCorrection nuc; + nuc.component_ids = {0}; + nuc.nuc_is_applied = true; + nuc.image_width = 2; + nuc.image_height = 1; + nuc.nuc_gains = {1.0f, 2.0f}; + nuc.nuc_offsets = {0.0f, 3.0f}; + snuc->set_nuc(nuc); + + // Write + StreamWriter writer; + Error err = snuc->write(writer); + REQUIRE(err.error_code == heif_error_Ok); + const std::vector bytes = writer.get_data(); + + // FullBox header (12) + 4 (count) + 4 (index) + 1 (flags) + 4 (width) + 4 (height) + // + 8 (2×gain) + 8 (2×offset) = 45 + std::vector expected = { + 0x00, 0x00, 0x00, 0x2D, 's', 'n', 'u', 'c', + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, // component_count = 1 + 0x00, 0x00, 0x00, 0x00, // index[0] = 0 + 0x80, // flags: nuc_is_applied = true + 0x00, 0x00, 0x00, 0x02, // image_width = 2 + 0x00, 0x00, 0x00, 0x01, // image_height = 1 + 0x3F, 0x80, 0x00, 0x00, // gain[0] = 1.0f + 0x40, 0x00, 0x00, 0x00, // gain[1] = 2.0f + 0x00, 0x00, 0x00, 0x00, // offset[0] = 0.0f + 0x40, 0x40, 0x00, 0x00 // offset[1] = 3.0f + }; + REQUIRE(bytes == expected); + + // Parse back + auto reader = std::make_shared(bytes.data(), bytes.size(), false); + BitstreamRange range(reader, bytes.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error == Error::Ok); + REQUIRE(range.error() == 0); + + REQUIRE(box->get_short_type() == fourcc("snuc")); + auto parsed = std::dynamic_pointer_cast(box); + REQUIRE(parsed != nullptr); + + const auto& n = parsed->get_nuc(); + REQUIRE(n.component_ids.size() == 1); + REQUIRE(n.component_ids[0] == 0); + REQUIRE(n.nuc_is_applied == true); + REQUIRE(n.image_width == 2); + REQUIRE(n.image_height == 1); + REQUIRE(n.nuc_gains.size() == 2); + REQUIRE(n.nuc_gains[0] == 1.0f); + REQUIRE(n.nuc_gains[1] == 2.0f); + REQUIRE(n.nuc_offsets.size() == 2); + REQUIRE(n.nuc_offsets[0] == 0.0f); + REQUIRE(n.nuc_offsets[1] == 3.0f); + + // Dump + Indent indent; + std::string dump_output = parsed->dump(indent); + REQUIRE(dump_output == "Box: snuc -----\n" + "size: 45 (header size: 12)\n" + "version: 0\n" + "flags: 0\n" + "component_count: 1\n" + " component_index[0]: 0\n" + "nuc_is_applied: 1\n" + "image_width: 2\n" + "image_height: 1\n" + "nuc_gains: 2 values\n" + "nuc_offsets: 2 values\n"); +} + + +TEST_CASE("snuc_bad_version") +{ + std::vector byteArray = { + 0x00, 0x00, 0x00, 0x2D, 's', 'n', 'u', 'c', + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, + 0x80, + 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x01, + 0x3F, 0x80, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x40, 0x00, 0x00 + }; + + auto reader = std::make_shared(byteArray.data(), byteArray.size(), false); + BitstreamRange range(reader, byteArray.size()); + std::shared_ptr box; + Error error = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(error.error_code == heif_error_Unsupported_feature); + REQUIRE(error.sub_error_code == heif_suberror_Unsupported_data_version); + REQUIRE(error.message == std::string("snuc box data version 1 is not implemented yet")); +} diff -Nru libheif-1.19.8/tests/uncompressed_decode.cc libheif-1.23.4/tests/uncompressed_decode.cc --- libheif-1.19.8/tests/uncompressed_decode.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" @@ -33,7 +33,7 @@ #include "uncompressed_decode.h" -void check_image_handle_size(struct heif_context *&context) { +void check_image_handle_size(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int ispe_width = heif_image_handle_get_ispe_width(handle); REQUIRE(ispe_width == 30); @@ -55,7 +55,7 @@ heif_context_free(context); } -void check_image_handle_size_subsampled(struct heif_context *&context) { +void check_image_handle_size_subsampled(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int ispe_width = heif_image_handle_get_ispe_width(handle); REQUIRE(ispe_width == 32); @@ -91,7 +91,7 @@ heif_context_free(context); } -void check_image_handle_no_depth_images(struct heif_context *&context) { +void check_image_handle_no_depth_images(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int has_depth = heif_image_handle_has_depth_image(handle); @@ -111,7 +111,7 @@ heif_context_free(context); } -void check_image_handle_no_thumbnails(struct heif_context *&context) { +void check_image_handle_no_thumbnails(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int numthumbs = heif_image_handle_get_number_of_thumbnails(handle); @@ -128,7 +128,7 @@ heif_context_free(context); } -void check_image_handle_no_aux_images(struct heif_context *&context) { +void check_image_handle_no_aux_images(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int num_aux = heif_image_handle_get_number_of_auxiliary_images(handle, 0); @@ -145,7 +145,7 @@ heif_context_free(context); } -void check_image_handle_no_metadata(struct heif_context *&context) { +void check_image_handle_no_metadata(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); int num_metadata_blocks = @@ -163,6 +163,63 @@ heif_context_free(context); } +// Regression test for commit 16252dab "unci: allocate tiles with correct size": +// decoding a single tile of an uncompressed image must use per-tile plane +// sizes rather than the full-image plane sizes carried in the source +// component descriptions. +void check_decode_individual_tiles(heif_context*& context, heif_colorspace colorspace, heif_chroma chroma) { + heif_image_handle* handle = get_primary_image_handle(context); + + heif_image_tiling tiling{}; + heif_error err = heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(tiling.num_columns >= 1); + REQUIRE(tiling.num_rows >= 1); + + for (uint32_t ty = 0; ty < tiling.num_rows; ty++) { + for (uint32_t tx = 0; tx < tiling.num_columns; tx++) { + INFO("tile (" << tx << "," << ty << ")"); + heif_image* tile = nullptr; + err = heif_image_handle_decode_image_tile(handle, &tile, colorspace, chroma, nullptr, tx, ty); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(tile != nullptr); + + int w = heif_image_get_primary_width(tile); + int h = heif_image_get_primary_height(tile); + REQUIRE(w == (int) tiling.tile_width); + REQUIRE(h == (int) tiling.tile_height); + + heif_image_release(tile); + } + } + + heif_image_handle_release(handle); +} + +TEST_CASE("decode individual tiles RGB") { + auto file = GENERATE(FILES); + auto context = get_context_for_test_file(file); + INFO("file name: " << file); + check_decode_individual_tiles(context, heif_colorspace_RGB, heif_chroma_444); + heif_context_free(context); +} + +TEST_CASE("decode individual tiles mono") { + auto file = GENERATE(MONO_FILES); + auto context = get_context_for_test_file(file); + INFO("file name: " << file); + check_decode_individual_tiles(context, heif_colorspace_monochrome, heif_chroma_monochrome); + heif_context_free(context); +} + +TEST_CASE("decode individual tiles YUV") { + auto file = GENERATE(YUV_FILES); + auto context = get_context_for_test_file(file); + INFO("file name: " << file); + check_decode_individual_tiles(context, heif_colorspace_YCbCr, heif_chroma_444); + heif_context_free(context); +} + TEST_CASE("check uncompressed is advertised") { REQUIRE(heif_have_decoder_for_format(heif_compression_uncompressed)); } diff -Nru libheif-1.19.8/tests/uncompressed_decode_generic_compression.cc libheif-1.23.4/tests/uncompressed_decode_generic_compression.cc --- libheif-1.19.8/tests/uncompressed_decode_generic_compression.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_generic_compression.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" diff -Nru libheif-1.19.8/tests/uncompressed_decode_mono.cc libheif-1.23.4/tests/uncompressed_decode_mono.cc --- libheif-1.19.8/tests/uncompressed_decode_mono.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_mono.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" diff -Nru libheif-1.19.8/tests/uncompressed_decode_rgb.cc libheif-1.23.4/tests/uncompressed_decode_rgb.cc --- libheif-1.19.8/tests/uncompressed_decode_rgb.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_rgb.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" @@ -33,7 +33,7 @@ #include "uncompressed_decode.h" -void check_image_size(struct heif_context *&context, int expect_alpha) { +void check_image_size(heif_context *&context, int expect_alpha) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image(handle); @@ -96,7 +96,7 @@ } -void check_image_content(struct heif_context *&context) { +void check_image_content(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image(handle); diff -Nru libheif-1.19.8/tests/uncompressed_decode_rgb16.cc libheif-1.23.4/tests/uncompressed_decode_rgb16.cc --- libheif-1.19.8/tests/uncompressed_decode_rgb16.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_rgb16.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,7 +26,7 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" @@ -34,7 +34,7 @@ #include "uncompressed_decode.h" -void check_image_size_rgb16(struct heif_context *&context, int expect_alpha) { +void check_image_size_rgb16(heif_context *&context, int expect_alpha) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image(handle); @@ -96,7 +96,7 @@ heif_context_free(context); } -void check_image_content_rgb16(struct heif_context *&context) { +void check_image_content_rgb16(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image(handle); diff -Nru libheif-1.19.8/tests/uncompressed_decode_rgb565.cc libheif-1.23.4/tests/uncompressed_decode_rgb565.cc --- libheif-1.19.8/tests/uncompressed_decode_rgb565.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_rgb565.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,7 +26,7 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" diff -Nru libheif-1.19.8/tests/uncompressed_decode_rgb7.cc libheif-1.23.4/tests/uncompressed_decode_rgb7.cc --- libheif-1.19.8/tests/uncompressed_decode_rgb7.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_rgb7.cc 2026-09-06 14:45:17.000000000 +0000 @@ -26,7 +26,7 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" diff -Nru libheif-1.19.8/tests/uncompressed_decode_ycbcr.cc libheif-1.23.4/tests/uncompressed_decode_ycbcr.cc --- libheif-1.19.8/tests/uncompressed_decode_ycbcr.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_ycbcr.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,7 +25,7 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include #include #include "test_utils.h" diff -Nru libheif-1.19.8/tests/uncompressed_decode_ycbcr420.cc libheif-1.23.4/tests/uncompressed_decode_ycbcr420.cc --- libheif-1.19.8/tests/uncompressed_decode_ycbcr420.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_ycbcr420.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,8 +25,9 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include +#include #include #include "test_utils.h" #include @@ -34,7 +35,7 @@ #include "uncompressed_decode.h" -void check_image_size_ycbcr_420(struct heif_context *&context) { +void check_image_size_ycbcr_420(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image_ycbcr(handle, heif_chroma_420); @@ -89,7 +90,7 @@ } -void check_image_size_ycbcr_420_16bit(struct heif_context *&context) { +void check_image_size_ycbcr_420_16bit(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image_ycbcr(handle, heif_chroma_420); @@ -143,7 +144,7 @@ heif_context_free(context); } -void check_image_content_ycbcr420(struct heif_context *&context) { +void check_image_content_ycbcr420(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image_ycbcr(handle, heif_chroma_420); @@ -453,7 +454,7 @@ heif_context_free(context); } -void check_image_content_ycbcr420_16bit(struct heif_context *&context) { +void check_image_content_ycbcr420_16bit(heif_context *&context) { heif_image_handle *handle = get_primary_image_handle(context); heif_image *img = get_primary_image_ycbcr(handle, heif_chroma_420); @@ -687,4 +688,60 @@ INFO("file name: " << file); check_image_content_ycbcr420_16bit(context); heif_context_free(context); -} \ No newline at end of file +} + +// Regression test for https://github.com/strukturag/libheif/issues/1881: +// YCbCr images with more than 14 bits per pixel could not be converted to RGB. +void check_image_content_ycbcr420_16bit_rgb(struct heif_context *&context) { + heif_image_handle *handle = get_primary_image_handle(context); + heif_image *img = get_primary_image(handle); + + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_R) == 16); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_G) == 16); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_B) == 16); + + // The image consists of uniformly colored 4x4 blocks. Block (bx, by) has color + // (bx + by) % 10 of this table (values from BT.601 full-range conversion of the + // 16-bit YCbCr samples in the file, allowing +-2 for float rounding differences). + static const int expected[10][3] = { + {0xFFA1, 0x0035, 0x0000}, // red + {0x001D, 0x806D, 0x000C}, // green (50%) + {0x0000, 0x0016, 0xFF88}, // blue + {0xFFE1, 0xFFFF, 0xFF87}, // white + {0x0000, 0x0001, 0x0000}, // black + {0xFFE0, 0xFFFF, 0x0000}, // yellow + {0x003E, 0xFFF0, 0xFFA8}, // cyan + {0x8071, 0x8094, 0x8044}, // gray (50%) + {0xFFCA, 0xA5C1, 0x0000}, // orange + {0xEEB8, 0x82B5, 0xEE73}, // pink + }; + + int stride_r, stride_g, stride_b; + const uint16_t *plane_r = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_R, &stride_r); + const uint16_t *plane_g = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_G, &stride_g); + const uint16_t *plane_b = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_B, &stride_b); + stride_r /= 2; + stride_g /= 2; + stride_b /= 2; + + for (int y = 0; y < 20; y++) { + for (int x = 0; x < 32; x++) { + INFO("pixel: " << x << "," << y); + const int *e = expected[(x / 4 + y / 4) % 10]; + REQUIRE(std::abs(plane_r[stride_r * y + x] - e[0]) <= 2); + REQUIRE(std::abs(plane_g[stride_g * y + x] - e[1]) <= 2); + REQUIRE(std::abs(plane_b[stride_b * y + x] - e[2]) <= 2); + } + } + + heif_image_release(img); + heif_image_handle_release(handle); +} + +TEST_CASE("check RGB conversion YCbCr 4:2:0 16 bit") { + auto file = GENERATE(YUV_16BIT_420_FILES); + auto context = get_context_for_test_file(file); + INFO("file name: " << file); + check_image_content_ycbcr420_16bit_rgb(context); + heif_context_free(context); +} diff -Nru libheif-1.19.8/tests/uncompressed_decode_ycbcr422.cc libheif-1.23.4/tests/uncompressed_decode_ycbcr422.cc --- libheif-1.19.8/tests/uncompressed_decode_ycbcr422.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_decode_ycbcr422.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,8 +25,9 @@ */ #include "catch_amalgamated.hpp" #include "libheif/heif.h" -#include "libheif/api_structs.h" +#include "api_structs.h" #include +#include #include #include "test_utils.h" #include @@ -685,3 +686,60 @@ check_image_content_ycbcr422_16bit(context); heif_context_free(context); } + + +// Regression test for https://github.com/strukturag/libheif/issues/1881: +// YCbCr images with more than 14 bits per pixel could not be converted to RGB. +void check_image_content_ycbcr422_16bit_rgb(struct heif_context *&context) { + heif_image_handle *handle = get_primary_image_handle(context); + heif_image *img = get_primary_image(handle); + + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_R) == 16); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_G) == 16); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_B) == 16); + + // The image consists of uniformly colored 4x4 blocks. Block (bx, by) has color + // (bx + by) % 10 of this table (values from BT.601 full-range conversion of the + // 16-bit YCbCr samples in the file, allowing +-2 for float rounding differences). + static const int expected[10][3] = { + {0xFFA1, 0x0035, 0x0000}, // red + {0x001D, 0x806D, 0x000C}, // green (50%) + {0x0000, 0x0016, 0xFF88}, // blue + {0xFFE1, 0xFFFF, 0xFF87}, // white + {0x0000, 0x0001, 0x0000}, // black + {0xFFE0, 0xFFFF, 0x0000}, // yellow + {0x003E, 0xFFF0, 0xFFA8}, // cyan + {0x8071, 0x8094, 0x8044}, // gray (50%) + {0xFFCA, 0xA5C1, 0x0000}, // orange + {0xEEB8, 0x82B5, 0xEE73}, // pink + }; + + int stride_r, stride_g, stride_b; + const uint16_t *plane_r = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_R, &stride_r); + const uint16_t *plane_g = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_G, &stride_g); + const uint16_t *plane_b = (const uint16_t *) heif_image_get_plane_readonly(img, heif_channel_B, &stride_b); + stride_r /= 2; + stride_g /= 2; + stride_b /= 2; + + for (int y = 0; y < 20; y++) { + for (int x = 0; x < 32; x++) { + INFO("pixel: " << x << "," << y); + const int *e = expected[(x / 4 + y / 4) % 10]; + REQUIRE(std::abs(plane_r[stride_r * y + x] - e[0]) <= 2); + REQUIRE(std::abs(plane_g[stride_g * y + x] - e[1]) <= 2); + REQUIRE(std::abs(plane_b[stride_b * y + x] - e[2]) <= 2); + } + } + + heif_image_release(img); + heif_image_handle_release(handle); +} + +TEST_CASE("check RGB conversion YCbCr 4:2:2 16 bit") { + auto file = GENERATE(YUV_16BIT_422_FILES); + auto context = get_context_for_test_file(file); + INFO("file name: " << file); + check_image_content_ycbcr422_16bit_rgb(context); + heif_context_free(context); +} diff -Nru libheif-1.19.8/tests/uncompressed_encode.cc libheif-1.23.4/tests/uncompressed_encode.cc --- libheif-1.19.8/tests/uncompressed_encode.cc 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_encode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -25,15 +25,17 @@ */ #include "catch_amalgamated.hpp" -#include "libheif/api_structs.h" +#include "api_structs.h" #include "libheif/heif.h" +#include "libheif/heif_uncompressed.h" +#include "libheif/heif_tiling.h" #include #include #include "test_utils.h" TEST_CASE("check have uncompressed") { - struct heif_error err; + heif_error err; heif_context *ctx = heif_context_alloc(); heif_encoder *enc; err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, @@ -49,10 +51,10 @@ } -struct heif_image *createImage_Mono() +heif_image *createImage_Mono() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_monochrome, @@ -101,10 +103,10 @@ } -struct heif_image *createImage_YCbCr() +heif_image *createImage_YCbCr() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_YCbCr, @@ -171,10 +173,10 @@ return image; } -struct heif_image* createImage_Mono_plus_alpha() +heif_image* createImage_Mono_plus_alpha() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_monochrome, @@ -230,10 +232,10 @@ return image; } -struct heif_image *createImage_RGB_interleaved() +heif_image *createImage_RGB_interleaved() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_RGB, @@ -339,10 +341,10 @@ } } -struct heif_image *createImage_RRGGBB_interleaved(heif_chroma chroma, int bit_depth, bool little_endian, bool with_alpha) +heif_image *createImage_RRGGBB_interleaved(heif_chroma chroma, int bit_depth, bool little_endian, bool with_alpha) { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_RGB, chroma, &image); @@ -417,10 +419,10 @@ return image; } -struct heif_image *createImage_RGBA_interleaved() +heif_image *createImage_RGBA_interleaved() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_RGB, @@ -486,10 +488,10 @@ return image; } -struct heif_image *createImage_RGBA_planar() +heif_image *createImage_RGBA_planar() { - struct heif_image *image; - struct heif_error err; + heif_image *image; + heif_error err; int w = 1024; int h = 768; err = heif_image_create(w, h, heif_colorspace_RGB, @@ -571,11 +573,11 @@ heif_context *ctx = heif_context_alloc(); heif_encoder *encoder; - struct heif_error err; + heif_error err; err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); REQUIRE(err.code == heif_error_Ok); - struct heif_encoding_options *options; + heif_encoding_options *options; options = heif_encoding_options_alloc(); options->macOS_compatibility_workaround = false; options->macOS_compatibility_workaround_no_nclx_profile = true; @@ -591,7 +593,7 @@ if (check_decode) { // read file back in - struct heif_context* decode_context; + heif_context* decode_context; decode_context = heif_context_alloc(); err = heif_context_read_from_file(decode_context, filename, NULL); REQUIRE(err.code == heif_error_Ok); @@ -709,6 +711,73 @@ } +// Boundary cases for the RRGGBB block-pixel encoder. 9 bit is the lowest valid +// depth (m_bytes_per_pixel becomes 4); 13 bit is the highest the block encoder +// accepts (m_bytes_per_pixel == 5). Both were adjacent to the out-of-bounds write +// that occurred for the (now rejected) <= 8 bit case (GHSA-46rp-pcq2-rpmr). +TEST_CASE("Encode RRRGGBB_LE 9 bit") +{ + heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 9, true, false); + do_encode(input_image, "encode_rrggbb_9_le.heif", false); +} + + +TEST_CASE("Encode RRRGGBB_BE 9 bit") +{ + heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 9, false, false); + do_encode(input_image, "encode_rrggbb_9_be.heif", false); +} + + +TEST_CASE("Encode RRRGGBB_LE 13 bit") +{ + heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_LE, 13, true, false); + do_encode(input_image, "encode_rrggbb_13_le.heif", false); +} + + +TEST_CASE("Encode RRRGGBB_BE 13 bit") +{ + heif_image *input_image = createImage_RRGGBB_interleaved(heif_chroma_interleaved_RRGGBB_BE, 13, false, false); + do_encode(input_image, "encode_rrggbb_13_be.heif", false); +} + + +// Root-cause check: a 16-bit interleaved channel (RRGGBB / RRGGBBAA) with a bit +// depth of <= 8 is self-inconsistent and must be rejected at image construction, +// before any encoder can read/write past the under-sized plane. +TEST_CASE("Reject <=8 bit 16-bit-interleaved channels") +{ + const heif_chroma formats[] = { + heif_chroma_interleaved_RRGGBB_LE, + heif_chroma_interleaved_RRGGBB_BE, + heif_chroma_interleaved_RRGGBBAA_LE, + heif_chroma_interleaved_RRGGBBAA_BE + }; + + for (heif_chroma chroma : formats) { + for (int bit_depth = 1; bit_depth <= 8; bit_depth++) { + heif_image* image; + heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, bit_depth); + REQUIRE(err.code != heif_error_Ok); + + heif_image_release(image); + } + + // The lowest valid depth (9 bit) is still accepted. + heif_image* image; + heif_error err = heif_image_create(64, 64, heif_colorspace_RGB, chroma, &image); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_interleaved, 64, 64, 9); + REQUIRE(err.code == heif_error_Ok); + heif_image_release(image); + } +} + + TEST_CASE("Encode RGBA") { heif_image *input_image = createImage_RGBA_interleaved(); @@ -776,3 +845,464 @@ heif_image *input_image = createImage_RGBA_planar(); do_encode(input_image, "encode_rgba_planar.heif", true); } + + +// Regression test for the heap OOB write reported as GHSA-5x55-x5pf-9c6g +// (https://github.com/strukturag/libheif/security/advisories/GHSA-5x55-x5pf-9c6g). +// Chroma destination offsets in unc_decoder_component_interleave and +// unc_decoder_mixed_interleave used the full-resolution tile origin against +// the subsampled chroma plane stride, so the second tile row in a 4:2:0 tiled +// unci image was written past the end of the chroma plane. The fix scales the +// tile origin into the chroma grid; this test encodes a 2x2-tiled YCbCr 4:2:0 +// unci with a distinctive per-tile chroma pattern and verifies the round-trip +// places each tile correctly. +static heif_image *createImage_YCbCr_420_tiled_pattern(int w, int h) +{ + heif_image *image; + heif_error err; + err = heif_image_create(w, h, heif_colorspace_YCbCr, heif_chroma_420, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_Y, w, h, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_Cb, w / 2, h / 2, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_Cr, w / 2, h / 2, 8); + REQUIRE(err.code == heif_error_Ok); + + int y_stride, cb_stride, cr_stride; + uint8_t *Y = heif_image_get_plane(image, heif_channel_Y, &y_stride); + uint8_t *Cb = heif_image_get_plane(image, heif_channel_Cb, &cb_stride); + uint8_t *Cr = heif_image_get_plane(image, heif_channel_Cr, &cr_stride); + + // Each tile (in a 2x2 tile grid) gets a unique chroma constant so that + // misplaced chroma tiles are detected. + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + Y[row * y_stride + col] = static_cast((row * 3 + col) & 0xFF); + } + } + int cw = w / 2; + int ch = h / 2; + for (int row = 0; row < ch; row++) { + int tile_row = (row < ch / 2) ? 0 : 1; + for (int col = 0; col < cw; col++) { + int tile_col = (col < cw / 2) ? 0 : 1; + // Distinct per-tile chroma constants. + Cb[row * cb_stride + col] = static_cast(0x10 + tile_row * 2 + tile_col); + Cr[row * cr_stride + col] = static_cast(0xA0 + tile_row * 2 + tile_col); + } + } + + return image; +} + +TEST_CASE("Encode tiled YCbCr 4:2:0 unci - chroma placement round-trip") +{ + const int W = 32; + const int H = 32; + const int TW = 16; + const int TH = 16; + + heif_unci_image_parameters params{}; + params.version = 1; + params.image_width = W; + params.image_height = H; + params.tile_width = TW; + params.tile_height = TH; + params.compression = heif_unci_compression_off; + + heif_image *input_image = createImage_YCbCr_420_tiled_pattern(W, H); + REQUIRE(input_image != nullptr); + + heif_context *ctx = heif_context_alloc(); + + heif_encoder *encoder; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_encoding_options *options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle *tiled_image; + err = heif_context_add_empty_unci_image(ctx, ¶ms, options, input_image, &tiled_image); + REQUIRE(err.code == heif_error_Ok); + + // Add each 16x16 tile carrying the corresponding sub-region of the input. + for (uint32_t ty = 0; ty < 2; ty++) { + for (uint32_t tx = 0; tx < 2; tx++) { + heif_image *tile; + err = heif_image_create(TW, TH, heif_colorspace_YCbCr, heif_chroma_420, &tile); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_Cb, TW / 2, TH / 2, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_Cr, TW / 2, TH / 2, 8); + REQUIRE(err.code == heif_error_Ok); + + int y_stride, cb_stride, cr_stride; + uint8_t *tY = heif_image_get_plane(tile, heif_channel_Y, &y_stride); + uint8_t *tCb = heif_image_get_plane(tile, heif_channel_Cb, &cb_stride); + uint8_t *tCr = heif_image_get_plane(tile, heif_channel_Cr, &cr_stride); + + int src_y_stride, src_cb_stride, src_cr_stride; + const uint8_t *sY = heif_image_get_plane(input_image, heif_channel_Y, &src_y_stride); + const uint8_t *sCb = heif_image_get_plane(input_image, heif_channel_Cb, &src_cb_stride); + const uint8_t *sCr = heif_image_get_plane(input_image, heif_channel_Cr, &src_cr_stride); + + for (int row = 0; row < TH; row++) { + memcpy(tY + row * y_stride, + sY + (ty * TH + row) * src_y_stride + tx * TW, + TW); + } + for (int row = 0; row < TH / 2; row++) { + memcpy(tCb + row * cb_stride, + sCb + (ty * (TH / 2) + row) * src_cb_stride + tx * (TW / 2), + TW / 2); + memcpy(tCr + row * cr_stride, + sCr + (ty * (TH / 2) + row) * src_cr_stride + tx * (TW / 2), + TW / 2); + } + + err = heif_context_add_image_tile(ctx, tiled_image, tx, ty, tile, encoder); + REQUIRE(err.code == heif_error_Ok); + heif_image_release(tile); + } + } + + err = heif_context_write_to_file(ctx, "encode_ycbcr420_tiled.heif"); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(tiled_image); + + // --- decode and check chroma placement (the bug placed second-tile-row + // chroma far past the end of the chroma plane). + + heif_context *decode_context = heif_context_alloc(); + err = heif_context_read_from_file(decode_context, "encode_ycbcr420_tiled.heif", NULL); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle *decode_handle = get_primary_image_handle(decode_context); + heif_image *decoded; + err = heif_decode_image(decode_handle, &decoded, heif_colorspace_YCbCr, heif_chroma_420, NULL); + REQUIRE(err.code == heif_error_Ok); + + int stride; + const uint8_t *cb = heif_image_get_plane_readonly(decoded, heif_channel_Cb, &stride); + REQUIRE(cb != nullptr); + for (int row = 0; row < H / 2; row++) { + int expected_tile_row = (row < H / 4) ? 0 : 1; + for (int col = 0; col < W / 2; col++) { + int expected_tile_col = (col < W / 4) ? 0 : 1; + uint8_t expected = static_cast(0x10 + expected_tile_row * 2 + expected_tile_col); + INFO("Cb row=" << row << " col=" << col); + REQUIRE(((int) cb[row * stride + col]) == expected); + } + } + + const uint8_t *cr = heif_image_get_plane_readonly(decoded, heif_channel_Cr, &stride); + REQUIRE(cr != nullptr); + for (int row = 0; row < H / 2; row++) { + int expected_tile_row = (row < H / 4) ? 0 : 1; + for (int col = 0; col < W / 2; col++) { + int expected_tile_col = (col < W / 4) ? 0 : 1; + uint8_t expected = static_cast(0xA0 + expected_tile_row * 2 + expected_tile_col); + INFO("Cr row=" << row << " col=" << col); + REQUIRE(((int) cr[row * stride + col]) == expected); + } + } + + heif_image_release(decoded); + heif_image_handle_release(decode_handle); + heif_context_free(decode_context); + + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_image_release(input_image); + heif_context_free(ctx); +} + + +// Regression test for the heap OOB write reported as GHSA-xpw3-9rhw-482x +// (https://github.com/strukturag/libheif/security/advisories/GHSA-xpw3-9rhw-482x). +// The uncompressed encoder sized its output buffer from the primary image +// dimensions but copied each component plane using that plane's actual size. +// A component plane larger than the primary image (e.g. an alpha plane that +// does not match the color planes, as produced by an image sequence with a +// mismatched alpha auxiliary track) was memcpy'd past the end of the buffer. +// The encoder now rejects such inconsistent images instead of overflowing. +TEST_CASE("Encode rejects oversized component plane") +{ + heif_image *image; + heif_error err = heif_image_create(2, 2, heif_colorspace_monochrome, + heif_chroma_monochrome, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_Y, 2, 2, 8); + REQUIRE(err.code == heif_error_Ok); + + // Alpha plane much larger than the 2x2 primary image. heif_image_add_plane + // does not validate the plane size against the image size, so this builds the + // same inconsistent image that the sequence decoder used to produce. + err = heif_image_add_plane(image, heif_channel_Alpha, 256, 256, 8); + REQUIRE(err.code == heif_error_Ok); + + heif_context *ctx = heif_context_alloc(); + heif_encoder *encoder; + err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle *output_image_handle = nullptr; + err = heif_context_encode_image(ctx, image, encoder, nullptr, &output_image_handle); + // Must fail cleanly rather than overflow the heap. + REQUIRE(err.code != heif_error_Ok); + + if (output_image_handle) { + heif_image_handle_release(output_image_handle); + } + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); +} + + +// Helper for the tile-add tests below: builds a YCbCr image with individually +// chosen chroma plane sizes. +static heif_image* createImage_YCbCr_customPlanes(int w, int h, heif_chroma chroma, + int cb_w, int cb_h, int cr_w, int cr_h) +{ + heif_image *image; + heif_error err = heif_image_create(w, h, heif_colorspace_YCbCr, chroma, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_Y, w, h, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_Cb, cb_w, cb_h, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_Cr, cr_w, cr_h, 8); + REQUIRE(err.code == heif_error_Ok); + + return image; +} + + +// heif_context_add_image_tile() hands the tile image directly to the encoder that was +// built from the prototype image passed to heif_context_add_empty_unci_image(). The +// encoder sizes its output buffer from its own configuration while copying from the +// tile's planes, so a tile that does not match that configuration used to write past +// the end of that buffer. +TEST_CASE("Add tile rejects images that do not match the unci configuration") +{ + const int TW = 16; + const int TH = 16; + + auto add_tile = [](heif_image *prototype, heif_image *tile) -> heif_error { + heif_unci_image_parameters params{}; + params.version = 1; + params.image_width = 2 * TW; + params.image_height = 2 * TH; + params.tile_width = TW; + params.tile_height = TH; + params.compression = heif_unci_compression_off; + + heif_context *ctx = heif_context_alloc(); + + heif_encoder *encoder; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_encoding_options *options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle *tiled_image; + err = heif_context_add_empty_unci_image(ctx, ¶ms, options, prototype, &tiled_image); + REQUIRE(err.code == heif_error_Ok); + + heif_error tile_err = heif_context_add_image_tile(ctx, tiled_image, 0, 0, tile, encoder); + + heif_image_handle_release(tiled_image); + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return tile_err; + }; + + SECTION("component plane larger than the tile") { + heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2); + // Declares the correct tile size, but its Cb plane is far larger than the + // subsampled size the encoder allocates for. + heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, 200, 200, TW / 2, TH / 2); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("chroma format differs from the prototype") { + heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2); + // A well-formed 4:4:4 tile: every plane matches its own image dimensions, so + // checking the tile against itself is not enough. The encoder still assumes + // the 4:2:0 subsampling of the prototype. + heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_444, TW, TH, TW, TH); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("tile has fewer components than the prototype") { + heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_444, TW, TH, TW, TH); + + heif_image *tile; + heif_error err = heif_image_create(TW, TH, heif_colorspace_monochrome, heif_chroma_monochrome, &tile); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("planar tile for an interleaved prototype") { + heif_image *prototype; + heif_error err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &prototype); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(prototype, heif_channel_interleaved, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + + heif_image *tile; + err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_444, &tile); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_R, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_G, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_B, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("interleaved plane smaller than the image") { + heif_image *prototype; + heif_error err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &prototype); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(prototype, heif_channel_interleaved, TW, TH, 8); + REQUIRE(err.code == heif_error_Ok); + + // The interleaved plane is not listed by get_used_planar_component_ids(), so a + // check that only walks the planar components does not see this at all. + heif_image *tile; + err = heif_image_create(TW, TH, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &tile); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(tile, heif_channel_interleaved, 2, 2, 8); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("tile size differs from the tile size of the image") { + heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2); + // Consistent in itself and matching the prototype configuration, but not the + // tile size of the image. It would be written to a wrong offset. + heif_image *tile = createImage_YCbCr_customPlanes(TW / 2, TH / 2, heif_chroma_420, TW / 4, TH / 4, TW / 4, TH / 4); + + REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } + + SECTION("a matching tile is still accepted") { + heif_image *prototype = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2); + heif_image *tile = createImage_YCbCr_customPlanes(TW, TH, heif_chroma_420, TW / 2, TH / 2, TW / 2, TH / 2); + + REQUIRE(add_tile(prototype, tile).code == heif_error_Ok); + + heif_image_release(tile); + heif_image_release(prototype); + } +} + + +// An image can be created without ever adding a pixel plane. The encoders access the planes +// implied by the colorspace and chroma format, so such an image used to make them dereference +// a plane that does not exist. +TEST_CASE("Encoder rejects images without pixel planes") +{ + auto try_encode = [](heif_image *image) -> heif_error { + heif_unci_image_parameters params{}; + params.version = 1; + params.image_width = 32; + params.image_height = 32; + params.tile_width = 16; + params.tile_height = 16; + params.compression = heif_unci_compression_off; + + heif_context *ctx = heif_context_alloc(); + + heif_encoding_options *options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle *handle = nullptr; + heif_error err = heif_context_add_empty_unci_image(ctx, ¶ms, options, image, &handle); + + if (handle) { + heif_image_handle_release(handle); + } + heif_encoding_options_free(options); + heif_context_free(ctx); + + return err; + }; + + const heif_chroma interleaved_formats[] = {heif_chroma_interleaved_RGB, + heif_chroma_interleaved_RGBA, + heif_chroma_interleaved_RRGGBB_LE, + heif_chroma_interleaved_RRGGBBAA_BE}; + + for (heif_chroma chroma : interleaved_formats) { + heif_image *image; + heif_error err = heif_image_create(16, 16, heif_colorspace_RGB, chroma, &image); + REQUIRE(err.code == heif_error_Ok); + // No heif_image_add_plane() call. + + REQUIRE(try_encode(image).code != heif_error_Ok); + + heif_image_release(image); + } + + SECTION("planar image without planes") { + heif_image *image; + heif_error err = heif_image_create(16, 16, heif_colorspace_YCbCr, heif_chroma_420, &image); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(try_encode(image).code != heif_error_Ok); + + heif_image_release(image); + } + + SECTION("a properly allocated image is still accepted") { + heif_image *image; + heif_error err = heif_image_create(16, 16, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &image); + REQUIRE(err.code == heif_error_Ok); + err = heif_image_add_plane(image, heif_channel_interleaved, 16, 16, 8); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(try_encode(image).code == heif_error_Ok); + + heif_image_release(image); + } +} diff -Nru libheif-1.19.8/tests/uncompressed_encode_multicomponent.cc libheif-1.23.4/tests/uncompressed_encode_multicomponent.cc --- libheif-1.19.8/tests/uncompressed_encode_multicomponent.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_encode_multicomponent.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,557 @@ +/* + libheif integration tests for uncompressed multi-component images + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_uncompressed.h" +#include "test_utils.h" +#include +#include +#include +#include + +static constexpr int kWidth = 8; +static constexpr int kHeight = 8; +static constexpr int kNumComponents = 4; + + +template +static T compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + assert(false && "compute_fill_value not specialized for this type"); + return T{}; +} + +template <> +float compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(comp * 37 + y * kWidth + x + 1) * 0.1f; +} + +template <> +double compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(comp * 37 + y * kWidth + x + 1) * 0.1; +} + +template <> +heif_complex32 compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + float base = static_cast(comp * 37 + y * kWidth + x + 1); + return {base * 0.1f, base * 0.2f}; +} + +template <> +heif_complex64 compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + double base = static_cast(comp * 37 + y * kWidth + x + 1); + return {base * 0.1, base * 0.2}; +} + +template <> +int8_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(((comp * 37 + y * kWidth + x + 1) % 256) - 128); +} + +template <> +int16_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(comp * 37 + y * kWidth + x + 1); +} + +template <> +int32_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(comp * 37 + y * kWidth + x + 1); +} + +template <> +uint8_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast((comp * 37 + y * kWidth + x + 1) & 0xFF); +} + +template <> +uint16_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return static_cast(comp * 37 + y * kWidth + x + 1); +} + +template <> +uint32_t compute_fill_value(uint32_t comp, uint32_t y, uint32_t x) +{ + return comp * 37 + y * kWidth + x + 1; +} + + +// Typed accessor helpers: get mutable pointer for filling +template +T* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride); + +template <> +uint8_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component(image, idx, out_stride); +} + +template <> +uint16_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_uint16(image, idx, out_stride); +} + +template <> +uint32_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_uint32(image, idx, out_stride); +} + +template <> +int16_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int16(image, idx, out_stride); +} + +template <> +int32_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int32(image, idx, out_stride); +} + +template <> +float* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_float32(image, idx, out_stride); +} + +template <> +double* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_float64(image, idx, out_stride); +} + +template <> +heif_complex32* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_complex32(image, idx, out_stride); +} + +template <> +heif_complex64* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_complex64(image, idx, out_stride); +} + + +// Typed accessor helpers: get const pointer for reading +template +const T* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride); + +template <> +const uint8_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_readonly(image, idx, out_stride); +} + +template <> +const uint16_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_uint16_readonly(image, idx, out_stride); +} + +template <> +const uint32_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_uint32_readonly(image, idx, out_stride); +} + +template <> +const int16_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int16_readonly(image, idx, out_stride); +} + +template <> +const int32_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int32_readonly(image, idx, out_stride); +} + +template <> +const float* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_float32_readonly(image, idx, out_stride); +} + +template <> +const double* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_float64_readonly(image, idx, out_stride); +} + +template <> +const heif_complex32* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_complex32_readonly(image, idx, out_stride); +} + +template <> +const heif_complex64* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_complex64_readonly(image, idx, out_stride); +} + + +template <> +int8_t* get_component_ptr(heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int8(image, idx, out_stride); +} + +template <> +const int8_t* get_component_ptr_readonly(const heif_image* image, uint32_t idx, size_t* out_stride) +{ + return heif_image_get_component_int8_readonly(image, idx, out_stride); +} + + +template +static heif_image* create_and_fill_image(heif_component_datatype datatype, int bit_depth) +{ + heif_image* image = nullptr; + heif_error err; + + err = heif_image_create(kWidth, kHeight, heif_colorspace_custom, + heif_chroma_planar, &image); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(image != nullptr); + + for (uint32_t c = 0; c < kNumComponents; c++) { + uint32_t id = 0; + err = heif_image_add_component(image, kWidth, kHeight, + heif_cmpd_component_type_monochrome, datatype, bit_depth, &id); + REQUIRE(err.code == heif_error_Ok); + //REQUIRE(idx == c); + + size_t stride = 0; + T* data = get_component_ptr(image, id, &stride); + REQUIRE(data != nullptr); + REQUIRE(stride >= kWidth); + + for (uint32_t y = 0; y < kHeight; y++) { + for (uint32_t x = 0; x < kWidth; x++) { + data[y * stride + x] = compute_fill_value(id, y, x); + } + } + } + + return image; +} + + +template +static bool values_equal(T a, T b) +{ + return a == b; +} + +template <> +bool values_equal(float a, float b) +{ + return std::abs(a - b) < 1e-5f; +} + +template <> +bool values_equal(double a, double b) +{ + return std::abs(a - b) < 1e-10; +} + +template <> +bool values_equal(heif_complex32 a, heif_complex32 b) +{ + return std::abs(a.real - b.real) < 1e-5f && std::abs(a.imaginary - b.imaginary) < 1e-5f; +} + +template <> +bool values_equal(heif_complex64 a, heif_complex64 b) +{ + return std::abs(a.real - b.real) < 1e-10 && std::abs(a.imaginary - b.imaginary) < 1e-10; +} + + +template +static void verify_image_data(const heif_image* image) +{ + uint32_t num_components = heif_image_get_number_of_used_components(image); + REQUIRE(num_components == kNumComponents); + + std::vector components(num_components); + heif_image_get_used_component_ids(image, components.data()); + + for (uint32_t c : components) { + REQUIRE(heif_image_get_component_width(image, c) == kWidth); + REQUIRE(heif_image_get_component_height(image, c) == kHeight); + REQUIRE(heif_image_get_component_type(image, c) == heif_cmpd_component_type_monochrome); + + size_t stride = 0; + const T* data = get_component_ptr_readonly(image, c, &stride); + REQUIRE(data != nullptr); + + for (uint32_t y = 0; y < kHeight; y++) { + for (uint32_t x = 0; x < kWidth; x++) { + T expected = compute_fill_value(c, y, x); + T actual = data[y * stride + x]; + REQUIRE(values_equal(expected, actual)); + } + } + } +} + + +template +static void test_multi_mono(heif_component_datatype datatype, int bit_depth, const char* output_filename) +{ + heif_image* image = create_and_fill_image(datatype, bit_depth); + + // Verify that data was written correctly before encode + verify_image_data(image); + + // Encode + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, image, encoder, nullptr, nullptr); + REQUIRE(err.code == heif_error_Ok); + + // Write to file + std::string output_path = get_tests_output_file_path(output_filename); + err = heif_context_write_to_file(ctx, output_path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); + + // Read back + heif_context* ctx2 = heif_context_alloc(); + err = heif_context_read_from_file(ctx2, output_path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx2, &handle); + REQUIRE(err.code == heif_error_Ok); + + heif_image* decoded = nullptr; + err = heif_decode_image(handle, &decoded, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + + verify_image_data(decoded); + + heif_image_release(decoded); + heif_image_handle_release(handle); + heif_context_free(ctx2); +} + + +TEST_CASE("Multi-mono uint8") +{ + test_multi_mono(heif_component_datatype_unsigned_integer, 8, "multi_mono_uint8.heif"); +} + +TEST_CASE("Multi-mono uint16") +{ + test_multi_mono(heif_component_datatype_unsigned_integer, 16, "multi_mono_uint16.heif"); +} + +TEST_CASE("Multi-mono uint12") +{ + test_multi_mono(heif_component_datatype_unsigned_integer, 12, "multi_mono_uint12.heif"); +} + +TEST_CASE("Multi-mono uint32") +{ + test_multi_mono(heif_component_datatype_unsigned_integer, 32, "multi_mono_uint32.heif"); +} + +TEST_CASE("Multi-mono int8") +{ + test_multi_mono(heif_component_datatype_signed_integer, 8, "multi_mono_int8.heif"); +} + +TEST_CASE("Multi-mono int16") +{ + test_multi_mono(heif_component_datatype_signed_integer, 16, "multi_mono_int16.heif"); +} + +TEST_CASE("Multi-mono int32") +{ + test_multi_mono(heif_component_datatype_signed_integer, 32, "multi_mono_int32.heif"); +} + +TEST_CASE("Multi-mono float32") +{ + test_multi_mono(heif_component_datatype_floating_point, 32, "multi_mono_float32.heif"); +} + +TEST_CASE("Multi-mono float64") +{ + test_multi_mono(heif_component_datatype_floating_point, 64, "multi_mono_float64.heif"); +} + +TEST_CASE("Multi-mono complex32") +{ + test_multi_mono(heif_component_datatype_complex_number, 64, "multi_mono_complex32.heif"); +} + +TEST_CASE("Multi-mono complex64") +{ + test_multi_mono(heif_component_datatype_complex_number, 128, "multi_mono_complex64.heif"); +} + + +// Test that mixing byte-aligned (16-bit) and non-byte-aligned (14-bit) components +// in the same image roundtrips correctly. This exercises the encoder's endianness +// handling: the 16-bit component triggers components_little_endian=true in the uncC +// box, but the 14-bit component uses MSB-first bit packing. +TEST_CASE("Mixed bpp: 16-bit and 14-bit components") +{ + constexpr int kW = 8; + constexpr int kH = 8; + constexpr int kBpp16 = 16; + constexpr int kBpp14 = 14; + constexpr uint16_t kMaxVal14 = (1 << kBpp14) - 1; + + // Create image + heif_image* image = nullptr; + heif_error err = heif_image_create(kW, kH, heif_colorspace_custom, + heif_chroma_planar, &image); + REQUIRE(err.code == heif_error_Ok); + + // Add 16-bit component (byte-aligned) + uint32_t idx0 = 0; + err = heif_image_add_component(image, kW, kH, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, kBpp16, &idx0); + REQUIRE(err.code == heif_error_Ok); + + // Add 14-bit component (non-byte-aligned) + uint32_t idx1 = 0; + err = heif_image_add_component(image, kW, kH, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, kBpp14, &idx1); + REQUIRE(err.code == heif_error_Ok); + + // Fill 16-bit component + { + size_t stride = 0; + uint16_t* data = heif_image_get_component_uint16(image, idx0, &stride); + REQUIRE(data != nullptr); + for (uint32_t y = 0; y < kH; y++) { + for (uint32_t x = 0; x < kW; x++) { + data[y * stride + x] = static_cast(y * kW + x + 100); + } + } + } + + // Fill 14-bit component (values must fit in 14 bits) + { + size_t stride = 0; + uint16_t* data = heif_image_get_component_uint16(image, idx1, &stride); + REQUIRE(data != nullptr); + for (uint32_t y = 0; y < kH; y++) { + for (uint32_t x = 0; x < kW; x++) { + data[y * stride + x] = static_cast((y * kW + x + 200) & kMaxVal14); + } + } + } + + // Encode + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + err = heif_context_get_encoder_for_format(ctx, heif_compression_uncompressed, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, image, encoder, nullptr, nullptr); + REQUIRE(err.code == heif_error_Ok); + + std::string output_path = get_tests_output_file_path("mixed_bpp_16_14.heif"); + err = heif_context_write_to_file(ctx, output_path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); + + // Decode and verify + heif_context* ctx2 = heif_context_alloc(); + err = heif_context_read_from_file(ctx2, output_path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx2, &handle); + REQUIRE(err.code == heif_error_Ok); + + heif_image* decoded = nullptr; + err = heif_decode_image(handle, &decoded, heif_colorspace_undefined, + heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(heif_image_get_number_of_used_components(decoded) == 2); + + // Verify 16-bit component + { + size_t stride = 0; + const uint16_t* data = heif_image_get_component_uint16_readonly(decoded, idx0, &stride); + REQUIRE(data != nullptr); + for (uint32_t y = 0; y < kH; y++) { + for (uint32_t x = 0; x < kW; x++) { + uint16_t expected = static_cast(y * kW + x + 100); + INFO("16-bit component at (" << x << "," << y << ")"); + REQUIRE(data[y * stride + x] == expected); + } + } + } + + // Verify 14-bit component + { + size_t stride = 0; + const uint16_t* data = heif_image_get_component_uint16_readonly(decoded, idx1, &stride); + REQUIRE(data != nullptr); + for (uint32_t y = 0; y < kH; y++) { + for (uint32_t x = 0; x < kW; x++) { + uint16_t expected = static_cast((y * kW + x + 200) & kMaxVal14); + INFO("14-bit component at (" << x << "," << y << ")"); + REQUIRE(data[y * stride + x] == expected); + } + } + } + + heif_image_release(decoded); + heif_image_handle_release(handle); + heif_context_free(ctx2); +} diff -Nru libheif-1.19.8/tests/uncompressed_idat_tiled.cc libheif-1.23.4/tests/uncompressed_idat_tiled.cc --- libheif-1.19.8/tests/uncompressed_idat_tiled.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_idat_tiled.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,230 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test: a multi-tile uncompressed 'unci' image whose data is stored +// in the 'idat' box (construction_method == 1) must decode correctly. +// +// The decoder reads each tile as a sub-range of the item data +// (unc_decoder::get_compressed_image_data_uncompressed() -> +// DataExtent::read_data(offset, size)). The 'idat' branch of +// Box_iloc::read_data() used to ignore that (offset, size) window: it read the +// whole extent and did `size -= extent.length` unconditionally, so any partial +// read (size < extent.length) underflowed `size` and returned +// "Not enough data present in 'iloc' to satisfy request." That made every tile +// of an idat-stored multi-tile image fail to decode. +// +// This builds a 4x2 monochrome image split into two 2x2 tiles, stored raw in +// 'idat', and checks that the full-image decode succeeds AND that every pixel +// (including the ones from the second tile) has the expected value, which also +// verifies the offset handling and not merely the absence of an error. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 4; +constexpr uint32_t HEIGHT = 2; +constexpr uint32_t TILE_COLS = 2; // two 2x2 tiles side by side +constexpr uint32_t TILE_ROWS = 1; + +// Raw pixel data, laid out per tile (component interleave, one 8-bit component), +// row-major within each tile: tile 0 covers x[0..1], tile 1 covers x[2..3]. +// +// image: row0: 10 11 | 12 13 +// row1: 20 21 | 22 23 +const std::vector kPixelData = { + 10, 11, 20, 21, // tile 0 (x0..1, y0..1) + 12, 13, 22, 23}; // tile 1 (x2..3, y0..1) + +// Expected decoded plane, indexed [y][x]. +constexpr uint8_t kExpected[HEIGHT][WIDTH] = { + {10, 11, 12, 13}, + {20, 21, 22, 23}}; + +std::vector build_heif_unci_idat_tiled() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: a single monochrome component (type 0). + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 1); + put_u16_be(cmpd_payload, 0); + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): component interleave, one 8-bit component, 2x1 tiles. + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 1); // component_count + put_u16_be(uncC_payload, 0); // component_index + uncC_payload.push_back(7); // component_bit_depth_minus_one -> 8 bit + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + uncC_payload.push_back(0); // sampling_type (no subsampling) + uncC_payload.push_back(0); // interleave_type (component) + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, TILE_COLS - 1); + put_u32_be(uncC_payload, TILE_ROWS - 1); + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(3); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat: the raw pixel data. + auto idat = make_box("idat", kPixelData); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(kPixelData.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci multi-tile image stored in idat decodes correctly") { + std::vector file = build_heif_unci_idat_tiled(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + // Full-image decode. Before the fix, reading the second (and, because of the + // size underflow, even the first) tile from the idat extent failed with + // "Not enough data present in 'iloc'". + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, + heif_colorspace_monochrome, heif_chroma_monochrome, + nullptr); + INFO("decode error code: " << err.code); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_get_width(img, heif_channel_Y) == static_cast(WIDTH)); + REQUIRE(heif_image_get_height(img, heif_channel_Y) == static_cast(HEIGHT)); + + int stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly(img, heif_channel_Y, &stride); + REQUIRE(plane != nullptr); + + for (uint32_t y = 0; y < HEIGHT; y++) { + for (uint32_t x = 0; x < WIDTH; x++) { + INFO("pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(plane[y * stride + x]) == static_cast(kExpected[y][x])); + } + } + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_colorconv.cc libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_colorconv.cc --- libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_colorconv.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_colorconv.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,257 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-w7mc-p8jc-p853, modeled on the reporter's PoC +// (a YCbCr 4:2:0 'unci' image with Y=16 bit and Cb=Cr=8 bit). +// +// The YCbCr->RGB color-conversion operators assume all color channels share a +// single bit depth. Op_YCbCr420_to_RRGGBBaa takes its sample width from the Y +// channel and reads Cb/Cr through a uint16_t*, so an image with 16-bit luma but +// 8-bit chroma (chroma planes allocated at 1 byte/sample) is read two bytes per +// sample past the end of each chroma plane: a heap out-of-bounds read whose +// bytes reach the decoded RGB output (information disclosure). +// +// The fix rejects a color conversion whose color channels (Y/Cb/Cr or R/G/B) do +// not all share one bit depth, at the entry of convert_colorspace(). This test +// builds such a file, confirms it still decodes natively to the mismatched-depth +// planar image (so the decoder path itself is unaffected), and then requires the +// conversion to a high-bit-depth interleaved RGB target to be refused cleanly +// with an error rather than over-reading the chroma planes. Under the unfixed +// code the RGB decode reproduces the reporter's ASAN trace (heap-buffer-overflow +// READ in Op_YCbCr420_to_RRGGBBaa::convert_colorspace). + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// Use a reasonably large image: reading the 8-bit chroma planes through a +// uint16_t* over-reads by roughly the chroma row width per row, so the image +// must be wide enough that this exceeds the plane's allocated (stride-padded) +// size and reaches an AddressSanitizer redzone. Small images (e.g. 8x8) hide +// the bug because the stride padding absorbs the doubled read. +constexpr uint32_t WIDTH = 512; +constexpr uint32_t HEIGHT = 512; +constexpr uint32_t CHROMA_WIDTH = WIDTH / 2; +constexpr uint32_t CHROMA_HEIGHT = HEIGHT / 2; + +// Build a minimal HEIF file with a single 'unci' item: mixed interleave, +// 4:2:0 sampling, Y=16 bit, Cb=8 bit, Cr=8 bit, uncompressed (no cmpC). +std::vector build_heif_unci_ycbcr_mismatched_luma_depth() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'unci'. + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: Y, Cb, Cr. + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 3); + put_u16_be(cmpd_payload, 1); // Y + put_u16_be(cmpd_payload, 2); // Cb + put_u16_be(cmpd_payload, 3); // Cr + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): mixed interleave, 4:2:0, Y=16 bit, Cb=8 bit, Cr=8 bit. + const uint8_t depths[3] = {16, 8, 8}; // Y, Cb, Cr + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 3); // component_count + for (uint16_t idx = 0; idx < 3; idx++) { + put_u16_be(uncC_payload, idx); // component_index + uncC_payload.push_back(static_cast(depths[idx] - 1)); // component_bit_depth_minus_one + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + } + uncC_payload.push_back(2); // sampling_type = 4:2:0 + uncC_payload.push_back(2); // interleave_type = mixed + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags (big-endian components) + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, 0); // num_tile_cols_minus_one + put_u32_be(uncC_payload, 0); // num_tile_rows_minus_one + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + // colr (nclx): BT.601 matrix so the YCbCr->RGB operators are candidates. + std::vector colr_payload; + append_fourcc(colr_payload, "nclx"); + put_u16_be(colr_payload, 6); // colour_primaries (smpte170m) + put_u16_be(colr_payload, 6); // transfer_characteristics (smpte170m) + put_u16_be(colr_payload, 6); // matrix_coefficients (smpte170m / BT.601) + colr_payload.push_back(0x80); // full_range_flag = 1 + auto colr = make_box("colr", colr_payload); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + append(ipco_payload, colr); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(4); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + ipma_payload.push_back(4); // non-essential, colr + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // --- Tile data: Y plane (row-major, 2 bytes/sample, big-endian), followed by + // the interleaved chroma block (Cb, Cr; 1 byte each per chroma position): + // 8*8*2 (Y) + 4*4*(1+1) (Cb+Cr interleaved) = 128 + 32 = 160 bytes. + std::vector tile_data; + tile_data.reserve(WIDTH * HEIGHT * 2 + CHROMA_WIDTH * CHROMA_HEIGHT * 2); + + for (uint32_t y = 0; y < HEIGHT; y++) { + for (uint32_t x = 0; x < WIDTH; x++) { + put_u16_be(tile_data, static_cast(0x1000 + x + WIDTH * y)); + } + } + for (uint32_t y = 0; y < CHROMA_HEIGHT; y++) { + for (uint32_t x = 0; x < CHROMA_WIDTH; x++) { + tile_data.push_back(static_cast(0x40 + y * CHROMA_WIDTH + x)); // Cb + tile_data.push_back(static_cast(0x80 + y * CHROMA_WIDTH + x)); // Cr + } + } + + auto idat = make_box("idat", tile_data); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(tile_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci YCbCr with mismatched luma/chroma bit depths refuses RGB conversion without heap overread") { + std::vector file = build_heif_unci_ycbcr_mismatched_luma_depth(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + // The file parses to a valid item with the expected geometry (this reads the + // item properties, without decoding pixels). + REQUIRE(heif_image_handle_get_width(handle) == static_cast(WIDTH)); + REQUIRE(heif_image_handle_get_height(handle) == static_cast(HEIGHT)); + + // Converting to a high-bit-depth interleaved RGB target selects + // Op_YCbCr420_to_RRGGBBaa, which is where the over-read occurred. With the fix + // the mismatched color-channel bit depths make the conversion unsupported, so + // the decode must fail cleanly (with the specific bit-depth suberror) rather + // than reading past the chroma planes. Under the unfixed code this decode + // reproduces the reporter's ASAN heap-buffer-overflow READ. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE, nullptr); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Unsupported_feature); + REQUIRE(err.subcode == heif_suberror_Unsupported_bit_depth); + REQUIRE(img == nullptr); + + if (img != nullptr) { + heif_image_release(img); + } + } + + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_encode.cc libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_encode.cc --- libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_encode.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_encode.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,351 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for the encode-path counterpart of GHSA-w7mc-p8jc-p853. +// +// The per-channel bit-depth check added for that advisory lives in +// convert_colorspace(). On the encode path that function is not always reached: +// Encoder::convert_colorspace_for_encoding() returns early when the image +// already has the colorspace and chroma format the encoder asked for, which is +// exactly the case for a YCbCr 4:2:0 image handed to the AV1 or HEVC encoders. +// The image then arrived at the plugin with Y at 10 bits and Cb/Cr at 8, and +// the aom and x265 plugins took the sample width from the luma channel and +// applied it to the chroma planes. HeifPixelImage allocates one byte per sample +// at 8 bits and two above that, so each chroma row was read at twice its +// allocated width: a heap out-of-bounds read whose bytes ended up in the +// encoded output. +// +// The fix does not live in the color conversion. Whether an image with +// differing per-channel bit depths can be encoded depends on the codec and on +// the encoder implementation (HEVC and H.264 can signal separate luma and +// chroma bit depths, AV1 and VVC cannot, and JPEG 2000 genuinely supports a +// precision per component), so each encoder plugin checks its own input. +// +// This test builds a 'unci' file with Y=10 bit and Cb=Cr=8 bit, confirms that +// it still decodes natively to a mismatched-depth planar image (the decoder is +// allowed to produce one), and then requires the encoders to refuse it cleanly +// instead of over-reading the chroma planes. Under the unfixed code the encode +// reproduces a heap-buffer-overflow READ in the encoder plugin. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// The over-read is roughly half a chroma row per row, so the image has to be +// wide enough that it exceeds the stride padding and reaches a sanitizer +// redzone. Small images hide the bug. +constexpr uint32_t WIDTH = 512; +constexpr uint32_t HEIGHT = 512; +constexpr uint32_t CHROMA_WIDTH = WIDTH / 2; +constexpr uint32_t CHROMA_HEIGHT = HEIGHT / 2; + +// 10 bits, not 16: it has to be a bit depth the encoders actually accept, or +// they would reject the image for an unrelated reason before ever reading the +// chroma planes, and the test would pass without testing anything. +constexpr int LUMA_BITS = 10; + +// Bit-packed big-endian writer for the luma samples. +class BitWriter { +public: + explicit BitWriter(std::vector& out) : m_out(out) {} + + void write(uint32_t value, int bits) { + m_acc = (m_acc << bits) | (value & ((1u << bits) - 1)); + m_nbits += bits; + while (m_nbits >= 8) { + m_nbits -= 8; + m_out.push_back(static_cast((m_acc >> m_nbits) & 0xFF)); + } + } + + void flush() { + if (m_nbits > 0) { + m_out.push_back(static_cast((m_acc << (8 - m_nbits)) & 0xFF)); + m_nbits = 0; + } + } + +private: + std::vector& m_out; + uint32_t m_acc = 0; + int m_nbits = 0; +}; + +// Build a minimal HEIF file with a single 'unci' item: mixed interleave, +// 4:2:0 sampling, Y=10 bit, Cb=8 bit, Cr=8 bit, uncompressed (no cmpC). +std::vector build_heif_unci_ycbcr_mismatched_luma_depth() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 3); + put_u16_be(cmpd_payload, 1); // Y + put_u16_be(cmpd_payload, 2); // Cb + put_u16_be(cmpd_payload, 3); // Cr + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): mixed interleave, 4:2:0, Y=10 bit, Cb=8 bit, Cr=8 bit. + const uint8_t depths[3] = {LUMA_BITS, 8, 8}; + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 3); // component_count + for (uint16_t idx = 0; idx < 3; idx++) { + put_u16_be(uncC_payload, idx); // component_index + uncC_payload.push_back(static_cast(depths[idx] - 1)); // component_bit_depth_minus_one + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + } + uncC_payload.push_back(2); // sampling_type = 4:2:0 + uncC_payload.push_back(2); // interleave_type = mixed + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags (big-endian components) + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, 0); // num_tile_cols_minus_one + put_u32_be(uncC_payload, 0); // num_tile_rows_minus_one + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + // colr (nclx). The values matter: if they disagree with the encoding target, + // Encoder::convert_colorspace_for_encoding() runs a color conversion and the + // guard in convert_colorspace() catches the image before it reaches the + // plugin. These are the defaults that heif-enc requests, so the early return + // is taken and the image reaches the encoder unconverted. + std::vector colr_payload; + append_fourcc(colr_payload, "nclx"); + put_u16_be(colr_payload, 1); // colour_primaries (BT.709) + put_u16_be(colr_payload, 13); // transfer_characteristics (sRGB) + put_u16_be(colr_payload, 6); // matrix_coefficients (BT.601) + colr_payload.push_back(0x80); // full_range_flag = 1 + auto colr = make_box("colr", colr_payload); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + append(ipco_payload, colr); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(4); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + ipma_payload.push_back(4); // non-essential, colr + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // Tile data: the bit-packed 10-bit luma plane, followed by the interleaved + // chroma block (Cb, Cr; one byte each per chroma position). + std::vector tile_data; + tile_data.reserve(WIDTH * HEIGHT * LUMA_BITS / 8 + CHROMA_WIDTH * CHROMA_HEIGHT * 2); + + { + BitWriter bw(tile_data); + for (uint32_t y = 0; y < HEIGHT; y++) { + for (uint32_t x = 0; x < WIDTH; x++) { + bw.write(0x200 + ((x + y) & 0xFF), LUMA_BITS); + } + } + bw.flush(); + } + + for (uint32_t y = 0; y < CHROMA_HEIGHT; y++) { + for (uint32_t x = 0; x < CHROMA_WIDTH; x++) { + tile_data.push_back(static_cast(0x40 + ((y * CHROMA_WIDTH + x) & 0x3F))); // Cb + tile_data.push_back(static_cast(0x80 + ((y * CHROMA_WIDTH + x) & 0x3F))); // Cr + } + } + + auto idat = make_box("idat", tile_data); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(tile_data.size())); + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +// Decode the crafted file in its native colorspace, the way heif-enc does, so +// that the mismatched per-channel bit depths survive into the encoder. +heif_image* decode_mismatched_image(heif_context* ctx) { + heif_image_handle* handle = nullptr; + heif_error err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + options->output_image_nclx_profile_passthrough = true; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, options); + + heif_decoding_options_free(options); + heif_image_handle_release(handle); + + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + return img; +} + +// Encoding must fail cleanly. It must never over-read the chroma planes, which +// is what a sanitizer build catches, and it must never silently succeed, which +// would mean the over-read bytes were encoded into the output. +void require_encode_refused(heif_image* img, heif_compression_format format) { + heif_context* out_ctx = heif_context_alloc(); + REQUIRE(out_ctx != nullptr); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(out_ctx, format, &encoder); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(encoder != nullptr); + + heif_image_handle* out_handle = nullptr; + err = heif_context_encode_image(out_ctx, img, encoder, nullptr, &out_handle); + + INFO("encode error (" << err.code << "/" << err.subcode << "): " + << (err.message ? err.message : "(null)")); + REQUIRE(err.code != heif_error_Ok); + + // An encoder may bail out earlier for a reason of its own (for example a + // build of x265 without high bit depth support), but when it is our check + // that fires, it must report the bit depth as the reason. + if (err.code == heif_error_Encoder_plugin_error) { + REQUIRE(err.subcode == heif_suberror_Unsupported_bit_depth); + } + + if (out_handle != nullptr) { + heif_image_handle_release(out_handle); + } + heif_encoder_release(encoder); + heif_context_free(out_ctx); +} + +} // namespace + +TEST_CASE("unci YCbCr with mismatched luma/chroma bit depths is refused by the encoders") +{ + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("Skipping test because uncompressed codec is not compiled."); + } + + std::vector file = build_heif_unci_ycbcr_mismatched_luma_depth(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image* img = decode_mismatched_image(ctx); + + // The decoder is allowed to produce this image: ISO/IEC 23001-17 declares + // component_bit_depth per component. It is the encoders that cannot take it. + REQUIRE(heif_image_get_colorspace(img) == heif_colorspace_YCbCr); + REQUIRE(heif_image_get_chroma_format(img) == heif_chroma_420); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_Y) == LUMA_BITS); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_Cb) == 8); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_Cr) == 8); + + if (heif_have_encoder_for_format(heif_compression_AV1)) { + require_encode_refused(img, heif_compression_AV1); + } + + if (heif_have_encoder_for_format(heif_compression_HEVC)) { + require_encode_refused(img, heif_compression_HEVC); + } + + heif_image_release(img); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_overflow.cc libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_overflow.cc --- libheif-1.19.8/tests/uncompressed_mixed_chroma_depth_overflow.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_overflow.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,272 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-x8r2-mggj-j6wr, modeled on the reporter's PoC +// ('overflow.heif': a Y8/Cb16/Cr8, 4:2:0, mixed-interleave 'unci' image). +// +// unc_decoder_mixed_interleave::processTile() handles Cb and Cr together: it +// reads and writes both chroma samples per pixel using only the byte width of +// whichever of the two components is declared first in the uncC component +// list. Each chroma plane is allocated according to its OWN component's bit +// depth, so when Cb and Cr are declared with different depths (here Cb=16 +// bit, Cr=8 bit), the second (Cr) write used the first (Cb) component's +// 2-byte width and column stride against a plane sized for 1 byte per sample: +// a heap out-of-bounds write, repeated once per chroma sample, with +// attacker-controlled bytes. +// +// This test builds a minimal 8x8 (chroma 4x4) file with that exact +// configuration and decodes it natively (heif_colorspace_undefined / +// heif_chroma_undefined, to avoid the unrelated, non-security limitation +// that YCbCr->RGB conversion does not support mismatched chroma bit depths). +// Under the unfixed decoder this reproduces the reporter's ASAN trace +// (heap-buffer-overflow WRITE in memcpy_to_native_endian(), called from +// unc_decoder_mixed_interleave::processTile()). With the fix, decoding must +// succeed and every Y/Cb/Cr sample must come back exactly as encoded. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 8; +constexpr uint32_t HEIGHT = 8; +constexpr uint32_t CHROMA_WIDTH = WIDTH / 2; +constexpr uint32_t CHROMA_HEIGHT = HEIGHT / 2; + +// Build a minimal HEIF file with a single 'unci' item: mixed interleave, +// 4:2:0 sampling, Y=8 bit, Cb=16 bit, Cr=8 bit, uncompressed (no cmpC). +std::vector build_heif_unci_mixed_chroma_depth_mismatch() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'unci'. + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: Y, Cb, Cr. + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 3); + put_u16_be(cmpd_payload, 1); // Y + put_u16_be(cmpd_payload, 2); // Cb + put_u16_be(cmpd_payload, 3); // Cr + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): mixed interleave, 4:2:0, Y=8 bit, Cb=16 bit, Cr=8 bit. + const uint8_t depths[3] = {8, 16, 8}; // Y, Cb, Cr + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 3); // component_count + for (uint16_t idx = 0; idx < 3; idx++) { + put_u16_be(uncC_payload, idx); // component_index + uncC_payload.push_back(static_cast(depths[idx] - 1)); // component_bit_depth_minus_one + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + } + uncC_payload.push_back(2); // sampling_type = 4:2:0 + uncC_payload.push_back(2); // interleave_type = mixed + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags (big-endian components) + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, 0); // num_tile_cols_minus_one + put_u32_be(uncC_payload, 0); // num_tile_rows_minus_one + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(3); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // --- Tile data: Y plane (row-major, 1 byte/sample), followed by the + // interleaved chroma block. get_tile_data_sizes() sums each component's + // own row size independently, which for these byte-aligned depths equals + // the actual interleaved byte count consumed by processTile(): 64 (Y) + + // 4*4*(2+1) (Cb+Cr interleaved) = 112 bytes. + std::vector tile_data; + tile_data.reserve(WIDTH * HEIGHT + CHROMA_WIDTH * CHROMA_HEIGHT * 3); + + for (uint32_t y = 0; y < HEIGHT; y++) { + for (uint32_t x = 0; x < WIDTH; x++) { + tile_data.push_back(static_cast((x + WIDTH * y) & 0xFF)); + } + } + for (uint32_t y = 0; y < CHROMA_HEIGHT; y++) { + for (uint32_t x = 0; x < CHROMA_WIDTH; x++) { + uint16_t cb = static_cast(0x1000 + y * CHROMA_WIDTH + x); + uint8_t cr = static_cast(0x40 + y * CHROMA_WIDTH + x); + put_u16_be(tile_data, cb); + tile_data.push_back(cr); + } + } + + auto idat = make_box("idat", tile_data); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(tile_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci mixed-interleave with mismatched Cb/Cr bit depths decodes without heap overflow") { + std::vector file = build_heif_unci_mixed_chroma_depth_mismatch(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + // Decode natively (no forced colorspace conversion): before the fix, this + // already reaches the vulnerable paired-chroma write in + // unc_decoder_mixed_interleave::processTile() and corrupts the heap. + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_get_bits_per_pixel(img, heif_channel_Y) == 8); + REQUIRE(heif_image_get_bits_per_pixel(img, heif_channel_Cb) == 16); + REQUIRE(heif_image_get_bits_per_pixel(img, heif_channel_Cr) == 8); + REQUIRE(heif_image_get_width(img, heif_channel_Cb) == static_cast(CHROMA_WIDTH)); + REQUIRE(heif_image_get_height(img, heif_channel_Cb) == static_cast(CHROMA_HEIGHT)); + + int stride = 0; + const uint8_t* y_plane = heif_image_get_plane_readonly(img, heif_channel_Y, &stride); + REQUIRE(y_plane != nullptr); + for (uint32_t y = 0; y < HEIGHT; y++) { + for (uint32_t x = 0; x < WIDTH; x++) { + INFO("Y at (" << x << "," << y << ")"); + REQUIRE(y_plane[y * stride + x] == static_cast((x + WIDTH * y) & 0xFF)); + } + } + + // The Cr plane is the one that was undersized relative to the (wrongly + // reused) 2-byte write width. Every sample must come back exactly as + // encoded, not corrupted or shifted by the overflowing Cb-sized writes. + const auto* cb_plane = reinterpret_cast( + heif_image_get_plane_readonly(img, heif_channel_Cb, &stride)); + REQUIRE(cb_plane != nullptr); + int cb_stride_elems = stride / 2; + for (uint32_t y = 0; y < CHROMA_HEIGHT; y++) { + for (uint32_t x = 0; x < CHROMA_WIDTH; x++) { + INFO("Cb at (" << x << "," << y << ")"); + REQUIRE(cb_plane[y * cb_stride_elems + x] == static_cast(0x1000 + y * CHROMA_WIDTH + x)); + } + } + + const uint8_t* cr_plane = heif_image_get_plane_readonly(img, heif_channel_Cr, &stride); + REQUIRE(cr_plane != nullptr); + for (uint32_t y = 0; y < CHROMA_HEIGHT; y++) { + for (uint32_t x = 0; x < CHROMA_WIDTH; x++) { + INFO("Cr at (" << x << "," << y << ")"); + REQUIRE(cr_plane[y * stride + x] == static_cast(0x40 + y * CHROMA_WIDTH + x)); + } + } + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_sequence_odd_chroma.cc libheif-1.23.4/tests/uncompressed_sequence_odd_chroma.cc --- libheif-1.19.8/tests/uncompressed_sequence_odd_chroma.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_sequence_odd_chroma.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,347 @@ +/* + libheif integration test for GHSA-4h82-g446-83fm. + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-4h82-g446-83fm: heap-buffer-overflow read when +// decoding an uncompressed ('uncv') HEIF *sequence* frame with YCbCr 4:2:0 +// subsampling and an odd declared luma height. +// +// Two independent defects combined here: +// +// 1. Chroma-geometry mismatch (the advisory's root cause). The sequence +// decode path builds the HeifPixelImage via the fallback branch of +// UncompressedImageCodec::create_image() (it does not clone pre-populated +// component descriptions). That branch sized the Cb/Cr planes with FLOOR +// division of the luma dimensions, while the RGB conversion +// (Op_YCbCr420_to_RGB24) indexes chroma with y/2 for every luma row and so +// needs ceil(height/2) rows. For height 129 the Cb/Cr plane was allocated +// 64 rows tall but read on row 64 -> a 1-byte heap out-of-bounds read. +// +// 2. Component-mapping defect (why the OOB was masked in recent releases). +// The same fallback loop compared desc_idx against the *live* +// get_component_descriptions().size(). add_component() grows that list, so +// after creating the Y plane the next iteration wrongly took the +// pre-populated branch and re-referenced Y instead of creating Cb, dropping +// the Cb plane. The conversion then failed early ("Internal error") before +// reaching the vulnerable read, hiding defect 1. +// +// The fix rounds the fallback chroma allocation up (defect 1) and anchors the +// branch decision to the description count captured before the loop (defect 2). +// This test builds a minimal 128x129 4:2:0 'uncv' sequence by hand and decodes +// it to RGB through the public heif_track_decode_next_image() API. Under +// AddressSanitizer the unfixed library aborts on the out-of-bounds read; the +// fixed library decodes a correctly sized RGB frame. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" + +#include +#include + +namespace { + +void put16(std::vector& v, uint16_t x) +{ + v.push_back(static_cast(x >> 8)); + v.push_back(static_cast(x)); +} + +void put32(std::vector& v, uint32_t x) +{ + v.push_back(static_cast(x >> 24)); + v.push_back(static_cast(x >> 16)); + v.push_back(static_cast(x >> 8)); + v.push_back(static_cast(x)); +} + +void put_fourcc(std::vector& v, const char* s) +{ + v.push_back(static_cast(s[0])); + v.push_back(static_cast(s[1])); + v.push_back(static_cast(s[2])); + v.push_back(static_cast(s[3])); +} + +std::vector box(const char* type, const std::vector& payload) +{ + std::vector b; + put32(b, static_cast(8 + payload.size())); + put_fourcc(b, type); + b.insert(b.end(), payload.begin(), payload.end()); + return b; +} + +std::vector concat(std::initializer_list> parts) +{ + std::vector out; + for (const auto& p : parts) { + out.insert(out.end(), p.begin(), p.end()); + } + return out; +} + +constexpr uint16_t WIDTH = 128; +constexpr uint16_t HEIGHT = 129; // odd -> floor chroma height 64, ceil 65 + +// Build a minimal, structurally valid HEIF image sequence with a single 'pict' +// track whose sole sample is a 128x129 YCbCr 4:2:0 uncompressed ('uncv') frame +// in component-interleave layout (Y plane, then Cb plane, then Cr plane). +std::vector build_uncv_sequence_file() +{ + // --- ftyp + std::vector ftyp_p; + put_fourcc(ftyp_p, "msf1"); + put32(ftyp_p, 0); + put_fourcc(ftyp_p, "msf1"); + put_fourcc(ftyp_p, "iso8"); + std::vector ftyp = box("ftyp", ftyp_p); + + // --- mvhd (v0) + std::vector mvhd_p; + put32(mvhd_p, 0); // version/flags + put32(mvhd_p, 0); // creation_time + put32(mvhd_p, 0); // modification_time + put32(mvhd_p, 1000); // timescale + put32(mvhd_p, 1); // duration + put32(mvhd_p, 0x00010000); // rate + put16(mvhd_p, 0x0100); // volume + put16(mvhd_p, 0); + put32(mvhd_p, 0); + put32(mvhd_p, 0); + for (uint32_t m : {0x00010000u,0u,0u,0u,0x00010000u,0u,0u,0u,0x40000000u}) put32(mvhd_p, m); + for (int i = 0; i < 6; i++) put32(mvhd_p, 0); + put32(mvhd_p, 2); // next_track_ID + std::vector mvhd = box("mvhd", mvhd_p); + + // --- tkhd (v0) + std::vector tkhd_p; + put32(tkhd_p, 0x00000007); // flags: enabled | in_movie | in_preview + put32(tkhd_p, 0); + put32(tkhd_p, 0); + put32(tkhd_p, 1); // track_ID + put32(tkhd_p, 0); + put32(tkhd_p, 1); // duration + put32(tkhd_p, 0); + put32(tkhd_p, 0); + put16(tkhd_p, 0); // layer + put16(tkhd_p, 0); // alternate_group + put16(tkhd_p, 0); // volume + put16(tkhd_p, 0); + for (uint32_t m : {0x00010000u,0u,0u,0u,0x00010000u,0u,0u,0u,0x40000000u}) put32(tkhd_p, m); + put32(tkhd_p, static_cast(WIDTH) << 16); // width (16.16) + put32(tkhd_p, static_cast(HEIGHT) << 16); // height (16.16) + std::vector tkhd = box("tkhd", tkhd_p); + + // --- mdhd (v0) + std::vector mdhd_p; + put32(mdhd_p, 0); + put32(mdhd_p, 0); + put32(mdhd_p, 0); + put32(mdhd_p, 1000); // timescale + put32(mdhd_p, 1); // duration + put16(mdhd_p, 0x55c4); // language 'und' + put16(mdhd_p, 0); + std::vector mdhd = box("mdhd", mdhd_p); + + // --- hdlr ('pict') + std::vector hdlr_p; + put32(hdlr_p, 0); + put32(hdlr_p, 0); + put_fourcc(hdlr_p, "pict"); + put32(hdlr_p, 0); + put32(hdlr_p, 0); + put32(hdlr_p, 0); + hdlr_p.push_back(0); + std::vector hdlr = box("hdlr", hdlr_p); + + // --- vmhd + std::vector vmhd_p; + put32(vmhd_p, 0x00000001); + put16(vmhd_p, 0); + put16(vmhd_p, 0); + put16(vmhd_p, 0); + put16(vmhd_p, 0); + std::vector vmhd = box("vmhd", vmhd_p); + + // --- cmpd (plain box): Y, Cb, Cr + std::vector cmpd_p; + put32(cmpd_p, 3); + put16(cmpd_p, 1); // Y + put16(cmpd_p, 2); // Cb + put16(cmpd_p, 3); // Cr + std::vector cmpd = box("cmpd", cmpd_p); + + // --- uncC (fullbox v0): component interleave, 4:2:0, 8-bit + std::vector uncC_p; + put32(uncC_p, 0); // version/flags + put32(uncC_p, 0); // profile + put32(uncC_p, 3); // component_count + for (uint16_t idx = 0; idx < 3; idx++) { + put16(uncC_p, idx); // component_index + uncC_p.push_back(7); // component_bit_depth_minus_1 (8 bit) + uncC_p.push_back(0); // component_format (unsigned int) + uncC_p.push_back(0); // component_align_size + } + uncC_p.push_back(2); // sampling_type = 4:2:0 + uncC_p.push_back(0); // interleave_type = component + uncC_p.push_back(0); // block_size + uncC_p.push_back(0); // flags + put32(uncC_p, 0); // pixel_size + put32(uncC_p, 0); // row_align_size + put32(uncC_p, 0); // tile_align_size + put32(uncC_p, 0); // num_tile_cols_minus_one + put32(uncC_p, 0); // num_tile_rows_minus_one + std::vector uncC = box("uncC", uncC_p); + + // --- uncv VisualSampleEntry (78-byte header + cmpd + uncC) + std::vector vse; + for (int i = 0; i < 6; i++) vse.push_back(0); // reserved + put16(vse, 1); // data_reference_index + put16(vse, 0); // pre_defined + put16(vse, 0); // reserved + put32(vse, 0); put32(vse, 0); put32(vse, 0); // pre_defined2[3] + put16(vse, WIDTH); + put16(vse, HEIGHT); + put32(vse, 0x00480000); // horizresolution 72 dpi + put32(vse, 0x00480000); // vertresolution 72 dpi + put32(vse, 0); // reserved + put16(vse, 1); // frame_count + for (int i = 0; i < 32; i++) vse.push_back(0); // compressorname + put16(vse, 0x0018); // depth + put16(vse, 0xFFFF); // pre_defined3 (-1) + std::vector uncv = box("uncv", concat({vse, cmpd, uncC})); + + // --- stsd + std::vector stsd_p; + put32(stsd_p, 0); + put32(stsd_p, 1); + stsd_p.insert(stsd_p.end(), uncv.begin(), uncv.end()); + std::vector stsd = box("stsd", stsd_p); + + // --- stts + std::vector stts_p; + put32(stts_p, 0); + put32(stts_p, 1); + put32(stts_p, 1); // sample_count + put32(stts_p, 1); // sample_delta + std::vector stts = box("stts", stts_p); + + // --- stsc + std::vector stsc_p; + put32(stsc_p, 0); + put32(stsc_p, 1); + put32(stsc_p, 1); // first_chunk + put32(stsc_p, 1); // samples_per_chunk + put32(stsc_p, 1); // sample_description_index + std::vector stsc = box("stsc", stsc_p); + + const uint32_t sample_size = + static_cast(WIDTH) * HEIGHT + // Y + 2u * (WIDTH / 2) * (HEIGHT / 2); // Cb + Cr (floor chroma in the stream) + + // --- stsz (single fixed sample size) + std::vector stsz_p; + put32(stsz_p, 0); + put32(stsz_p, sample_size); // fixed sample size + put32(stsz_p, 1); // sample_count + std::vector stsz = box("stsz", stsz_p); + + auto assemble = [&](uint32_t chunk_offset) { + std::vector stco_p; + put32(stco_p, 0); + put32(stco_p, 1); + put32(stco_p, chunk_offset); + std::vector stco = box("stco", stco_p); + + std::vector stbl = box("stbl", concat({stsd, stts, stsc, stsz, stco})); + std::vector minf = box("minf", concat({vmhd, stbl})); + std::vector mdia = box("mdia", concat({mdhd, hdlr, minf})); + std::vector trak = box("trak", concat({tkhd, mdia})); + return box("moov", concat({mvhd, trak})); + }; + + std::vector moov0 = assemble(0); + uint32_t mdat_payload_off = static_cast(ftyp.size() + moov0.size() + 8 /* mdat header */); + std::vector moov = assemble(mdat_payload_off); + + // --- mdat: Y plane, then Cb plane, then Cr plane (floor-sized chroma) + std::vector mdat_payload; + mdat_payload.reserve(sample_size); + for (uint32_t y = 0; y < HEIGHT; y++) + for (uint32_t x = 0; x < WIDTH; x++) + mdat_payload.push_back(static_cast((x + y) & 0xFF)); + for (int plane = 0; plane < 2; plane++) + for (uint32_t y = 0; y < HEIGHT / 2u; y++) + for (uint32_t x = 0; x < WIDTH / 2u; x++) + mdat_payload.push_back(0x80); + REQUIRE(mdat_payload.size() == sample_size); + std::vector mdat = box("mdat", mdat_payload); + + return concat({ftyp, moov, mdat}); +} + +} // namespace + + +TEST_CASE("uncv 4:2:0 sequence with odd luma height decodes without OOB") +{ + std::vector file = build_uncv_sequence_file(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(heif_context_has_sequence(ctx) == 1); + + heif_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track != nullptr); + + uint16_t tw = 0, th = 0; + REQUIRE(heif_track_get_image_resolution(track, &tw, &th).code == heif_error_Ok); + REQUIRE(tw == WIDTH); + REQUIRE(th == HEIGHT); + + // Decode the single frame to interleaved RGB. This exercises + // Op_YCbCr420_to_RGB24, the sink of the out-of-bounds read. With the unfixed + // library this aborts under AddressSanitizer; with the fix it must return a + // correctly sized RGB image (and, thanks to the component-mapping fix, all + // three YCbCr planes are present so the conversion no longer fails early). + heif_image* img = nullptr; + heif_error derr = heif_track_decode_next_image(track, &img, + heif_colorspace_RGB, + heif_chroma_interleaved_RGB, + nullptr); + REQUIRE(derr.code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_get_width(img, heif_channel_interleaved) == WIDTH); + REQUIRE(heif_image_get_height(img, heif_channel_interleaved) == HEIGHT); + + heif_image_release(img); + heif_track_release(track); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_tile_range_overflow.cc libheif-1.23.4/tests/uncompressed_tile_range_overflow.cc --- libheif-1.19.8/tests/uncompressed_tile_range_overflow.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_tile_range_overflow.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,239 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for GHSA-hh47-fhqr-cj2r (incomplete fix of +// GHSA-73p7-m7gg-w2jv / CVE-2026-62292). +// +// A crafted 'unci' image with generic (zlib, full_item) compression advertises a +// 4096x4096 grid of 1x1 tiles. Large alignment values make the computed size of +// the last tile 2^40 bytes, so for the final tile index (2^24 - 1) the range +// arithmetic in unc_decoder::get_compressed_image_data_uncompressed() overflows: +// +// range_start_offset = 2^40 * (2^24 - 1) = 2^64 - 2^40 +// range_size = 2^40 +// range_start_offset + range_size = 2^64 -> 0 (uint64_t wrap) +// +// The old addition-form check `range_start_offset + range_size > data->size()` +// was bypassed (0 > 1 is false) and the code reached memcpy() with an +// out-of-range source pointer and a 1 TiB length: an out-of-bounds read / SIGSEGV +// reachable through the public heif_image_handle_decode_image_tile(). +// +// The fix uses the overflow-safe subtraction form. This test builds the fixture +// in memory, opens it (structurally valid, must succeed), reads the advertised +// tiling and requests the last tile: decoding must now return a structured +// heif_error_Invalid_input instead of crashing. +// +// The file uses real zlib generic compression, so the test only runs when the +// library was built with zlib (guarded in tests/CMakeLists.txt). + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 4096; +constexpr uint32_t HEIGHT = 4096; +constexpr uint32_t TILE_COLS = 4096; +constexpr uint32_t TILE_ROWS = 4096; +constexpr uint32_t COMPONENTS = 256; + +// zlib.compress(b"A") - a valid zlib stream that decompresses to a single byte. +const std::vector kZlibOneByte = { + 0x78, 0x9c, 0x73, 0x04, 0x00, 0x00, 0x42, 0x00, 0x42}; + +// Build a minimal HEIF file with a single 'unci' item using generic zlib +// (full_item) compression and a 4096x4096 tile grid. +std::vector build_heif_unci_overflow_tiling() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'unci'. + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: COMPONENTS monochrome components (type 0). + std::vector cmpd_payload; + put_u32_be(cmpd_payload, COMPONENTS); + for (uint32_t i = 0; i < COMPONENTS; i++) { + put_u16_be(cmpd_payload, 0); + } + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): component interleave, 8-bit, huge alignment values. + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, COMPONENTS); // component_count + for (uint32_t i = 0; i < COMPONENTS; i++) { + put_u16_be(uncC_payload, static_cast(i)); // component_index + uncC_payload.push_back(7); // component_bit_depth_minus_one -> 8 bit + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + } + uncC_payload.push_back(0); // sampling_type (no subsampling) + uncC_payload.push_back(0); // interleave_type (component) + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0xFFFFFFFF); // row_align_size + put_u32_be(uncC_payload, 0x80000000); // tile_align_size + put_u32_be(uncC_payload, TILE_COLS - 1); + put_u32_be(uncC_payload, TILE_ROWS - 1); + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + // cmpC: zlib, compressed_unit_type = full_item (0). No icef box. + std::vector cmpC_payload; + append_fourcc(cmpC_payload, "zlib"); + cmpC_payload.push_back(0); // heif_cmpC_compressed_unit_type_full_item + auto cmpC = make_box("cmpC", cmpC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + append(ipco_payload, cmpC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(4); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + ipma_payload.push_back(0x80 | 4); // essential, cmpC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // idat: the zlib payload (decompresses to 1 byte). + auto idat = make_box("idat", kZlibOneByte); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(kZlibOneByte.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci tile range overflow returns error instead of crashing") { + std::vector file = build_heif_unci_overflow_tiling(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // The file is structurally valid, so opening it must succeed. The bug is only + // reachable by then decoding a high-index advertised tile. + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + heif_image_tiling tiling; + heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(tiling.num_columns == TILE_COLS); + REQUIRE(tiling.num_rows == TILE_ROWS); + + // Request the last advertised tile. Before the fix this reached memcpy() with a + // wrapped source pointer and a 1 TiB length (out-of-bounds read / SIGSEGV). It + // must now return a structured invalid-input error. + heif_image* img = nullptr; + err = heif_image_handle_decode_image_tile(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + nullptr, + tiling.num_columns - 1, tiling.num_rows - 1); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(img == nullptr); + + if (img) { + heif_image_release(img); + } + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/tests/uncompressed_wide_component_colorconv.cc libheif-1.23.4/tests/uncompressed_wide_component_colorconv.cc --- libheif-1.19.8/tests/uncompressed_wide_component_colorconv.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/tests/uncompressed_wide_component_colorconv.cc 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,256 @@ +/* + libheif unit tests + + MIT License + + Copyright (c) 2026 Dirk Farin + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +// Regression test for OSS-Fuzz 5154611212910592 (sequence_fuzzer). +// +// ISO/IEC 23001-17 lets an 'unci' component declare a bit depth of up to 256 +// bits. libheif accepts up to 128 of those on purpose: the byte-aligned +// component decoder stores 32/64/128 bit samples so that they can be read back +// through the component API. The color-conversion operators, however, are all written +// for 8-bit or 16-bit integer samples. They access the planes through +// uint8_t* / uint16_t* and derive shift amounts and midpoint values from the +// bit depth. A 64-bit monochrome component converted to YCbCr 4:2:0 evaluated +// '128 << (bit_depth - 8)', shifting an 'int' by 56: undefined behaviour +// (UndefinedBehaviorSanitizer abort in Op_mono_to_YCbCr420::convert_colorspace). +// +// Each operator now declines a bit depth it cannot access, so an operator that +// cannot handle a wide sample never offers itself to the pipeline and pipeline +// construction fails; convert_colorspace() keeps a backstop check behind that. +// This test builds such a file, confirms the decoder path itself is unaffected +// (the untransformed decode still returns the 64-bit monochrome plane), and +// requires the conversions that used to run the unsupported operators to fail +// cleanly instead. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t WIDTH = 4; +constexpr uint32_t HEIGHT = 4; +constexpr uint32_t BYTES_PER_SAMPLE = 8; // 64 bit + +// Build a minimal HEIF file with a single 'unci' item: one monochrome +// component of 64 bits, component interleave, no subsampling, uncompressed. +std::vector build_heif_unci_mono64() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + append_fourcc(ftyp_payload, "heic"); + auto ftyp = make_box("ftyp", ftyp_payload); + + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); + append_fourcc(hdlr_payload, "pict"); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + std::vector pitm_payload; + put_u16_be(pitm_payload, 1); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf: item 1 = 'unci'. + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "unci"); + append_cstr(infe_payload, ""); + auto infe = make_box("infe", infe_payload, /*full=*/true, /*version=*/2); + + std::vector iinf_payload; + put_u16_be(iinf_payload, 1); + append(iinf_payload, infe); + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, WIDTH); + put_u32_be(ispe_payload, HEIGHT); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd: a single monochrome component. + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 1); + put_u16_be(cmpd_payload, 0); // monochrome + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): component interleave, no subsampling, one 64-bit component. + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 1); // component_count + put_u16_be(uncC_payload, 0); // component_index + uncC_payload.push_back(63); // component_bit_depth_minus_one -> 64 bit + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(0); // component_align_size + uncC_payload.push_back(0); // sampling_type = none + uncC_payload.push_back(0); // interleave_type = component + uncC_payload.push_back(0); // block_size + uncC_payload.push_back(0); // flags (big-endian components) + put_u32_be(uncC_payload, 0); // pixel_size + put_u32_be(uncC_payload, 0); // row_align_size + put_u32_be(uncC_payload, 0); // tile_align_size + put_u32_be(uncC_payload, 0); // num_tile_cols_minus_one + put_u32_be(uncC_payload, 0); // num_tile_rows_minus_one + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe); + append(ipco_payload, cmpd); + append(ipco_payload, uncC); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1); // entry_count + put_u16_be(ipma_payload, 1); // item_ID 1 + ipma_payload.push_back(3); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe + ipma_payload.push_back(0x80 | 2); // essential, cmpd + ipma_payload.push_back(0x80 | 3); // essential, uncC + auto ipma = make_box("ipma", ipma_payload, /*full=*/true); + + std::vector iprp_payload; + append(iprp_payload, ipco); + append(iprp_payload, ipma); + auto iprp = make_box("iprp", iprp_payload); + + // Tile data: one plane of 64-bit big-endian samples. + std::vector tile_data; + tile_data.reserve(WIDTH * HEIGHT * BYTES_PER_SAMPLE); + for (uint32_t i = 0; i < WIDTH * HEIGHT; i++) { + for (uint32_t b = 0; b < BYTES_PER_SAMPLE; b++) { + tile_data.push_back(static_cast(i + b)); + } + } + auto idat = make_box("idat", tile_data); + + // iloc (version 1): item 1 stored in idat (construction_method=1). + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8) | (0 << 4) | 0); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + put_u16_be(iloc_payload, 0x0001); // construction_method=1 (idat) + put_u16_be(iloc_payload, 0); // data_reference_index + put_u16_be(iloc_payload, 1); // extent_count + put_u32_be(iloc_payload, 0); // extent_offset (within idat) + put_u32_be(iloc_payload, static_cast(tile_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, pitm); + append(meta_payload, iinf); + append(meta_payload, iprp); + append(meta_payload, iloc); + append(meta_payload, idat); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + +TEST_CASE("unci with a 64-bit component refuses color conversion instead of shifting out of range") { + std::vector file = build_heif_unci_mono64(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_get_primary_image_handle(ctx, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + + REQUIRE(heif_image_handle_get_width(handle) == static_cast(WIDTH)); + REQUIRE(heif_image_handle_get_height(handle) == static_cast(HEIGHT)); + + // The decoder itself still handles the wide component: decoding without a + // colorspace conversion returns the monochrome plane at its declared 64 bits. + // The guard sits behind the nop check, so this path must keep working. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + INFO("native decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(heif_image_get_bits_per_pixel(img, heif_channel_Y) == 64); + heif_image_release(img); + } + + // Converting to YCbCr 4:2:0 selects Op_mono_to_YCbCr420, which computed the + // chroma midpoint as '128 << (bit_depth - 8)'. With a 64-bit component that + // shifted an 'int' by 56. The conversion must now be refused cleanly. + // + // Op_mono_to_YCbCr420::state_after_conversion() declines a sample wider than 16 + // bits, so pipeline construction fails before the catch-all in + // convert_colorspace() is reached: the subcode is Unsupported_color_conversion. + // Should the operators ever stop declining wide samples, the catch-all answers + // with Unsupported_bit_depth instead. Either refusal is correct here; what + // matters is that the conversion does not run and nothing shifts out of range. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_YCbCr, heif_chroma_420, nullptr); + INFO("YCbCr decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Unsupported_feature); + REQUIRE((err.subcode == heif_suberror_Unsupported_color_conversion || + err.subcode == heif_suberror_Unsupported_bit_depth)); + REQUIRE(img == nullptr); + + if (img != nullptr) { + heif_image_release(img); + } + } + + // The same applies to an RGB target, which would read the 8-byte samples + // through a uint8_t* / uint16_t* view of the plane. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, nullptr); + INFO("RGB decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code != heif_error_Ok); + REQUIRE(img == nullptr); + + if (img != nullptr) { + heif_image_release(img); + } + } + + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.19.8/third-party/aom.cmd libheif-1.23.4/third-party/aom.cmd --- libheif-1.19.8/third-party/aom.cmd 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/third-party/aom.cmd 2026-09-06 14:45:17.000000000 +0000 @@ -10,11 +10,11 @@ : # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir): : # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat" -git clone -b v3.12.1 --depth 1 https://aomedia.googlesource.com/aom +git clone -b v3.15.0 --depth 1 https://aomedia.googlesource.com/aom cd aom -cmake -S . -B build.libavif -G Ninja -DCMAKE_INSTALL_PREFIX="$(pwd)/dist" -DCMAKE_BUILD_TYPE=Release -DENABLE_DOCS=0 -DENABLE_EXAMPLES=0 -DENABLE_TESTDATA=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0 +cmake -S . -B build.libavif -G Ninja -DCMAKE_INSTALL_PREFIX="$(pwd)/dist" -DCMAKE_BUILD_TYPE=Release -DENABLE_APPS=0 -DENABLE_DOCS=0 -DENABLE_EXAMPLES=0 -DENABLE_TESTDATA=0 -DENABLE_TESTS=0 -DENABLE_TOOLS=0 ninja -C build.libavif ninja -C build.libavif install cd .. diff -Nru libheif-1.19.8/third-party/dav1d.cmd libheif-1.23.4/third-party/dav1d.cmd --- libheif-1.19.8/third-party/dav1d.cmd 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/third-party/dav1d.cmd 2026-09-06 14:45:17.000000000 +0000 @@ -10,7 +10,7 @@ : # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir): : # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat" -git clone -b 1.5.0 --depth 1 https://code.videolan.org/videolan/dav1d.git +git clone -b 1.5.3 --depth 1 https://code.videolan.org/videolan/dav1d.git cd dav1d diff -Nru libheif-1.19.8/third-party/libpng.cmd libheif-1.23.4/third-party/libpng.cmd --- libheif-1.19.8/third-party/libpng.cmd 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.4/third-party/libpng.cmd 2026-09-06 14:45:17.000000000 +0000 @@ -0,0 +1,13 @@ +: # If you want to use a local libpng build, please check that the WITH_LIBPNG_INTERNAL CMake variable is set correctly. + +: # The odd choice of comment style in this file is to try to share this script between *nix and win32. + +git clone --single-branch https://github.com/pnggroup/libpng.git + +cd libpng +mkdir build +cd build +cmake -G Ninja -DCMAKE_INSTALL_PREFIX="$(pwd)/dist" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DPNG_TESTS=OFF -DPNG_TOOLS=OFF .. +ninja +ninja install +cd ../.. diff -Nru libheif-1.19.8/third-party/svt.cmd libheif-1.23.4/third-party/svt.cmd --- libheif-1.19.8/third-party/svt.cmd 2025-04-27 18:28:09.000000000 +0000 +++ libheif-1.23.4/third-party/svt.cmd 2026-09-06 14:45:17.000000000 +0000 @@ -2,10 +2,10 @@ : # cmake and ninja must be in your PATH for compiling. -: # If you want to enable the SVT-AV1 encoder, please check that the WITH_SvtEnc and WITH_SvtEnc_BUILTIN CMake variables are set correctly. +: # If you want to enable the SVT-AV1 encoder, please check that the WITH_SvtEnc and WITH_SvtEnc_PLUGIN CMake variables have correct values. : # You will also have to set the PKG_CONFIG_PATH to "third-party/SVT-AV1/Build/linux/Release" so that the local SVT-AV1 library is found. -git clone -b v3.0.0 --depth 1 https://gitlab.com/AOMediaCodec/SVT-AV1.git +git clone -b v4.1.0 --depth 1 https://gitlab.com/AOMediaCodec/SVT-AV1.git cd SVT-AV1 cd Build/linux