Version in base suite: 1.19.8-1+deb13u1 Version in overlay suite: 1.23.4-1~deb13u1 Base version: libheif_1.23.4-1~deb13u1 Target version: libheif_1.23.6-2~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/libh/libheif/libheif_1.23.4-1~deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/libh/libheif/libheif_1.23.6-2~deb13u1.dsc /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic |binary /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/corpus/unci-pixel-interleave-wide-tile-timeout.heic |binary /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/sequence_corpus/j2ki-siz-tile-alloc-oom.heif |binary /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/tests/data/pclr_zero_columns.heic |binary libheif-1.23.6/CMakeLists.txt | 10 libheif-1.23.6/README.md | 17 libheif-1.23.6/SECURITY.md | 8 libheif-1.23.6/debian/changelog | 120 - libheif-1.23.6/debian/control | 6 libheif-1.23.6/debian/rules | 4 libheif-1.23.6/examples/heif_dec.cc | 6 libheif-1.23.6/examples/heif_enc.cc | 20 libheif-1.23.6/examples/heif_gen_bayer.cc | 1 libheif-1.23.6/examples/heif_info.cc | 2 libheif-1.23.6/examples/sdl.hh | 2 libheif-1.23.6/funding.json | 2 libheif-1.23.6/fuzzing/box_fuzzer.cc | 2 libheif-1.23.6/heifio/decoder.h | 16 libheif-1.23.6/heifio/decoder_heif.cc | 13 libheif-1.23.6/heifio/decoder_raw.cc | 4 libheif-1.23.6/heifio/decoder_webp.cc | 14 libheif-1.23.6/libheif/api/libheif/heif_color.cc | 14 libheif-1.23.6/libheif/api/libheif/heif_context.cc | 20 libheif-1.23.6/libheif/api/libheif/heif_context.h | 11 libheif-1.23.6/libheif/api/libheif/heif_encoding.h | 5 libheif-1.23.6/libheif/api/libheif/heif_error.h | 7 libheif-1.23.6/libheif/api/libheif/heif_image.h | 19 libheif-1.23.6/libheif/api/libheif/heif_items.cc | 4 libheif-1.23.6/libheif/api/libheif/heif_regions.cc | 4 libheif-1.23.6/libheif/api/libheif/heif_tiling.cc | 2 libheif-1.23.6/libheif/api/libheif/heif_tiling.h | 12 libheif-1.23.6/libheif/bitstream.cc | 21 libheif-1.23.6/libheif/bitstream.h | 11 libheif-1.23.6/libheif/box.cc | 387 +-- libheif-1.23.6/libheif/box.h | 108 - libheif-1.23.6/libheif/codecs/avc_boxes.cc | 7 libheif-1.23.6/libheif/codecs/avc_boxes.h | 2 libheif-1.23.6/libheif/codecs/avc_dec.cc | 45 libheif-1.23.6/libheif/codecs/avc_dec.h | 2 libheif-1.23.6/libheif/codecs/avc_enc.cc | 14 libheif-1.23.6/libheif/codecs/avc_enc.h | 4 libheif-1.23.6/libheif/codecs/avif_boxes.cc | 192 + libheif-1.23.6/libheif/codecs/avif_boxes.h | 14 libheif-1.23.6/libheif/codecs/avif_dec.cc | 25 libheif-1.23.6/libheif/codecs/avif_dec.h | 2 libheif-1.23.6/libheif/codecs/avif_enc.cc | 10 libheif-1.23.6/libheif/codecs/avif_enc.h | 2 libheif-1.23.6/libheif/codecs/decoder.cc | 98 libheif-1.23.6/libheif/codecs/decoder.h | 41 libheif-1.23.6/libheif/codecs/encoder.cc | 22 libheif-1.23.6/libheif/codecs/encoder.h | 16 libheif-1.23.6/libheif/codecs/hevc_boxes.cc | 136 + libheif-1.23.6/libheif/codecs/hevc_boxes.h | 6 libheif-1.23.6/libheif/codecs/hevc_dec.cc | 41 libheif-1.23.6/libheif/codecs/hevc_dec.h | 2 libheif-1.23.6/libheif/codecs/hevc_enc.cc | 60 libheif-1.23.6/libheif/codecs/hevc_enc.h | 4 libheif-1.23.6/libheif/codecs/jpeg2000_boxes.cc | 87 libheif-1.23.6/libheif/codecs/jpeg2000_boxes.h | 26 libheif-1.23.6/libheif/codecs/jpeg2000_dec.cc | 28 libheif-1.23.6/libheif/codecs/jpeg2000_dec.h | 2 libheif-1.23.6/libheif/codecs/jpeg2000_enc.h | 8 libheif-1.23.6/libheif/codecs/jpeg_boxes.cc | 7 libheif-1.23.6/libheif/codecs/jpeg_boxes.h | 2 libheif-1.23.6/libheif/codecs/jpeg_dec.cc | 33 libheif-1.23.6/libheif/codecs/jpeg_dec.h | 7 libheif-1.23.6/libheif/codecs/jpeg_enc.cc | 8 libheif-1.23.6/libheif/codecs/jpeg_enc.h | 8 libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.cc | 70 libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.h | 20 libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.cc | 59 libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.h | 21 libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.cc | 324 ++- libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.h | 16 libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.cc | 14 libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.h | 5 libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc | 83 libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc | 74 libheif-1.23.6/libheif/codecs/uncompressed/unc_enc.h | 8 libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder.cc | 12 libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc | 5 libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc | 6 libheif-1.23.6/libheif/codecs/vvc_boxes.cc | 11 libheif-1.23.6/libheif/codecs/vvc_boxes.h | 2 libheif-1.23.6/libheif/codecs/vvc_dec.cc | 54 libheif-1.23.6/libheif/codecs/vvc_dec.h | 2 libheif-1.23.6/libheif/codecs/vvc_enc.cc | 14 libheif-1.23.6/libheif/codecs/vvc_enc.h | 4 libheif-1.23.6/libheif/color-conversion/alpha.cc | 437 ++-- libheif-1.23.6/libheif/color-conversion/bayer_bilinear.cc | 22 libheif-1.23.6/libheif/color-conversion/chroma_sampling.cc | 425 +-- libheif-1.23.6/libheif/color-conversion/colorconversion.cc | 469 +++- libheif-1.23.6/libheif/color-conversion/colorconversion.h | 124 - libheif-1.23.6/libheif/color-conversion/hdr_sdr.cc | 84 libheif-1.23.6/libheif/color-conversion/monochrome.cc | 39 libheif-1.23.6/libheif/color-conversion/rgb2rgb.cc | 98 libheif-1.23.6/libheif/color-conversion/rgb2yuv.cc | 84 libheif-1.23.6/libheif/color-conversion/rgb2yuv_sharp.cc | 54 libheif-1.23.6/libheif/color-conversion/yuv2rgb.cc | 104 libheif-1.23.6/libheif/compression.h | 30 libheif-1.23.6/libheif/compression_brotli.cc | 35 libheif-1.23.6/libheif/compression_zlib.cc | 23 libheif-1.23.6/libheif/context.cc | 21 libheif-1.23.6/libheif/file.cc | 3 libheif-1.23.6/libheif/file_layout.cc | 2 libheif-1.23.6/libheif/image-items/avc.cc | 2 libheif-1.23.6/libheif/image-items/avif.cc | 9 libheif-1.23.6/libheif/image-items/grid.cc | 144 + libheif-1.23.6/libheif/image-items/grid.h | 50 libheif-1.23.6/libheif/image-items/hevc.cc | 86 libheif-1.23.6/libheif/image-items/hevc.h | 2 libheif-1.23.6/libheif/image-items/iden.cc | 4 libheif-1.23.6/libheif/image-items/image_item.cc | 207 + libheif-1.23.6/libheif/image-items/image_item.h | 34 libheif-1.23.6/libheif/image-items/jpeg.cc | 9 libheif-1.23.6/libheif/image-items/jpeg2000.cc | 2 libheif-1.23.6/libheif/image-items/mask_image.cc | 7 libheif-1.23.6/libheif/image-items/mask_image.h | 2 libheif-1.23.6/libheif/image-items/overlay.cc | 135 - libheif-1.23.6/libheif/image-items/overlay.h | 7 libheif-1.23.6/libheif/image-items/tiled.cc | 12 libheif-1.23.6/libheif/image-items/tiled.h | 2 libheif-1.23.6/libheif/image-items/unc_image.cc | 68 libheif-1.23.6/libheif/image-items/unc_image.h | 5 libheif-1.23.6/libheif/image-items/vvc.cc | 2 libheif-1.23.6/libheif/image/image_description.cc | 13 libheif-1.23.6/libheif/image/image_description.h | 21 libheif-1.23.6/libheif/image/pixelimage.cc | 621 ++++- libheif-1.23.6/libheif/image/pixelimage.h | 66 libheif-1.23.6/libheif/logging.h | 37 libheif-1.23.6/libheif/mini.cc | 16 libheif-1.23.6/libheif/mini.h | 2 libheif-1.23.6/libheif/nclx.cc | 19 libheif-1.23.6/libheif/nclx.h | 8 libheif-1.23.6/libheif/omaf_boxes.cc | 7 libheif-1.23.6/libheif/omaf_boxes.h | 2 libheif-1.23.6/libheif/plugins/decoder_ffmpeg.cc | 205 + libheif-1.23.6/libheif/plugins/decoder_openjpeg.cc | 147 + libheif-1.23.6/libheif/plugins/encoder_aom.cc | 5 libheif-1.23.6/libheif/plugins/encoder_kvazaar.cc | 25 libheif-1.23.6/libheif/plugins/nalu_utils.cc | 7 libheif-1.23.6/libheif/security_limits.cc | 9 libheif-1.23.6/libheif/security_limits.h | 4 libheif-1.23.6/libheif/sequences/seq_boxes.cc | 267 +- libheif-1.23.6/libheif/sequences/seq_boxes.h | 57 libheif-1.23.6/libheif/sequences/track_visual.cc | 49 libheif-1.23.6/scripts/run-ci.sh | 14 libheif-1.23.6/tests/CMakeLists.txt | 42 libheif-1.23.6/tests/add_channel_checks.cc | 195 + libheif-1.23.6/tests/alpha_composite_decode.cc | 127 + libheif-1.23.6/tests/avc_box.cc | 2 libheif-1.23.6/tests/box_dump_limit.cc | 110 + libheif-1.23.6/tests/conversion.cc | 881 +++++++- libheif-1.23.6/tests/conversion_operator_contract.cc | 686 ++++++ libheif-1.23.6/tests/decoded_bit_depth_mismatch.cc | 225 ++ libheif-1.23.6/tests/encode.cc | 59 libheif-1.23.6/tests/encode_handle_queries.cc | 178 + libheif-1.23.6/tests/encode_hevc_bitdepth.cc | 185 + libheif-1.23.6/tests/encode_plane_layout.cc | 267 ++ libheif-1.23.6/tests/extend_to_size_checks.cc | 115 + libheif-1.23.6/tests/file_layout.cc | 11 libheif-1.23.6/tests/fill_channel.cc | 96 libheif-1.23.6/tests/hevc_brands.cc | 369 +++ libheif-1.23.6/tests/hevc_sps.cc | 147 + libheif-1.23.6/tests/idat.cc | 2 libheif-1.23.6/tests/inband_coded_size_limit.cc | 168 + libheif-1.23.6/tests/jpeg2000.cc | 246 ++ libheif-1.23.6/tests/jpeg2000_openjpeg_grid_limit.cc | 385 +++ libheif-1.23.6/tests/jpeg2000_signed_components.cc | 225 ++ libheif-1.23.6/tests/mini_box.cc | 10 libheif-1.23.6/tests/mini_security_limits.cc | 140 + libheif-1.23.6/tests/nal_split.cc | 75 libheif-1.23.6/tests/omaf_boxes.cc | 2 libheif-1.23.6/tests/overlay_amplification.cc | 5 libheif-1.23.6/tests/overlay_offsets.cc | 769 +++++++ libheif-1.23.6/tests/overlay_spec_parse.cc | 218 ++ libheif-1.23.6/tests/parallel_grid_deadlock.cc | 64 libheif-1.23.6/tests/pixelimage_overlay.cc | 593 +++++ libheif-1.23.6/tests/plane_layout.cc | 390 +++ libheif-1.23.6/tests/sdtp_sample_limit.cc | 88 libheif-1.23.6/tests/sequence_nonvisual_sample_entry.cc | 272 ++ libheif-1.23.6/tests/sequence_vfr_durations.cc | 175 + libheif-1.23.6/tests/tai.cc | 155 + libheif-1.23.6/tests/tiling_combined_images.cc | 1071 ++++++++++ libheif-1.23.6/tests/track_reference_duplicates.cc | 140 + libheif-1.23.6/tests/uncompressed_alpha_composite_depth_mismatch.cc | 300 ++ libheif-1.23.6/tests/uncompressed_bayer_depth_conversion.cc | 196 + libheif-1.23.6/tests/uncompressed_box.cc | 30 libheif-1.23.6/tests/uncompressed_encode.cc | 15 libheif-1.23.6/tests/uncompressed_generic_compression_tiles.cc | 810 +++++++ libheif-1.23.6/tests/uncompressed_grid_tile_mismatch.cc | 589 +++++ libheif-1.23.6/tests/uncompressed_grid_wide_alpha.cc | 406 +++ libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane.cc | 184 + libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane_internal.cc | 131 + libheif-1.23.6/tests/uncompressed_mixed_chroma_depth_colorconv.cc | 88 libheif-1.23.6/tests/uncompressed_mixed_interleave_bit_depth.cc | 440 ++++ libheif-1.23.6/tests/uncompressed_mixed_rgb_depth_to_sdr.cc | 183 + libheif-1.23.6/tests/uncompressed_sequence_tiles.cc | 397 +++ libheif-1.23.6/tests/uncompressed_tile_data_fetch.cc | 467 ++++ libheif-1.23.6/tests/uncompressed_tile_range_overflow.cc | 137 - libheif-1.23.6/tests/write_to_file.cc | 179 + libheif-1.23.6/tests/zero_length_memcpy.cc | 144 + libheif-1.23.6/third-party/aom.cmd | 2 203 files changed, 18450 insertions(+), 2217 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp7pyv1858/libheif_1.23.4-1~deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp7pyv1858/libheif_1.23.6-2~deb13u1.dsc: no acceptable signature found diff -Nru libheif-1.23.4/CMakeLists.txt libheif-1.23.6/CMakeLists.txt --- libheif-1.23.4/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/CMakeLists.txt 2026-10-05 16:16:00.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.23.4) +project(libheif LANGUAGES C CXX VERSION 1.23.6) # 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. @@ -589,6 +589,7 @@ 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_TESTING "Build the unit tests" ON) option(BUILD_DEVELOPMENT_TOOLS "Build development tools (heif-gen-bayer, etc.)" OFF) @@ -604,7 +605,11 @@ set(CMAKE_CXX_VISIBILITY_PRESET default) endif () -add_subdirectory(heifio) +# heifio is a helper library for reading and writing image files. +# It is only needed by the example programs and the unit tests. +if(WITH_EXAMPLES OR BUILD_TESTING) + add_subdirectory(heifio) +endif() if(WITH_EXAMPLES) add_subdirectory (examples) @@ -656,7 +661,6 @@ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) endif() -option(BUILD_TESTING "" ON) set(CMAKE_CTEST_ARGUMENTS "--output-on-failure") include(CTest) if(BUILD_TESTING) diff -Nru libheif-1.23.4/README.md libheif-1.23.6/README.md --- libheif-1.23.4/README.md 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/README.md 2026-10-05 16:16:00.000000000 +0000 @@ -15,8 +15,8 @@ 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. +> **Project status (September 2026).** libheif and libde265 are maintained by a single independent developer with almost +> no recurring funding, while 61 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). @@ -296,6 +296,13 @@ * 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 FFMPEG decoder handles HEVC images with 8, 9, 10 and 12 bits per sample only. Images with 11 bits or with 13 to 15 bits + have to be decoded with libde265. Use libde265 v1.1.3 or later for these, since earlier versions do not decode image sequences + with more than 12 bits correctly. + +* HEVC images can have up to 15 bits per sample. HEVC itself allows 16 bits, but the `hvcC` box of the file format cannot signal + this bit depth. libheif refuses to encode HEVC images with 16 bits per sample. + * 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 @@ -431,8 +438,8 @@ 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 +From January to September 2026, 61 security advisories were published for libheif, most of them found with +automated tools by organizations that use libheif in their products, and nine 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). @@ -470,7 +477,7 @@ ### Sponsors -As of August 2026, the recurring sponsorship for libheif and libde265 amounts to **$41 per month**, against a goal of +As of September 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. diff -Nru libheif-1.23.4/SECURITY.md libheif-1.23.6/SECURITY.md --- libheif-1.23.4/SECURITY.md 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/SECURITY.md 2026-10-05 16:16:00.000000000 +0000 @@ -108,6 +108,10 @@ * 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 that are only reachable with a codec library version that has a known, already fixed + vulnerability (for example an old OpenJPEG without its CVE-2020-6851 patch). libheif adds + pre-decode checks for such cases where they are cheap, but the vulnerability and its CVE + belong to the codec library. Build against a maintained codec version before reporting. * 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 @@ -148,8 +152,8 @@ 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 +To make the workload concrete: from January to September 2026, 61 security advisories were +published for libheif (4 rated critical, 20 high), and nine 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. diff -Nru libheif-1.23.4/debian/changelog libheif-1.23.6/debian/changelog --- libheif-1.23.4/debian/changelog 2026-09-09 06:41:39.000000000 +0000 +++ libheif-1.23.6/debian/changelog 2026-10-22 18:43:00.000000000 +0000 @@ -1,43 +1,89 @@ -libheif (1.23.4-1~deb13u1) trixie-security; urgency=high +libheif (1.23.6-2~deb13u1) trixie-security; urgency=high * Non-maintainer upload by the Security Team. - * 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. + * Rebuild sid's 1.23.6-2 for trixie-security, incorporating the security + fixes in upstream 1.23.5 and 1.23.6 described below. + * Disable heif-view and remove its binary package and SDL build dependency, + preserving trixie's binary package set. - -- Aron Xu Wed, 09 Sep 2026 14:41:39 +0800 + -- Aron Xu Fri, 23 Oct 2026 02:43:00 +0800 + +libheif (1.23.6-2) unstable; urgency=medium + + * Fix dependencies and building for kvazaar plugin on loong64. + * Correct changelog entries for upstream fixes in 1.23.5 / 1.23.6 + + -- Joachim Bauch Sun, 11 Oct 2026 12:02:02 +0200 + +libheif (1.23.6-1) unstable; urgency=medium + + * New upstream version 1.23.6 + * Various security fixes: + - Fixes memory exhaustion in the OpenJPEG decoder plugin. + See upstream commit c91d16e (GHSA-h4h8-qgvc-m7r2). + - Fixes memory exhaustion in the uncompressed decoder with generic + compression. See upstream commit 585607a (GHSA-fcmw-5764-7rq8). + - Fixes CPU exhaustion in the unci decoder with generic compression. + See upstream commit 5d35b98 (GHSA-6fqc-p7r8-2g36). + - Fixes heap out-of-bounds read in libsharpyuv. See upstream commit + 8dc3239 (GHSA-q7mw-2fmm-5q94). + - Fixes invalid bit depth reported for image handle. See upstream + commit 770b0d8 (GHSA-vv35-6hxg-95x8). + - Fixes null pointer dereference when decoding an image sequence. + See upstream commit 2b87e15 (GHSA-8p2c-9wv4-rx4p). + - Fixes applying image size limit with the FFmpeg decoder plugin. + See upstream commit 9e2c938 (GHSA-86ch-j429-fpr2). + - Fixes reachable assertion when decoding a grid image. + See upstream commit 858398f (GHSA-3gwx-cjv4-jr74). + - Fixes heap out-of-bounds read in the FFmpeg decoder plugin. + See upstream commit 1d24f41 (GHSA-hqpw-h8p6-22q9). + - Fixes use after free of an error message in the WebP/raw loaders + of heif-enc. See upstream commit a1645b6 (GHSA-hvwh-xpj6-ph5x). + - Fixes cases where decoded images could contain sample values above + their declared bit depth. See upstream commit 6e7c728 + (GHSA-v5rj-g4wv-j5w3). + * Unpackaged upstream version 1.23.5 contains the following security + fixes: + - Fixes memory exhaustion through container and bitstream image size + mismatch. See upstream commit 6ce2bba (GHSA-v8qw-hwjv-44hw). + - Fixes heap use-after-free and double free when encoding image with + TAI timestamp. See upstream commit 45a40ca (GHSA-qwpf-5wf7-r996). + - Fixes memory amplification through a JPEG 2000 pclr box. See + upstream commit ba47ddd (GHSA-9c75-9g8r-4728). + - Fixes heap out-of-bounds read in alpha compositing for uncompressed + (unci) images. See upstream commits e379fd5 and 6f58ba3 + (GHSA-r7gr-2xm2-23wf). + - Fixes incomplete OpenJPEG pre-decode security limits allowing + unsafe decode through the public API. + See upstream commit a44ad89 (GHSA-q492-cfcm-895h). + - Fixes heap out-of-bounds read in the uncompressed encode. + See upstream commit c727622 (GHSA-qfj5-c4pq-q998). + - Fixes applying heif_security_limits when parsing a mini box or the + av1C/hvcC blob embedded in it. + See upstream commit 24219cb (GHSA-7pwf-qh74-p35w). + * Enable kvazaar plugin on loong64 now that #1127385 is resolved. + + -- Joachim Bauch Sat, 10 Oct 2026 23:24:21 +0200 + +libheif (1.23.4-1) unstable; urgency=medium + + * New upstream version 1.23.4 + * Various security fixes (Closes: #1148137): + - Fixes unbound parsing of iinf child boxes. See upstream commits + 2ebb06f, 19fd108, eb339b7 and 92b79c6 (GHSA-vg7w-rp49-4fc2). + - Fixes unbounded recursion when checking reference cycles. + See upstream commits b1e4d72 and 7538a4f (GHSA-xrp2-63fq-jm8q). + - Fixes decoder deadlock through lock-order inversion. See upstream + commit f8beb40 (GHSA-prgh-72vc-3xmc). + - Fixes heap out-of-bounds read when encoding images with different + luma and chroma bit depths. See upstream commit 51360e1 + (GHSA-fqpw-fj22-78w4). + - Fixes memory leak in heif_track_get_next_raw_sequence_sample. + See upstream commit 3d9c246 (GHSA-4rv4-953r-p24q). + - Fixes heap out-of-bounds read in the WebCodecs decoder plugin. + See upstream commits fb8d945 and 604d837 (GHSA-rhgw-q5g8-xjh2). + + -- Joachim Bauch Thu, 17 Sep 2026 21:27:18 +0200 libheif (1.23.3-1) unstable; urgency=medium diff -Nru libheif-1.23.4/debian/control libheif-1.23.6/debian/control --- libheif-1.23.4/debian/control 2026-09-09 06:41:39.000000000 +0000 +++ libheif-1.23.6/debian/control 2026-10-22 18:43:00.000000000 +0000 @@ -14,7 +14,7 @@ libde265-dev (>= 1.0.7), libgdk-pixbuf-2.0-dev, libjpeg-dev, - libkvazaar-dev [!loong64], + libkvazaar-dev, libopenjp2-7-dev, libpng-dev, # librav1e-dev is built by src:rust-rav1e which is not available on all platforms. @@ -51,7 +51,7 @@ libheif-plugin-jpegenc (= ${binary:Version}), libheif-plugin-j2kdec (= ${binary:Version}), libheif-plugin-j2kenc (= ${binary:Version}), - libheif-plugin-kvazaar (= ${binary:Version}) [!loong64], + libheif-plugin-kvazaar (= ${binary:Version}), 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 @@ -372,7 +372,7 @@ libheif-plugin-j2kenc (>= ${source:Version}), libheif-plugin-jpegdec (>= ${source:Version}), libheif-plugin-jpegenc (>= ${source:Version}), - libheif-plugin-kvazaar (>= ${source:Version}) [!loong64], + libheif-plugin-kvazaar (>= ${source:Version}), 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}), diff -Nru libheif-1.23.4/debian/rules libheif-1.23.6/debian/rules --- libheif-1.23.4/debian/rules 2026-09-09 06:40:35.000000000 +0000 +++ libheif-1.23.6/debian/rules 2026-10-22 18:43:00.000000000 +0000 @@ -37,8 +37,8 @@ -DWITH_JPEG_DECODER_PLUGIN=ON \ -DWITH_JPEG_ENCODER=ON \ -DWITH_JPEG_ENCODER_PLUGIN=ON \ - -DWITH_KVAZAAR=$(if $(filter loong64,$(DEB_HOST_ARCH)),OFF,ON) \ - -DWITH_KVAZAAR_PLUGIN=$(if $(filter loong64,$(DEB_HOST_ARCH)),OFF,ON) \ + -DWITH_KVAZAAR=ON \ + -DWITH_KVAZAAR_PLUGIN=ON \ -DWITH_LIBDE265_PLUGIN=ON \ -DWITH_LIBSHARPYUV=ON \ -DWITH_OpenJPEG_DECODER=ON \ diff -Nru libheif-1.23.4/examples/heif_dec.cc libheif-1.23.6/examples/heif_dec.cc --- libheif-1.23.4/examples/heif_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/examples/heif_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -337,8 +337,8 @@ encoder->chroma(false, depth_bit_depth), nullptr); if (err.code) { - heif_image_handle_release(depth_handle); std::cerr << "Could not decode depth image: " << err.message << "\n"; + heif_image_handle_release(depth_handle); return 1; } @@ -392,17 +392,17 @@ encoder->chroma(false, aux_bit_depth), nullptr); if (err.code) { - heif_image_handle_release(aux_handle); std::cerr << "Could not decode auxiliary image: " << err.message << "\n"; + heif_image_handle_release(aux_handle); return 1; } const char* auxTypeC = nullptr; err = heif_image_handle_get_auxiliary_type(aux_handle, &auxTypeC); if (err.code) { + std::cerr << "Could not get type of auxiliary image: " << err.message << "\n"; heif_image_release(aux_image); heif_image_handle_release(aux_handle); - std::cerr << "Could not get type of auxiliary image: " << err.message << "\n"; return 1; } diff -Nru libheif-1.23.4/examples/heif_enc.cc libheif-1.23.6/examples/heif_enc.cc --- libheif-1.23.4/examples/heif_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/examples/heif_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -356,7 +356,6 @@ {(char* const) "list-encoders", no_argument, &list_encoders, 1}, {(char* const) "encoder", required_argument, 0, 'e'}, {(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}, @@ -1906,6 +1905,11 @@ case OPTION_RAW_ENDIAN: raw_input_params.big_endian = (std::string(optarg) == "big"); break; + case '?': + // An unknown option, or an option without its argument. getopt_long() has + // printed which one it is. + std::cerr << "Use '" << argv[0] << " --help' for the list of options.\n"; + return 5; } } @@ -2052,9 +2056,12 @@ return 5; } + // Releases the encoder on every way out of main(). This is defined after the context, so + // the encoder is released before the context is freed. + std::unique_ptr encoder_releaser(encoder, heif_encoder_release); + if (option_show_parameters) { list_encoder_parameters(encoder); - heif_encoder_release(encoder); return 0; } @@ -2101,6 +2108,8 @@ set_params(encoder, raw_params); struct heif_encoding_options* options = heif_encoding_options_alloc(); + std::unique_ptr options_releaser(options, heif_encoding_options_free); + options->save_two_colr_boxes_when_ICC_and_nclx_available = (uint8_t) two_colr_boxes; if (chroma_downsampling == "average") { @@ -2153,8 +2162,6 @@ } if (ret != 0) { - heif_encoding_options_free(options); - heif_encoder_release(encoder); return ret; } @@ -2162,8 +2169,6 @@ ret = add_mime_item(context.get()); if (ret != 0) { - heif_encoding_options_free(options); - heif_encoder_release(encoder); return ret; } @@ -2180,9 +2185,6 @@ return 5; } - heif_encoding_options_free(options); - heif_encoder_release(encoder); - return 0; } diff -Nru libheif-1.23.4/examples/heif_gen_bayer.cc libheif-1.23.6/examples/heif_gen_bayer.cc --- libheif-1.23.4/examples/heif_gen_bayer.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/examples/heif_gen_bayer.cc 2026-10-05 16:16:00.000000000 +0000 @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include diff -Nru libheif-1.23.4/examples/heif_info.cc libheif-1.23.6/examples/heif_info.cc --- libheif-1.23.4/examples/heif_info.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/examples/heif_info.cc 2026-10-05 16:16:00.000000000 +0000 @@ -991,8 +991,8 @@ 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); std::cerr << "Could not decode primage image: " << err.message << "\n"; + heif_image_handle_release(handle); return 1; } diff -Nru libheif-1.23.4/examples/sdl.hh libheif-1.23.6/examples/sdl.hh --- libheif-1.23.4/examples/sdl.hh 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/examples/sdl.hh 2026-10-05 16:16:00.000000000 +0000 @@ -24,7 +24,7 @@ SOFTWARE. */ -#include +#include #include #include diff -Nru libheif-1.23.4/funding.json libheif-1.23.6/funding.json --- libheif-1.23.4/funding.json 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/funding.json 2026-10-05 16:16:00.000000000 +0000 @@ -7,7 +7,7 @@ "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.", + "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, 61 security advisories had to be investigated, fixed and released in unpaid evenings and weekends.", "webpageUrl": { "url": "https://github.com/farindk" } diff -Nru libheif-1.23.4/fuzzing/box_fuzzer.cc libheif-1.23.6/fuzzing/box_fuzzer.cc --- libheif-1.23.4/fuzzing/box_fuzzer.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/fuzzing/box_fuzzer.cc 2026-10-05 16:16:00.000000000 +0000 @@ -47,7 +47,7 @@ box->get_type(); box->get_type_string(); Indent indent; - box->dump(indent); + box->dump_to_string(indent); } return 0; Binary files /srv/release.debian.org/tmp/Xg5l_5F0C7/libheif-1.23.4/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic and /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/corpus/j2kH-sdtp-dump-timeout.heic differ Binary files /srv/release.debian.org/tmp/Xg5l_5F0C7/libheif-1.23.4/fuzzing/data/corpus/unci-pixel-interleave-wide-tile-timeout.heic and /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/corpus/unci-pixel-interleave-wide-tile-timeout.heic differ Binary files /srv/release.debian.org/tmp/Xg5l_5F0C7/libheif-1.23.4/fuzzing/data/sequence_corpus/j2ki-siz-tile-alloc-oom.heif and /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/fuzzing/data/sequence_corpus/j2ki-siz-tile-alloc-oom.heif differ diff -Nru libheif-1.23.4/heifio/decoder.h libheif-1.23.6/heifio/decoder.h --- libheif-1.23.4/heifio/decoder.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/heifio/decoder.h 2026-10-05 16:16:00.000000000 +0000 @@ -29,6 +29,7 @@ #include "libheif/heif.h" #include +#include #include struct InputImage @@ -39,4 +40,19 @@ heif_orientation orientation = heif_orientation_normal; }; + +// libheif's heif_error.message is owned by the object whose function returned the error +// (the heif_context, heif_image_handle or heif_image) and becomes invalid once that object +// is released. A loader that releases the object and then returns the error has to copy +// the message first (GHSA-hvwh-xpj6-ph5x). This copies it into a thread-local string, so +// that it stays valid for the caller and 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). +inline 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()}; +} + #endif //LIBHEIF_DECODER_H diff -Nru libheif-1.23.4/heifio/decoder_heif.cc libheif-1.23.6/heifio/decoder_heif.cc --- libheif-1.23.4/heifio/decoder_heif.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/heifio/decoder_heif.cc 2026-10-05 16:16:00.000000000 +0000 @@ -32,19 +32,6 @@ 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(); diff -Nru libheif-1.23.4/heifio/decoder_raw.cc libheif-1.23.6/heifio/decoder_raw.cc --- libheif-1.23.4/heifio/decoder_raw.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/heifio/decoder_raw.cc 2026-10-05 16:16:00.000000000 +0000 @@ -191,8 +191,10 @@ params.datatype, params.bit_depth, &component_idx); if (err.code != heif_error_Ok) { + // the message is stored in the image that we release + heif_error stable = stable_error(err); heif_image_release(image); - return err; + return stable; } // Get writable pointer and copy data row by row diff -Nru libheif-1.23.4/heifio/decoder_webp.cc libheif-1.23.6/heifio/decoder_webp.cc --- libheif-1.23.4/heifio/decoder_webp.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/heifio/decoder_webp.cc 2026-10-05 16:16:00.000000000 +0000 @@ -192,9 +192,11 @@ image_ptr = std::shared_ptr(image, [](heif_image* img) { heif_image_release(img); }); + // The message of an error of heif_image_add_plane() is stored in the image, which is + // released when we return. err = heif_image_add_plane(image, heif_channel_interleaved, (int)width, (int)height, 8); if (err.code) - return err; + return stable_error(err); size_t stride; uint8_t* ptr = heif_image_get_plane2(image, heif_channel_interleaved, &stride); // decode into heif image @@ -241,19 +243,21 @@ uint8_t* ptr[4] = {}; const int uv_width = (width + 1) / 2; const int uv_height = (height + 1) / 2; + // The message of an error of heif_image_add_plane() is stored in the image, which is + // released when we return. err = heif_image_add_plane(image, heif_channel_Y, (int)width, (int)height, 8); if (err.code) - return err; + return stable_error(err); err = heif_image_add_plane(image, heif_channel_Cb, uv_width, uv_height, 8); if (err.code) - return err; + return stable_error(err); err = heif_image_add_plane(image, heif_channel_Cr, uv_width, uv_height, 8); if (err.code) - return err; + return stable_error(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; + return stable_error(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]); diff -Nru libheif-1.23.4/libheif/api/libheif/heif_color.cc libheif-1.23.6/libheif/api/libheif/heif_color.cc --- libheif-1.23.4/libheif/api/libheif/heif_color.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_color.cc 2026-10-05 16:16:00.000000000 +0000 @@ -139,9 +139,10 @@ 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()); + const auto& profile_data = raw_profile->get_data(); + if (!profile_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(out_data, profile_data.data(), profile_data.size()); + } } else { Error err(heif_error_Color_profile_does_not_exist, @@ -347,9 +348,10 @@ 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()); + const auto& profile_data = raw_profile->get_data(); + if (!profile_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(out_data, profile_data.data(), profile_data.size()); + } } else { Error err(heif_error_Color_profile_does_not_exist, diff -Nru libheif-1.23.4/libheif/api/libheif/heif_context.cc libheif-1.23.6/libheif/api/libheif/heif_context.cc --- libheif-1.23.4/libheif/api/libheif/heif_context.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_context.cc 2026-10-05 16:16:00.000000000 +0000 @@ -252,8 +252,24 @@ #else std::ofstream ostr(filename, std::ios_base::binary); #endif - ostr.write(static_cast(data), size); - // TODO: handle write errors + if (!ostr) { + return Error(heif_error_Encoding_error, + heif_suberror_Cannot_write_output_data, + std::string("Cannot open file '") + filename + "' for writing").error_struct(ctx->context.get()); + } + + ostr.write(static_cast(data), static_cast(size)); + + // Part of the data may still be in the buffer of the stream. It is written when the file is + // closed, so a full disk may only show up here. + ostr.close(); + + if (!ostr) { + return Error(heif_error_Encoding_error, + heif_suberror_Cannot_write_output_data, + std::string("Cannot write to file '") + filename + "'").error_struct(ctx->context.get()); + } + return Error::Ok.error_struct(ctx->context.get()); } diff -Nru libheif-1.23.4/libheif/api/libheif/heif_context.h libheif-1.23.6/libheif/api/libheif/heif_context.h --- libheif-1.23.4/libheif/api/libheif/heif_context.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_context.h 2026-10-05 16:16:00.000000000 +0000 @@ -119,12 +119,15 @@ // ========================= 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. +// A heif_context represents a HEIF file. It is used either for reading a file +// (heif_context_read_from_...() and decoding its images), or for writing a new file +// (encoding images into the context and heif_context_write()), but not for both. +// A context that has been read from a file cannot be written, and an image that has been +// added to a context by encoding cannot be decoded from that context: write the file and +// read it into a new context for that. -// Allocate a new context for reading HEIF files. +// Allocate a new context for reading or for writing a HEIF file. // Has to be freed again with heif_context_free(). LIBHEIF_API heif_context* heif_context_alloc(void); diff -Nru libheif-1.23.4/libheif/api/libheif/heif_encoding.h libheif-1.23.6/libheif/api/libheif/heif_encoding.h --- libheif-1.23.4/libheif/api/libheif/heif_encoding.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_encoding.h 2026-10-05 16:16:00.000000000 +0000 @@ -346,6 +346,11 @@ // '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(). +// +// The returned handle refers to an image that is being written. Use it to attach thumbnails, +// metadata or properties to the image, or to make it the primary image. It cannot be used +// to decode the image, and the functions that report the coded format of an image (bit +// depths, preferred colorspace, alpha channel) return an error or "unknown" for it. LIBHEIF_API heif_error heif_context_encode_image(heif_context*, const heif_image* image, diff -Nru libheif-1.23.4/libheif/api/libheif/heif_error.h libheif-1.23.6/libheif/api/libheif/heif_error.h --- libheif-1.23.4/libheif/api/libheif/heif_error.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_error.h 2026-10-05 16:16:00.000000000 +0000 @@ -297,6 +297,13 @@ heif_suberror_code subcode; // textual error message (is always defined, you do not have to check for NULL) + // + // The string is owned by libheif. For many errors, it is stored in the object on which the + // function that returned the error was called (the heif_context, heif_image_handle, + // heif_image, heif_track, ...). It is only valid until the next function is called on that + // object, and at most until that object or the heif_context it belongs to is released. + // If you need the message for longer, copy it. In particular, read or copy it before you + // free the heif_context or release the image whose function returned the error. const char* message; } heif_error; diff -Nru libheif-1.23.4/libheif/api/libheif/heif_image.h libheif-1.23.6/libheif/api/libheif/heif_image.h --- libheif-1.23.4/libheif/api/libheif/heif_image.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_image.h 2026-10-05 16:16:00.000000000 +0000 @@ -298,6 +298,10 @@ // Extends the image size to match the given size by extending the right and bottom borders. // The border areas are filled with zero. +// The target 'width' and 'height' must each be at least the image's current size; this +// function only grows the image and cannot shrink it. If a smaller size is requested, +// 'heif_error_Usage_error' / 'heif_suberror_Invalid_parameter_value' is returned and the +// image is left unchanged. LIBHEIF_API heif_error heif_image_extend_to_size_fill_with_zero(heif_image* image, uint32_t width, uint32_t height); @@ -364,9 +368,24 @@ * 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}. * + *

The interleaved formats define the bit depth of their components: + * {@code heif_chroma_interleaved_RGB} and {@code heif_chroma_interleaved_RGBA} have 8 bits, + * the {@code heif_chroma_interleaved_RRGGBB...} formats have 9 to 16 bits. + * Other bit depths are rejected for these formats. + * *

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. * + *

Planes with a bit depth of 9 to 16 bits store each sample in a 16-bit word. The sample + * values that are written into the plane have to be within the range of {@code bit_depth} bits, + * i.e. the unused upper bits must be zero. The encoding functions return an error for an image + * that contains larger values. + * + *

An image with an interleaved chroma format carries its alpha inside the interleaved + * plane. Adding a separate {@code heif_channel_Alpha} plane to such an image is rejected + * with an error. Use one of the interleaved formats with alpha + * (e.g. {@code heif_chroma_interleaved_RGBA}) instead. + * * @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 diff -Nru libheif-1.23.4/libheif/api/libheif/heif_items.cc libheif-1.23.6/libheif/api/libheif/heif_items.cc --- libheif-1.23.4/libheif/api/libheif/heif_items.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_items.cc 2026-10-05 16:16:00.000000000 +0000 @@ -167,7 +167,9 @@ if (out_data) { *out_data = new uint8_t[dataResult->size()]; - memcpy(*out_data, dataResult->data(), dataResult->size()); + if (!dataResult->empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(*out_data, dataResult->data(), dataResult->size()); + } } return heif_error_success; diff -Nru libheif-1.23.4/libheif/api/libheif/heif_regions.cc libheif-1.23.6/libheif/api/libheif/heif_regions.cc --- libheif-1.23.4/libheif/api/libheif/heif_regions.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_regions.cc 2026-10-05 16:16:00.000000000 +0000 @@ -748,7 +748,9 @@ *y = mask->y; *width = mask->width; *height = mask->height; - memcpy(data, mask->mask_data.data(), mask->mask_data.size()); + if (!mask->mask_data.empty()) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(data, mask->mask_data.data(), mask->mask_data.size()); + } return heif_error_success; } return heif_error_invalid_parameter_value; diff -Nru libheif-1.23.4/libheif/api/libheif/heif_tiling.cc libheif-1.23.6/libheif/api/libheif/heif_tiling.cc --- libheif-1.23.4/libheif/api/libheif/heif_tiling.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_tiling.cc 2026-10-05 16:16:00.000000000 +0000 @@ -40,7 +40,7 @@ return heif_error_null_pointer_argument; } - *tiling = handle->image->get_heif_image_tiling(); + *tiling = handle->image->get_image_tiling_with_alpha(); // 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 diff -Nru libheif-1.23.4/libheif/api/libheif/heif_tiling.h libheif-1.23.6/libheif/api/libheif/heif_tiling.h --- libheif-1.23.4/libheif/api/libheif/heif_tiling.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/api/libheif/heif_tiling.h 2026-10-05 16:16:00.000000000 +0000 @@ -63,6 +63,9 @@ // 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). +// The tiling describes the tiles that can be decoded independently. An image is reported +// as a single tile when its internal tiles cannot be decoded on their own, or when its +// alpha channel is stored with a different tiling. 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); @@ -96,6 +99,10 @@ /** * @brief Encodes an array of images into a grid. * + * All tiles have to have the same size, colorspace, chroma format and bit depths, and they have + * to consist of the same components. For example, either all tiles have an alpha channel or + * none of them has. A grid whose tiles differ in this is refused when it is decoded. + * * @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. @@ -123,6 +130,11 @@ const heif_encoding_options* encoding_options, heif_image_handle** out_grid_image_handle); +// Encodes an image and adds it as the tile at position (tile_x; tile_y) of a tiled image, +// for example one that was created with heif_context_add_grid_image(). +// All tiles of an image have to have the same colorspace, chroma format and bit depths, and +// they have to consist of the same components. For example, either all tiles have an alpha +// channel or none of them has. A grid whose tiles differ in this is refused when it is decoded. LIBHEIF_API heif_error heif_context_add_image_tile(heif_context* ctx, heif_image_handle* tiled_image, diff -Nru libheif-1.23.4/libheif/bitstream.cc libheif-1.23.6/libheif/bitstream.cc --- libheif-1.23.4/libheif/bitstream.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/bitstream.cc 2026-10-05 16:16:00.000000000 +0000 @@ -79,7 +79,9 @@ { if (copy) { m_owned_data = new uint8_t[m_length]; - memcpy(m_owned_data, data, size); + if (size > 0) { // memcpy() from a NULL pointer is UB even for size 0 (until C2y/N3322), see read() + memcpy(m_owned_data, data, size); + } m_data = m_owned_data; } @@ -112,7 +114,14 @@ return false; } - memcpy(data, &m_data[m_position], size); + // Do not call memcpy() with a NULL pointer, even when size == 0. 'data' is NULL when the + // caller reads into an empty std::vector (e.g. a box with an empty payload), and passing + // NULL to memcpy() is undefined behaviour in C17 and C++. It trips UBSan's nonnull-attribute + // check on glibc's memcpy() declaration. C2y (WG14 N3322) makes zero-length operations on + // NULL pointers well-defined, but we cannot rely on that for many years. + if (size > 0) { + memcpy(data, &m_data[m_position], size); + } m_position += size; return true; @@ -1053,7 +1062,9 @@ m_data.resize(required_size); } - memcpy(m_data.data() + m_position, vec.data(), vec.size()); + if (!vec.empty()) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(m_data.data() + m_position, vec.data(), vec.size()); + } m_position += vec.size(); } @@ -1068,7 +1079,9 @@ const auto& data = writer.get_data(); - memcpy(m_data.data() + m_position, data.data(), data.size()); + if (!data.empty()) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(m_data.data() + m_position, data.data(), data.size()); + } m_position += data.size(); } diff -Nru libheif-1.23.4/libheif/bitstream.h libheif-1.23.6/libheif/bitstream.h --- libheif-1.23.4/libheif/bitstream.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/bitstream.h 2026-10-05 16:16:00.000000000 +0000 @@ -159,7 +159,16 @@ StreamReader::grow_status wait_for_file_size(uint64_t target_size) override; - bool read(void* data, size_t size) override { return !m_func_table->read(data, size, m_userdata); } + bool read(void* data, size_t size) override + { + if (size == 0) { + // Do not hand a NULL buffer (an empty std::vector) to the user's read callback; it may pass + // it on to memcpy(), which is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read(). + return true; + } + + return !m_func_table->read(data, size, m_userdata); + } bool seek(uint64_t position) override { return !m_func_table->seek(position, m_userdata); } diff -Nru libheif-1.23.4/libheif/box.cc libheif-1.23.6/libheif/box.cc --- libheif-1.23.4/libheif/box.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/box.cc 2026-10-05 16:16:00.000000000 +0000 @@ -404,9 +404,8 @@ } -std::string BoxHeader::dump(Indent& indent) const +void BoxHeader::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; sstr << indent << "Box: " << get_type_string(); const char* debug_name = debug_box_name(); if (debug_name) { @@ -418,7 +417,7 @@ sstr << indent << "size: " << get_box_size() << " (header size: " << get_header_size() << ")\n"; - return sstr.str(); + return; } @@ -1025,26 +1024,24 @@ } -std::string Box::dump(Indent& indent) const +void Box::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << BoxHeader::dump(indent); + BoxHeader::dump(sstr, indent, full_log); - return sstr.str(); + return; } -std::string FullBox::dump(Indent& indent) const +void FullBox::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "version: " << ((int) m_version) << "\n" - << indent << "flags: " << std::hex << m_flags << "\n"; + << indent << "flags: " << std::hex << m_flags << std::dec << "\n"; - return sstr.str(); + return; } @@ -1212,10 +1209,8 @@ } -std::string Box::dump_children(Indent& indent, bool with_index) const +void Box::dump_children(std::ostream& sstr, Indent& indent, bool full_log, bool with_index) const { - std::ostringstream sstr; - bool first = true; int idx=1; @@ -1233,11 +1228,10 @@ idx++; } - sstr << childBox->dump(indent); + childBox->dump(sstr, indent, full_log); + reset_stream_format(sstr); } indent--; - - return sstr.str(); } @@ -1307,11 +1301,10 @@ } -std::string Box_other::dump(Indent& indent) const +void Box_other::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << BoxHeader::dump(indent); + BoxHeader::dump(sstr, indent, full_log); // --- show raw box content @@ -1322,20 +1315,19 @@ } else { sstr << indent << "invalid box size " << get_box_size() << " (smaller than header)\n"; - return sstr.str(); + return; } sstr << write_raw_data_as_hex(m_data.data(), len, indent.get_string() + "data: ", indent.get_string() + " "); - return sstr.str(); + return; } -std::string Box_Error::dump(Indent& indent) const +void Box_Error::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; sstr << indent << '\'' << fourcc_to_string(m_box_type_with_parse_error) << "' parse error: " << m_error.message << "\n"; sstr << indent << "fatality: "; switch (m_fatality) { @@ -1344,7 +1336,7 @@ case parse_error_fatality::optional: sstr << "optional\n"; break; } - return sstr.str(); + return; } parse_error_fatality Box_Error::get_parse_error_fatality() const @@ -1393,11 +1385,10 @@ } -std::string Box_ftyp::dump(Indent& indent) const +void Box_ftyp::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << BoxHeader::dump(indent); + BoxHeader::dump(sstr, indent, full_log); sstr << indent << "major brand: " << fourcc_to_string(m_major_brand) << "\n" << indent << "minor version: "; @@ -1419,7 +1410,7 @@ } sstr << "\n"; - return sstr.str(); + return; } @@ -1455,11 +1446,10 @@ } -std::string Box_free::dump(Indent& indent) const +void Box_free::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << BoxHeader::dump(indent); - return sstr.str(); + BoxHeader::dump(sstr, indent, full_log); + return; } @@ -1493,13 +1483,12 @@ } -std::string Box_meta::dump(Indent& indent) const +void Box_meta::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << dump_children(indent); + Box::dump(sstr, indent, full_log); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -1535,15 +1524,14 @@ } -std::string Box_hdlr::dump(Indent& indent) const +void Box_hdlr::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "pre_defined: " << m_pre_defined << "\n" << indent << "handler_type: " << fourcc_to_string(m_handler_type) << "\n" << indent << "name: " << m_name << "\n"; - return sstr.str(); + return; } @@ -1586,13 +1574,12 @@ } -std::string Box_pitm::dump(Indent& indent) const +void Box_pitm::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "item_ID: " << m_item_ID << "\n"; - return sstr.str(); + return; } @@ -1804,10 +1791,9 @@ } -std::string Box_iloc::dump(Indent& indent) const +void Box_iloc::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const Item& item : m_items) { sstr << indent << "item ID: " << item.item_ID << "\n" @@ -1827,7 +1813,7 @@ sstr << "\n"; } - return sstr.str(); + return; } @@ -2628,10 +2614,9 @@ } -std::string Box_infe::dump(Indent& indent) const +void Box_infe::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "item_ID: " << m_item_ID << "\n" << indent << "item_protection_index: " << m_item_protection_index << "\n" @@ -2649,7 +2634,7 @@ sstr << indent << "hidden item: " << std::boolalpha << m_hidden_item << "\n"; - return sstr.str(); + return; } @@ -2691,14 +2676,13 @@ } -std::string Box_iinf::dump(Indent& indent) const +void Box_iinf::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); - sstr << dump_children(indent); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -2738,14 +2722,13 @@ } -std::string Box_iprp::dump(Indent& indent) const +void Box_iprp::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); - sstr << dump_children(indent); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -2768,14 +2751,13 @@ } -std::string Box_ipco::dump(Indent& indent) const +void Box_ipco::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); - sstr << dump_children(indent, true); + dump_children(sstr, indent, full_log, true); - return sstr.str(); + return; } @@ -2805,10 +2787,9 @@ } -std::string Box_pixi::dump(Indent& indent) const +void Box_pixi::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "bits_per_channel: "; @@ -2819,7 +2800,7 @@ sstr << "\n"; - return sstr.str(); + return; } @@ -2855,15 +2836,14 @@ } -std::string Box_pasp::dump(Indent& indent) const +void Box_pasp::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "hSpacing: " << hSpacing << "\n"; sstr << indent << "vSpacing: " << vSpacing << "\n"; - return sstr.str(); + return; } @@ -2888,14 +2868,13 @@ } -std::string Box_lsel::dump(Indent& indent) const +void Box_lsel::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "layer_id: " << layer_id << "\n"; - return sstr.str(); + return; } @@ -2922,15 +2901,14 @@ } -std::string Box_clli::dump(Indent& indent) const +void Box_clli::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "max_content_light_level: " << clli.max_content_light_level << "\n"; sstr << indent << "max_pic_average_light_level: " << clli.max_pic_average_light_level << "\n"; - return sstr.str(); + return; } @@ -2973,10 +2951,9 @@ } -std::string Box_mdcv::dump(Indent& indent) const +void Box_mdcv::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "display_primaries (x,y): "; sstr << "(" << mdcv.display_primaries_x[0] << ";" << mdcv.display_primaries_y[0] << "), "; @@ -2987,7 +2964,7 @@ sstr << indent << "max display mastering luminance: " << mdcv.max_display_mastering_luminance << "\n"; sstr << indent << "min display mastering luminance: " << mdcv.min_display_mastering_luminance << "\n"; - return sstr.str(); + return; } @@ -3031,16 +3008,15 @@ } -std::string Box_amve::dump(Indent& indent) const +void Box_amve::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "ambient_illumination: " << amve.ambient_illumination << "\n"; sstr << indent << "ambient_light_x: " << amve.ambient_light_x << "\n"; sstr << indent << "ambient_light_y: " << amve.ambient_light_y << "\n"; - return sstr.str(); + return; } @@ -3072,14 +3048,13 @@ } -std::string Box_ndwt::dump(Indent& indent) const +void Box_ndwt::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "diffuse_white_luminance: " << m_diffuse_white_luminance << "\n"; - return sstr.str(); + return; } @@ -3160,10 +3135,9 @@ } -std::string Box_cclv::dump(Indent& indent) const +void Box_cclv::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "ccv_primaries_present_flag: " << m_ccv_primaries_valid << "\n"; if (m_ccv_primaries_valid) { @@ -3177,7 +3151,7 @@ sstr << indent << "ccv_max_luminance_value: " << m_ccv_max_luminance_value << "\n"; sstr << indent << "ccv_avg_luminance_value: " << m_ccv_avg_luminance_value << "\n"; - return sstr.str(); + return; } @@ -3310,15 +3284,14 @@ } -std::string Box_ispe::dump(Indent& indent) const +void Box_ispe::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "image width: " << m_image_width << "\n" << indent << "image height: " << m_image_height << "\n"; - return sstr.str(); + return; } @@ -3500,10 +3473,9 @@ } -std::string Box_ipma::dump(Indent& indent) const +void Box_ipma::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const Entry& entry : m_entries) { sstr << indent << "associations for item ID: " << entry.item_ID << "\n"; @@ -3515,7 +3487,7 @@ indent--; } - return sstr.str(); + return; } @@ -3644,10 +3616,9 @@ } -std::string Box_auxC::dump(Indent& indent) const +void Box_auxC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "aux type: " << m_aux_type << "\n" << indent << "aux subtypes: "; @@ -3657,7 +3628,7 @@ sstr << "\n"; - return sstr.str(); + return; } @@ -3686,14 +3657,13 @@ } -std::string Box_irot::dump(Indent& indent) const +void Box_irot::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "rotation: " << m_rotation << " degrees (CCW)\n"; - return sstr.str(); + return; } @@ -3725,10 +3695,9 @@ } -std::string Box_imir::dump(Indent& indent) const +void Box_imir::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "mirror direction: "; switch (m_axis) { @@ -3743,7 +3712,7 @@ break; } - return sstr.str(); + return; } @@ -3786,15 +3755,14 @@ } -std::string Box_iscl::dump(Indent& indent) const +void Box_iscl::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); 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(); + return; } @@ -3856,10 +3824,9 @@ } -std::string Box_clap::dump(Indent& indent) const +void Box_clap::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "clean_aperture: " << m_clean_aperture_width.numerator << "/" << m_clean_aperture_width.denominator << " x " @@ -3870,7 +3837,7 @@ << m_vertical_offset.numerator << "/" << m_vertical_offset.denominator << "\n"; - return sstr.str(); + return; } @@ -4045,11 +4012,10 @@ } - // --- check for duplicate references - - if (auto error = check_for_double_references()) { - return error; - } + // Note: the same item may be listed several times within one reference entry. + // ISO/IEC 14496-12 does not forbid it, and derived images rely on it: an 'iovl' + // that places the same input image at two positions references it twice in its + // 'dimg' entry (one offset per reference). Conformance file C021 does this. #if 0 @@ -4148,26 +4114,6 @@ } -Error Box_iref::check_for_double_references() const -{ - for (const auto& ref : m_references) { - std::set to_ids; - for (const auto to_id : ref.to_item_ID) { - if (to_ids.find(to_id) == to_ids.end()) { - to_ids.insert(to_id); - } - else { - return {heif_error_Invalid_input, - heif_suberror_Unspecified, - "'iref' has double references"}; - } - } - } - - return Error::Ok; -} - - void Box_iref::derive_box_version() { uint8_t version = 0; @@ -4192,10 +4138,6 @@ Error Box_iref::write(StreamWriter& writer) const { - if (auto error = check_for_double_references()) { - return error; - } - size_t box_start = reserve_box_header_space(writer); int id_size = ((get_version() == 0) ? 2 : 4); @@ -4221,10 +4163,9 @@ } -std::string Box_iref::dump(Indent& indent) const +void Box_iref::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& ref : m_references) { sstr << indent << "reference with type '" << ref.header.get_type_string() << "'" @@ -4236,7 +4177,7 @@ sstr << "\n"; } - return sstr.str(); + return; } @@ -4450,10 +4391,9 @@ } -std::string Box_rref::dump(Indent& indent) const +void Box_rref::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "reference types: "; for (size_t i = 0; i < m_reference_types.size(); i++) { @@ -4462,7 +4402,7 @@ } sstr << "\n"; - return sstr.str(); + return; } @@ -4488,10 +4428,9 @@ } -std::string Box_idat::dump(Indent& indent) const +void Box_idat::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); if (get_box_size() >= get_header_size()) { sstr << indent << "number of data bytes: " << get_box_size() - get_header_size() << "\n"; @@ -4499,7 +4438,7 @@ sstr << indent << "number of data bytes is invalid\n"; } - return sstr.str(); + return; } @@ -4570,12 +4509,11 @@ } -std::string Box_grpl::dump(Indent& indent) const +void Box_grpl::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << dump_children(indent); - return sstr.str(); + Box::dump(sstr, indent, full_log); + dump_children(sstr, indent, full_log); + return; } @@ -4642,10 +4580,9 @@ } -std::string Box_EntityToGroup::dump(Indent& indent) const +void Box_EntityToGroup::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "group id: " << group_id << "\n" << indent << "entity IDs: "; @@ -4664,7 +4601,7 @@ sstr << "\n"; - return sstr.str(); + return; } @@ -4685,16 +4622,15 @@ } -std::string Box_ster::dump(Indent& indent) const +void Box_ster::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "group id: " << group_id << "\n" << indent << "left image ID: " << entity_ids[0] << "\n" << indent << "right image ID: " << entity_ids[1] << "\n"; - return sstr.str(); + return; } @@ -4745,10 +4681,9 @@ } -std::string Box_pymd::dump(Indent& indent) const +void Box_pymd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box_EntityToGroup::dump(indent); + Box_EntityToGroup::dump(sstr, indent, full_log); sstr << indent << "tile size: " << tile_size_x << "x" << tile_size_y << "\n"; @@ -4761,7 +4696,7 @@ layerNr++; } - return sstr.str(); + return; } @@ -4773,13 +4708,12 @@ } -std::string Box_dinf::dump(Indent& indent) const +void Box_dinf::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << dump_children(indent); + Box::dump(sstr, indent, full_log); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -4832,13 +4766,12 @@ } -std::string Box_dref::dump(Indent& indent) const +void Box_dref::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << dump_children(indent); + Box::dump(sstr, indent, full_log); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -4879,15 +4812,14 @@ } -std::string Box_url::dump(Indent& indent) const +void Box_url::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - //sstr << dump_children(indent); + Box::dump(sstr, indent, full_log); + //dump_children(sstr, indent, full_log); sstr << indent << "location: " << m_location << "\n"; - return sstr.str(); + return; } @@ -4906,15 +4838,14 @@ return range.get_error(); } -std::string Box_udes::dump(Indent& indent) const +void Box_udes::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "lang: " << m_lang << "\n"; sstr << indent << "name: " << m_name << "\n"; sstr << indent << "description: " << m_description << "\n"; sstr << indent << "tags: " << m_tags << "\n"; - return sstr.str(); + return; } Error Box_udes::write(StreamWriter& writer) const @@ -4962,10 +4893,9 @@ } -std::string Box_cmin::dump(Indent& indent) const +void Box_cmin::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "principal-point: " << m_matrix.principal_point_x << ", " << m_matrix.principal_point_y << "\n"; if (m_matrix.is_anisotropic) { sstr << indent << "focal-length: " << m_matrix.focal_length_x << ", " << m_matrix.focal_length_y << "\n"; @@ -4976,7 +4906,7 @@ sstr << indent << "no skew\n"; } - return sstr.str(); + return; } @@ -5237,10 +5167,9 @@ } -std::string Box_cmex::dump(Indent& indent) const +void Box_cmex::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "camera position (um): "; sstr << m_matrix.pos_x << " ; "; sstr << m_matrix.pos_y << " ; "; @@ -5264,7 +5193,7 @@ sstr << indent << "world coordinate system id: " << m_matrix.world_coordinate_system_id << "\n"; - return sstr.str(); + return; } @@ -5434,12 +5363,11 @@ } -std::string Box_taic::dump(Indent& indent) const { - std::ostringstream sstr; - sstr << Box::dump(indent); +void Box_taic::dump(std::ostream& sstr, Indent& indent, bool full_log) const { + Box::dump(sstr, indent, full_log); sstr << dump(m_info, indent); - return sstr.str(); + return; } Error Box_taic::write(StreamWriter& writer) const { @@ -5487,14 +5415,13 @@ } -std::string Box_itai::dump(Indent& indent) const { - std::ostringstream sstr; - sstr << Box::dump(indent); +void Box_itai::dump(std::ostream& sstr, Indent& indent, bool full_log) const { + Box::dump(sstr, indent, full_log); 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(); + return; } @@ -5609,12 +5536,11 @@ return range.get_error(); } -std::string Box_elng::dump(Indent& indent) const +void Box_elng::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "extended_language: " << m_lang << "\n"; - return sstr.str(); + return; } Error Box_elng::write(StreamWriter& writer) const @@ -5646,12 +5572,11 @@ } -std::string Box_gimi_content_id::dump(Indent& indent) const +void Box_gimi_content_id::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "content ID: " << m_content_id << "\n"; - return sstr.str(); + return; } diff -Nru libheif-1.23.4/libheif/box.h libheif-1.23.6/libheif/box.h --- libheif-1.23.4/libheif/box.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/box.h 2026-10-05 16:16:00.000000000 +0000 @@ -147,7 +147,17 @@ Error parse_header(BitstreamRange& range); - virtual std::string dump(Indent&) const; + virtual void dump(std::ostream&, Indent&, bool full_log) const; + + // Convenience wrapper that collects dump() into a string. By default only a + // bounded amount of each box is written (see MAX_DUMP_ENTRIES); pass + // full_log=true for the complete, potentially very large, output. + std::string dump_to_string(Indent& indent, bool full_log = false) const + { + std::ostringstream sstr; + dump(sstr, indent, full_log); + return sstr.str(); + } virtual bool is_full_box_header() const { return false; } @@ -206,7 +216,7 @@ void patch_file_pointers_recursively(StreamWriter&, size_t offset); - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; template [[nodiscard]] std::shared_ptr get_child_box() const { @@ -300,7 +310,7 @@ Error write_children(StreamWriter& writer) const; - std::string dump_children(Indent&, bool with_index = false) const; + void dump_children(std::ostream&, Indent&, bool full_log, bool with_index = false) const; // --- writing @@ -318,7 +328,7 @@ public: bool is_full_box_header() const override { return true; } - std::string dump(Indent& indent) const override; + void dump(std::ostream&, Indent& indent, bool full_log) const override; void derive_box_version() override { set_version(0); } @@ -363,7 +373,7 @@ Error write(StreamWriter& writer) const override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; @@ -387,7 +397,7 @@ Error write(StreamWriter& writer) const override { return {heif_error_Usage_error, heif_suberror_Unspecified, "Cannot write dummy error box."}; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override; @@ -412,7 +422,7 @@ set_short_type(fourcc("ftyp")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "File Type"; } @@ -452,7 +462,7 @@ set_short_type(fourcc("free")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Free Space"; } @@ -471,7 +481,7 @@ set_short_type(fourcc("meta")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Metadata"; } @@ -488,7 +498,7 @@ set_short_type(fourcc("hdlr")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Handler Reference"; } @@ -519,7 +529,7 @@ set_short_type(fourcc("pitm")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Primary Item"; } @@ -548,7 +558,7 @@ void set_use_tmp_file(bool flag); - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Location"; } @@ -649,7 +659,7 @@ set_short_type(fourcc("infe")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Info Entry"; } @@ -711,7 +721,7 @@ set_short_type(fourcc("iinf")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Information"; } @@ -735,7 +745,7 @@ set_short_type(fourcc("iprp")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Properties"; } @@ -766,7 +776,7 @@ const std::shared_ptr& property, const std::shared_ptr&) const; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Property Container"; } @@ -793,7 +803,7 @@ m_image_height = height; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image Spatial Extents"; } @@ -821,7 +831,7 @@ set_short_type(fourcc("ipma")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Property Association"; } @@ -886,7 +896,7 @@ bool is_essential() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image Properties for Auxiliary Images"; } @@ -913,7 +923,7 @@ bool is_transformative_property() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image Rotation"; } @@ -950,7 +960,7 @@ void set_mirror_direction(heif_transform_mirror_direction dir) { m_axis = dir; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image Mirroring"; } @@ -991,7 +1001,7 @@ m_target_height_denominator = h_den; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image Scaling"; } @@ -1022,7 +1032,7 @@ bool is_transformative_property() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Clean Aperture"; } @@ -1084,7 +1094,7 @@ }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Reference"; } @@ -1105,8 +1115,6 @@ void derive_box_version() override; - Error check_for_double_references() const; - private: std::vector m_references; @@ -1133,7 +1141,7 @@ bool is_essential() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Required Reference Types"; } @@ -1158,7 +1166,7 @@ class Box_idat : public Box { public: - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item Data"; } @@ -1197,7 +1205,7 @@ set_short_type(fourcc("grpl")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Groups List"; } @@ -1209,7 +1217,7 @@ class Box_EntityToGroup : public FullBox { public: - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -1239,7 +1247,7 @@ set_short_type(fourcc("ster")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Stereo pair"; } @@ -1260,7 +1268,7 @@ set_short_type(fourcc("pymd")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Image pyramid group"; } @@ -1307,7 +1315,7 @@ set_short_type(fourcc("dinf")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Data Information"; } @@ -1324,7 +1332,7 @@ set_short_type(fourcc("dref")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Data Reference"; } @@ -1344,7 +1352,7 @@ set_flags(1); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Data Entry URL"; } @@ -1385,7 +1393,7 @@ } } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Pixel Information"; } @@ -1412,7 +1420,7 @@ uint32_t hSpacing = 1; uint32_t vSpacing = 1; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Pixel Aspect Ratio"; } @@ -1435,7 +1443,7 @@ uint16_t layer_id = 0; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Layer Selection"; } @@ -1461,7 +1469,7 @@ heif_content_light_level clli; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Content Light Level Information"; } @@ -1481,7 +1489,7 @@ heif_mastering_display_colour_volume mdcv; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Master Display Colour Volume"; } @@ -1501,7 +1509,7 @@ heif_ambient_viewing_environment amve; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Ambient Viewing Environment"; } @@ -1528,7 +1536,7 @@ void set_diffuse_white_luminance(uint32_t luminance) { m_diffuse_white_luminance = luminance; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Nominal Diffuse White"; } @@ -1570,7 +1578,7 @@ uint32_t get_avg_luminance() const { return *m_ccv_avg_luminance_value; } void set_avg_luminance(uint32_t luminance) { m_ccv_avg_luminance_value = luminance; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; // TODO const char* debug_box_name() const override { return ""; } @@ -1651,7 +1659,7 @@ } }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Camera Intrinsic Matrix"; } @@ -1708,7 +1716,7 @@ std::array calculate_rotation_matrix() const; }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Camera Extrinsic Matrix"; } @@ -1762,7 +1770,7 @@ set_short_type(fourcc("udes")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "User Description"; } @@ -1858,7 +1866,7 @@ static std::string dump(const heif_tai_clock_info& info, Indent&); - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "TAI Clock Information"; } @@ -1936,7 +1944,7 @@ initialize_heif_tai_timestamp_packet(&m_timestamp); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Item TAI Timestamp"; } @@ -2004,7 +2012,7 @@ bool is_transformative_property() const override { return false; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "GIMI Content ID"; } @@ -2043,7 +2051,7 @@ set_short_type(fourcc("elng")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Extended language"; } diff -Nru libheif-1.23.4/libheif/codecs/avc_boxes.cc libheif-1.23.6/libheif/codecs/avc_boxes.cc --- libheif-1.23.4/libheif/codecs/avc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -166,10 +166,9 @@ return Error::Ok; } -std::string Box_avcC::dump(Indent& indent) const +void Box_avcC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); 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" @@ -224,7 +223,7 @@ sstr << std::dec; } - return sstr.str(); + return; } std::string Box_avcC::profileIndicationAsText() const diff -Nru libheif-1.23.4/libheif/codecs/avc_boxes.h libheif-1.23.6/libheif/codecs/avc_boxes.h --- libheif-1.23.4/libheif/codecs/avc_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -86,7 +86,7 @@ void append_pps_nal(const uint8_t* data, size_t size); - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter &writer) const override; diff -Nru libheif-1.23.4/libheif/codecs/avc_dec.cc libheif-1.23.6/libheif/codecs/avc_dec.cc --- libheif-1.23.4/libheif/codecs/avc_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -22,7 +22,9 @@ #include "avc_boxes.h" #include "error.h" #include "context.h" +#include "plugins/nalu_utils.h" +#include #include @@ -47,26 +49,51 @@ } -Result> Decoder_AVC::get_coded_image_size_from_config() const +Result> Decoder_AVC::get_max_coded_image_size(const std::vector& compressed_data) const { - const auto& sps_set = m_avcC->getSequenceParameterSets(); + // `compressed_data` is the combined configuration + bitstream buffer about to be + // pushed to the decoder. Scan it for every SPS NAL unit and return the largest coded picture + // size any of them declares. An SPS carried in the item data (not just in avcC) + // drives the decoder's buffer allocation and can be far larger than the + // container 'ispe', so the config record alone is not a sufficient gate. + bool found = false; + uint32_t max_width = 0; + uint32_t max_height = 0; + + for (const auto& nal : split_nal_units_4byte_length_prefixed(compressed_data.data(), compressed_data.size())) { + const uint8_t* nal_data = nal.first; + size_t nal_size = nal.second; + + // AVC NAL unit header (1 byte): forbidden_zero_bit(1), nal_ref_idc(2), nal_unit_type(5) + if (nal_size < 1) { + continue; + } + int nal_type = nal_data[0] & 0x1F; + if (nal_type != AVC_NAL_UNIT_SPS_NUT) { + continue; + } - 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, + Error e = parse_sps_for_avcC_configuration(nal_data, nal_size, &scratch, &cropped_w, &cropped_h, &coded); if (e) { - return e; + // A malformed SPS we cannot parse is skipped rather than failing the whole + // decode; the decoder plugin applies its own limits when it reaches it. + continue; } - return std::optional{coded}; + found = true; + max_width = std::max(max_width, coded.width); + max_height = std::max(max_height, coded.height); + } + + if (!found) { + return std::optional{}; } - return std::optional{}; + return std::optional{ImageSize{max_width, max_height}}; } diff -Nru libheif-1.23.4/libheif/codecs/avc_dec.h libheif-1.23.6/libheif/codecs/avc_dec.h --- libheif-1.23.4/libheif/codecs/avc_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -46,7 +46,7 @@ Result> read_bitstream_configuration_data() const override; - Result> get_coded_image_size_from_config() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; private: const std::shared_ptr m_avcC; diff -Nru libheif-1.23.4/libheif/codecs/avc_enc.cc libheif-1.23.6/libheif/codecs/avc_enc.cc --- libheif-1.23.4/libheif/codecs/avc_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -25,6 +25,7 @@ #include "api_structs.h" #include +#include #include "plugins/nalu_utils.h" @@ -175,15 +176,16 @@ } -std::optional Encoder_AVC::encode_sequence_get_data() +std::optional Encoder_AVC::encode_sequence_extract_data() { - if (m_output_image_complete) { - m_output_image_complete = false; - return std::move(m_current_output_data); - } - else { + // Non-VCL NALs can arrive ahead of their picture (x264 emits an SEI together with the + // SPS/PPS headers). They are held back until the slice data has been collected. + if (!m_output_image_complete) { return std::nullopt; } + + m_output_image_complete = false; + return std::exchange(m_current_output_data, std::nullopt); } Error Encoder_AVC::get_data(heif_encoder* encoder) diff -Nru libheif-1.23.4/libheif/codecs/avc_enc.h libheif-1.23.6/libheif/codecs/avc_enc.h --- libheif-1.23.4/libheif/codecs/avc_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avc_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -51,7 +51,7 @@ Error encode_sequence_flush(heif_encoder* encoder) override; - std::optional encode_sequence_get_data() override; + std::optional encode_sequence_extract_data() override; std::shared_ptr get_sample_description_box(const CodedImageData&) const override; @@ -59,7 +59,7 @@ 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. + // Whether the avcC is complete and was returned in an encode_sequence_extract_data() call. bool m_avcC_has_SPS = false; bool m_avcC_has_PPS = false; std::shared_ptr m_avcC; diff -Nru libheif-1.23.4/libheif/codecs/avif_boxes.cc libheif-1.23.6/libheif/codecs/avif_boxes.cc --- libheif-1.23.4/libheif/codecs/avif_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -27,6 +27,7 @@ #include "file.h" #include #include +#include #include #include @@ -112,10 +113,9 @@ } -std::string Box_av1C::dump(Indent& indent) const +void Box_av1C::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); const auto& c = m_configuration; // abbreviation @@ -144,7 +144,7 @@ } sstr << std::dec << "\n"; - return sstr.str(); + return; } @@ -198,14 +198,13 @@ } -std::string Box_a1op::dump(Indent& indent) const +void Box_a1op::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "op-index: " << ((int) op_index) << "\n"; - return sstr.str(); + return; } @@ -238,14 +237,13 @@ } -std::string Box_a1lx::dump(Indent& indent) const +void Box_a1lx::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "layer-sizes: [" << layer_size[0] << "," << layer_size[1] << "," << layer_size[2] << "]\n"; - return sstr.str(); + return; } @@ -561,3 +559,173 @@ return true; } + + +// Parse a single OBU_SEQUENCE_HEADER payload far enough to recover the coded +// frame size (max_frame_width_minus_1 / max_frame_height_minus_1). The reader +// must be positioned at the first bit of the sequence_header_obu() payload. +// +// Returns false when the header could not be parsed within the available data +// (i.e. the reader ran past its end), in which case the recovered size is +// unreliable and must be ignored. This mirrors the bit layout in +// fill_av1C_configuration_from_stream() up to the frame size fields; it is kept +// separate so the security scan cannot be perturbed by the colour-config parsing. +static bool parse_av1_seq_header_max_frame_size(BitReader& reader, + uint32_t* out_max_width, + uint32_t* out_max_height) +{ + uint32_t dummy; // throw away value + + bool decoder_model_info_present = false; + int buffer_delay_length_minus1 = 0; + + reader.get_bits(3); // seq_profile + reader.get_bits(1); // still_picture + + bool reduced_still_picture = reader.get_bits(1); + if (reduced_still_picture) { + reader.get_bits(5); // seq_level_idx[0] + } + else { + bool timing_info_present_flag = reader.get_bits(1); + if (timing_info_present_flag) { + // --- skip timing info + reader.skip_bytes(2 * 4); + bool equal_picture_interval = reader.get_bits(1); + if (equal_picture_interval) { + reader.get_uvlc(&dummy); + } + + // --- skip decoder_model_info + decoder_model_info_present = reader.get_bits(1); + if (decoder_model_info_present) { + buffer_delay_length_minus1 = reader.get_bits(5); + reader.skip_bits(32); + reader.skip_bits(10); + } + } + + bool initial_display_delay_present_flag = reader.get_bits(1); + int operating_points_cnt_minus1 = reader.get_bits(5); + for (int i = 0; i <= operating_points_cnt_minus1; i++) { + reader.skip_bits(12); // operating_point_idc + auto level = (int) reader.get_bits(5); + if (level > 7) { + reader.skip_bits(1); // tier + } + + if (decoder_model_info_present) { + bool decoder_model_present_for_this = reader.get_bits(1); + if (decoder_model_present_for_this) { + int n = buffer_delay_length_minus1 + 1; + reader.skip_bits(n); + reader.skip_bits(n); + reader.skip_bits(1); + } + } + + if (initial_display_delay_present_flag) { + bool initial_display_delay_present_for_this = reader.get_bits(1); + if (initial_display_delay_present_for_this) { + reader.get_bits(4); + } + } + } + } + + int frame_width_bits_minus1 = reader.get_bits(4); + int frame_height_bits_minus1 = reader.get_bits(4); + uint32_t max_frame_width_minus1 = reader.get_bits(frame_width_bits_minus1 + 1); + uint32_t max_frame_height_minus1 = reader.get_bits(frame_height_bits_minus1 + 1); + + // If parsing consumed more bits than the buffer held, get_bits() returned + // zero-padded values and the size is not trustworthy. Reject it so we neither + // wrongly accept nor wrongly reject based on garbage. + if (reader.get_bits_remaining() < 0) { + return false; + } + + *out_max_width = max_frame_width_minus1 + 1; + *out_max_height = max_frame_height_minus1 + 1; + return true; +} + + +bool find_max_av1_frame_size_in_stream(const uint8_t* data, size_t dataSize, + uint32_t* out_max_width, uint32_t* out_max_height) +{ + // The combined AV1 bitstream that libheif pushes to a decoder plugin consists + // of the av1C configOBUs followed by the item/sample data. A conforming AVIF + // carries exactly one OBU_SEQUENCE_HEADER in the item data (and may duplicate + // it in configOBUs). We walk every OBU and keep the largest frame size found + // in any sequence header, because that is the buffer a decoder will allocate, + // independent of the (possibly much smaller) 'ispe' dimensions. + + if (data == nullptr || dataSize == 0 || + dataSize > (size_t) std::numeric_limits::max()) { + return false; + } + + BitReader reader(data, (int) dataSize); + + bool found = false; + uint32_t max_width = 0; + uint32_t max_height = 0; + + while (reader.get_bits_remaining() >= 8) { + obu_header_info header = read_obu_header_type(reader); + + // read_obu_header_type() may read past the end on truncated input. + if (reader.get_bits_remaining() < 0) { + break; + } + + // The reader is byte-aligned after the OBU header; this is where the payload + // begins. We parse a sequence header from a separate reader bounded to the + // payload so that neither the seq-header parse nor a following OBU can read + // across the declared OBU boundary. + size_t payload_start = reader.get_current_byte_index(); + if (payload_start >= dataSize) { + break; + } + + if (header.type == HEIF_OBU_SEQUENCE_HEADER) { + size_t avail = dataSize - payload_start; + size_t payload_len = avail; + if (header.has_size && header.size < payload_len) { + payload_len = (size_t) header.size; + } + + BitReader seq_reader(data + payload_start, (int) payload_len); + uint32_t w = 0, h = 0; + if (parse_av1_seq_header_max_frame_size(seq_reader, &w, &h)) { + found = true; + // Track the largest width and height independently. A conforming file + // has a single sequence header (the AVIF spec even requires identical + // headers when several are present), so this equals that header's size. + // For a non-conforming file with differing headers it over-approximates + // rather than under-approximates the buffer any frame could demand, + // which is the safe direction for a security gate. + max_width = std::max(max_width, w); + max_height = std::max(max_height, h); + } + } + + // Advance to the next OBU. Without an explicit size we cannot locate it. + if (!header.has_size) { + break; + } + if (header.size > (uint64_t) std::numeric_limits::max()) { + break; + } + + reader.skip_bytes((uint32_t) header.size); + } + + if (found) { + *out_max_width = max_width; + *out_max_height = max_height; + } + + return found; +} diff -Nru libheif-1.23.4/libheif/codecs/avif_boxes.h libheif-1.23.6/libheif/codecs/avif_boxes.h --- libheif-1.23.4/libheif/codecs/avif_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -88,7 +88,7 @@ }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; bool get_headers(std::vector* dest) const { @@ -134,7 +134,7 @@ uint8_t op_index = 0; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -153,7 +153,7 @@ uint32_t layer_size[3]{}; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -168,4 +168,12 @@ bool fill_av1C_configuration_from_stream(Box_av1C::configuration* out_config, const uint8_t* data, int dataSize); +// Scan a combined AV1 bitstream (av1C configOBUs + item/sample data) for every +// OBU_SEQUENCE_HEADER and return the largest coded frame size (in luma samples) +// declared by any of them. Returns false when no parseable sequence header was +// found, in which case the size outputs are left untouched. Used to enforce the +// security limits before the bitstream reaches any AV1 decoder plugin. +bool find_max_av1_frame_size_in_stream(const uint8_t* data, size_t dataSize, + uint32_t* out_max_width, uint32_t* out_max_height); + #endif diff -Nru libheif-1.23.4/libheif/codecs/avif_dec.cc libheif-1.23.6/libheif/codecs/avif_dec.cc --- libheif-1.23.4/libheif/codecs/avif_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -38,6 +38,31 @@ } +Result> Decoder_AVIF::get_max_coded_image_size(const std::vector& compressed_data) const +{ + // The AV1 sequence header carries the coded frame size + // (max_frame_width_minus_1 / max_frame_height_minus_1). Per the AVIF + // specification it is present in the image item data, and it may additionally + // be duplicated in the av1C configOBUs. A decoder allocates buffers for that + // coded size, which can be far larger than the (possibly deliberately small) + // 'ispe' dimensions on which the container-level size checks are based. + // + // `compressed_data` is the combined configOBUs + item data buffer that is about + // to be pushed to the decoder plugin. Scan it for the largest sequence-header + // frame size so the shared decode path can reject over-limit inputs before ANY + // AV1 plugin (aom, dav1d, ffmpeg, ...) allocates a frame buffer. + uint32_t max_width = 0; + uint32_t max_height = 0; + if (!find_max_av1_frame_size_in_stream(compressed_data.data(), compressed_data.size(), + &max_width, &max_height)) { + // No parseable sequence header (e.g. a non-sync frame in a sequence). + return std::optional{}; + } + + return std::optional{ImageSize{max_width, max_height}}; +} + + int Decoder_AVIF::get_luma_bits_per_pixel() const { Box_av1C::configuration config = m_av1C->get_configuration(); diff -Nru libheif-1.23.4/libheif/codecs/avif_dec.h libheif-1.23.6/libheif/codecs/avif_dec.h --- libheif-1.23.4/libheif/codecs/avif_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -46,6 +46,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; + private: const std::shared_ptr m_av1C; }; diff -Nru libheif-1.23.4/libheif/codecs/avif_enc.cc libheif-1.23.6/libheif/codecs/avif_enc.cc --- libheif-1.23.4/libheif/codecs/avif_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -25,6 +25,7 @@ #include "api_structs.h" #include +#include enum heif_av1_obu_type : uint8_t { @@ -183,6 +184,11 @@ codedImage.codingConstraints.intra_pred_used = true; codedImage.codingConstraints.all_ref_pics_intra = m_all_refs_intra; + if (codedImage.bitstream.empty() && codedImage.properties.empty()) { + // The encoder is still buffering frames and returned nothing. Leave no output pending. + return {}; + } + m_current_output_data = std::move(codedImage); return {}; @@ -199,9 +205,9 @@ } -std::optional Encoder_AVIF::encode_sequence_get_data() +std::optional Encoder_AVIF::encode_sequence_extract_data() { - return std::move(m_current_output_data); + return std::exchange(m_current_output_data, std::nullopt); } diff -Nru libheif-1.23.4/libheif/codecs/avif_enc.h libheif-1.23.6/libheif/codecs/avif_enc.h --- libheif-1.23.4/libheif/codecs/avif_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/avif_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -53,7 +53,7 @@ Error encode_sequence_flush(heif_encoder* encoder) override; - std::optional encode_sequence_get_data() override; + std::optional encode_sequence_extract_data() override; std::shared_ptr get_sample_description_box(const CodedImageData&) const override; diff -Nru libheif-1.23.4/libheif/codecs/decoder.cc libheif-1.23.6/libheif/codecs/decoder.cc --- libheif-1.23.4/libheif/codecs/decoder.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/decoder.cc 2026-10-05 16:16:00.000000000 +0000 @@ -64,7 +64,8 @@ Result*> DataExtent::read_data() const { - if (!m_raw.empty()) { + // A raw extent has no file to read from. All its data is in m_raw, even when that is empty. + if (!m_raw.empty() || m_source == Source::Raw) { return &m_raw; } else if (m_source == Source::Image) { @@ -112,7 +113,7 @@ { std::vector data; - if (!m_raw.empty()) { + if (!m_raw.empty() || m_source == Source::Raw) { // 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 @@ -139,7 +140,16 @@ } else { // file range - Error err = m_file->append_data_from_file_range(data, m_offset, m_size); + + // The requested range has to be within the sample. The subtraction form avoids + // a uint64_t wrap in 'offset + size'. + if (offset > m_size || size > m_size - offset) { + return Error{heif_error_Invalid_input, + heif_suberror_End_of_data, + "Requested data range exceeds the sample data"}; + } + + Error err = m_file->append_data_from_file_range(data, m_offset + offset, static_cast(size)); if (err) { return err; } @@ -314,6 +324,35 @@ } +std::vector> +split_nal_units_4byte_length_prefixed(const uint8_t* data, size_t size) +{ + std::vector> units; + + size_t ptr = 0; + while (ptr + 4 <= size) { + uint32_t nal_size = (uint32_t(data[ptr]) << 24) | (uint32_t(data[ptr + 1]) << 16) | + (uint32_t(data[ptr + 2]) << 8) | uint32_t(data[ptr + 3]); + ptr += 4; + + // A length that runs past the end of the buffer means the stream is + // malformed; stop rather than read out of bounds. + if (nal_size > size - ptr) { + break; + } + + if (nal_size > 0) { + units.emplace_back(data + ptr, (size_t) nal_size); + } + + ptr += nal_size; + } + + return units; +} + + + void Decoder::release_decoder() { if (m_decoder) { @@ -428,14 +467,34 @@ 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(); + // Fetch the compressed data once. The same buffer is used both to enforce the + // security limits below and to feed the decoder, so we neither re-read the + // iloc extents nor rebuild the combined config+bitstream buffer twice. + auto dataResult = get_compressed_data(upload_configuration_NALs); + if (!dataResult) { + return dataResult.error(); + } + + // 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." + }; + } + + // Reject memory-bomb inputs whose coded picture size exceeds libheif's + // security limits, before handing any bytes to the decoder plugin. The coded + // (pre-crop) size the decoder will allocate lives in the bitstream: the SPS for + // AVC/HEVC/VVC, the Sequence Header OBU for AV1/AVIF, or the SOF marker for + // JPEG. It can be far larger than the 'ispe' dimensions, and for the NAL codecs + // an SPS may sit in the item data rather than only in the config record, so we + // scan the whole buffer that is about to be pushed. Codecs that expose no coded + // size return nullopt and skip the check. + auto codedSize = get_max_coded_image_size(*dataResult); if (codedSize.is_error()) { return codedSize.error(); } @@ -486,23 +545,6 @@ } } - auto dataResult = get_compressed_data(upload_configuration_NALs); - if (!dataResult) { - return dataResult.error(); - } - - // 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); diff -Nru libheif-1.23.4/libheif/codecs/decoder.h libheif-1.23.6/libheif/codecs/decoder.h --- libheif-1.23.4/libheif/codecs/decoder.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/decoder.h 2026-10-05 16:16:00.000000000 +0000 @@ -45,6 +45,19 @@ }; +// Split a buffer of NAL units, each prefixed by a 4-byte big-endian length, into +// non-owning (pointer, length) spans that point into `data`. Parsing stops at the +// first malformed length (a prefix or payload that runs past the end of the +// buffer); NAL units already collected are still returned. +// +// This mirrors exactly how the AVC/HEVC/VVC decoder plugins walk the combined +// configuration+bitstream buffer they are handed (see e.g. decoder_libde265.cc), +// so the returned spans are the same NAL units the codec will actually see. It is +// used to scan for in-band SPS NAL units when enforcing the security limits. +std::vector> +split_nal_units_4byte_length_prefixed(const uint8_t* data, size_t size); + + // Specifies the input data for decoding. // For images, this points to the iloc extents. // For sequences, this points to the track data. @@ -106,19 +119,25 @@ // 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; - // 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 the largest *coded* picture size declared anywhere in `compressed_data` + // (the exact buffer that will be pushed to the decoder), 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 coded size with a near-equal-sized + // conformance window, so the displayed image looks small while the decoder + // still allocates the full uncropped buffer. + // + // The coded size lives in the bitstream, not (only) in the codec configuration + // record: the SPS for AVC/HEVC/VVC (which may appear in the item data, not just + // in avcC/hvcC/vvcC), the Sequence Header OBU for AV1/AVIF, or the SOF marker + // for JPEG. Overrides therefore scan the whole passed buffer and return the + // maximum, so an over-limit size cannot be smuggled past the container 'ispe'. // - // 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. + // Returns nullopt when the codec exposes no coded size in this buffer (e.g. no + // SPS/sequence-header/SOF present, as in a non-sync sequence frame). + // Returns Error only on a structurally invalid header. [[nodiscard]] virtual Result> - get_coded_image_size_from_config() const + get_max_coded_image_size(const std::vector& /*compressed_data*/) const { return std::optional{}; } diff -Nru libheif-1.23.4/libheif/codecs/encoder.cc libheif-1.23.6/libheif/codecs/encoder.cc --- libheif-1.23.4/libheif/codecs/encoder.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/encoder.cc 2026-10-05 16:16:00.000000000 +0000 @@ -113,6 +113,16 @@ extern void fill_default_color_conversion_options_ext(heif_color_conversion_options_ext& options); +Error Encoder::check_input_image_layout(const HeifPixelImage& image) const +{ + if (image.get_colorspace() == heif_colorspace_custom) { + return Error::Ok; + } + + return image.check_plane_layout(); +} + + Result> Encoder::convert_colorspace_for_encoding(const std::shared_ptr& image, heif_encoder* encoder, const heif_color_profile_nclx* user_requested_output_nclx, @@ -128,6 +138,18 @@ } + if (Error err = check_input_image_layout(*image)) { + return err; + } + + // The image comes from the application, so nothing guarantees that its samples are within + // the bit depth it declared. The conversion operators and the encoder libraries behind the + // plugins assume that they are (libsharpyuv indexes its gamma tables with the samples, x264 + // aborts on an internal assertion), so this is checked once here for all of them. + if (Error err = image->check_sample_value_ranges()) { + return err; + } + heif_colorspace colorspace = image->get_colorspace(); heif_chroma chroma = image->get_chroma_format(); diff -Nru libheif-1.23.4/libheif/codecs/encoder.h libheif-1.23.6/libheif/codecs/encoder.h --- libheif-1.23.4/libheif/codecs/encoder.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/encoder.h 2026-10-05 16:16:00.000000000 +0000 @@ -60,6 +60,16 @@ // 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; } + // Checks that an image handed to this encoder has a plane layout the encoder plugins can rely + // on: exactly the planes of its colorspace and chroma format, each once and at the size its + // channel implies (HeifPixelImage::check_plane_layout()). Planes with channel + // heif_channel_unknown are tolerated. Images with a custom colorspace pass unchecked: they + // have no defined layout, and the uncompressed encoder writes their components as they are. + // This is the libheif-side check, made before the image is colour-converted for the plugin + // (or handed over unchanged when no conversion is needed). A plugin that supports only a + // subset of the layouts checks its input itself (see plugins/encoder_input_check.h). + virtual Error check_input_image_layout(const HeifPixelImage& image) const; + Result> convert_colorspace_for_encoding(const std::shared_ptr& image, heif_encoder* encoder, const heif_color_profile_nclx* user_requested_output_nclx, @@ -84,7 +94,11 @@ virtual Error encode_sequence_flush(heif_encoder* encoder) { return {}; } - virtual std::optional encode_sequence_get_data() { return std::nullopt; } + // Hands out the coded data collected since the previous call and leaves nothing pending + // in the encoder, so that the next call returns std::nullopt until new data has arrived. + // The result may carry only properties (e.g. the codec configuration box that some + // encoders attach at the end of the sequence) and an empty bitstream. + virtual std::optional encode_sequence_extract_data() { return std::nullopt; } virtual std::shared_ptr get_sample_description_box(const CodedImageData&) const { return {}; } }; diff -Nru libheif-1.23.4/libheif/codecs/hevc_boxes.cc libheif-1.23.6/libheif/codecs/hevc_boxes.cc --- libheif-1.23.4/libheif/codecs/hevc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -123,11 +123,11 @@ uint8_t byte = 0; for (int b = 0; b < 8; b++) { + byte = (uint8_t) (byte << 1); + if (general_constraint_indicator_flags[i * 8 + b]) { byte |= 1; } - - byte = (uint8_t) (byte << 1); } writer.write8(byte); @@ -191,16 +191,90 @@ } +bool HEVCDecoderConfigurationRecord::is_miaf_profile() const +{ + // The MIAF HEVC Basic, Advanced and Extended profiles (ISO/IEC 23000-22, A.3.2, A.4.2, A.5.2) + // list the HEVC profiles of their images. + // + // They also limit the images to the Main tier and to level 6. This is not checked here: + // the encoders do not all signal the level that the image needs (kvazaar signals level 6.2 + // for every image), so the level in 'hvcC' does not tell whether the image is within + // the limit. + + if (general_profile_space != 0) { + return false; + } + + // Main and Main Still Picture (Basic), Main 10 and Main 10 Still Picture (Advanced). + // A bitstream in the Main 10 Still Picture profile is also one in the Main 10 profile. + + if (is_profile_compatible(Profile_Main) || + is_profile_compatible(Profile_MainStillPicture) || + is_profile_compatible(Profile_Main10)) { + return true; + } + + if (!is_profile_compatible(Profile_RExt)) { + // There is no MIAF profile with the high throughput profiles, the screen content coding + // profiles or any other profile of HEVC. + return false; + } + + // The format range extensions profiles are told apart by nine of the constraint flags. + // A bitstream conforms to a profile when each of its flags is greater than or equal to the + // flag of the profile in Table A.2 of ITU-T H.265. This way, a profile that is not listed + // here is accepted when it is a subset of a listed one, like Main 4:4:4 Intra, which is a + // subset of Main 4:4:4 10 Intra. + + const int first_flag = 4; // general_max_12bit_constraint_flag + const int num_flags = 9; + + static const bool miaf_profiles[][num_flags] = { + // max 12 bit, max 10 bit, max 8 bit, max 4:2:2, max 4:2:0, max monochrome, + // intra, one picture only, lower bit rate + + // MIAF HEVC Advanced profile + {1, 1, 1, 1, 1, 0, 1, 0, 0}, // Main Intra + {1, 1, 0, 1, 1, 0, 1, 0, 0}, // Main 10 Intra + {1, 1, 0, 1, 0, 0, 1, 0, 0}, // Main 4:2:2 10 Intra + + // MIAF HEVC Extended profile + {1, 1, 1, 0, 0, 0, 0, 0, 1}, // Main 4:4:4 + {1, 1, 0, 0, 0, 0, 0, 0, 1}, // Main 4:4:4 10 + {1, 1, 0, 0, 0, 0, 1, 0, 0}, // Main 4:4:4 10 Intra + {1, 1, 1, 0, 0, 0, 1, 1, 0}, // Main 4:4:4 Still Picture + {1, 1, 1, 1, 1, 1, 0, 0, 1}, // Monochrome + {1, 1, 0, 1, 1, 1, 0, 0, 1} // Monochrome 10 + }; + + for (const auto& profile : miaf_profiles) { + bool conforms = true; + + for (int i = 0; i < num_flags; i++) { + if (profile[i] && !general_constraint_indicator_flags[first_flag + i]) { + conforms = false; + break; + } + } + + if (conforms) { + return true; + } + } + + return false; +} + + 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 +void Box_hvcC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); const auto& c = m_configuration; // abbreviation @@ -281,7 +355,7 @@ indent--; } - return sstr.str(); + return; } @@ -312,7 +386,9 @@ { std::vector nal; nal.resize(size); - memcpy(nal.data(), data, size); + if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(nal.data(), data, size); + } append_nal_data(nal); } @@ -613,6 +689,12 @@ reader.skip_bits(4); uint8_t nMaxSubLayersMinus1 = reader.get_bits8(3); + if (nMaxSubLayersMinus1 > 6) { + // sps_max_sub_layers_minus1 is in the range 0..6 (H.265 section 7.4.3.2.1). + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS sps_max_sub_layers_minus1 out of range"}; + } config->temporal_id_nested = reader.get_bits8(1); @@ -623,9 +705,11 @@ config->general_profile_idc = reader.get_bits8(5); config->general_profile_compatibility_flags = reader.get_bits32(32); - reader.skip_bits(16); // skip reserved bits - reader.skip_bits(16); // skip reserved bits - reader.skip_bits(16); // skip reserved bits + // general_progressive_source_flag up to the last of the constraint flags. Within the range + // extensions and the screen content coding extensions, they tell the profiles apart. + for (int i = 0; i < HEVCDecoderConfigurationRecord::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) { + config->general_constraint_indicator_flags[i] = (reader.get_bits(1) != 0); + } config->general_level_idc = reader.get_bits8(8); @@ -645,9 +729,13 @@ for (int i = 0; i < nMaxSubLayersMinus1; i++) { if (layer_profile_present[i]) { + // Same 88 bits as the general profile above: space, tier, idc, + // compatibility flags, then 48 bits of source and constraint flags. reader.skip_bits(2 + 1 + 5); reader.skip_bits(32); reader.skip_bits(16); + reader.skip_bits(16); + reader.skip_bits(16); } if (layer_level_present[i]) { @@ -664,9 +752,17 @@ "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)) { + uint32_t value; + if (!reader.get_uvlc(&value)) { + return invalidUVLC; + } + if (value > 15) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS seq_parameter_set_id out of range"}; + } + + if (!reader.get_uvlc(&value)) { return invalidUVLC; } if (value > 3) { @@ -686,6 +782,11 @@ !reader.get_uvlc(height)) { return invalidUVLC; } + if (*width == 0 || *height == 0 || *width > 65535 || *height > 65535) { + return Error{heif_error_Invalid_input, + heif_suberror_Invalid_parameter_value, + "SPS picture size out of range"}; + } if (coded_size) { coded_size->width = *width; @@ -713,7 +814,7 @@ 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) { + if (crop_w >= *width || crop_h >= *height) { return Error{heif_error_Invalid_input, heif_suberror_Invalid_parameter_value, "SPS conformance window exceeds image dimensions"}; @@ -742,6 +843,13 @@ } config->bit_depth_chroma = (uint8_t) (value + 8); + if (reader.get_bits_remaining() < 0) { + // The reader returns zeros past the end, so a truncated SPS would + // otherwise parse with made-up values. + return Error{heif_error_Invalid_input, + heif_suberror_End_of_data, + "SPS header is truncated"}; + } // --- init static configuration fields --- diff -Nru libheif-1.23.4/libheif/codecs/hevc_boxes.h libheif-1.23.6/libheif/codecs/hevc_boxes.h --- libheif-1.23.4/libheif/codecs/hevc_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -84,6 +84,10 @@ bool get_general_profile_compatibility_flag(int idx) const; bool is_profile_compatible(Profile) const; + + // Whether the profile is one of those that the MIAF profiles for HEVC list for their images + // (ISO/IEC 23000-22, A.3 to A.5). The tier and the level are not considered. + bool is_miaf_profile() const; }; @@ -101,7 +105,7 @@ bool is_essential() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "HEVC Configuration Item"; } diff -Nru libheif-1.23.4/libheif/codecs/hevc_dec.cc libheif-1.23.6/libheif/codecs/hevc_dec.cc --- libheif-1.23.4/libheif/codecs/hevc_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -24,6 +24,7 @@ #include "context.h" #include "plugins/nalu_utils.h" +#include #include @@ -51,29 +52,53 @@ } -Result> Decoder_HEVC::get_coded_image_size_from_config() const +Result> Decoder_HEVC::get_max_coded_image_size(const std::vector& compressed_data) const { - const auto& nal_arrays = m_hvcC->get_configuration().m_nal_array; + // `compressed_data` is the combined configuration + bitstream buffer about to be + // pushed to the decoder. Scan it for every SPS NAL unit and return the largest coded picture + // size any of them declares. An SPS carried in the item data (not just in hvcC) + // drives the decoder's buffer allocation and can be far larger than the + // container 'ispe', so the config record alone is not a sufficient gate. + bool found = false; + uint32_t max_width = 0; + uint32_t max_height = 0; + + for (const auto& nal : split_nal_units_4byte_length_prefixed(compressed_data.data(), compressed_data.size())) { + const uint8_t* nal_data = nal.first; + size_t nal_size = nal.second; - for (const auto& arr : nal_arrays) { - if (arr.m_NAL_unit_type != HEVC_NAL_UNIT_SPS_NUT || arr.m_nal_units.empty()) { + // HEVC NAL unit header (2 bytes): forbidden_zero_bit(1), nal_unit_type(6), ... + if (nal_size < 2) { + continue; + } + int nal_type = (nal_data[0] >> 1) & 0x3F; + if (nal_type != HEVC_NAL_UNIT_SPS_NUT) { 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, + Error e = parse_sps_for_hvcC_configuration(nal_data, nal_size, &scratch, &cropped_w, &cropped_h, &coded); if (e) { + // An SPS we cannot parse has an unknown coded size, so it must not reach the + // decoder plugin: not every plugin enforces the size limits before allocating. return e; } - return std::optional{coded}; + found = true; + max_width = std::max(max_width, coded.width); + max_height = std::max(max_height, coded.height); + } + + if (!found) { + // Later samples of a sequence reuse the parameter sets of earlier ones, and + // those were already checked when they were pushed. + return std::optional{}; } - return std::optional{}; + return std::optional{ImageSize{max_width, max_height}}; } diff -Nru libheif-1.23.4/libheif/codecs/hevc_dec.h libheif-1.23.6/libheif/codecs/hevc_dec.h --- libheif-1.23.4/libheif/codecs/hevc_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -47,7 +47,7 @@ Result> read_bitstream_configuration_data() const override; - Result> get_coded_image_size_from_config() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; private: const std::shared_ptr m_hvcC; diff -Nru libheif-1.23.4/libheif/codecs/hevc_enc.cc libheif-1.23.6/libheif/codecs/hevc_enc.cc --- libheif-1.23.4/libheif/codecs/hevc_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -24,11 +24,37 @@ #include "context.h" #include "api_structs.h" +#include #include +#include #include "plugins/nalu_utils.h" +// The HEVCDecoderConfigurationRecord ('hvcC', ISO/IEC 14496-15) stores bitDepthLumaMinus8 and +// bitDepthChromaMinus8 in 3 bits each. It can signal 8 to 15 bits per sample, but not the +// 16 bits that HEVC itself allows. Writing a 16-bit image would put a bit depth of 8 into +// its 'hvcC' box, so we refuse to encode it. +static const int MAX_BIT_DEPTH_IN_HVCC = 15; + +static Error check_bit_depth_fits_hvcC(int bit_depth) +{ + if (bit_depth > MAX_BIT_DEPTH_IN_HVCC) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "HEVC images with more than 15 bits per sample cannot be written, " + "because the 'hvcC' box cannot signal their bit depth."}; + } + + return Error::Ok; +} + +static Error check_bit_depth_fits_hvcC(const HEVCDecoderConfigurationRecord& config) +{ + return check_bit_depth_fits_hvcC(std::max(config.bit_depth_luma, config.bit_depth_chroma)); +} + + // 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, @@ -37,6 +63,10 @@ { CodedImageData codedImage; + if (Error bitDepthErr = check_bit_depth_fits_hvcC(image->get_visual_image_bits_per_pixel())) { + return bitDepthErr; + } + auto hvcC = std::make_shared(); heif_image c_api_image; @@ -69,6 +99,11 @@ if ((data[0] >> 1) == HEVC_NAL_UNIT_SPS_NUT) { parse_sps_for_hvcC_configuration(data, size, &hvcC->get_configuration(), &encoded_width, &encoded_height); + // The encoder decides about the coded bit depth, it may differ from the one of the input image. + if (Error bitDepthErr = check_bit_depth_fits_hvcC(hvcC->get_configuration())) { + return bitDepthErr; + } + codedImage.encoded_image_width = encoded_width; codedImage.encoded_image_height = encoded_height; } @@ -122,6 +157,10 @@ uint32_t framerate_num, uint32_t framerate_denom, uintptr_t frame_number) { + if (Error bitDepthErr = check_bit_depth_fits_hvcC(image->get_visual_image_bits_per_pixel())) { + return bitDepthErr; + } + heif_image c_api_image; c_api_image.image = image; @@ -170,17 +209,15 @@ } -std::optional Encoder_HEVC::encode_sequence_get_data() +std::optional Encoder_HEVC::encode_sequence_extract_data() { - return std::move(m_current_output_data); + return std::exchange(m_current_output_data, std::nullopt); } Error Encoder_HEVC::get_data(heif_encoder* encoder) { //CodedImageData codedImage; - bool got_some_data = false; - for (;;) { uint8_t* data; int size; @@ -196,8 +233,6 @@ 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); @@ -208,6 +243,10 @@ parse_sps_for_hvcC_configuration(data, size, &m_hvcC->get_configuration(), &m_encoded_image_width, &m_encoded_image_height); + + if (Error bitDepthErr = check_bit_depth_fits_hvcC(m_hvcC->get_configuration())) { + return bitDepthErr; + } } switch (nal_type) { @@ -243,7 +282,11 @@ } } - if (!got_some_data) { + // No coded image to report when the encoder returned no NALs, or only parameter sets. + // x265, for example, emits VPS/SPS/PPS from encoder_headers() as soon as the sequence + // encoder is opened, so the first get_data() after start_sequence_encoding() sees only + // headers. Those went into m_hvcC above and m_current_output_data stays disengaged. + if (!m_current_output_data) { return {}; } @@ -257,8 +300,7 @@ // 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_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; diff -Nru libheif-1.23.4/libheif/codecs/hevc_enc.h libheif-1.23.6/libheif/codecs/hevc_enc.h --- libheif-1.23.4/libheif/codecs/hevc_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/hevc_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -51,7 +51,7 @@ Error encode_sequence_flush(heif_encoder* encoder) override; - std::optional encode_sequence_get_data() override; + std::optional encode_sequence_extract_data() override; std::shared_ptr get_sample_description_box(const CodedImageData&) const override; @@ -59,7 +59,7 @@ 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. + // Whether the hvcC is complete and was returned in an encode_sequence_extract_data() call. bool m_hvcC_has_VPS = false; bool m_hvcC_has_SPS = false; bool m_hvcC_has_PPS = false; diff -Nru libheif-1.23.4/libheif/codecs/jpeg2000_boxes.cc libheif-1.23.6/libheif/codecs/jpeg2000_boxes.cc --- libheif-1.23.4/libheif/codecs/jpeg2000_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg2000_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -64,10 +64,9 @@ return range.get_error(); } -std::string Box_cdef::dump(Indent& indent) const +void Box_cdef::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& channel : m_channels) { sstr << indent << "channel_index: " << channel.channel_index @@ -75,7 +74,7 @@ << ", channel_association: " << channel.channel_association << "\n"; } - return sstr.str(); + return; } @@ -146,10 +145,9 @@ } -std::string Box_cmap::dump(Indent& indent) const +void Box_cmap::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& component : m_components) { sstr << indent << "component_index: " << component.component_index @@ -157,7 +155,7 @@ << ", palette_colour: " << (int) (component.palette_colour) << "\n"; } - return sstr.str(); + return; } @@ -181,13 +179,29 @@ { uint16_t num_entries = range.read16(); uint8_t num_palette_columns = range.read8(); + + // ISO/IEC 15444-1 (Table I.12) requires NPC to be in the range 1 to 255. + // A palette without columns carries no entry data, so the per-entry byte + // count below would be zero and could not bound num_entries. The entry loop + // then allocated up to 65535 empty PaletteEntry vectors from an 11-byte box, + // and nested 'j2kH' containers could repeat this hundreds of times without + // any of it being charged to the memory limits (GHSA-9c75-9g8r-4728). + if (num_palette_columns == 0) { + return Error(heif_error_Invalid_input, + heif_suberror_Invalid_J2K_codestream, + "pclr box declares zero palette columns"); + } + for (uint8_t i = 0; i < num_palette_columns; i++) { - uint8_t bit_depth = range.read8(); - if (bit_depth & 0x80) { + // B_i (Table I.13): the high bit marks signed values, the low 7 bits hold + // the column precision minus one (0..37 for 1..38 bits). + uint8_t b = range.read8(); + if (b & 0x80) { return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "pclr with signed data is not supported"); } + uint8_t bit_depth = static_cast((b & 0x7F) + 1); if (bit_depth > 16) { return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, @@ -195,21 +209,35 @@ } 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). + // Number of bytes each palette entry occupies in the box: each C_ji value is + // padded to a whole number of bytes (I.5.3.4). 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). The precision is at + // least 1 bit, so every entry occupies at least one byte and the bound is + // never vacuous. num_entries is a uint16_t and bytes_per_entry is at most + // 255 * 2, so the product cannot overflow. size_t bytes_per_entry = 0; for (uint8_t bd : m_bitDepths) { - bytes_per_entry += (bd <= 8) ? 1 : 2; + bytes_per_entry += (bd + 7) / 8; } - if (bytes_per_entry != 0 && - num_entries > range.get_remaining_bytes() / bytes_per_entry) { + if (static_cast(num_entries) * bytes_per_entry > range.get_remaining_bytes()) { return Error(heif_error_Invalid_input, heif_suberror_End_of_data, "pclr box declares more entries than the box contains"); } + // Each entry is stored as its own vector, so the palette costs noticeably + // more memory than the bytes it occupies in the file. Charge that storage + // to the memory limits before allocating it. + size_t bytes_per_stored_entry = sizeof(PaletteEntry) + num_palette_columns * sizeof(uint16_t); + if (auto err = m_memory_handle.alloc(num_entries, bytes_per_stored_entry, + limits, "the 'pclr' palette")) { + return err; + } + + m_entries.reserve(num_entries); + for (uint16_t j = 0; j < num_entries; j++) { PaletteEntry entry; for (size_t i = 0; i < m_bitDepths.size(); i++) { @@ -227,10 +255,9 @@ } -std::string Box_pclr::dump(Indent& indent) const +void Box_pclr::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "NE: " << m_entries.size(); sstr << ", NPC: " << (int) get_num_columns(); @@ -241,7 +268,7 @@ // TODO: maybe dump entries too? sstr << "\n"; - return sstr.str(); + return; } @@ -256,8 +283,8 @@ writer.write16(get_num_entries()); writer.write8(get_num_columns()); - for (uint8_t b : m_bitDepths) { - writer.write8(b); + for (uint8_t bd : m_bitDepths) { + writer.write8(static_cast(bd - 1)); // B_i stores the precision minus one } for (PaletteEntry entry : m_entries) { for (unsigned long int i = 0; i < entry.columns.size(); i++) { @@ -314,10 +341,9 @@ return range.get_error(); } -std::string Box_j2kL::dump(Indent& indent) const +void Box_j2kL::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& layer : m_layers) { sstr << indent << "layer_id: " << layer.layer_id @@ -325,7 +351,7 @@ << ", decode_layers: " << layer.decode_layers << "\n"; } - return sstr.str(); + return; } @@ -351,14 +377,13 @@ return read_children(range, READ_CHILDREN_ALL, limits); } -std::string Box_j2kH::dump(Indent& indent) const +void Box_j2kH::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); - sstr << dump_children(indent); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } diff -Nru libheif-1.23.4/libheif/codecs/jpeg2000_boxes.h libheif-1.23.6/libheif/codecs/jpeg2000_boxes.h --- libheif-1.23.4/libheif/codecs/jpeg2000_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg2000_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -23,6 +23,7 @@ #include "box.h" #include "file.h" +#include "security_limits.h" #include "context.h" #include #include @@ -56,7 +57,7 @@ public: Box_cdef() { set_short_type(fourcc("cdef")); } - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter &writer) const override; @@ -140,7 +141,7 @@ public: Box_cmap() { set_short_type(fourcc("cmap")); } - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter &writer) const override; @@ -187,7 +188,7 @@ set_short_type(fourcc("pclr")); } - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter &writer) const override; @@ -211,6 +212,9 @@ /** * Get the bit depths for the columns in this palette box. * + * These are the column precisions in bits (1 to 16), not the raw B_i + * field of the box, which stores the precision minus one. + * * @return the bit depths as a read-only vector. */ const std::vector& get_bit_depths() const @@ -218,14 +222,15 @@ return m_bitDepths; } - const uint8_t get_num_entries() const + // NE is a 16-bit field (1 to 1024 per ISO/IEC 15444-1 Table I.12). + uint16_t get_num_entries() const { - return (uint8_t)(m_entries.size()); + return static_cast(m_entries.size()); } - const uint8_t get_num_columns() const + uint8_t get_num_columns() const { - return (uint8_t)(m_bitDepths.size()); + return static_cast(m_bitDepths.size()); } void add_entry(const PaletteEntry& entry) @@ -241,7 +246,7 @@ * This will reset any existing columns and entries. * * @param num_columns the number of columns (e.g. 3 for RGB) - * @param bit_depth the bit depth for each column (e.g. 8 for 24-bit RGB) + * @param bit_depth the bit depth for each column in bits, 1 to 16 (e.g. 8 for 24-bit RGB) */ void set_columns(uint8_t num_columns, uint8_t bit_depth); @@ -251,6 +256,7 @@ private: std::vector m_bitDepths; std::vector m_entries; + MemoryHandle m_memory_handle; }; @@ -268,7 +274,7 @@ public: Box_j2kL() { set_short_type(fourcc("j2kL")); } - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter &writer) const override; @@ -315,7 +321,7 @@ bool is_essential() const override { return true; } - std::string dump(Indent &) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; // Default write behaviour for a container is to write children diff -Nru libheif-1.23.4/libheif/codecs/jpeg2000_dec.cc libheif-1.23.6/libheif/codecs/jpeg2000_dec.cc --- libheif-1.23.4/libheif/codecs/jpeg2000_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg2000_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -32,6 +32,34 @@ } +Result> Decoder_JPEG2000::get_max_coded_image_size(const std::vector& compressed_data) const +{ + // The JPEG 2000 coded size is the reference grid (Xsiz, Ysiz) declared in the + // SIZ marker of the codestream, which lives in the item data. A decoder performs + // its tile and coefficient arithmetic over the full reference grid, not just the + // visible window (Xsiz-XOsiz, Ysiz-YOsiz), so the reference grid is the size the + // decoder effectively allocates over and can be far larger than the container + // 'ispe'. Parse it here so the shared decode path can reject over-limit inputs + // before ANY J2K plugin (openjpeg, ffmpeg, ...) runs -- the openjpeg plugin has + // its own equivalent gate (GHSA-q492-cfcm-895h), this makes the check + // backend-independent. + JPEG2000MainHeader header; + Error err = header.parseHeader(compressed_data); + if (err) { + // We do not hand a codestream to the decoder plugin when we cannot read its SIZ + // marker segment ourselves. The SIZ marker segment has to follow the SOC marker + // directly, but the decoder libraries are more lenient: they skip what they do not + // know (OpenJPEG skips arbitrary data) and use the SIZ marker segment they find + // later. Passing such a codestream on would let the plugin work with a reference + // grid that was never checked against the limits. + return err; + } + + // A reference grid without any pixels is rejected by the size check of the caller. + return std::optional{ImageSize{header.getXSize(), header.getYSize()}}; +} + + int Decoder_JPEG2000::get_luma_bits_per_pixel() const { Result> imageDataResult = get_compressed_data(true); diff -Nru libheif-1.23.4/libheif/codecs/jpeg2000_dec.h libheif-1.23.6/libheif/codecs/jpeg2000_dec.h --- libheif-1.23.4/libheif/codecs/jpeg2000_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg2000_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -49,6 +49,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; + private: const std::shared_ptr m_j2kH; }; diff -Nru libheif-1.23.4/libheif/codecs/jpeg2000_enc.h libheif-1.23.6/libheif/codecs/jpeg2000_enc.h --- libheif-1.23.4/libheif/codecs/jpeg2000_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg2000_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -42,7 +42,7 @@ std::shared_ptr get_sample_description_box(const CodedImageData&) const override; - bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + bool encode_sequence_started() const override { return m_sequence_started; } Error encode_sequence_frame(const std::shared_ptr& image, heif_encoder* encoder, @@ -62,6 +62,7 @@ m_codedImageData->frame_nr = frame_number; m_codedImageData->is_sync_frame = true; + m_sequence_started = true; return {}; } @@ -71,13 +72,14 @@ return {}; } - std::optional encode_sequence_get_data() override + std::optional encode_sequence_extract_data() override { - return std::move(m_codedImageData); + return std::exchange(m_codedImageData, std::nullopt); } private: std::optional m_codedImageData; + bool m_sequence_started = false; }; diff -Nru libheif-1.23.4/libheif/codecs/jpeg_boxes.cc libheif-1.23.6/libheif/codecs/jpeg_boxes.cc --- libheif-1.23.4/libheif/codecs/jpeg_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -38,14 +38,13 @@ } -std::string Box_jpgC::dump(Indent& indent) const +void Box_jpgC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "num bytes: " << m_data.size() << "\n"; - return sstr.str(); + return; } diff -Nru libheif-1.23.4/libheif/codecs/jpeg_boxes.h libheif-1.23.6/libheif/codecs/jpeg_boxes.h --- libheif-1.23.4/libheif/codecs/jpeg_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -41,7 +41,7 @@ void set_data(const std::vector& data) { m_data = data; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; diff -Nru libheif-1.23.4/libheif/codecs/jpeg_dec.cc libheif-1.23.6/libheif/codecs/jpeg_dec.cc --- libheif-1.23.4/libheif/codecs/jpeg_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -56,7 +56,15 @@ return dataResult.error(); } - const std::vector& data = *dataResult; + return parse_SOF(*dataResult); +} + + +Error Decoder_JPEG::parse_SOF(const std::vector& data) +{ + if (m_config) { + return Error::Ok; + } Error error_invalidSOF{heif_error_Invalid_input, heif_suberror_Unspecified, @@ -71,6 +79,9 @@ ConfigInfo info; info.sample_precision = data[i + 4]; + // SOF layout: FF Cx | Lf(2) | precision(1) | Y=height(2) | X=width(2) | Nf(1) + info.coded_height = (uint32_t(data[i + 5]) << 8) | data[i + 6]; + info.coded_width = (uint32_t(data[i + 7]) << 8) | data[i + 8]; info.nComponents = data[i + 9]; if (i + 11 + 3 * info.nComponents >= data.size()) { @@ -140,6 +151,26 @@ } +Result> Decoder_JPEG::get_max_coded_image_size(const std::vector& compressed_data) const +{ + // The JPEG coded size lives in the SOF marker inside the bitstream (there is no + // separate configuration record that carries it). `compressed_data` is the + // buffer about to be handed to the decoder, so an oversized SOF is caught here + // before libjpeg allocates, regardless of the (possibly tiny) 'ispe'. + Error err = const_cast(this)->parse_SOF(compressed_data); + if (err) { + // No parseable SOF marker: skip the gate and let the decoder handle it. + return std::optional{}; + } + + if (m_config->coded_width == 0 || m_config->coded_height == 0) { + return std::optional{}; + } + + return std::optional{ImageSize{m_config->coded_width, m_config->coded_height}}; +} + + Error Decoder_JPEG::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { Error err = const_cast(this)->parse_SOF(); diff -Nru libheif-1.23.4/libheif/codecs/jpeg_dec.h libheif-1.23.6/libheif/codecs/jpeg_dec.h --- libheif-1.23.4/libheif/codecs/jpeg_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -48,6 +48,8 @@ Result> read_bitstream_configuration_data() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; + private: const std::shared_ptr m_jpgC; // Optional jpgC box. May be NULL. @@ -58,11 +60,16 @@ uint8_t nComponents = 0; uint8_t h_sampling[3]{}; uint8_t v_sampling[3]{}; + + // Coded frame size from the SOF marker (the buffer the decoder allocates). + uint32_t coded_width = 0; + uint32_t coded_height = 0; }; std::optional m_config; Error parse_SOF(); + Error parse_SOF(const std::vector& data); }; #endif diff -Nru libheif-1.23.4/libheif/codecs/jpeg_enc.cc libheif-1.23.6/libheif/codecs/jpeg_enc.cc --- libheif-1.23.4/libheif/codecs/jpeg_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -79,9 +79,11 @@ break; } - size_t oldsize = vec.size(); - vec.resize(oldsize + size); - memcpy(vec.data() + oldsize, data, size); + if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + size_t oldsize = vec.size(); + vec.resize(oldsize + size); + memcpy(vec.data() + oldsize, data, size); + } } #if 0 diff -Nru libheif-1.23.4/libheif/codecs/jpeg_enc.h libheif-1.23.6/libheif/codecs/jpeg_enc.h --- libheif-1.23.4/libheif/codecs/jpeg_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/jpeg_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -45,7 +45,7 @@ std::shared_ptr get_sample_description_box(const CodedImageData&) const override; - bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + bool encode_sequence_started() const override { return m_sequence_started; } Error encode_sequence_frame(const std::shared_ptr& image, heif_encoder* encoder, @@ -65,6 +65,7 @@ m_codedImageData->frame_nr = frame_number; m_codedImageData->is_sync_frame = true; + m_sequence_started = true; return {}; } @@ -74,13 +75,14 @@ return {}; } - std::optional encode_sequence_get_data() override + std::optional encode_sequence_extract_data() override { - return std::move(m_codedImageData); + return std::exchange(m_codedImageData, std::nullopt); } private: std::optional m_codedImageData; + bool m_sequence_started = false; }; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -203,10 +203,9 @@ } -std::string Box_cmpd::dump(Indent& indent) const +void Box_cmpd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& component : m_components) { sstr << indent << "component_type: " << component.get_component_type_name(); @@ -216,7 +215,7 @@ } } - return sstr.str(); + return; } Error Box_cmpd::write(StreamWriter& writer) const @@ -374,10 +373,9 @@ -std::string Box_uncC::dump(Indent& indent) const +void Box_uncC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "profile: " << m_profile; if (m_profile != 0) { @@ -416,7 +414,7 @@ sstr << indent << "num_tile_rows: " << m_num_tile_rows << "\n"; } - return sstr.str(); + return; } @@ -769,13 +767,12 @@ } -std::string Box_cmpC::dump(Indent& indent) const +void Box_cmpC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "compression_type: " << fourcc_to_string(m_compression_type) << "\n"; sstr << indent << "compressed_entity_type: " << (int)m_compressed_unit_type << "\n"; - return sstr.str(); + return; } Error Box_cmpC::write(StreamWriter& writer) const @@ -885,15 +882,14 @@ } -std::string Box_icef::dump(Indent& indent) const +void Box_icef::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "num_compressed_units: " << m_unit_infos.size() << "\n"; for (CompressedUnitInfo unit_info: m_unit_infos) { sstr << indent << "unit_offset: " << unit_info.unit_offset << ", unit_size: " << unit_info.unit_size << "\n"; } - return sstr.str(); + return; } Error Box_icef::write(StreamWriter& writer) const @@ -1048,18 +1044,17 @@ } -std::string Box_cpat::dump(Indent& indent) const +void Box_cpat::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "pattern_width: " << get_pattern_width() << "\n"; sstr << indent << "pattern_height: " << get_pattern_height() << "\n"; for (const auto& pixel : m_pattern.pixels) { sstr << indent << "component index: " << pixel.cmpd_index << ", gain: " << pixel.component_gain << "\n"; } - return sstr.str(); + return; } @@ -1134,11 +1129,10 @@ } -std::string Box_splz::dump(Indent& indent) const +void Box_splz::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "component_count: " << m_pattern.component_ids.size() << "\n"; for (size_t i = 0; i < m_pattern.component_ids.size(); i++) { @@ -1160,7 +1154,7 @@ } } - return sstr.str(); + return; } @@ -1248,11 +1242,10 @@ } -std::string Box_sbpm::dump(Indent& indent) const +void Box_sbpm::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "component_count: " << m_map.component_ids.size() << "\n"; for (size_t i = 0; i < m_map.component_ids.size(); i++) { @@ -1277,7 +1270,7 @@ << ", column=" << m_map.bad_pixels[i].column << "\n"; } - return sstr.str(); + return; } @@ -1376,11 +1369,10 @@ } -std::string Box_snuc::dump(Indent& indent) const +void Box_snuc::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "component_count: " << m_nuc.component_ids.size() << "\n"; for (size_t i = 0; i < m_nuc.component_ids.size(); i++) { @@ -1395,7 +1387,7 @@ sstr << indent << "nuc_gains: " << num_pixels << " values\n"; sstr << indent << "nuc_offsets: " << num_pixels << " values\n"; - return sstr.str(); + return; } @@ -1448,11 +1440,10 @@ } -std::string Box_cloc::dump(Indent& indent) const +void Box_cloc::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); static const char* location_names[] = { "h=0, v=0.5", // 0 @@ -1470,7 +1461,7 @@ } sstr << "\n"; - return sstr.str(); + return; } @@ -1528,14 +1519,13 @@ } -std::string Box_gimi_component_content_ids::dump(Indent& indent) const +void Box_gimi_component_content_ids::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (size_t i = 0; i < m_content_ids.size(); i++) { sstr << indent << "[" << i << "] content ID: " << m_content_ids[i] << "\n"; } - return sstr.str(); + return; } diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.h libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.h --- libheif-1.23.4/libheif/codecs/uncompressed/unc_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -46,7 +46,7 @@ set_short_type(fourcc("cmpd")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -103,7 +103,7 @@ set_version(is_minimized() ? 1 : 0); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -271,7 +271,7 @@ set_short_type(fourcc("cmpC")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; uint32_t get_compression_type() const { return m_compression_type; } @@ -327,7 +327,7 @@ m_unit_infos[tile_idx] = unit_info; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -368,7 +368,7 @@ void set_pattern(const BayerPatternCmpd& pattern) { m_pattern = pattern; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -397,7 +397,7 @@ void set_pattern(const PolarizationPattern& pattern) { m_pattern = pattern; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -425,7 +425,7 @@ 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; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; protected: @@ -451,7 +451,7 @@ const SensorNonUniformityCorrection& get_nuc() const { return m_nuc; } void set_nuc(const SensorNonUniformityCorrection& nuc) { m_nuc = nuc; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; protected: @@ -477,7 +477,7 @@ 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; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; protected: @@ -522,7 +522,7 @@ bool is_transformative_property() const override { return false; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "GIMI Component Content IDs"; } diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -396,6 +396,62 @@ } +UncompressedImageCodec::TileAccess +UncompressedImageCodec::get_tile_access(const std::shared_ptr& uncC, + const std::shared_ptr& cmpC, + const std::shared_ptr& icef, + uint32_t* out_units_per_tile) +{ + if (!cmpC) { + return TileAccess::direct; + } + + // Without an icef box, the complete item is a single compressed unit. + if (!icef || !uncC) { + return TileAccess::whole_item; + } + + switch (cmpC->get_compressed_unit_type()) { + case heif_cmpC_compressed_unit_type_image_tile: + if (out_units_per_tile) { + *out_units_per_tile = 1; + } + return TileAccess::compressed_units; + + case heif_cmpC_compressed_unit_type_image_row: + case heif_cmpC_compressed_unit_type_image_pixel: { + // Rows and pixels are parts of a tile and the units are listed in the order of + // the uncompressed data. As all tiles have the same layout, each tile consists of + // the same number of consecutive units. We do not have to know what exactly the + // file writer considers a row. + + // With tile-component interleave, the data of a tile is not contiguous. + if (uncC->get_interleave_type() == interleave_mode_tile_component) { + return TileAccess::whole_item; + } + + uint64_t num_tiles = static_cast(uncC->get_number_of_tile_columns()) * uncC->get_number_of_tile_rows(); + uint64_t num_units = icef->get_units().size(); + + if (num_tiles == 0 || num_units == 0 || num_units % num_tiles != 0) { + return TileAccess::whole_item; + } + + if (out_units_per_tile) { + *out_units_per_tile = static_cast(num_units / num_tiles); + } + return TileAccess::compressed_units; + } + + case heif_cmpC_compressed_unit_type_full_item: + case heif_cmpC_compressed_unit_type_image: + break; + } + + return TileAccess::whole_item; +} + + Error UncompressedImageCodec::decode_uncompressed_image_tile(const HeifContext* context, heif_item_id ID, std::shared_ptr& img, @@ -462,7 +518,8 @@ decoder->ensure_channel_list(img); std::vector tile_data; - Error err = decoder->fetch_tile_data(dataExtent, properties, tile_x0, tile_y0, tile_data); + MemoryHandle tile_data_memory_handle; + Error err = decoder->fetch_tile_data(dataExtent, properties, tile_x0, tile_y0, tile_data, tile_data_memory_handle); if (err) { return err; } diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.h libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.h --- libheif-1.23.4/libheif/codecs/uncompressed/unc_codec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_codec.h 2026-10-05 16:16:00.000000000 +0000 @@ -59,6 +59,27 @@ std::shared_ptr& img, uint32_t tile_x0, uint32_t tile_y0); + // How the data of a single tile of the uncC tiling can be obtained. + enum class TileAccess : uint8_t { + // No generic compression. The tile data is read directly from the item data. + direct, + + // Each tile is a run of consecutive compressed units: a single unit for + // 'image_tile' units, several units for 'image_row' and 'image_pixel' units. + compressed_units, + + // The compressed units span several tiles (full item, full image of a component), + // or they cannot be assigned to the tiles. The item has to be decompressed as a whole, + // hence the tiles cannot be decoded independently. + whole_item + }; + + // 'out_units_per_tile' is only set for TileAccess::compressed_units. + static TileAccess get_tile_access(const std::shared_ptr& uncC, + const std::shared_ptr& cmpC, + const std::shared_ptr& icef, + uint32_t* out_units_per_tile = nullptr); + struct unci_properties { std::shared_ptr ispe; std::shared_ptr cmpd; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.cc 2026-10-05 16:16:00.000000000 +0000 @@ -59,10 +59,27 @@ } +// The size of the data of one tile, as the sum of the sizes from get_tile_data_sizes(). +static Result sum_tile_data_sizes(const std::vector& sizes) +{ + uint64_t tile_size = 0; + for (uint64_t size : sizes) { + if (size > UINT64_MAX - tile_size) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile size exceeds 64-bit range"}; + } + tile_size += size; + } + + return tile_size; +} + + 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) + std::vector& tile_data, + MemoryHandle& tile_data_memory_handle) { 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"}; @@ -75,10 +92,43 @@ const auto& sizes = *sizesResult; uint32_t tileIdx = tile_x + tile_y * (m_width / m_tile_width); - if (sizes.size() == 1) { + // 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; + + const auto tile_access = UncompressedImageCodec::get_tile_access(m_uncC, properties.cmpC, properties.icef); + + if (tile_access == UncompressedImageCodec::TileAccess::whole_item) { + // decode_image() decompresses such an item once for all tiles, and the image item + // does not offer decoding of individual tiles (GHSA-6fqc-p7r8-2g36). + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, + "Internal error: unci tiles cannot be decoded independently for this generic compression"}; + } + else if (tile_access == UncompressedImageCodec::TileAccess::compressed_units) { + // The compressed units are the complete tile, and get_compressed_image_data_uncompressed() + // returns the whole tile irrespective of the requested range. Fetch it only once. + // The scattered per-component reads below would decompress the same units again for + // each component and concatenate the copies, growing 'tile_data' to num_components + // times the tile size without any memory accounting (GHSA-fcmw-5764-7rq8). + + Result tileSizeResult = sum_tile_data_sizes(sizes); + if (!tileSizeResult) { + return tileSizeResult.error(); + } + + // The requested size is the size of the whole tile. Units that decompress to + // a different size are rejected. + Error err = get_compressed_image_data_uncompressed(dataExtent, properties, &tile_data, 0, *tileSizeResult, tileIdx, nullptr); + if (err) { + return err; + } + } + else 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); + Error err = get_compressed_image_data_uncompressed(dataExtent, properties, &tile_data, tile_start_offset, sizes[0], tileIdx, nullptr); + if (err) { + return err; + } } else { // Scattered per-component reads (tile_component interleave) @@ -94,12 +144,23 @@ return err; } + // Charge the component data to the memory budget before appending it, so that + // the accumulated tile data cannot grow beyond the security limits. + if (Error memErr = tile_data_memory_handle.alloc(channel_data.size(), limits, "unci tile data")) { + return memErr; + } + tile_data.insert(tile_data.end(), channel_data.begin(), channel_data.end()); component_offset += size * num_tiles; } + + return Error::Ok; } - return Error::Ok; + // The reading and decompression functions only bound the memory while they run. + // 'tile_data' outlives them, so charge it to the memory budget for as long as the + // caller keeps it. + return tile_data_memory_handle.alloc(tile_data.size(), limits, "unci tile data"); } @@ -131,49 +192,123 @@ 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" - }; - } + uint32_t units_per_tile = 0; + const auto tile_access = UncompressedImageCodec::get_tile_access(m_uncC, cmpC_box, icef_box, &units_per_tile); + + if (tile_access != UncompressedImageCodec::TileAccess::compressed_units) { + // An item that has to be decompressed as a whole is handled by decode_image(), + // which decompresses it once for all tiles (GHSA-6fqc-p7r8-2g36). + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, + "Internal error: unci tiles cannot be decoded independently for this generic compression"}; + } + + // --- the tile consists of 'units_per_tile' consecutive compressed units + + const auto& units = icef_box->get_units(); + + const uint64_t first_unit = static_cast(tile_idx) * units_per_tile; + if (first_unit >= units.size() || + units_per_tile > units.size() - first_unit) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "no icef-box entry for tile index" + }; + } + + // Decompress only the units of this tile. 'range_size' is the size of the tile, so + // there cannot be more data than this in a valid file. Stopping there prevents small + // units from inflating far beyond the tile size (GHSA-fcmw-5764-7rq8). + uint64_t remaining_size = range_size; + + // The decompression functions only bound the memory of the unit they are working on. + // This handle accounts for the units of the tile that have been decompressed already. + // The complete tile is charged by the caller afterwards. + MemoryHandle accumulated_memory_handle; - const auto unit = units[tile_idx]; + for (uint32_t i = 0; i < units_per_tile; i++) { + const auto unit = units[first_unit + i]; - // 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); + auto dataResult = do_decompress_data(cmpC_box, *readingResult, limits, remaining_size); 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(); + if (dataResult->size() > remaining_size) { + return { + heif_error_Invalid_input, + heif_suberror_Decompression_invalid_data, + "compressed units of unci image contain more data than the image tile" + }; + } + + remaining_size -= dataResult->size(); + + if (Error memErr = accumulated_memory_handle.alloc(dataResult->size(), limits, + "unci icef decompressed units")) { + return memErr; + } + + if (units_per_tile == 1) { + *data = std::move(*dataResult); + } + else { + data->insert(data->end(), dataResult->begin(), dataResult->end()); } + } + + // Too little data is rejected as well. The tile decoders would read the missing + // data as zeros. + if (remaining_size != 0) { + return { + heif_error_Invalid_input, + heif_suberror_End_of_data, + "compressed units of unci image contain less data than the image tile" + }; + } + + return Error::Ok; +} + + +Result > unc_decoder::decompress_whole_item(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + uint64_t expected_size) const +{ + 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; + + Result*> readResult = dataExtent.read_data(); + if (!readResult) { + return readResult.error(); + } + + // Do not move the data out of the extent. This is the read cache of the extent and + // an emptied cache makes the next access read the data again, which a raw extent cannot do. + const std::vector& compressed_bytes = **readResult; + + // 'expected_size' is the size of the data of all tiles, so there cannot be more data + // than this in a valid file. Stopping there bounds the decompression by the image + // size instead of the memory budget. - const std::vector compressed_bytes = std::move(**readResult); + std::vector data; + if (icef_box) { // 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. + // building `data`; the result is accounted by the caller. MemoryHandle accumulated_memory_handle; for (Box_icef::CompressedUnitInfo unit_info : icef_box->get_units()) { @@ -193,80 +328,59 @@ 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); + auto dataResult = do_decompress_data(cmpC_box, compressed_unit_data, limits, expected_size - data.size()); if (!dataResult) { return dataResult.error(); } - const std::vector uncompressed_unit_data = std::move(*dataResult); + if (dataResult->size() > expected_size - data.size()) { + return Error{ + heif_error_Invalid_input, + heif_suberror_Decompression_invalid_data, + "compressed unci image contains more data than the image tiles" + }; + } - if (Error memErr = accumulated_memory_handle.alloc(uncompressed_unit_data.size(), limits, + if (Error memErr = accumulated_memory_handle.alloc(dataResult->size(), limits, "unci icef decompressed units")) { return memErr; } - data->insert(data->end(), uncompressed_unit_data.data(), uncompressed_unit_data.data() + uncompressed_unit_data.size()); + data.insert(data.end(), dataResult->begin(), dataResult->end()); } - - 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); + auto dataResult = do_decompress_data(cmpC_box, compressed_bytes, limits, expected_size); 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" - }; - } + data = std::move(*dataResult); + } - // cut out the range that we actually need - memcpy(data->data(), data->data() + range_start_offset, range_size); - data->resize(range_size); + // Too little data is rejected as well. The tile decoders would read the missing + // data as zeros. + if (data.size() < expected_size) { + return Error{ + heif_error_Invalid_input, + heif_suberror_End_of_data, + "compressed unci image contains less data than the image tiles" + }; } - return Error::Ok; + return data; } Result > unc_decoder::do_decompress_data(std::shared_ptr& cmpC_box, const std::vector& compressed_data, - const heif_security_limits* limits) const + const heif_security_limits* limits, + uint64_t max_output_size) const { if (cmpC_box->get_compression_type() == fourcc("brot")) { #if HAVE_BROTLI - return decompress_brotli(compressed_data, limits); + return decompress_brotli(compressed_data, limits, max_output_size); #else return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_generic_compression_method, @@ -275,7 +389,7 @@ } else if (cmpC_box->get_compression_type() == fourcc("zlib")) { #if HAVE_ZLIB - return decompress_zlib(compressed_data, limits); + return decompress_zlib(compressed_data, limits, max_output_size); #else return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_generic_compression_method, @@ -284,7 +398,7 @@ } else if (cmpC_box->get_compression_type() == fourcc("defl")) { #if HAVE_ZLIB - return decompress_deflate(compressed_data, limits); + return decompress_deflate(compressed_data, limits, max_output_size); #else return Error(heif_error_Unsupported_feature, heif_suberror_Unsupported_generic_compression_method, @@ -310,10 +424,66 @@ ensure_channel_list(img); + // The extent and properties the tile data is fetched from. + const DataExtent* tile_extent = &extent; + const UncompressedImageCodec::unci_properties* tile_properties = &properties; + + DataExtent decompressed_extent; + UncompressedImageCodec::unci_properties decompressed_properties; + + if (UncompressedImageCodec::get_tile_access(m_uncC, properties.cmpC, properties.icef) == + UncompressedImageCodec::TileAccess::whole_item) { + // The compressed units do not correspond to the tiles, so the item has to be + // decompressed completely to get the data of any tile. Do this once for all tiles + // and continue as if the item was not compressed. Decompressing the item again for + // each tile needs time proportional to num_tiles * item_size (GHSA-6fqc-p7r8-2g36). + + auto sizesResult = get_tile_data_sizes(); + if (!sizesResult) { + return sizesResult.error(); + } + + Result tileSizeResult = sum_tile_data_sizes(*sizesResult); + if (!tileSizeResult) { + return tileSizeResult.error(); + } + + const uint64_t num_tiles = static_cast(m_uncC->get_number_of_tile_columns()) * m_uncC->get_number_of_tile_rows(); + if (num_tiles != 0 && *tileSizeResult > UINT64_MAX / num_tiles) { + return {heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed image data size exceeds 64-bit range"}; + } + + auto dataResult = decompress_whole_item(extent, properties, *tileSizeResult * num_tiles); + if (!dataResult) { + return dataResult.error(); + } + + // May be nullptr for a raw data extent, in which case no limit applies. + const heif_security_limits* limits = extent.m_file ? extent.m_file->get_security_limits() : nullptr; + + // The decompressed data is kept until all tiles are decoded, so charge it to the memory budget. + if (Error memErr = decompressed_extent.m_raw_memory_handle.alloc(dataResult->size(), limits, + "decompressed unci image data")) { + return memErr; + } + + decompressed_extent.m_file = extent.m_file; + decompressed_extent.m_raw = std::move(*dataResult); + + decompressed_properties = properties; + decompressed_properties.cmpC = nullptr; + decompressed_properties.icef = nullptr; + + tile_extent = &decompressed_extent; + tile_properties = &decompressed_properties; + } + 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); + MemoryHandle tile_data_memory_handle; + Error error = fetch_tile_data(*tile_extent, *tile_properties, tile_x0 / tile_width, tile_y0 / tile_height, tile_data, tile_data_memory_handle); if (error) { return error; } diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.h libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.h --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder.h 2026-10-05 16:16:00.000000000 +0000 @@ -30,6 +30,7 @@ #include "unc_boxes.h" class HeifPixelImage; +class MemoryHandle; struct DataExtent; struct heif_security_limits; @@ -48,10 +49,13 @@ virtual void ensure_channel_list(std::shared_ptr& img) {} + // The size of 'tile_data' is charged to 'tile_data_memory_handle', which has to + // live as long as 'tile_data'. Error fetch_tile_data(const DataExtent& dataExtent, const UncompressedImageCodec::unci_properties& properties, uint32_t tile_x, uint32_t tile_y, - std::vector& tile_data); + std::vector& tile_data, + MemoryHandle& tile_data_memory_handle); virtual Error decode_tile(const std::vector& tile_data, std::shared_ptr& img, @@ -81,9 +85,17 @@ uint32_t tile_idx, const Box_iloc::Item* item) const; + // Decompress the complete item. This is used when the compressed units do not give + // access to the individual tiles (UncompressedImageCodec::TileAccess::whole_item). + // The item has to decompress to exactly 'expected_size' bytes. + Result> decompress_whole_item(const DataExtent& dataExtent, + const UncompressedImageCodec::unci_properties& properties, + uint64_t expected_size) const; + Result> do_decompress_data(std::shared_ptr& cmpC_box, const std::vector& compressed_data, - const heif_security_limits* limits) const; + const heif_security_limits* limits, + uint64_t max_output_size = UINT64_MAX) const; const uint32_t m_width; const uint32_t m_height; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.cc 2026-10-05 16:16:00.000000000 +0000 @@ -168,6 +168,20 @@ 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; + + // The coding of the two samples in the bitstream is taken from their 'uncC' components. + entry.chroma_bits_per_component_sample[0] = component.component_bit_depth; + entry.chroma_component_alignment[0] = component.component_align_size; + + for (const Box_uncC::Component& other : m_uncC->get_components()) { + heif_channel other_channel; + if (map_uncompressed_component_to_channel(m_cmpd, other, &other_channel) && + other_channel == paired_channel) { + entry.chroma_bits_per_component_sample[1] = other.component_bit_depth; + entry.chroma_component_alignment[1] = other.component_align_size; + break; + } + } } entry.bits_per_component_sample = component.component_bit_depth; entry.component_alignment = component.component_align_size; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.h libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.h --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_legacybase.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_legacybase.h 2026-10-05 16:16:00.000000000 +0000 @@ -175,6 +175,11 @@ 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}; + // How each of the two chroma samples is coded in the bitstream: the number of bits of + // the value and the component_align_size. This is not the same as the storage width + // above: a 12-bit sample is stored in two bytes, but coded with 12 bits. + uint16_t chroma_bits_per_component_sample[2] = {0, 0}; + uint8_t chroma_component_alignment[2] = {0, 0}; uint16_t bits_per_component_sample; uint8_t component_alignment; uint32_t bytes_per_tile_row_src; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_mixed_interleave.cc 2026-10-05 16:16:00.000000000 +0000 @@ -29,24 +29,67 @@ Result> unc_decoder_mixed_interleave::get_tile_data_sizes() const { uint64_t tile_size = 0; + bool chroma_counted = false; 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; + uint64_t bits_per_row; + + if (entry.channel == heif_channel_Cb || entry.channel == heif_channel_Cr) { + // The two chroma components are stored together, like the components of a pixel in + // pixel interleave mode. They are counted once, at the first of the two. + if (chroma_counted) { + continue; + } + chroma_counted = true; + + // Returns the bit position after one Cb/Cr pair that starts at bit position 'pos'. + // This mirrors how processTile() reads the pair: a byte-aligned component starts on a + // byte boundary and occupies component_align_size bytes, any other component + // occupies exactly its bit depth. + auto end_of_chroma_pair = [&entry](uint64_t pos) { + for (int c = 0; c < 2; c++) { + if (entry.chroma_component_alignment[c] != 0) { + pos = (pos + 7) & ~uint64_t{7}; + pos += entry.chroma_component_alignment[c] * 8; + } + else { + pos += entry.chroma_bits_per_component_sample[c]; + } + } + return pos; + }; + + // The first pair starts at the byte-aligned start of the row. All following pairs + // start at the same position within a byte as the second one, so they all have the + // size of the second pair. + uint64_t end_of_first_pair = end_of_chroma_pair(0); + uint64_t end_of_second_pair = end_of_chroma_pair(end_of_first_pair); + + bits_per_row = 0; + if (entry.tile_width > 0) { + bits_per_row = end_of_first_pair + (end_of_second_pair - end_of_first_pair) * (entry.tile_width - 1); + } + } + 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; + } + + bits_per_row = uint64_t{bits_per_component} * entry.tile_width; } - if (bits_per_component != 0 && entry.tile_width > UINT32_MAX / bits_per_component) { + if (bits_per_row > UINT32_MAX) { 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; + uint32_t bytes_per_row = static_cast((bits_per_row + 7) / 8); // rows end on a byte boundary + skip_to_alignment(bytes_per_row, m_uncC->get_row_align_size()); + + tile_size += uint64_t{bytes_per_row} * entry.tile_height; } if (m_uncC->get_tile_align_size() != 0) { @@ -87,7 +130,7 @@ 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 + srcBits.markRowStart(); uint64_t dst_row_number = tile_y + channel_y0; uint64_t chroma_dst_row_offset[2]; @@ -102,21 +145,37 @@ // 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). + // + // A sample is coded with the bit depth of its component, as in pixel interleave + // mode, not with the width of the plane's storage word. Reading the full storage + // width instead put the bits of the neighbouring sample (or the alignment + // padding) into the upper bits of the value, so that a plane declared with, say, + // 12 bits received samples of up to 65535 (GHSA-v5rj-g4wv-j5w3). What such + // samples can do downstream is shown by GHSA-q7mw-2fmm-5q94. for (int c = 0; c < 2; c++) { + if (entry.chroma_component_alignment[c] != 0) { + srcBits.skip_to_byte_boundary(); + int numPadBits = (entry.chroma_component_alignment[c] * 8) - entry.chroma_bits_per_component_sample[c]; + srcBits.skip_bits(numPadBits); + } + 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); + int val = srcBits.get_bits(entry.chroma_bits_per_component_sample[c]); memcpy_to_native_endian(entry.chroma_dst_plane[c] + chroma_dst_row_offset[c] + dst_column_offset, val, bytes_per_sample); } } + srcBits.handleRowAlignment(m_uncC->get_row_align_size()); haveProcessedChromaForThisTile = true; } } } else { for (uint32_t tile_y = 0; tile_y < entry.tile_height; tile_y++) { + srcBits.markRowStart(); 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); + srcBits.handleRowAlignment(m_uncC->get_row_align_size()); } } } diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_decoder_pixel_interleave.cc 2026-10-05 16:16:00.000000000 +0000 @@ -27,38 +27,58 @@ 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; + // All pixels of a row have the same component layout, so the number of bits a pixel + // adds to the row is the same for every pixel column. Compute it once instead of + // iterating over every column, which was O(tile_width) and slow for a very wide image + // (GHSA-vr5j-9r89-725x). + // + // When a component starts on a byte boundary (component_alignment > 0), the row bit + // position is rounded up to a byte boundary before that component. The row position + // before a pixel is then a byte multiple, so from the second pixel on every pixel adds + // the same 'pad' padding bits in addition to 'bits_per_pixel'. The first pixel starts + // at offset 0, which is already byte-aligned, so it has no padding. Hence: + // + // bits_per_row = bits_per_pixel * tile_width + pad * (tile_width - 1) + // + // Without a byte-aligned component, 'pad' is 0 and this is bits_per_pixel * tile_width. + + uint64_t bits_per_pixel = 0; + bool has_byte_aligned_component = false; + + for (const ChannelListEntry& entry : channelList) { + uint32_t bits_per_component = entry.bits_per_component_sample; + if (entry.component_alignment > 0) { + has_byte_aligned_component = true; + + 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_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_pixel += bits_per_component; + } - 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; + if (m_uncC->get_pixel_size() != 0) { + uint64_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; + } + + uint64_t pad = has_byte_aligned_component ? (8 - bits_per_pixel % 8) % 8 : 0; + + uint64_t bits_per_row = 0; + if (m_tile_width > 0) { + bits_per_row = bits_per_pixel * m_tile_width + pad * (m_tile_width - 1); + } + + // The per-column loop that this replaces accumulated the row size in a uint32_t and + // rejected anything that did not fit, so keep that limit. + if (bits_per_row > UINT32_MAX) { + return Error{heif_error_Invalid_input, heif_suberror_Invalid_image_size, + "uncompressed tile row size exceeds 32-bit range"}; } - uint32_t bytes_per_row = (bits_per_row + 7) / 8; + uint32_t bytes_per_row = (static_cast(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); diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.h libheif-1.23.6/libheif/codecs/uncompressed/unc_enc.h --- libheif-1.23.4/libheif/codecs/uncompressed/unc_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -42,7 +42,7 @@ std::shared_ptr get_sample_description_box(const CodedImageData&) const override; - bool encode_sequence_started() const override { return m_codedImageData.has_value(); } + bool encode_sequence_started() const override { return m_sequence_started; } Error encode_sequence_frame(const std::shared_ptr& image, heif_encoder* encoder, @@ -64,6 +64,7 @@ m_codedImageData->frame_nr = frame_number; m_codedImageData->is_sync_frame = true; + m_sequence_started = true; return {}; } @@ -73,13 +74,14 @@ return {}; } - std::optional encode_sequence_get_data() override + std::optional encode_sequence_extract_data() override { - return std::move(m_codedImageData); + return std::exchange(m_codedImageData, std::nullopt); } private: std::optional m_codedImageData; + bool m_sequence_started = false; }; diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder.cc 2026-10-05 16:16:00.000000000 +0000 @@ -176,6 +176,18 @@ heif_suberror_Unspecified, "Image has an interleaved chroma format, but no interleaved pixel plane."}; } + + // Alpha of an interleaved image lives inside the interleaved plane (RGBA, RRGGBBAA). The + // interleaved encoders take their component list from the chroma format, so a separate alpha + // plane would make them address a fourth component that the chroma format does not have + // (GHSA-qfj5-c4pq-q998). HeifPixelImage::add_channel() refuses to build such an image, but + // transfer_channel_from_image_as() can still assemble one. + if (prototype_image->has_channel(heif_channel_Alpha)) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image has an interleaved chroma format and a separate alpha plane. " + "Alpha has to be part of the interleaved format."}; + } } else if (prototype_image->get_used_planar_component_ids().empty()) { return Error{heif_error_Invalid_input, diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_bytealign_pixel_interleave.cc 2026-10-05 16:16:00.000000000 +0000 @@ -61,7 +61,10 @@ { auto cmpd_ids = image->get_component_ids_interleaved(); - bool save_alpha = image->has_alpha(); + // Whether the pixels carry alpha is a property of the interleaved chroma format, which is also + // where the component list above comes from. has_alpha() would additionally report a separate + // alpha plane, which get_unc_encoder() rejects for interleaved images. + bool save_alpha = is_interleaved_with_alpha(image->get_chroma_format()); m_bytes_per_pixel = save_alpha ? 8 : 6; assert(cmpd_ids.size() == m_bytes_per_pixel/2); diff -Nru libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc --- libheif-1.23.4/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/uncompressed/unc_encoder_rgb_pixel_interleave.cc 2026-10-05 16:16:00.000000000 +0000 @@ -56,7 +56,11 @@ : unc_encoder(image) { auto cmpd_ids = image->get_component_ids_interleaved(); - bool save_alpha = image->has_alpha(); + + // Whether the pixels carry alpha is a property of the interleaved chroma format, which is also + // where the component list above comes from. has_alpha() would additionally report a separate + // alpha plane, which get_unc_encoder() rejects for interleaved images. + bool save_alpha = is_interleaved_with_alpha(image->get_chroma_format()); m_bytes_per_pixel = save_alpha ? 4 : 3; assert(cmpd_ids.size() == m_bytes_per_pixel); diff -Nru libheif-1.23.4/libheif/codecs/vvc_boxes.cc libheif-1.23.6/libheif/codecs/vvc_boxes.cc --- libheif-1.23.4/libheif/codecs/vvc_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -230,7 +230,9 @@ { std::vector nal; nal.resize(size); - memcpy(nal.data(), data, size); + if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + memcpy(nal.data(), data, size); + } append_nal_data(nal); } @@ -366,10 +368,9 @@ } -std::string Box_vvcC::dump(Indent& indent) const +void Box_vvcC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); const auto& c = m_configuration; // abbreviation @@ -409,7 +410,7 @@ indent--; } - return sstr.str(); + return; } diff -Nru libheif-1.23.4/libheif/codecs/vvc_boxes.h libheif-1.23.6/libheif/codecs/vvc_boxes.h --- libheif-1.23.4/libheif/codecs/vvc_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -72,7 +72,7 @@ }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; bool get_headers(std::vector* dest) const; diff -Nru libheif-1.23.4/libheif/codecs/vvc_dec.cc libheif-1.23.6/libheif/codecs/vvc_dec.cc --- libheif-1.23.4/libheif/codecs/vvc_dec.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_dec.cc 2026-10-05 16:16:00.000000000 +0000 @@ -24,6 +24,7 @@ #include "context.h" #include "plugins/nalu_utils.h" +#include #include @@ -57,23 +58,52 @@ } -Result> Decoder_VVC::get_coded_image_size_from_config() const +Result> Decoder_VVC::get_max_coded_image_size(const std::vector& compressed_data) const { - const std::vector* sps = m_vvcC->get_first_nal_of_type(VVC_NAL_UNIT_SPS_NUT); - if (!sps || sps->empty()) { - return std::optional{}; + // `compressed_data` is the combined configuration + bitstream buffer about to be + // pushed to the decoder. Scan it for every SPS NAL unit and return the largest coded picture + // size any of them declares. An SPS carried in the item data (not just in vvcC) + // drives the decoder's buffer allocation and can be far larger than the + // container 'ispe', so the config record alone is not a sufficient gate. + bool found = false; + uint32_t max_width = 0; + uint32_t max_height = 0; + + for (const auto& nal : split_nal_units_4byte_length_prefixed(compressed_data.data(), compressed_data.size())) { + const uint8_t* nal_data = nal.first; + size_t nal_size = nal.second; + + // VVC NAL unit header (2 bytes): forbidden_zero_bit(1), nuh_reserved_zero_bit(1), + // nuh_layer_id(6), nal_unit_type(5), nuh_temporal_id_plus1(3) + if (nal_size < 2) { + continue; + } + int nal_type = (nal_data[1] >> 3) & 0x1F; + if (nal_type != VVC_NAL_UNIT_SPS_NUT) { + continue; + } + + 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(nal_data, nal_size, &scratch, + &cropped_w, &cropped_h, &coded); + if (e) { + // A malformed SPS we cannot parse is skipped rather than failing the whole + // decode; the decoder plugin applies its own limits when it reaches it. + continue; + } + + found = true; + max_width = std::max(max_width, coded.width); + max_height = std::max(max_height, coded.height); } - 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; + if (!found) { + return std::optional{}; } - return std::optional{coded}; + return std::optional{ImageSize{max_width, max_height}}; } diff -Nru libheif-1.23.4/libheif/codecs/vvc_dec.h libheif-1.23.6/libheif/codecs/vvc_dec.h --- libheif-1.23.4/libheif/codecs/vvc_dec.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_dec.h 2026-10-05 16:16:00.000000000 +0000 @@ -47,7 +47,7 @@ Result> read_bitstream_configuration_data() const override; - Result> get_coded_image_size_from_config() const override; + Result> get_max_coded_image_size(const std::vector& compressed_data) const override; private: const std::shared_ptr m_vvcC; diff -Nru libheif-1.23.4/libheif/codecs/vvc_enc.cc libheif-1.23.6/libheif/codecs/vvc_enc.cc --- libheif-1.23.4/libheif/codecs/vvc_enc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_enc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -25,6 +25,7 @@ #include "api_structs.h" #include +#include #include "plugins/nalu_utils.h" @@ -175,17 +176,15 @@ } -std::optional Encoder_VVC::encode_sequence_get_data() +std::optional Encoder_VVC::encode_sequence_extract_data() { - return std::move(m_current_output_data); + return std::exchange(m_current_output_data, std::nullopt); } Error Encoder_VVC::get_data(heif_encoder* encoder) { //CodedImageData codedImage; - bool got_some_data = false; - for (;;) { uint8_t* data; int size; @@ -201,8 +200,6 @@ 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); @@ -249,7 +246,10 @@ } } - if (!got_some_data) { + // No coded image to report when the encoder returned no NALs, or only parameter sets + // (an encoder may emit VPS/SPS/PPS as soon as the sequence encoder is opened, before any + // slice data). Those went into m_vvcC above and m_current_output_data stays disengaged. + if (!m_current_output_data) { return {}; } diff -Nru libheif-1.23.4/libheif/codecs/vvc_enc.h libheif-1.23.6/libheif/codecs/vvc_enc.h --- libheif-1.23.4/libheif/codecs/vvc_enc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/codecs/vvc_enc.h 2026-10-05 16:16:00.000000000 +0000 @@ -54,13 +54,13 @@ Error encode_sequence_flush(heif_encoder* encoder) override; - std::optional encode_sequence_get_data() override; + std::optional encode_sequence_extract_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. + // Whether the vvcC is complete and was returned in an encode_sequence_extract_data() call. bool m_vvcC_has_VPS = false; bool m_vvcC_has_SPS = false; bool m_vvcC_has_PPS = false; diff -Nru libheif-1.23.4/libheif/color-conversion/alpha.cc libheif-1.23.6/libheif/color-conversion/alpha.cc --- libheif-1.23.4/libheif/color-conversion/alpha.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/alpha.cc 2026-10-05 16:16:00.000000000 +0000 @@ -71,8 +71,17 @@ 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) { + !input_state.has_alpha() || + target_state.has_alpha()) { + return {}; + } + + // A Bayer image (colorspace filter_array with chroma planar, which has the same value as + // heif_chroma_monochrome) passes the chroma test above, but convert_colorspace() only copies + // the Y/Cb/Cr/R/G/B planes and would return an image without its filter-array plane. A filter + // array with an alpha plane is not a plane layout that the pipeline accepts + // (HeifPixelImage::check_plane_layout()), so decline it instead of declaring a state for it. + if (input_state.colorspace == heif_colorspace_filter_array) { return {}; } @@ -87,7 +96,7 @@ // --- drop alpha plane output_state = input_state; - output_state.has_alpha = false; + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Trivial); @@ -134,31 +143,33 @@ 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) { + // The colour planes and the alpha plane are all read through the single 'Pixel' type + // below, so every plane must be stored with sizeof(Pixel) bytes per sample. A file may + // declare a different depth per plane ('unci'); reading a one-byte plane as two-byte + // samples would run past its end (GHSA-r7gr-2xm2-23wf), and a plane wider than two + // bytes cannot be read through either instance. Decline anything else. + if (!input_state.all_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { 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) { + // The colour planes are converted to RGB at one common depth and the alpha plane is + // composited at that depth, so all of them have to agree (0 = the colour planes differ). + int color_bpp = input_state.get_uniform_color_bits_per_pixel(); + if (color_bpp == 0 || (input_state.has_alpha() && input_state.bits_per_pixel_alpha != color_bpp)) { return {}; } // only drop alpha plane if it is not needed in output - if ((input_state.chroma != heif_chroma_monochrome && + // Bayer images (colorspace filter_array, chroma planar == monochrome) are not composited; + // there is no way back from RGB to a filter array. + if (input_state.colorspace == heif_colorspace_filter_array || + (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) { + !input_state.has_alpha() || + target_state.has_alpha()) { return {}; } @@ -173,7 +184,7 @@ // --- drop alpha plane output_state = input_state; - output_state.has_alpha = false; + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Trivial); @@ -181,6 +192,59 @@ } +// Composites one plane onto a background: out = (in * a + bkg * (alpha_max - a)) >> bpp_alpha. +// With 'checkerboard_square_size' == 0 the background is 'bkg1' everywhere; otherwise the +// squares alternate between 'bkg1' and 'bkg2'. All three planes are stored with sizeof(Pixel) +// bytes per sample; the strides are in samples. +template +static void composite_plane(const Pixel* p_in, size_t stride_in, + const Pixel* p_alpha, size_t stride_alpha, + Pixel* p_out, size_t stride_out, + uint32_t width, uint32_t height, + int bpp_alpha, Pixel bkg1, Pixel bkg2, uint32_t checkerboard_square_size) +{ + // The composite 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. The caller rejects + // anything wider. + using composite_t = std::conditional_t<(sizeof(Pixel) > 1), uint32_t, int>; + + const Pixel alpha_max = static_cast((1 << bpp_alpha) - 1); + + if (checkerboard_square_size == 0) { + 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(bkg1) * (alpha_max - a); + p_out[y * stride_out + x] = static_cast(composite >> bpp_alpha); + } + } + else { + for (uint32_t y = 0; y < height; y++) + for (uint32_t x = 0; x < width; x++) { + uint8_t parity = (x / checkerboard_square_size + y / 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); + } + } +} + + +// Luma of a background colour given as 16-bit RGB, with BT.601 weights. Used to composite +// onto a luma-only image. +static uint16_t background_luma16(uint16_t r, uint16_t g, uint16_t b) +{ + return static_cast((299u * r + 587u * g + 114u * b + 500u) / 1000u); +} + + template Result> Op_flatten_alpha_plane::convert_colorspace(const std::shared_ptr& input_raw, @@ -190,160 +254,184 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - std::shared_ptr input = input_raw; + // The guards up to the bit-depth check are only reachable by a direct caller; + // state_after_conversion() does not offer this operation in these cases. + + if (options_ext.alpha_composition_mode == heif_alpha_composition_mode_none) { + return Error{heif_error_Usage_error, + heif_suberror_Unspecified, + "Op_flatten_alpha_plane: no alpha composition mode"}; + } + + if (!input_raw->has_channel(heif_channel_Alpha)) { + return Error{heif_error_Usage_error, + heif_suberror_Unspecified, + "Op_flatten_alpha_plane: image has no alpha plane"}; + } + + // The colour planes are converted at, and the alpha plane is composited at, one common depth + // through the 'Pixel' type of this instance. + int color_bpp = input_state.get_uniform_color_bits_per_pixel(); + int bpp_alpha = input_raw->get_bits_per_pixel(heif_channel_Alpha); + + if (color_bpp == 0 || + bpp_alpha != color_bpp || + bytes_per_sample_for_bit_depth(color_bpp) != static_cast(sizeof(Pixel))) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + "Op_flatten_alpha_plane: colour and alpha planes must share one bit depth"}; + } + + // See composite_plane(): the composite accumulates in 32 bits, which is exact for samples + // of up to 16 bits. Reject anything wider instead of overflowing. + if (color_bpp > 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."}; + } + + // The background colours are given as 16-bit values. A checkerboard with a square size of 0 + // is a solid background. + const int shift = 16 - color_bpp; + const uint32_t square_size = (options_ext.alpha_composition_mode == heif_alpha_composition_mode_checkerboard) ? + options_ext.checkerboard_square_size : 0; + + // --- luma-only image: composite the Y plane directly + + bool luma_only = (input_raw->get_colorspace() == heif_colorspace_monochrome || + (input_raw->get_colorspace() == heif_colorspace_YCbCr && + input_raw->get_chroma_format() == heif_chroma_monochrome)); + + if (luma_only) { + // There is no operator that converts RGB back to a luma-only image, and for a grey + // background the round trip through RGB would be an identity anyway. Composite the luma + // plane against the luma of the background colour instead. + uint32_t width = input_raw->get_width(heif_channel_Y); + uint32_t height = input_raw->get_height(heif_channel_Y); + + if (input_raw->get_width(heif_channel_Alpha) != width || + input_raw->get_height(heif_channel_Alpha) != height) { + return Error::InternalError; + } + + auto outimg = std::make_shared(); + outimg->create(input_raw->get_width(), input_raw->get_height(), + input_raw->get_colorspace(), input_raw->get_chroma_format()); + + if (Error err = outimg->add_channel(heif_channel_Y, width, height, color_bpp, limits)) { + return err; + } + + Pixel bkg1 = static_cast(background_luma16(options_ext.background_red, + options_ext.background_green, + options_ext.background_blue) >> shift); + Pixel bkg2 = static_cast(background_luma16(options_ext.secondary_background_red, + options_ext.secondary_background_green, + options_ext.secondary_background_blue) >> shift); + + size_t stride_in, stride_alpha, stride_out; + const Pixel* p_in = (const Pixel*) input_raw->get_channel_memory(heif_channel_Y, &stride_in); + const Pixel* p_alpha = (const Pixel*) input_raw->get_channel_memory(heif_channel_Alpha, &stride_alpha); + Pixel* p_out = (Pixel*) outimg->get_channel_memory(heif_channel_Y, &stride_out); + + composite_plane(p_in, stride_in / sizeof(Pixel), + p_alpha, stride_alpha / sizeof(Pixel), + p_out, stride_out / sizeof(Pixel), + width, height, bpp_alpha, bkg1, bkg2, square_size); + + return outimg; + } + + // --- colour image: convert to planar RGB, composite each colour plane, convert back 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, + std::shared_ptr input; + { + Result> convInput = ::convert_colorspace(input_raw, heif_colorspace_RGB, heif_chroma_444, input_state.nclx, - input_state.bits_per_pixel, + color_bpp, options, &options_ext_skip_alpha, limits); if (!convInput) { return convInput.error(); } - else { - input = *convInput; - } + + 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))); + // The conversion above was asked for 'color_bpp' and carries the alpha plane through. + if (!input->has_channel(heif_channel_Alpha) || + input->get_bits_per_pixel(heif_channel_Alpha) != bpp_alpha || + input->get_width(heif_channel_Alpha) != width || + input->get_height(heif_channel_Alpha) != height) { + return Error::InternalError; + } - 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); - } - } + auto outimg = std::make_shared(); + outimg->create(width, height, input->get_colorspace(), input->get_chroma_format()); + size_t stride_alpha; + const Pixel* p_alpha = (const Pixel*) input->get_channel_memory(heif_channel_Alpha, &stride_alpha); + stride_alpha /= sizeof(Pixel); + + struct PlaneBackground + { + heif_channel channel; + uint16_t bkg1; + uint16_t bkg2; + }; + + const PlaneBackground planes[3] = { + {heif_channel_R, options_ext.background_red, options_ext.secondary_background_red}, + {heif_channel_G, options_ext.background_green, options_ext.secondary_background_green}, + {heif_channel_B, options_ext.background_blue, options_ext.secondary_background_blue}}; + + for (const PlaneBackground& plane : planes) { + // 'input' was converted to planar RGB above, so its planes carry the depth we composite + // at. Do not take the depth from target_state: this operation keeps the source + // colorspace, so for a YCbCr target the R/G/B fields there are 0 (plane absent), and + // add_channel() would refuse the zero depth. + if (input->get_bits_per_pixel(plane.channel) != color_bpp) { + return Error::InternalError; + } + + if (Error err = outimg->add_channel(plane.channel, width, height, color_bpp, limits)) { + return err; + } + + size_t stride_in, stride_out; + const Pixel* p_in = (const Pixel*) input->get_channel_memory(plane.channel, &stride_in); + Pixel* p_out = (Pixel*) outimg->get_channel_memory(plane.channel, &stride_out); + + composite_plane(p_in, stride_in / sizeof(Pixel), + p_alpha, stride_alpha, + p_out, stride_out / sizeof(Pixel), + width, height, bpp_alpha, + static_cast(plane.bkg1 >> shift), + static_cast(plane.bkg2 >> shift), + square_size); + } + + // Back to the colorspace and chroma of the source image. + Result> convOutput = ::convert_colorspace(outimg, + input_raw->get_colorspace(), + input_raw->get_chroma_format(), + input_state.nclx, + color_bpp, + options, &options_ext_skip_alpha, + limits); + if (!convOutput) { + return convOutput.error(); } - 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; - } + return convOutput; } template class Op_flatten_alpha_plane; @@ -356,9 +444,13 @@ 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) { + // Only applicable when the colour planes share one depth and the alpha plane differs from + // it. With mixed colour depths there is no depth to bring the alpha plane to; such images + // are equalized, alpha included, by Op_to_sdr_planes instead. + int color_bpp = input_state.get_uniform_color_bits_per_pixel(); + if (!input_state.has_alpha() || + color_bpp == 0 || + input_state.bits_per_pixel_alpha == color_bpp) { return {}; } @@ -370,16 +462,25 @@ return {}; } + // A Bayer image (colorspace filter_array with chroma planar, which has the same value as + // heif_chroma_monochrome) passes the chroma test above, but convert_colorspace() only copies + // the Y/Cb/Cr/R/G/B planes and would return the alpha plane alone. Decline it, like + // Op_drop_alpha_plane does. + if (input_state.colorspace == heif_colorspace_filter_array) { + 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)) { + if (input_state.get_bytes_per_sample(heif_channel_Alpha) > 2 || + bytes_per_sample_for_bit_depth(color_bpp) > 2) { return {}; } std::vector states; ColorState output_state = input_state; - output_state.alpha_bits_per_pixel = input_state.bits_per_pixel; + output_state.bits_per_pixel_alpha = color_bpp; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -414,7 +515,13 @@ } int input_alpha_bpp = input->get_bits_per_pixel(heif_channel_Alpha); - int target_bpp = input_state.bits_per_pixel; + int target_bpp = input_state.get_uniform_color_bits_per_pixel(); + if (target_bpp == 0) { + // Only reachable by a direct caller; state_after_conversion() declines mixed colour depths. + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + "Op_adjust_alpha_bit_depth: colour planes with differing bit depths"}; + } uint32_t alpha_width = input->get_width(heif_channel_Alpha); uint32_t alpha_height = input->get_height(heif_channel_Alpha); @@ -423,7 +530,10 @@ return err; } - if (input_alpha_bpp <= 8 && target_bpp > 8) { + int input_bytes = bytes_per_sample_for_bit_depth(input_alpha_bpp); + int target_bytes = bytes_per_sample_for_bit_depth(target_bpp); + + if (input_bytes == 1 && target_bytes == 2) { // Upscale: 8-bit alpha -> HDR using bit replication const uint8_t* p_in; size_t stride_in; @@ -440,7 +550,7 @@ 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) { + else if (input_bytes == 2 && target_bytes == 1) { // Downscale: HDR alpha -> 8-bit const uint16_t* p_in; size_t stride_in; @@ -458,7 +568,7 @@ p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift); } } - else if (input_alpha_bpp > 8 && target_bpp > 8) { + else if (input_bytes == 2 && target_bytes == 2) { // HDR alpha -> different HDR: rescale within uint16_t const uint16_t* p_in; size_t stride_in; @@ -485,7 +595,7 @@ } } } - else { + else if (input_bytes == 1 && target_bytes == 1) { // SDR alpha -> different SDR (both <= 8) const uint8_t* p_in; size_t stride_in; @@ -510,6 +620,11 @@ } } } + else { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Alpha bit depth adjustment only supports 8- and 16-bit sample storage."}; + } return outimg; } diff -Nru libheif-1.23.4/libheif/color-conversion/bayer_bilinear.cc libheif-1.23.6/libheif/color-conversion/bayer_bilinear.cc --- libheif-1.23.4/libheif/color-conversion/bayer_bilinear.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/bayer_bilinear.cc 2026-10-05 16:16:00.000000000 +0000 @@ -40,15 +40,17 @@ ColorState output_state; output_state.colorspace = heif_colorspace_RGB; - output_state.has_alpha = false; - if (input_state.bits_per_pixel == 8) { + int bpp = input_state.bits_per_pixel_filter_array; + int bytes_per_sample = input_state.get_bytes_per_sample(heif_channel_filter_array); + + if (bpp == 8) { output_state.chroma = heif_chroma_interleaved_RGB; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); } - else if (input_state.bits_per_pixel > 8 && input_state.bits_per_pixel <= 16) { + else if (bytes_per_sample == 2) { output_state.chroma = heif_chroma_interleaved_RRGGBB_LE; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(bpp); } else { return {}; @@ -101,9 +103,13 @@ } int bpp = input->get_bits_per_pixel(heif_channel_filter_array); - bool hdr = bpp > 8; + int bytes_per_sample = bytes_per_sample_for_bit_depth(bpp); + + if (bytes_per_sample != 1 && bytes_per_sample != 2) { + return Error::InternalError; + } - heif_chroma out_chroma = hdr ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB; + heif_chroma out_chroma = (bytes_per_sample == 2) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB; auto outimg = std::make_shared(); @@ -200,7 +206,7 @@ } }; - if (hdr) { + if (bytes_per_sample == 2) { demosaic(reinterpret_cast(in_p), reinterpret_cast(out_p), in_stride / 2, out_stride / 2); diff -Nru libheif-1.23.4/libheif/color-conversion/chroma_sampling.cc libheif-1.23.6/libheif/color-conversion/chroma_sampling.cc --- libheif-1.23.4/libheif/color-conversion/chroma_sampling.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/chroma_sampling.cc 2026-10-05 16:16:00.000000000 +0000 @@ -43,13 +43,12 @@ return {}; } - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { - return {}; - } - - if (has_samples_wider_than_16bit(input_state)) { + // The three colour planes are read through the same 'Pixel' type, so they must be stored + // with sizeof(Pixel) bytes per sample, and the conversion derives its shifts and midpoints + // from one bit depth, so they must also share it ('unci' may declare a depth per plane). + // The alpha plane is copied through at its own width. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } @@ -69,9 +68,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -89,8 +87,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - int bpp_y = input->get_bits_per_pixel(heif_channel_Y); int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb); int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr); @@ -102,19 +98,10 @@ bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); } - if (!hdr) { - if (bpp_y > 8 || - bpp_cb > 8 || - bpp_cr > 8) { - return Error::InternalError; - } - } - else { - if (bpp_y <= 8 || - bpp_cb <= 8 || - bpp_cr <= 8) { - return Error::InternalError; - } + if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cb) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cr) != static_cast(sizeof(Pixel))) { + return Error::InternalError; } @@ -160,14 +147,12 @@ 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; - in_cb_stride /= 2; - in_cr_stride /= 2; - out_y_stride /= 2; - out_cb_stride /= 2; - out_cr_stride /= 2; - } + in_y_stride /= sizeof(Pixel); + in_cb_stride /= sizeof(Pixel); + in_cr_stride /= sizeof(Pixel); + out_y_stride /= sizeof(Pixel); + out_cb_stride /= sizeof(Pixel); + out_cr_stride /= sizeof(Pixel); // We only copy the alpha, do not access it as 16 bit @@ -232,12 +217,12 @@ // 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); + size_t copyWidth = static_cast(width) * sizeof(Pixel); memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth); if (has_alpha) { - uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); } } @@ -271,13 +256,12 @@ return {}; } - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { - return {}; - } - - if (has_samples_wider_than_16bit(input_state)) { + // The three colour planes are read through the same 'Pixel' type, so they must be stored + // with sizeof(Pixel) bytes per sample, and the conversion derives its shifts and midpoints + // from one bit depth, so they must also share it ('unci' may declare a depth per plane). + // The alpha plane is copied through at its own width. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } @@ -297,9 +281,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -317,8 +300,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - int bpp_y = input->get_bits_per_pixel(heif_channel_Y); int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb); int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr); @@ -330,19 +311,10 @@ bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); } - if (!hdr) { - if (bpp_y > 8 || - bpp_cb > 8 || - bpp_cr > 8) { - return Error::InternalError; - } - } - else { - if (bpp_y <= 8 || - bpp_cb <= 8 || - bpp_cr <= 8) { - return Error::InternalError; - } + if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cb) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cr) != static_cast(sizeof(Pixel))) { + return Error::InternalError; } @@ -401,14 +373,12 @@ } - if (hdr) { - in_y_stride /= 2; - in_cb_stride /= 2; - in_cr_stride /= 2; - out_y_stride /= 2; - out_cb_stride /= 2; - out_cr_stride /= 2; - } + in_y_stride /= sizeof(Pixel); + in_cb_stride /= sizeof(Pixel); + in_cr_stride /= sizeof(Pixel); + out_y_stride /= sizeof(Pixel); + out_cb_stride /= sizeof(Pixel); + out_cr_stride /= sizeof(Pixel); // --- fill right border if the image size is odd @@ -438,12 +408,12 @@ // 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); + size_t copyWidth = static_cast(width) * sizeof(Pixel); memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth); if (has_alpha) { - uint32_t alphaCopyWidth = (bpp_a>8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); } } @@ -477,13 +447,12 @@ return {}; } - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { - return {}; - } - - if (has_samples_wider_than_16bit(input_state)) { + // The three colour planes are read through the same 'Pixel' type, so they must be stored + // with sizeof(Pixel) bytes per sample, and the conversion derives its shifts and midpoints + // from one bit depth, so they must also share it ('unci' may declare a depth per plane). + // The alpha plane is copied through at its own width. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } @@ -499,9 +468,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -510,6 +478,98 @@ } + +/* + * Upsample one 4:2:0 chroma plane to the full image resolution. + * Strides are given in units of Pixel. + * + * We assume that chroma pixels are located in the center of 2x2 luma pixels. + * The image border 'b' is handled separately. + * The right and bottom border are not processed when the size is odd. + * Then, each 2x2 square between 4 chroma samples is computed in one iteration. + * + * Upsampling weights are 3/4, 1/4. For example: + * A = 3/4*3/4 * C1 + 3/4*1/4 * C2 + 1/4*3/4 * C3 + 1/4*1/4 * C4 + * + * +---+---+---+---+ + * | b | b | b | b | + * +---C1--+---C2--+ + * | b | A | | b | + * +---+---+---+---+ + * | b | | | b | + * +---C3--+---C4--+ + * | b | b | b | b | + * +---+---+---+---+ + */ +template +static void upsample_chroma_plane_bilinear(const Pixel* in, size_t in_stride, + Pixel* out, size_t out_stride, + uint32_t width, uint32_t height) +{ + // interpolate between two chroma samples with weights 3/4 and 1/4 + auto mix_3_1 = [](Pixel a, Pixel b) { return (Pixel) ((3 * a + 1 * b + 2) / 4); }; + + // --- fill borders + + // Fill one image row from one chroma row, including both corner pixels. + auto upsample_row = [&](const Pixel* in_row, Pixel* out_row) { + out_row[0] = in_row[0]; + + for (uint32_t cx = 0; cx < (width - 1) / 2; cx++) { + Pixel a = in_row[cx]; + Pixel b = in_row[cx + 1]; + out_row[2 * cx + 1] = mix_3_1(a, b); + out_row[2 * cx + 2] = mix_3_1(b, a); + } + + if (width % 2 == 0) { + out_row[width - 1] = in_row[width / 2 - 1]; + } + }; + + // Fill one image column from one chroma column. The corner pixels are already set by upsample_row(). + auto upsample_column = [&](const Pixel* in_column, Pixel* out_column) { + for (uint32_t cy = 0; cy < (height - 1) / 2; cy++) { + Pixel a = in_column[cy * in_stride]; + Pixel b = in_column[(cy + 1) * in_stride]; + out_column[(2 * cy + 1) * out_stride] = mix_3_1(a, b); + out_column[(2 * cy + 2) * out_stride] = mix_3_1(b, a); + } + }; + + upsample_row(in, out); // top border + upsample_column(in, out); // left border + + if (width % 2 == 0) { + upsample_column(in + width / 2 - 1, out + width - 1); // right border + } + + if (height % 2 == 0) { + upsample_row(in + (height / 2 - 1) * in_stride, out + (height - 1) * out_stride); // bottom border + } + + + // --- bilinear filtering of inner part + + for (uint32_t y = 1; y < height - 1; y += 2) { + for (uint32_t x = 1; x < width - 1; x += 2) { + uint32_t cx = x / 2; + uint32_t cy = y / 2; + + Pixel c00 = in[cy * in_stride + cx]; + Pixel c01 = in[cy * in_stride + cx + 1]; + Pixel c10 = in[(cy + 1) * in_stride + cx]; + Pixel c11 = in[(cy + 1) * in_stride + cx + 1]; + + out[(y + 0) * out_stride + x + 0] = (Pixel) ((c00 * 3 * 3 + c01 * 1 * 3 + c10 * 3 * 1 + c11 * 1 * 1 + 8) / 16); + out[(y + 0) * out_stride + x + 1] = (Pixel) ((c00 * 1 * 3 + c01 * 3 * 3 + c10 * 1 * 1 + c11 * 3 * 1 + 8) / 16); + out[(y + 1) * out_stride + x + 0] = (Pixel) ((c00 * 3 * 1 + c01 * 1 * 1 + c10 * 3 * 3 + c11 * 1 * 3 + 8) / 16); + out[(y + 1) * out_stride + x + 1] = (Pixel) ((c00 * 1 * 1 + c01 * 3 * 1 + c10 * 1 * 3 + c11 * 3 * 3 + 8) / 16); + } + } +} + + template Result> Op_YCbCr420_bilinear_to_YCbCr444::convert_colorspace(const std::shared_ptr& input, @@ -519,8 +579,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - int bpp_y = input->get_bits_per_pixel(heif_channel_Y); int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb); int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr); @@ -532,19 +590,10 @@ bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); } - if (!hdr) { - if (bpp_y > 8 || - bpp_cb > 8 || - bpp_cr > 8) { - return Error::InternalError; - } - } - else { - if (bpp_y <= 8 || - bpp_cb <= 8 || - bpp_cr <= 8) { - return Error::InternalError; - } + if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cb) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cr) != static_cast(sizeof(Pixel))) { + return Error::InternalError; } @@ -600,134 +649,25 @@ } - if (hdr) { - in_y_stride /= 2; - in_cb_stride /= 2; - in_cr_stride /= 2; - out_y_stride /= 2; - out_cb_stride /= 2; - out_cr_stride /= 2; - } - - /* - * We assume that chroma pixels are located in the center of 2x2 luma pixels. - * The image border 'b' is handled separately. - * The right and bottom border are not processed when the size is odd. - * Then, each 2x2 square between 4 chroma samples is computed in one iteration. - * - * Upsampling weights are 3/4, 1/4. For example: - * A = 3/4*3/4 * C1 + 3/4*1/4 * C2 + 1/4*3/4 * C3 + 1/4*1/4 * C4 - * - * +---+---+---+---+ - * | b | b | b | b | - * +---C1--+---C2--+ - * | b | A | | b | - * +---+---+---+---+ - * | b | | | b | - * +---C3--+---C4--+ - * | b | b | b | b | - * +---+---+---+---+ - */ - - // --- fill borders - - // top left corner - out_cb[0] = in_cb[0]; - out_cr[0] = in_cr[0]; - - // top border - for (uint32_t cx = 0; cx < (width - 1) / 2; cx++) { - out_cb[0 * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[cx / 2] + 1 * in_cb[cx / 2 + 1] + 2) / 4); - out_cb[0 * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[cx / 2] + 3 * in_cb[cx / 2 + 1] + 2) / 4); - out_cr[0 * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[cx / 2] + 1 * in_cr[cx / 2 + 1] + 2) / 4); - out_cr[0 * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[cx / 2] + 3 * in_cr[cx / 2 + 1] + 2) / 4); - } - - // top right corner - if (width % 2 == 0) { - out_cb[width - 1] = in_cb[width / 2 - 1]; - out_cr[width - 1] = in_cr[width / 2 - 1]; - } - - // left border - for (uint32_t cy = 0; cy < (height - 1) / 2; cy++) { - out_cb[(2 * cy + 1) * out_cb_stride + 0] = (Pixel) ((3 * in_cb[cy / 2 * in_cb_stride] + 1 * in_cb[(cy / 2 + 1) * in_cb_stride] + 2) / 4); - out_cb[(2 * cy + 2) * out_cb_stride + 0] = (Pixel) ((1 * in_cb[cy / 2 * in_cb_stride] + 3 * in_cb[(cy / 2 + 1) * in_cb_stride] + 2) / 4); - out_cr[(2 * cy + 1) * out_cr_stride + 0] = (Pixel) ((3 * in_cr[cy / 2 * in_cr_stride] + 1 * in_cr[(cy / 2 + 1) * in_cr_stride] + 2) / 4); - out_cr[(2 * cy + 2) * out_cr_stride + 0] = (Pixel) ((1 * in_cr[cy / 2 * in_cr_stride] + 3 * in_cr[(cy / 2 + 1) * in_cr_stride] + 2) / 4); - } - - // bottom left corner - if (height % 2 == 0) { - out_cb[(height - 1) * out_cb_stride] = in_cb[(height / 2 - 1) * in_cb_stride]; - out_cr[(height - 1) * out_cr_stride] = in_cr[(height / 2 - 1) * in_cr_stride]; - } - - // right border - if (width % 2 == 0) { - for (uint32_t cy = 0; cy < (height - 1) / 2; cy++) { - out_cb[(2 * cy + 1) * out_cb_stride + width - 1] = (Pixel) ((3 * in_cb[cy / 2 * in_cb_stride + width / 2 - 1] + 1 * in_cb[(cy / 2 + 1) * in_cb_stride + width / 2 - 1] + 2) / 4); - out_cb[(2 * cy + 2) * out_cb_stride + width - 1] = (Pixel) ((1 * in_cb[cy / 2 * in_cb_stride + width / 2 - 1] + 3 * in_cb[(cy / 2 + 1) * in_cb_stride + width / 2 - 1] + 2) / 4); - out_cr[(2 * cy + 1) * out_cr_stride + width - 1] = (Pixel) ((3 * in_cr[cy / 2 * in_cr_stride + width / 2 - 1] + 1 * in_cr[(cy / 2 + 1) * in_cr_stride + width / 2 - 1] + 2) / 4); - out_cr[(2 * cy + 2) * out_cr_stride + width - 1] = (Pixel) ((1 * in_cr[cy / 2 * in_cr_stride + width / 2 - 1] + 3 * in_cr[(cy / 2 + 1) * in_cr_stride + width / 2 - 1] + 2) / 4); - } - } - - // bottom border - if (height % 2 == 0) { - for (uint32_t cx = 0; cx < (width - 1) / 2; cx++) { - out_cb[(height - 1) * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2] + 1 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2 + 1] + 2) / 4); - out_cb[(height - 1) * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2] + 3 * in_cb[(height / 2 - 1) * in_cb_stride + cx / 2 + 1] + 2) / 4); - out_cr[(height - 1) * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2] + 1 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2 + 1] + 2) / 4); - out_cr[(height - 1) * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2] + 3 * in_cr[(height / 2 - 1) * in_cr_stride + cx / 2 + 1] + 2) / 4); - } - } - - // bottom right corner - if (width % 2 == 0 && height % 2 == 0) { - out_cb[(height - 1) * out_cb_stride + width - 1] = in_cb[(height / 2 - 1) * in_cb_stride + width / 2 - 1]; - out_cr[(height - 1) * out_cr_stride + width - 1] = in_cr[(height / 2 - 1) * in_cr_stride + width / 2 - 1]; - } - - - // --- bilinear filtering of inner part + in_y_stride /= sizeof(Pixel); + in_cb_stride /= sizeof(Pixel); + in_cr_stride /= sizeof(Pixel); + out_y_stride /= sizeof(Pixel); + out_cb_stride /= sizeof(Pixel); + out_cr_stride /= sizeof(Pixel); - uint32_t x, y; - for (y = 1; y < height - 1; y += 2) { - for (x = 1; x < width - 1; x += 2) { - uint32_t cx = x / 2; - uint32_t cy = y / 2; - - Pixel cb00 = in_cb[cy * in_cb_stride + cx]; - Pixel cr00 = in_cr[cy * in_cr_stride + cx]; - Pixel cb01 = in_cb[cy * in_cb_stride + cx + 1]; - Pixel cr01 = in_cr[cy * in_cr_stride + cx + 1]; - Pixel cb10 = in_cb[(cy + 1) * in_cb_stride + cx]; - Pixel cr10 = in_cr[(cy + 1) * in_cr_stride + cx]; - Pixel cb11 = in_cb[(cy + 1) * in_cb_stride + cx + 1]; - Pixel cr11 = in_cr[(cy + 1) * in_cr_stride + cx + 1]; - - out_cb[(y + 0) * out_cb_stride + x + 0] = (Pixel) ((cb00 * 3 * 3 + cb01 * 1 * 3 + cb10 * 3 * 1 + cb11 * 1 * 1 + 8) / 16); - out_cb[(y + 0) * out_cb_stride + x + 1] = (Pixel) ((cb00 * 1 * 3 + cb01 * 3 * 3 + cb10 * 1 * 1 + cb11 * 3 * 1 + 8) / 16); - out_cb[(y + 1) * out_cb_stride + x + 0] = (Pixel) ((cb00 * 3 * 1 + cb01 * 1 * 1 + cb10 * 3 * 3 + cb11 * 1 * 3 + 8) / 16); - out_cb[(y + 1) * out_cb_stride + x + 1] = (Pixel) ((cb00 * 1 * 1 + cb01 * 3 * 1 + cb10 * 1 * 3 + cb11 * 3 * 3 + 8) / 16); - - out_cr[(y + 0) * out_cr_stride + x + 0] = (Pixel) ((cr00 * 3 * 3 + cr01 * 1 * 3 + cr10 * 3 * 1 + cr11 * 1 * 1 + 8) / 16); - out_cr[(y + 0) * out_cr_stride + x + 1] = (Pixel) ((cr00 * 1 * 3 + cr01 * 3 * 3 + cr10 * 1 * 1 + cr11 * 3 * 1 + 8) / 16); - out_cr[(y + 1) * out_cr_stride + x + 0] = (Pixel) ((cr00 * 3 * 1 + cr01 * 1 * 1 + cr10 * 3 * 3 + cr11 * 1 * 3 + 8) / 16); - out_cr[(y + 1) * out_cr_stride + x + 1] = (Pixel) ((cr00 * 1 * 1 + cr01 * 3 * 1 + cr10 * 1 * 3 + cr11 * 3 * 3 + 8) / 16); - } - } + upsample_chroma_plane_bilinear(in_cb, in_cb_stride, out_cb, out_cb_stride, width, height); + upsample_chroma_plane_bilinear(in_cr, in_cr_stride, out_cr, out_cr_stride, width, height); // 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); + for (uint32_t y = 0; y < height; y++) { + size_t copyWidth = static_cast(width) * sizeof(Pixel); memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth); if (has_alpha) { - uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); } } @@ -762,13 +702,12 @@ return {}; } - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { - return {}; - } - - if (has_samples_wider_than_16bit(input_state)) { + // The three colour planes are read through the same 'Pixel' type, so they must be stored + // with sizeof(Pixel) bytes per sample, and the conversion derives its shifts and midpoints + // from one bit depth, so they must also share it ('unci' may declare a depth per plane). + // The alpha plane is copied through at its own width. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } @@ -784,9 +723,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = input_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -804,8 +742,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - int bpp_y = input->get_bits_per_pixel(heif_channel_Y); int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb); int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr); @@ -817,19 +753,10 @@ bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); } - if (!hdr) { - if (bpp_y > 8 || - bpp_cb > 8 || - bpp_cr > 8) { - return Error::InternalError; - } - } - else { - if (bpp_y <= 8 || - bpp_cb <= 8 || - bpp_cr <= 8) { - return Error::InternalError; - } + if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cb) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cr) != static_cast(sizeof(Pixel))) { + return Error::InternalError; } @@ -885,14 +812,12 @@ } - if (hdr) { - in_y_stride /= 2; - in_cb_stride /= 2; - in_cr_stride /= 2; - out_y_stride /= 2; - out_cb_stride /= 2; - out_cr_stride /= 2; - } + in_y_stride /= sizeof(Pixel); + in_cb_stride /= sizeof(Pixel); + in_cr_stride /= sizeof(Pixel); + out_y_stride /= sizeof(Pixel); + out_cb_stride /= sizeof(Pixel); + out_cr_stride /= sizeof(Pixel); /* * We assume that chroma pixels are located in the center of 2x1 luma pixels. @@ -953,12 +878,12 @@ // 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); + size_t copyWidth = static_cast(width) * sizeof(Pixel); memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth); if (has_alpha) { - uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); } } diff -Nru libheif-1.23.4/libheif/color-conversion/colorconversion.cc libheif-1.23.6/libheif/color-conversion/colorconversion.cc --- libheif-1.23.4/libheif/color-conversion/colorconversion.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/colorconversion.cc 2026-10-05 16:16:00.000000000 +0000 @@ -147,23 +147,281 @@ #endif -bool ColorState::operator==(const ColorState& b) const +ColorState::ColorState(heif_colorspace cs, heif_chroma chr, bool with_alpha, int bpp) + : colorspace(cs), chroma(chr) { - bool mainParamsMatch = (colorspace == b.colorspace && - chroma == b.chroma && - has_alpha == b.has_alpha && - bits_per_pixel == b.bits_per_pixel); + set_color_bits_per_pixel(bpp); + bits_per_pixel_alpha = with_alpha ? bpp : 0; +} - if (!mainParamsMatch) { - return false; + +ColorState ColorState::from_image_planes(const HeifPixelImage& image) +{ + ColorState state; + state.colorspace = image.get_colorspace(); + state.chroma = image.get_chroma_format(); + + // Record the bit depth of every plane the image has. They may differ from each other + // (e.g. 'unci' declares a depth per component), which is why ColorState keeps one value + // per plane instead of a single image-wide depth. + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, + heif_channel_R, heif_channel_G, heif_channel_B, + heif_channel_Alpha, heif_channel_filter_array}) { + if (image.has_channel(channel)) { + state.set_bits_per_pixel(channel, image.get_bits_per_pixel(channel)); + } + } + + // Interleaved RGB formats keep all components in one plane. Represent them by their + // per-component depth so that operators see the same R/G/B (and alpha) fields as for + // planar RGB. + if (image.has_channel(heif_channel_interleaved)) { + int bpp = image.get_bits_per_pixel(heif_channel_interleaved); + state.set_bits_per_pixel(heif_channel_interleaved, bpp); + if (is_interleaved_with_alpha(image.get_chroma_format())) { + state.bits_per_pixel_alpha = bpp; + } + } + + return state; +} + + +int ColorState::get_bits_per_pixel(heif_channel channel) const +{ + switch (channel) { + case heif_channel_R: + return bits_per_pixel_R; + case heif_channel_G: + return bits_per_pixel_G; + case heif_channel_B: + return bits_per_pixel_B; + case heif_channel_Y: + return bits_per_pixel_Y; + case heif_channel_Cb: + return bits_per_pixel_Cb; + case heif_channel_Cr: + return bits_per_pixel_Cr; + case heif_channel_Alpha: + return bits_per_pixel_alpha; + case heif_channel_filter_array: + return bits_per_pixel_filter_array; + case heif_channel_interleaved: + return bits_per_pixel_R; + default: + return 0; + } +} + + +void ColorState::set_bits_per_pixel(heif_channel channel, int bpp) +{ + switch (channel) { + case heif_channel_R: + bits_per_pixel_R = bpp; + break; + case heif_channel_G: + bits_per_pixel_G = bpp; + break; + case heif_channel_B: + bits_per_pixel_B = bpp; + break; + case heif_channel_Y: + bits_per_pixel_Y = bpp; + break; + case heif_channel_Cb: + bits_per_pixel_Cb = bpp; + break; + case heif_channel_Cr: + bits_per_pixel_Cr = bpp; + break; + case heif_channel_Alpha: + bits_per_pixel_alpha = bpp; + break; + case heif_channel_filter_array: + bits_per_pixel_filter_array = bpp; + break; + case heif_channel_interleaved: + bits_per_pixel_R = bits_per_pixel_G = bits_per_pixel_B = bpp; + break; + default: + break; + } +} + + +void ColorState::set_color_bits_per_pixel(int bpp) +{ + bits_per_pixel_R = bits_per_pixel_G = bits_per_pixel_B = 0; + bits_per_pixel_Y = bits_per_pixel_Cb = bits_per_pixel_Cr = 0; + bits_per_pixel_filter_array = 0; + + if (colorspace == heif_colorspace_filter_array) { + bits_per_pixel_filter_array = bpp; + return; } - if (has_alpha && b.has_alpha) { - if (get_alpha_bits_per_pixel() != b.get_alpha_bits_per_pixel()) { + switch (chroma) { + case heif_chroma_planar: // == heif_chroma_monochrome + if (colorspace == heif_colorspace_RGB) { + bits_per_pixel_R = bits_per_pixel_G = bits_per_pixel_B = bpp; + } + else { + bits_per_pixel_Y = bpp; + } + break; + + case heif_chroma_420: + case heif_chroma_422: + bits_per_pixel_Y = bits_per_pixel_Cb = bits_per_pixel_Cr = bpp; + break; + + case heif_chroma_444: + if (colorspace == heif_colorspace_RGB) { + bits_per_pixel_R = bits_per_pixel_G = bits_per_pixel_B = bpp; + } + else { + bits_per_pixel_Y = bits_per_pixel_Cb = bits_per_pixel_Cr = bpp; + } + break; + + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RGBA: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBB_LE: + case heif_chroma_interleaved_RRGGBBAA_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + bits_per_pixel_R = bits_per_pixel_G = bits_per_pixel_B = bpp; + break; + + default: + break; + } +} + + +int ColorState::get_uniform_color_bits_per_pixel() const +{ + int uniform = 0; + + for (int bpp : {bits_per_pixel_R, bits_per_pixel_G, bits_per_pixel_B, + bits_per_pixel_Y, bits_per_pixel_Cb, bits_per_pixel_Cr, + bits_per_pixel_filter_array}) { + if (bpp == 0) { + continue; // plane does not exist + } + + if (uniform == 0) { + uniform = bpp; + } + else if (bpp != uniform) { + return 0; + } + } + + return uniform; +} + + +int ColorState::get_uniform_bits_per_pixel() const +{ + int uniform = get_uniform_color_bits_per_pixel(); + + if (uniform != 0 && bits_per_pixel_alpha != 0 && bits_per_pixel_alpha != uniform) { + return 0; + } + + return uniform; +} + + +int ColorState::get_max_color_bits_per_pixel() const +{ + return std::max({bits_per_pixel_R, bits_per_pixel_G, bits_per_pixel_B, + bits_per_pixel_Y, bits_per_pixel_Cb, bits_per_pixel_Cr, + bits_per_pixel_filter_array}); +} + + +int ColorState::get_max_bits_per_pixel() const +{ + return std::max({bits_per_pixel_R, bits_per_pixel_G, bits_per_pixel_B, + bits_per_pixel_Y, bits_per_pixel_Cb, bits_per_pixel_Cr, + bits_per_pixel_alpha, bits_per_pixel_filter_array}); +} + + +// Applies 'pred' to the depth of every existing plane (planes with depth 0 do not exist and +// are skipped) and returns whether it holds for all of them. +template +static bool all_existing_planes_satisfy(const ColorState& s, bool include_alpha, Pred pred) +{ + for (int bpp : {s.bits_per_pixel_R, s.bits_per_pixel_G, s.bits_per_pixel_B, + s.bits_per_pixel_Y, s.bits_per_pixel_Cb, s.bits_per_pixel_Cr, + s.bits_per_pixel_filter_array}) { + if (bpp != 0 && !pred(bpp)) { return false; } } + if (include_alpha && s.bits_per_pixel_alpha != 0 && !pred(s.bits_per_pixel_alpha)) { + return false; + } + + return true; +} + + +int ColorState::get_bytes_per_sample(heif_channel channel) const +{ + return bytes_per_sample_for_bit_depth(get_bits_per_pixel(channel)); // 0 if the plane does not exist +} + + +int ColorState::get_max_bytes_per_sample() const +{ + // The bit depth to sample width mapping is monotonic, so the widest plane is the deepest. + return bytes_per_sample_for_bit_depth(get_max_bits_per_pixel()); +} + + +bool ColorState::color_channels_have_bytes_per_sample(int bytes) const +{ + return all_existing_planes_satisfy(*this, false, [bytes](int bpp) { + return bytes_per_sample_for_bit_depth(bpp) == bytes; + }); +} + + +bool ColorState::all_channels_have_bytes_per_sample(int bytes) const +{ + return all_existing_planes_satisfy(*this, true, [bytes](int bpp) { + return bytes_per_sample_for_bit_depth(bpp) == bytes; + }); +} + + +bool ColorState::has_same_planes(const ColorState& b) const +{ + return (colorspace == b.colorspace && + chroma == b.chroma && + bits_per_pixel_R == b.bits_per_pixel_R && + bits_per_pixel_G == b.bits_per_pixel_G && + bits_per_pixel_B == b.bits_per_pixel_B && + bits_per_pixel_Y == b.bits_per_pixel_Y && + bits_per_pixel_Cb == b.bits_per_pixel_Cb && + bits_per_pixel_Cr == b.bits_per_pixel_Cr && + bits_per_pixel_alpha == b.bits_per_pixel_alpha && + bits_per_pixel_filter_array == b.bits_per_pixel_filter_array); +} + + +bool ColorState::operator==(const ColorState& b) const +{ + if (!has_same_planes(b)) { + return false; + } + if (colorspace == heif_colorspace_YCbCr) { bool ycbcr_parameters_match = nclx.equal_except_transfer_curve(b.nclx); @@ -202,12 +460,27 @@ std::ostream& operator<<(std::ostream& ostr, const ColorState& state) { - ostr << "colorspace=" << state.colorspace << " chroma=" << state.chroma - << " bpp(R)=" << state.bits_per_pixel - << " alpha=" << (state.has_alpha ? "yes" : "no"); + ostr << "colorspace=" << state.colorspace << " chroma=" << state.chroma; + + auto print_plane = [&ostr](const char* name, int bpp) { + if (bpp != 0) { + ostr << " bpp(" << name << ")=" << bpp; + } + }; + + print_plane("Y", state.bits_per_pixel_Y); + print_plane("Cb", state.bits_per_pixel_Cb); + print_plane("Cr", state.bits_per_pixel_Cr); + print_plane("R", state.bits_per_pixel_R); + print_plane("G", state.bits_per_pixel_G); + print_plane("B", state.bits_per_pixel_B); + print_plane("filter_array", state.bits_per_pixel_filter_array); - 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.has_alpha()) { + ostr << " alpha_bpp=" << state.bits_per_pixel_alpha; + } + else { + ostr << " alpha=no"; } if (state.colorspace == heif_colorspace_YCbCr) { @@ -276,6 +549,13 @@ } +const std::vector>& ColorConversionPipeline::get_operations() +{ + init_ops(); + return m_operation_pool; +} + + bool ColorConversionPipeline::construct_pipeline(const ColorState& input_state, const ColorState& target_state, const heif_color_conversion_options& options, @@ -435,6 +715,21 @@ } +Error ColorConversionPipeline::check_operation_output(const std::shared_ptr& image, + const ColorState& declared_state) +{ + if (image && + ColorState::from_image_planes(*image).has_same_planes(declared_state) && + !image->check_plane_layout()) { + return Error::Ok; + } + + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion, + "Internal error: a color conversion step did not return the image format it declared"}; +} + + std::string ColorConversionPipeline::debug_dump_pipeline() const { std::ostringstream ostr; @@ -468,6 +763,16 @@ out = *outResult; } + // The pipeline was planned with the states that the operations declared in + // state_after_conversion(). The next operation reads the planes that its input state + // lists, with the bit depths given there, and the caller expects an image in the target + // state. An operation that returns an image with other planes than it declared must not + // get any further: this is the only place that sees both the declaration and the image. + if (Error err = check_operation_output(out, step.output_state)) { + assert(false); // fail on debug builds + return err; + } + // copy metadata over to new image out->copy_metadata_from(*in); @@ -528,10 +833,19 @@ // --- prepare conversion - ColorState input_state; - 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()); + // The operators read the planes that the colorspace and chroma format imply, with the sizes + // they imply. Refuse anything else up front instead of letting an operator run into a missing, + // duplicate or undersized plane: a plane set that does not match the format, or a colorspace + // without a defined layout (custom multi-component data has nothing to convert). Planes with + // channel heif_channel_unknown (the padding components of 'unci') are tolerated; they belong + // to no colour model and are not carried into the output. + if (Error err = input->check_plane_layout()) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_image_type, + "Color conversion: " + err.message}; + } + + ColorState input_state = ColorState::from_image_planes(*input); if (input->has_nclx_color_profile()) { input_state.nclx = input->get_color_profile_nclx(); } @@ -540,11 +854,6 @@ 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; @@ -569,30 +878,39 @@ // interleaved output format. // For planar formats, we include an alpha plane when included in the input. + bool output_has_alpha; + if (num_interleaved_components_per_plane(target_chroma) > 1) { - output_state.has_alpha = is_interleaved_with_alpha(target_chroma); + output_has_alpha = is_interleaved_with_alpha(target_chroma); } else { if (options_ext->alpha_composition_mode != heif_alpha_composition_mode_none) { - output_state.has_alpha = false; + output_has_alpha = false; } else { - output_state.has_alpha = input_state.has_alpha; + output_has_alpha = input_state.has_alpha(); } } - if (output_bpp) { - output_state.bits_per_pixel = output_bpp; - } + // --- output colour bit depth (0 = keep the input depth) + int output_color_bpp = output_bpp; // interleaved RGB formats always have to be 8-bit if (target_chroma == heif_chroma_interleaved_RGB || target_chroma == heif_chroma_interleaved_RGBA) { - output_state.bits_per_pixel = 8; + output_color_bpp = 8; } + // The depth that the input's colour planes share, or 0 if they differ ('unci' declares a + // depth per component). Where a single input depth is needed below and the planes do not + // agree, the widest plane is used: that is the lossless choice, and a conversion that cannot + // widen the narrower planes to it is declined by the operators, instead of one plane's depth + // being picked silently. + int uniform_input_bpp = input_state.get_uniform_color_bits_per_pixel(); + int input_color_bpp = (uniform_input_bpp != 0) ? uniform_input_bpp : input_state.get_max_color_bits_per_pixel(); + // interleaved RRGGBB formats have to be >8-bit. // If we don't know a target bit-depth, use 10 bit. @@ -600,24 +918,47 @@ target_chroma == heif_chroma_interleaved_RRGGBB_BE || target_chroma == heif_chroma_interleaved_RRGGBBAA_LE || target_chroma == heif_chroma_interleaved_RRGGBBAA_BE) && - output_state.bits_per_pixel <= 8) { - output_state.bits_per_pixel = 10; + (output_color_bpp != 0 ? output_color_bpp : input_color_bpp) <= 8) { + output_color_bpp = 10; } - // Output alpha should always match the output color BPP - output_state.alpha_bits_per_pixel = output_state.bits_per_pixel; + bool same_plane_layout = (target_colorspace == input_state.colorspace && + target_chroma == input_state.chroma); + + int output_alpha_bpp; + + if (output_color_bpp == 0 && same_plane_layout) { + // No depth change requested and the plane layout stays the same: keep the colour planes + // exactly as they are, even when their depths differ from each other. The alpha plane is + // brought to the colour depth when there is one (the operators expect them to agree); + // when the colour planes differ, it is kept as it is, too. + output_alpha_bpp = (uniform_input_bpp != 0) ? uniform_input_bpp : input_state.bits_per_pixel_alpha; + } + else { + if (output_color_bpp == 0) { + output_color_bpp = input_color_bpp; + } + + output_state.set_color_bits_per_pixel(output_color_bpp); + + // Output alpha should always match the output color BPP + output_alpha_bpp = output_color_bpp; + } + + output_state.bits_per_pixel_alpha = output_has_alpha ? output_alpha_bpp : 0; ColorConversionPipeline pipeline; 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}; - } - if (pipeline.is_nop()) { + if (success && pipeline.is_nop()) { return input; } - else { + + { + // The check below also runs when no pipeline could be built, so that the caller gets + // the specific reason (a plane wider than 16 bits) instead of the generic "unsupported + // color conversion" error. + // // 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). @@ -629,11 +970,19 @@ // 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. + // operator's state_after_conversion(), and every operator declares there which + // sample width it can read (ColorState::color_channels_have_bytes_per_sample() and + // friends, derived from the same bit depth to storage width mapping that + // HeifPixelImage uses), 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. + // + // Planes of differing depth within one image (GHSA-w7mc-p8jc-p853 read 8-bit chroma + // planes with the 16-bit luma sample width) are handled the same way: each operator + // declares in state_after_conversion() whether it needs the colour planes to share + // one depth (ColorState::color_channels_have_same_bpp()), so no pipeline is built + // through an operator that cannot handle the image, while Op_to_sdr_planes, which + // lowers every plane on its own, can still equalize such an image to 8 bits. for (heif_channel channel : channels) { if (input->get_bits_per_pixel(channel) > 16) { @@ -642,32 +991,14 @@ "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); + if (!success) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_color_conversion}; } + + return pipeline.convert_image(input, limits); } diff -Nru libheif-1.23.4/libheif/color-conversion/colorconversion.h libheif-1.23.6/libheif/color-conversion/colorconversion.h --- libheif-1.23.4/libheif/color-conversion/colorconversion.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/colorconversion.h 2026-10-05 16:16:00.000000000 +0000 @@ -32,9 +32,21 @@ { heif_colorspace colorspace = heif_colorspace_undefined; 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 + + // Bit depth of each plane. A value of 0 means that the plane does not exist in this state. + // The depths may differ from each other (e.g. 'unci' declares a depth per component). + // + // The interleaved RGB chroma formats store all components in a single plane. They are + // represented by their per-component depth in R/G/B (and alpha, if the format has one), + // so that operators see the same fields as for planar RGB. + int bits_per_pixel_R = 0; + int bits_per_pixel_G = 0; + int bits_per_pixel_B = 0; + int bits_per_pixel_Y = 0; + int bits_per_pixel_Cb = 0; + int bits_per_pixel_Cr = 0; + int bits_per_pixel_alpha = 0; + int bits_per_pixel_filter_array = 0; // 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'. @@ -42,11 +54,66 @@ 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) {} + // Convenience constructor: all colour planes of the given colorspace/chroma get 'bpp' and, + // if 'with_alpha' is set, an alpha plane of the same depth is added. + ColorState(heif_colorspace cs, heif_chroma chr, bool with_alpha, int bpp); + + // The state that describes the planes of an image: its colorspace, its chroma format and + // the bit depth of every plane it has. The nclx is left at its default. + static ColorState from_image_planes(const HeifPixelImage& image); + + // True if both states have the same colorspace, the same chroma format and the same planes + // with the same bit depths. Unlike operator==(), this does not look at the nclx. + bool has_same_planes(const ColorState&) const; + + bool has_alpha() const { return bits_per_pixel_alpha != 0; } + + // Bit depth of a single plane, 0 if the plane does not exist. + // 'heif_channel_interleaved' maps to the R/G/B depth. + int get_bits_per_pixel(heif_channel channel) const; + void set_bits_per_pixel(heif_channel channel, int bpp); + + // Sets all colour planes that exist for the current colorspace/chroma to 'bpp' and clears + // all other colour planes. The alpha plane is not touched. + // Call this after 'colorspace' and 'chroma' have been set. + void set_color_bits_per_pixel(int bpp); + + // The bit depth shared by all existing colour planes (R/G/B, Y/Cb/Cr, or the filter array). + // Returns 0 if the colour planes have different depths or if there is no colour plane. + // There is deliberately no accessor for "the" depth of an image whose planes differ: a + // caller that needs one depth for all planes has to handle the 0 (usually by declining + // the conversion) and cannot silently pick up the depth of just one plane. + int get_uniform_color_bits_per_pixel() const; + + // Like get_uniform_color_bits_per_pixel(), but an alpha plane, if present, must have that + // same depth too. + int get_uniform_bits_per_pixel() const; + + // Maximum bit depth over all existing colour planes (alpha excluded), 0 if there is none. + int get_max_color_bits_per_pixel() const; + + // Maximum bit depth over all existing planes, including alpha. + int get_max_bits_per_pixel() const; + + // True if all existing colour planes (R/G/B or Y/Cb/Cr) have the same bit depth. + bool color_channels_have_same_bpp() const { return get_uniform_color_bits_per_pixel() != 0; } + + // True if all existing planes, including alpha, have the same bit depth. + bool all_channels_have_same_bpp() const { return get_uniform_bits_per_pixel() != 0; } + + // Number of bytes HeifPixelImage stores per sample of the given plane (1, 2, 4, 8 or 16), + // 0 if the plane does not exist. Operators access samples through uint8_t or uint16_t + // pointers, so they declare the sample width they can handle, not just a bit depth range. + int get_bytes_per_sample(heif_channel channel) const; + + // Largest sample width over all existing planes, including alpha. + int get_max_bytes_per_sample() const; + + // True if all existing colour planes (R/G/B or Y/Cb/Cr) are stored with 'bytes' per sample. + bool color_channels_have_bytes_per_sample(int bytes) const; - // 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; } + // True if all existing planes, including alpha, are stored with 'bytes' per sample. + bool all_channels_have_bytes_per_sample(int bytes) const; bool operator==(const ColorState&) const; }; @@ -54,25 +121,13 @@ 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); -} +// Note on sample widths: HeifPixelImage stores a plane with 1, 2, 4, 8 or 16 bytes per +// sample depending on its bit depth (bytes_per_sample_for_bit_depth() in pixelimage.h); +// 'unci' components may be up to 128 bits wide. Every conversion operator, however, reads +// samples through uint8_t* or uint16_t*. An operator therefore declares in +// state_after_conversion() which sample width it accepts (ColorState:: +// color_channels_have_bytes_per_sample() and friends) instead of relying on an 8-bit +// SDR/HDR split, which would misread any plane wider than 16 bits. // 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. @@ -111,6 +166,13 @@ const heif_color_conversion_options& options, const heif_color_conversion_options_ext& options_ext) const = 0; + // Converts 'input', which is in 'input_state', into 'target_state', which is one of the + // states that state_after_conversion() returned for this input state. + // The returned image has to have exactly the planes of 'target_state': the pipeline is + // planned with the declared states, so the next operation reads the planes that the state + // lists, with the bit depths given there. ColorConversionPipeline::convert_image() checks + // this. An image that contradicts the declared state is an error in the operation: it + // triggers an assertion in debug builds and fails the conversion otherwise. virtual Result> convert_colorspace(const std::shared_ptr& input, const ColorState& input_state, @@ -127,6 +189,16 @@ static void init_ops(); static void release_ops(); + // All conversion operations that a pipeline can be built from. + static const std::vector>& get_operations(); + + // Checks the image that a conversion operation returned against the state that the + // operation declared for it in state_after_conversion(): the same colorspace, the same + // chroma format and the same planes with the same bit depths, each plane with the size + // that its channel implies. + static Error check_operation_output(const std::shared_ptr& image, + const ColorState& declared_state); + bool is_nop() const { return m_conversion_steps.empty(); } bool construct_pipeline(const ColorState& input_state, diff -Nru libheif-1.23.4/libheif/color-conversion/hdr_sdr.cc libheif-1.23.6/libheif/color-conversion/hdr_sdr.cc --- libheif-1.23.4/libheif/color-conversion/hdr_sdr.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/hdr_sdr.cc 2026-10-05 16:16:00.000000000 +0000 @@ -28,11 +28,24 @@ 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 && - input_state.chroma != heif_chroma_422 && - input_state.chroma != heif_chroma_444) || - input_state.bits_per_pixel != 8) { // TODO: support for <8 bpp + 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 {}; + } + + // A Bayer image (colorspace filter_array with chroma planar, which has the same value as + // heif_chroma_monochrome) passes the chroma test above, but the loop below only handles + // the Y/Cb/Cr/R/G/B/alpha planes and would return an image without any plane. Decline it, + // so that the Bayer operator demosaics first and the depth change applies to its RGB result. + if (input_state.colorspace == heif_colorspace_filter_array) { + return {}; + } + + // Every plane, alpha included, is widened from 8 bits, so all of them must be 8 bits + // (get_uniform_bits_per_pixel() is 0 when the planes differ). + if (input_state.get_uniform_bits_per_pixel() != 8) { // TODO: support for <8 bpp return {}; } @@ -41,8 +54,8 @@ // 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) { + int target_bpp = target_state.get_uniform_color_bits_per_pixel(); // 0 if the target planes differ + if (target_bpp <= 8 || target_bpp > 16) { return {}; } @@ -53,8 +66,10 @@ // --- increase bit depth 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; + output_state.set_color_bits_per_pixel(target_bpp); + if (output_state.has_alpha()) { + output_state.bits_per_pixel_alpha = target_bpp; + } states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -87,12 +102,12 @@ if (input->has_channel(channel)) { uint32_t width = input->get_width(channel); uint32_t height = input->get_height(channel); - if (auto err = outimg->add_channel(channel, width, height, target_state.bits_per_pixel, limits)) { + if (auto err = outimg->add_channel(channel, width, height, target_state.get_uniform_color_bits_per_pixel(), limits)) { return err; } int input_bits = input->get_bits_per_pixel(channel); - int output_bits = target_state.bits_per_pixel; + int output_bits = target_state.get_uniform_color_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 @@ -136,21 +151,35 @@ 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 && - input_state.chroma != heif_chroma_422 && - input_state.chroma != heif_chroma_444) || - input_state.bits_per_pixel == 8) { + 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 {}; } - if (target_state.bits_per_pixel != 8) { + // A Bayer image (colorspace filter_array with chroma planar, which has the same value as + // heif_chroma_monochrome) passes the chroma test above, but the loop below only handles + // the Y/Cb/Cr/R/G/B/alpha planes and would return an image without any plane. Decline it, + // so that the Bayer operator demosaics first and the depth change applies to its RGB result. + if (input_state.colorspace == heif_colorspace_filter_array) { 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)) { + // Nothing to do when every colour plane is already 8 bits. The planes may differ from each + // other ('unci' declares a depth per component); the loop below handles each plane on its + // own, so a mixed image such as 8/8/16 is equalized to 8/8/8 here. For such an image + // get_uniform_color_bits_per_pixel() is 0, which does not match the 8, so it is offered. + if (input_state.get_uniform_color_bits_per_pixel() == 8) { + return {}; + } + + if (target_state.get_uniform_color_bits_per_pixel() != 8) { + return {}; + } + + // Every channel, alpha included, is read as uint8_t or uint16_t samples. + if (input_state.get_max_bytes_per_sample() > 2) { return {}; } @@ -161,8 +190,10 @@ // --- output bit depth = 8 output_state = input_state; - output_state.bits_per_pixel = 8; - output_state.alpha_bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + if (output_state.has_alpha()) { + output_state.bits_per_pixel_alpha = 8; + } states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -195,8 +226,9 @@ heif_channel_Alpha}) { if (input->has_channel(channel)) { int input_bits = input->get_bits_per_pixel(channel); + int input_bytes = bytes_per_sample_for_bit_depth(input_bits); - if (input_bits > 8) { + if (input_bytes == 2) { uint32_t width = input->get_width(channel); uint32_t height = input->get_height(channel); if (auto err = outimg->add_channel(channel, width, height, 8, limits)) { @@ -261,8 +293,12 @@ int in = p_in[y * stride_in + x]; p_out[y * stride_out + x] = (uint8_t) ((in * mulFactor) >> 8); } - } else { + } else if (input_bits == 8) { outimg->copy_new_channel_from(input, channel, channel, limits); + } else { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Op_to_sdr_planes: only 8- and 16-bit sample storage is supported"}; } } } diff -Nru libheif-1.23.4/libheif/color-conversion/monochrome.cc libheif-1.23.6/libheif/color-conversion/monochrome.cc --- libheif-1.23.4/libheif/color-conversion/monochrome.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/monochrome.cc 2026-10-05 16:16:00.000000000 +0000 @@ -33,7 +33,8 @@ return {}; } - if (has_samples_wider_than_16bit(input_state)) { + // The luma plane is read as uint8_t or uint16_t samples; alpha is copied at its own width. + if (input_state.get_bytes_per_sample(heif_channel_Y) > 2) { return {}; } @@ -45,9 +46,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; states.emplace_back(output_state, SpeedCosts_OptimizedSoftware); @@ -91,7 +91,9 @@ } - if (input_bpp <= 8) { + int bytes_per_sample = bytes_per_sample_for_bit_depth(input_bpp); + + if (bytes_per_sample == 1) { uint8_t* out_cb, * out_cr, * out_y; size_t out_cb_stride = 0, out_cr_stride = 0, out_y_stride = 0; @@ -115,7 +117,7 @@ width); } } - else { + else if (bytes_per_sample == 2) { uint16_t* out_cb, * out_cr, * out_y; size_t out_cb_stride = 0, out_cr_stride = 0, out_y_stride = 0; @@ -145,6 +147,11 @@ width * 2); } } + else { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unsupported_bit_depth, + "Monochrome to YCbCr conversion only supports 8- and 16-bit sample storage."}; + } if (has_alpha) { const uint8_t* in_a; @@ -155,7 +162,7 @@ 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); + size_t memory_width = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(alpha_bpp)); for (uint32_t y = 0; y < height; y++) { memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], memory_width); @@ -176,11 +183,11 @@ if (input_state.colorspace != heif_colorspace_monochrome || input_state.chroma != heif_chroma_monochrome || - input_state.bits_per_pixel != 8) { + input_state.bits_per_pixel_Y != 8) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_Y) { return {}; } @@ -190,11 +197,11 @@ // --- convert to RGB24 - if (input_state.has_alpha == false) { + if (!input_state.has_alpha()) { output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGB; - output_state.has_alpha = false; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -204,8 +211,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGBA; - output_state.has_alpha = true; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 8; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -232,7 +239,7 @@ bool has_alpha = input->has_channel(heif_channel_Alpha); - if (target_state.has_alpha) { + if (target_state.has_alpha()) { outimg->create(width, height, heif_colorspace_RGB, heif_chroma_interleaved_32bit); } else { @@ -258,7 +265,7 @@ uint32_t x, y; for (y = 0; y < height; y++) { - if (target_state.has_alpha == false) { + if (!target_state.has_alpha()) { for (x = 0; x < width; x++) { uint8_t v = in_y[x + y * in_y_stride]; out_p[y * out_p_stride + 3 * x + 0] = v; diff -Nru libheif-1.23.4/libheif/color-conversion/rgb2rgb.cc libheif-1.23.6/libheif/color-conversion/rgb2rgb.cc --- libheif-1.23.4/libheif/color-conversion/rgb2rgb.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/rgb2rgb.cc 2026-10-05 16:16:00.000000000 +0000 @@ -33,11 +33,16 @@ { if (input_state.colorspace != heif_colorspace_RGB || input_state.chroma != heif_chroma_444 || - input_state.bits_per_pixel != 8) { + input_state.bits_per_pixel_R != 8) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_R) { + return {}; + } + + // The interleaved output has one depth for all components. + if (!input_state.color_channels_have_same_bpp()) { return {}; } @@ -50,8 +55,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGBA; - output_state.has_alpha = true; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 8; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -59,8 +64,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGB; - output_state.has_alpha = false; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -77,7 +82,7 @@ const heif_security_limits* limits) const { bool has_alpha = input->has_channel(heif_channel_Alpha); - bool want_alpha = target_state.has_alpha; + bool want_alpha = target_state.has_alpha(); if (input->get_bits_per_pixel(heif_channel_R) != 8 || input->get_bits_per_pixel(heif_channel_G) != 8 || @@ -160,15 +165,21 @@ if (input_state.colorspace != heif_colorspace_RGB || input_state.chroma != heif_chroma_444 || - input_state.bits_per_pixel <= 8) { + input_state.bits_per_pixel_R <= 8) { + return {}; + } + + // All planes, alpha included, are read as uint16_t samples. + if (!input_state.all_channels_have_bytes_per_sample(2)) { return {}; } - if (has_samples_wider_than_16bit(input_state)) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_R) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + // The interleaved output has one depth for all components. + if (!input_state.color_channels_have_same_bpp()) { return {}; } @@ -178,11 +189,11 @@ // --- convert to RRGGBB_BE - if (input_state.has_alpha == false) { + if (!input_state.has_alpha()) { output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RRGGBB_BE; - output_state.has_alpha = false; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -192,8 +203,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RRGGBBAA_BE; - output_state.has_alpha = true; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_R; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -210,17 +221,17 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - if (input->get_bits_per_pixel(heif_channel_R) <= 8 || - input->get_bits_per_pixel(heif_channel_G) <= 8 || - input->get_bits_per_pixel(heif_channel_B) <= 8) { + if (bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_R)) != 2 || + bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_G)) != 2 || + bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_B)) != 2) { return Error::InternalError; } bool input_has_alpha = input->has_channel(heif_channel_Alpha); - bool output_has_alpha = input_has_alpha || target_state.has_alpha; + bool output_has_alpha = input_has_alpha || target_state.has_alpha(); if (input_has_alpha) { - if (input->get_bits_per_pixel(heif_channel_Alpha) <= 8) { + if (bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_Alpha)) != 2) { return Error::InternalError; } @@ -318,11 +329,11 @@ if (input_state.colorspace != heif_colorspace_RGB || input_state.chroma != heif_chroma_444 || - input_state.bits_per_pixel != 8) { + input_state.bits_per_pixel_R != 8) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_R) { return {}; } @@ -332,11 +343,11 @@ // --- convert to RRGGBB_BE - if (input_state.has_alpha == false) { + if (!input_state.has_alpha()) { output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RRGGBB_BE; - output_state.has_alpha = false; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -346,8 +357,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RRGGBBAA_BE; - output_state.has_alpha = true; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_R; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -372,7 +383,7 @@ //int bpp = input->get_bits_per_pixel(heif_channel_R); bool input_has_alpha = input->has_channel(heif_channel_Alpha); - bool output_has_alpha = input_has_alpha || target_state.has_alpha; + bool output_has_alpha = input_has_alpha || target_state.has_alpha(); if (input_has_alpha && input->get_bits_per_pixel(heif_channel_Alpha) != 8) { return Error::InternalError; @@ -454,11 +465,12 @@ if (input_state.colorspace != heif_colorspace_RGB || (input_state.chroma != heif_chroma_interleaved_RRGGBB_BE && input_state.chroma != heif_chroma_interleaved_RRGGBBAA_BE) || - input_state.bits_per_pixel <= 8) { + input_state.bits_per_pixel_R <= 8) { return {}; } - if (has_samples_wider_than_16bit(input_state)) { + // Interleaved RRGGBB samples are two bytes each. + if (!input_state.color_channels_have_bytes_per_sample(2)) { return {}; } @@ -470,9 +482,8 @@ output_state.colorspace = heif_colorspace_RGB; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = target_state.has_alpha() ? input_state.bits_per_pixel_R : 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -490,7 +501,7 @@ { bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_LE || input->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE); - bool want_alpha = target_state.has_alpha; + bool want_alpha = target_state.has_alpha(); auto outimg = std::make_shared(); @@ -575,7 +586,7 @@ if (input_state.colorspace != heif_colorspace_RGB || (input_state.chroma != heif_chroma_interleaved_RGB && input_state.chroma != heif_chroma_interleaved_RGBA) || - input_state.bits_per_pixel != 8) { + input_state.bits_per_pixel_R != 8) { return {}; } @@ -587,9 +598,8 @@ output_state.colorspace = heif_colorspace_RGB; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = target_state.has_alpha() ? input_state.bits_per_pixel_R : 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -606,7 +616,7 @@ const heif_security_limits* limits) const { bool has_alpha = input->get_chroma_format() == heif_chroma_interleaved_RGBA; - bool want_alpha = target_state.has_alpha; + bool want_alpha = target_state.has_alpha(); auto outimg = std::make_shared(); @@ -681,7 +691,7 @@ // 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)) { + if (!input_state.color_channels_have_bytes_per_sample(2)) { return {}; } @@ -702,8 +712,8 @@ output_state.chroma = heif_chroma_interleaved_RRGGBB_LE; } - output_state.has_alpha = false; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); } @@ -722,8 +732,8 @@ output_state.chroma = heif_chroma_interleaved_RRGGBBAA_LE; } - output_state.has_alpha = true; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; states.emplace_back(output_state, SpeedCosts_Unoptimized); } diff -Nru libheif-1.23.4/libheif/color-conversion/rgb2yuv.cc libheif-1.23.6/libheif/color-conversion/rgb2yuv.cc --- libheif-1.23.4/libheif/color-conversion/rgb2yuv.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/rgb2yuv.cc 2026-10-05 16:16:00.000000000 +0000 @@ -34,27 +34,36 @@ const heif_color_conversion_options& options, const heif_color_conversion_options_ext& options_ext) const { - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { + if (input_state.colorspace != heif_colorspace_RGB || + input_state.chroma != heif_chroma_444) { return {}; } - if (has_samples_wider_than_16bit(input_state)) { + // All three colour planes are read through the same 'Pixel' type and converted with one + // set of range constants, so they must share one bit depth and be stored with + // sizeof(Pixel) bytes per sample. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } // TODO: add support for <8 bpp - if (input_state.bits_per_pixel < 8) { + if (input_state.bits_per_pixel_R < 8) { return {}; } - if (input_state.colorspace != heif_colorspace_RGB || - input_state.chroma != heif_chroma_444) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_R) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + // The output is planar YCbCr, and its chroma format is taken from the target state below. + // A target with another chroma format (monochrome or an interleaved RGB format) would be + // declared as a YCbCr state with that chroma format, which no image can have: + // convert_colorspace() then returns an image with Y, Cb and Cr planes that contradicts + // the declared state. + if (target_state.chroma != heif_chroma_420 && + target_state.chroma != heif_chroma_422 && + target_state.chroma != heif_chroma_444) { return {}; } @@ -80,8 +89,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -91,8 +100,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -110,8 +119,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - uint32_t width = input->get_width(); uint32_t height = input->get_height(); @@ -120,7 +127,7 @@ int subV = chroma_v_subsampling(chroma); int bpp = input->get_bits_per_pixel(heif_channel_R); - if (bpp < 8 || (bpp > 8) != hdr) { + if (bpp < 8 || bytes_per_sample_for_bit_depth(bpp) != static_cast(sizeof(Pixel))) { return Error::InternalError; } @@ -182,14 +189,12 @@ out_a = nullptr; } - if (hdr) { - in_r_stride /= 2; - in_g_stride /= 2; - in_b_stride /= 2; - out_y_stride /= 2; - out_cb_stride /= 2; - out_cr_stride /= 2; - } + in_r_stride /= sizeof(Pixel); + in_g_stride /= sizeof(Pixel); + in_b_stride /= sizeof(Pixel); + out_y_stride /= sizeof(Pixel); + out_cb_stride /= sizeof(Pixel); + out_cr_stride /= sizeof(Pixel); uint16_t halfRange = (uint16_t) (1 << (bpp - 1)); int32_t fullRange = (1 << bpp) - 1; @@ -306,7 +311,7 @@ if (has_alpha) { int bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); - int alphaCopyWidth = (bpp_a > 8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); for (y = 0; y < height; y++) { memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); @@ -340,11 +345,12 @@ input_state.chroma == heif_chroma_interleaved_RRGGBB_LE || input_state.chroma == heif_chroma_interleaved_RRGGBBAA_BE || input_state.chroma == heif_chroma_interleaved_RRGGBBAA_LE) || - input_state.bits_per_pixel <= 8) { + input_state.bits_per_pixel_R <= 8) { return {}; } - if (has_samples_wider_than_16bit(input_state)) { + // The interleaved samples are assembled from two bytes each. + if (!input_state.color_channels_have_bytes_per_sample(2)) { return {}; } @@ -369,8 +375,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.set_color_bits_per_pixel(input_state.bits_per_pixel_R); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we generate an alpha plane if the source contains data output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -483,8 +489,10 @@ float g = static_cast((in[2 + le] << 8) | in[3 - le]); float b = static_cast((in[4 + le] << 8) | in[5 - le]); - int dx = (x + 1 < width) ? bytesPerPixel : 0; - int dy = (y + 1 < height) ? (int)in_p_stride : 0; + // Byte offsets to the right and lower pixels of the 2x2 block averaged + // into one chroma sample. Reuse the edge pixel when a neighbor is absent. + size_t dx = (x + 1 < width) ? static_cast(bytesPerPixel) : 0; + size_t dy = (y + 1 < height) ? in_p_stride : 0; r += static_cast((in[0 + le + dx] << 8) | in[1 - le + dx]); g += static_cast((in[2 + le + dx] << 8) | in[3 - le + dx]); @@ -544,7 +552,7 @@ // 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) { + if (input_state.bits_per_pixel_R != 8) { return {}; } @@ -565,8 +573,8 @@ output_state.colorspace = heif_colorspace_YCbCr; output_state.chroma = target_state.chroma; - output_state.has_alpha = target_state.has_alpha; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = target_state.has_alpha() ? 8 : 0; output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -617,7 +625,7 @@ int chroma_height = (height + chromaSubV - 1) / chromaSubV; const bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_32bit); - const bool want_alpha = target_state.has_alpha; + const bool want_alpha = target_state.has_alpha(); 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) || @@ -847,7 +855,7 @@ // 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) { + if (input_state.bits_per_pixel_R != 8) { return {}; } @@ -865,8 +873,8 @@ output_state.colorspace = heif_colorspace_YCbCr; output_state.chroma = heif_chroma_444; - output_state.has_alpha = target_state.has_alpha; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = target_state.has_alpha() ? 8 : 0; output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -891,7 +899,7 @@ outimg->create(width, height, heif_colorspace_YCbCr, heif_chroma_444); const bool has_alpha = (input->get_chroma_format() == heif_chroma_interleaved_32bit); - const bool want_alpha = target_state.has_alpha; + const bool want_alpha = target_state.has_alpha(); if (auto err = outimg->add_channel(heif_channel_Y, width, height, 8, limits) || outimg->add_channel(heif_channel_Cb, width, height, 8, limits) || diff -Nru libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.cc libheif-1.23.6/libheif/color-conversion/rgb2yuv_sharp.cc --- libheif-1.23.4/libheif/color-conversion/rgb2yuv_sharp.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/rgb2yuv_sharp.cc 2026-10-05 16:16:00.000000000 +0000 @@ -86,13 +86,22 @@ return {}; } - if (input_state.bits_per_pixel != 8 && input_state.bits_per_pixel != 10 && - input_state.bits_per_pixel != 12 && input_state.bits_per_pixel != 16) { + if (input_state.bits_per_pixel_R != 8 && input_state.bits_per_pixel_R != 10 && + input_state.bits_per_pixel_R != 12 && input_state.bits_per_pixel_R != 16) { return {}; } - if (target_state.bits_per_pixel != 8 && target_state.bits_per_pixel != 10 && - target_state.bits_per_pixel != 12) { + // Planar input is handed to libsharpyuv with a single sample width and stride. + if (!input_state.color_channels_have_same_bpp()) { + return {}; + } + + if (target_state.chroma != heif_chroma_420) { + return {}; + } + + if (target_state.bits_per_pixel_Y != 8 && target_state.bits_per_pixel_Y != 10 && + target_state.bits_per_pixel_Y != 12) { return {}; } @@ -105,11 +114,7 @@ // 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) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_R) { return {}; } @@ -124,8 +129,8 @@ output_state.colorspace = heif_colorspace_YCbCr; 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.set_color_bits_per_pixel(target_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = target_state.has_alpha() ? target_state.bits_per_pixel_Y : 0; output_state.nclx = target_state.nclx; states.emplace_back(output_state, SpeedCosts_Slow); @@ -167,9 +172,9 @@ input->get_chroma_format() == heif_chroma_interleaved_RRGGBBAA_BE || (input->get_chroma_format() == heif_chroma_444 && input->has_channel(heif_channel_Alpha)); - bool want_alpha = target_state.has_alpha; + bool want_alpha = target_state.has_alpha(); - int output_bits = target_state.bits_per_pixel; + int output_bits = target_state.bits_per_pixel_Y; 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)) { @@ -182,13 +187,9 @@ } } - int input_bytes_per_sample = - (input_chroma == heif_chroma_interleaved_RGB || - input_chroma == heif_chroma_interleaved_RGBA || - (input_chroma == heif_chroma_444 && - input->get_bits_per_pixel(heif_channel_R) <= 8)) - ? 1 - : 2; + int input_bytes_per_sample = bytes_per_sample_for_bit_depth( + input->get_bits_per_pixel(input_chroma == heif_chroma_444 ? heif_channel_R + : heif_channel_interleaved)); const uint8_t* in_r, * in_g, * in_b, * in_a = nullptr; size_t in_stride = 0; @@ -247,6 +248,19 @@ int input_bytes_per_pixel = (has_alpha ? 4 : 3) * input_bytes_per_sample; int rgb_step = planar_input ? input_bytes_per_sample : input_bytes_per_pixel; + // libsharpyuv uses the sample values as indices into its gamma tables, which are sized + // for the bit depth we pass, and it does not check the range itself. A sample above + // that range makes it read far outside the table, and the value found there is used + // as the index for a second lookup. Images from the application are checked when they + // are passed to the encoder, but this operator also runs on decoded images, and a decoder + // may hand through whatever the bitstream contained. Refuse such an image here, right + // in front of the call that depends on it. + if (Error err = input->check_sample_value_ranges()) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + err.message}; + } + int sharpyuv_ok = SharpYuvConvert(in_r, in_g, in_b, rgb_step, (int)in_stride, input_bits, out_y, (int)out_y_stride, out_cb, (int)out_cb_stride, diff -Nru libheif-1.23.4/libheif/color-conversion/yuv2rgb.cc libheif-1.23.6/libheif/color-conversion/yuv2rgb.cc --- libheif-1.23.4/libheif/color-conversion/yuv2rgb.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/color-conversion/yuv2rgb.cc 2026-10-05 16:16:00.000000000 +0000 @@ -58,25 +58,23 @@ // BT.2020 NCL. A correct CL path needs EOTF inversion on Cb/Cr, not the linear matrix below. - bool hdr = !std::is_same::value; - - if ((input_state.bits_per_pixel > 8) != hdr) { + // The three colour planes are read through the same 'Pixel' type, so they must be stored + // with sizeof(Pixel) bytes per sample, and the conversion derives its shifts and midpoints + // from one bit depth, so they must also share it ('unci' may declare a depth per plane). + // The alpha plane is copied through at its own width. + if (!input_state.color_channels_have_same_bpp() || + !input_state.color_channels_have_bytes_per_sample(static_cast(sizeof(Pixel)))) { return {}; } // TODO: add support for <8 bpp - if (input_state.bits_per_pixel < 8) { + if (input_state.bits_per_pixel_Y < 8) { 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)) { + if (matrix == 16 && input_state.bits_per_pixel_Y > 14) { return {}; } @@ -88,9 +86,8 @@ output_state.colorspace = heif_colorspace_RGB; 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; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; // we simply keep the old alpha plane states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -107,8 +104,6 @@ const heif_color_conversion_options_ext& options_ext, const heif_security_limits* limits) const { - bool hdr = !std::is_same::value; - heif_chroma chroma = input->get_chroma_format(); int bpp_y = input->get_bits_per_pixel(heif_channel_Y); @@ -122,19 +117,10 @@ bpp_a = input->get_bits_per_pixel(heif_channel_Alpha); } - if (!hdr) { - if (bpp_y != 8 || - bpp_cb != 8 || - bpp_cr != 8) { - return Error::InternalError; - } - } - else { - if (bpp_y == 8 || - bpp_cb == 8 || - bpp_cr == 8) { - return Error::InternalError; - } + if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cb) != static_cast(sizeof(Pixel)) || + bytes_per_sample_for_bit_depth(bpp_cr) != static_cast(sizeof(Pixel))) { + return Error::InternalError; } @@ -202,14 +188,12 @@ int shiftH = chroma_h_subsampling(chroma) - 1; int shiftV = chroma_v_subsampling(chroma) - 1; - if (hdr) { - in_y_stride /= 2; - in_cb_stride /= 2; - in_cr_stride /= 2; - out_r_stride /= 2; - out_g_stride /= 2; - out_b_stride /= 2; - } + in_y_stride /= sizeof(Pixel); + in_cb_stride /= sizeof(Pixel); + in_cr_stride /= sizeof(Pixel); + out_r_stride /= sizeof(Pixel); + out_g_stride /= sizeof(Pixel); + out_b_stride /= sizeof(Pixel); int matrix_coeffs = 2; bool full_range_flag = true; @@ -298,7 +282,7 @@ } if (has_alpha) { - int alphaCopyWidth = (bpp_a>8 ? width * 2 : width); + size_t alphaCopyWidth = static_cast(width) * static_cast(bytes_per_sample_for_bit_depth(bpp_a)); memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth); } } @@ -325,10 +309,12 @@ } } + // All three planes are read as 8-bit samples, so they must all be 8 bits + // (get_uniform_color_bits_per_pixel() is 0 when they differ). if (input_state.colorspace != heif_colorspace_YCbCr || input_state.chroma != heif_chroma_420 || - input_state.bits_per_pixel != 8 || - input_state.has_alpha == true) { + input_state.get_uniform_color_bits_per_pixel() != 8 || + input_state.has_alpha()) { return {}; } @@ -348,8 +334,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGB; - output_state.has_alpha = false; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 0; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -458,13 +444,15 @@ // Note: no input alpha channel required. It will be filled up with 0xFF. + // All three planes are read as 8-bit samples, so they must all be 8 bits + // (get_uniform_color_bits_per_pixel() is 0 when they differ). if (input_state.colorspace != heif_colorspace_YCbCr || input_state.chroma != heif_chroma_420 || - input_state.bits_per_pixel != 8) { + input_state.get_uniform_color_bits_per_pixel() != 8) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_Y) { return {}; } @@ -484,8 +472,8 @@ output_state.colorspace = heif_colorspace_RGB; output_state.chroma = heif_chroma_interleaved_RGBA; - output_state.has_alpha = true; - output_state.bits_per_pixel = 8; + output_state.set_color_bits_per_pixel(8); + output_state.bits_per_pixel_alpha = 8; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -592,17 +580,20 @@ } } + // The conversion derives its shifts from one bit depth, so the three planes must share + // it (get_uniform_color_bits_per_pixel() is 0 when they differ) and it must be > 8. if (input_state.colorspace != heif_colorspace_YCbCr || input_state.chroma != heif_chroma_420 || - input_state.bits_per_pixel <= 8) { + input_state.get_uniform_color_bits_per_pixel() <= 8) { return {}; } - if (has_samples_wider_than_16bit(input_state)) { + // All planes, alpha included, are read as uint16_t samples. + if (!input_state.all_channels_have_bytes_per_sample(2)) { return {}; } - if (input_state.has_alpha && input_state.get_alpha_bits_per_pixel() != input_state.bits_per_pixel) { + if (input_state.has_alpha() && input_state.bits_per_pixel_alpha != input_state.bits_per_pixel_Y) { return {}; } @@ -618,19 +609,19 @@ // --- convert to YCbCr output_state.colorspace = heif_colorspace_RGB; - output_state.chroma = (input_state.has_alpha ? + output_state.chroma = (input_state.has_alpha() ? heif_chroma_interleaved_RRGGBBAA_LE : heif_chroma_interleaved_RRGGBB_LE); - output_state.has_alpha = input_state.has_alpha; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; states.emplace_back(output_state, SpeedCosts_Unoptimized); output_state.colorspace = heif_colorspace_RGB; - output_state.chroma = (input_state.has_alpha ? + output_state.chroma = (input_state.has_alpha() ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE); - output_state.has_alpha = input_state.has_alpha; - output_state.bits_per_pixel = input_state.bits_per_pixel; + output_state.set_color_bits_per_pixel(input_state.bits_per_pixel_Y); + output_state.bits_per_pixel_alpha = input_state.bits_per_pixel_alpha; states.emplace_back(output_state, SpeedCosts_Unoptimized); @@ -694,9 +685,8 @@ }; } - if (auto err = outimg->add_channel(heif_channel_Alpha, width, height, bpp, limits)) { - return err; - } + // The alpha samples go into the fourth component of the interleaved RRGGBBAA plane below. + // The output must not carry a separate alpha plane in addition to that. } uint8_t* out_p; diff -Nru libheif-1.23.4/libheif/compression.h libheif-1.23.6/libheif/compression.h --- libheif-1.23.4/libheif/compression.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/compression.h 2026-10-05 16:16:00.000000000 +0000 @@ -77,14 +77,18 @@ * This is assumed to be in RFC 1950 format, which is the normal zlib format. * * @param compressed_input the compressed data to be decompressed - * @param output pointer to the resulting vector of decompressed data - * @return success (Ok) or an error on failure (usually corrupt data) + * @param limits security limits that bound the size of the decompressed data (may be nullptr) + * @param max_output_size the largest size the decompressed data may have. Decompression + * stops with an error as soon as more data is produced. Use this when the expected + * size is known in advance. + * @return the decompressed data or an error on failure (usually corrupt data) * * @sa decompress_deflate * @sa compress_zlib */ Result> decompress_zlib(const std::vector& compressed_input, - const heif_security_limits* limits); + const heif_security_limits* limits, + uint64_t max_output_size = UINT64_MAX); /** * Decompress "deflate" compressed data. @@ -92,14 +96,18 @@ * This is assumed to be in RFC 1951 format, which is the deflate format. * * @param compressed_input the compressed data to be decompressed - * @param output pointer to the resulting vector of decompressed data - * @return success (Ok) or an error on failure (usually corrupt data) + * @param limits security limits that bound the size of the decompressed data (may be nullptr) + * @param max_output_size the largest size the decompressed data may have. Decompression + * stops with an error as soon as more data is produced. Use this when the expected + * size is known in advance. + * @return the decompressed data or an error on failure (usually corrupt data) * * @sa decompress_zlib * @sa compress_deflate */ Result> decompress_deflate(const std::vector& compressed_input, - const heif_security_limits* limits); + const heif_security_limits* limits, + uint64_t max_output_size = UINT64_MAX); #endif @@ -110,11 +118,15 @@ * Brotli is described at https://brotli.org/ * * @param compressed_input the compressed data to be decompressed - * @param output pointer to the resulting vector of decompressed data - * @return success (Ok) or an error on failure (usually corrupt data) + * @param limits security limits that bound the size of the decompressed data (may be nullptr) + * @param max_output_size the largest size the decompressed data may have. Decompression + * stops with an error as soon as more data is produced. Use this when the expected + * size is known in advance. + * @return the decompressed data or an error on failure (usually corrupt data) */ Result> decompress_brotli(const std::vector& compressed_input, - const heif_security_limits* limits); + const heif_security_limits* limits, + uint64_t max_output_size = UINT64_MAX); std::vector compress_brotli(const uint8_t* input, size_t size); #endif diff -Nru libheif-1.23.4/libheif/compression_brotli.cc libheif-1.23.6/libheif/compression_brotli.cc --- libheif-1.23.4/libheif/compression_brotli.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/compression_brotli.cc 2026-10-05 16:16:00.000000000 +0000 @@ -35,7 +35,8 @@ Result> decompress_brotli(const std::vector &compressed_input, - const heif_security_limits* limits) + const heif_security_limits* limits, + uint64_t max_output_size) { BrotliDecoderResult result = BROTLI_DECODER_RESULT_ERROR; std::vector buffer(BUF_SIZE, 0); @@ -53,27 +54,43 @@ // output, bypassing max_memory_block_size / max_total_memory (GHSA-24wx-9w62-c96w). MemoryHandle output_memory_handle; + // Append the data decoded into `buffer` to `output`. + auto append_decoded_data = [&]() -> Error { + size_t n_new_bytes = static_cast(std::distance(buffer.data(), next_output)); + + // Stop as soon as there is more data than the caller expects. This keeps a + // decompression bomb from inflating up to the security limits when the size + // of the decompressed data is known in advance (GHSA-fcmw-5764-7rq8). + if (n_new_bytes > max_output_size - output.size()) { + return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, + "Decompressed brotli data is larger than expected."); + } + + 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); + return Error::Ok; + }; + while (true) { result = BrotliDecoderDecompressStream(state.get(), &available_in, &next_in, &available_out, &next_output, 0); if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_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; + if (Error err = append_decoded_data()) { + return err; } - 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) { - 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; + if (Error err = append_decoded_data()) { + return err; } - output.insert(output.end(), buffer.data(), buffer.data() + n_new_bytes); break; } else if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) diff -Nru libheif-1.23.4/libheif/compression_zlib.cc libheif-1.23.6/libheif/compression_zlib.cc --- libheif-1.23.4/libheif/compression_zlib.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/compression_zlib.cc 2026-10-05 16:16:00.000000000 +0000 @@ -82,7 +82,8 @@ Result> do_inflate(const std::vector& compressed_input, int windowSize, - const heif_security_limits* limits) + const heif_security_limits* limits, + uint64_t max_output_size) { if (compressed_input.empty()) { return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, @@ -162,6 +163,16 @@ // account for and append decoded data to output size_t n_new_bytes = dst.size() - strm.avail_out; + + // Stop as soon as there is more data than the caller expects. This keeps a + // decompression bomb from inflating up to the security limits when the size + // of the decompressed data is known in advance (GHSA-fcmw-5764-7rq8). + if (n_new_bytes > max_output_size - output.size()) { + inflateEnd(&strm); + return Error(heif_error_Invalid_input, heif_suberror_Decompression_invalid_data, + "Decompressed zlib/deflate data is larger than expected."); + } + if (Error memErr = output_memory_handle.alloc(n_new_bytes, limits, "zlib/deflate decompression output")) { inflateEnd(&strm); return memErr; @@ -188,14 +199,16 @@ Result> decompress_zlib(const std::vector& compressed_input, - const heif_security_limits* limits) + const heif_security_limits* limits, + uint64_t max_output_size) { - return do_inflate(compressed_input, 15, limits); + return do_inflate(compressed_input, 15, limits, max_output_size); } Result> decompress_deflate(const std::vector& compressed_input, - const heif_security_limits* limits) + const heif_security_limits* limits, + uint64_t max_output_size) { - return do_inflate(compressed_input, -15, limits); + return do_inflate(compressed_input, -15, limits, max_output_size); } #endif diff -Nru libheif-1.23.4/libheif/context.cc libheif-1.23.6/libheif/context.cc --- libheif-1.23.4/libheif/context.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/context.cc 2026-10-05 16:16:00.000000000 +0000 @@ -670,6 +670,21 @@ } } + + // --- component descriptions of derived images + // + // A derived image (grid, overlay, 'iden') takes its component descriptions from its + // input images. An input image that comes later in the file than the derived image + // was not known yet when the derived image was read above. Now that all images are + // known, describe the images that are still without a description. This does nothing + // for images that have one. + + for (auto& pair : m_all_images) { + if (!pair.second->get_item_error()) { + pair.second->populate_component_descriptions(); + } + } + if (!m_primary_image) { return Error(heif_error_Invalid_input, heif_suberror_Nonexisting_item_referenced, @@ -1982,8 +1997,10 @@ else { // uncompressed data, plain copy - data_array.resize(size); - memcpy(data_array.data(), data, size); + if (size > 0) { // memcpy() with a NULL pointer is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read() + data_array.resize(size); + memcpy(data_array.data(), data, size); + } } // copy the data into the file, store the pointer to it in an iloc box entry diff -Nru libheif-1.23.4/libheif/file.cc libheif-1.23.6/libheif/file.cc --- libheif-1.23.4/libheif/file.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/file.cc 2026-10-05 16:16:00.000000000 +0000 @@ -353,7 +353,8 @@ } Indent indent; - sstr << box->dump(indent); + box->dump(sstr, indent, false); + reset_stream_format(sstr); } return sstr.str(); diff -Nru libheif-1.23.4/libheif/file_layout.cc libheif-1.23.6/libheif/file_layout.cc --- libheif-1.23.4/libheif/file_layout.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/file_layout.cc 2026-10-05 16:16:00.000000000 +0000 @@ -213,7 +213,7 @@ } BitstreamRange mini_box_range(m_stream_reader, mini_box_start, end_of_mini_box); std::shared_ptr mini_box; - err = Box::read(mini_box_range, &mini_box, heif_get_global_security_limits()); + err = Box::read(mini_box_range, &mini_box, limits); if (err) { return err; } diff -Nru libheif-1.23.4/libheif/image/image_description.cc libheif-1.23.6/libheif/image/image_description.cc --- libheif-1.23.4/libheif/image/image_description.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image/image_description.cc 2026-10-05 16:16:00.000000000 +0000 @@ -113,12 +113,6 @@ } -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; @@ -133,12 +127,7 @@ 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_tai_timestamp = other.m_tai_timestamp; m_gimi_sample_content_id = other.m_gimi_sample_content_id; diff -Nru libheif-1.23.4/libheif/image/image_description.h libheif-1.23.6/libheif/image/image_description.h --- libheif-1.23.4/libheif/image/image_description.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image/image_description.h 2026-10-05 16:16:00.000000000 +0000 @@ -153,10 +153,13 @@ }; +// TODO: the colorspace and the chroma format of the image should be part of this class, +// together with a function that compares two descriptions for having the same format. +// See the notes at ImageItem_Grid::TileFormat (image-items/grid.h). class ImageDescription { public: - virtual ~ImageDescription(); + virtual ~ImageDescription() = default; // TODO: Decide who is responsible for writing the colr boxes. // Currently it is distributed over various places. @@ -270,15 +273,17 @@ 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); + // Version-aware copy into a freshly initialized packet, mirroring + // heif_tai_timestamp_packet_alloc() + heif_tai_timestamp_packet_copy(). + heif_tai_timestamp_packet packet{}; + packet.version = 1; + heif_tai_timestamp_packet_copy(&packet, tai); + m_tai_timestamp = packet; return Error::Ok; } [[nodiscard]] const heif_tai_timestamp_packet* get_tai_timestamp() const { - return m_tai_timestamp; + return m_tai_timestamp ? &*m_tai_timestamp : nullptr; } // --- GIMI content ID @@ -470,7 +475,9 @@ std::optional m_amve; std::optional m_nominal_diffuse_white_luminance; - heif_tai_timestamp_packet* m_tai_timestamp = nullptr; + // Stored by value so that the compiler-generated copy/move operations + // duplicate it correctly (GHSA-qwpf-5wf7-r996). std::nullopt == "no timestamp". + std::optional m_tai_timestamp; // Empty string means "no content id assigned". std::string m_gimi_sample_content_id; diff -Nru libheif-1.23.4/libheif/image/pixelimage.cc libheif-1.23.6/libheif/image/pixelimage.cc --- libheif-1.23.4/libheif/image/pixelimage.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image/pixelimage.cc 2026-10-05 16:16:00.000000000 +0000 @@ -23,6 +23,7 @@ #include "common_utils.h" #include "security_limits.h" +#include #include #include #include @@ -30,6 +31,8 @@ #include #include #include +#include +#include #include #include "codecs/uncompressed/unc_types.h" @@ -344,10 +347,65 @@ } +static const char* channel_name(heif_channel channel) +{ + switch (channel) { + case heif_channel_Y: return "Y"; + case heif_channel_Cb: return "Cb"; + case heif_channel_Cr: return "Cr"; + case heif_channel_R: return "R"; + case heif_channel_G: return "G"; + case heif_channel_B: return "B"; + case heif_channel_Alpha: return "alpha"; + case heif_channel_interleaved: return "interleaved"; + case heif_channel_filter_array: return "filter_array"; + case heif_channel_depth: return "depth"; + case heif_channel_disparity: return "disparity"; + default: return "unknown"; + } +} + + 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) { + // An interleaved image carries its alpha inside the interleaved plane (the RGBA and RRGGBBAA + // formats). A separate alpha plane next to it would let the chroma format and the set of planes + // disagree about whether the image has alpha. The interleaved encoders of the uncompressed codec + // took the component list from the chroma format but the alpha decision from the planes and read + // past the end of the component list (GHSA-qfj5-c4pq-q998). Callers that want alpha have to use + // an interleaved format with alpha instead. + + if (channel == heif_channel_Alpha && num_interleaved_components_per_plane(m_chroma) > 1) { + return {heif_error_Usage_error, + heif_suberror_Unspecified, + "Cannot add a separate alpha plane to an image with an interleaved chroma format. " + "Use an interleaved format with alpha (e.g. heif_chroma_interleaved_RGBA) instead."}; + } + + // A Cb/Cr plane must have exactly the chroma-subsampled size of the logical image + // (round_up), the invariant has_standard_plane_sizes() and check_plane_layout() + // enforce elsewhere. Building one at a different size makes the plane's actual + // allocation disagree with the size that code derives from the logical image + // dimensions, and that mismatch is a memory-safety hazard: an oversized plane + // fooled the per-row fill in extend_to_size_with_zero() into an unsigned underflow + // and a ~4 GB out-of-bounds write (GHSA-j2rv-58fh-w8pw), an undersized one caused + // out-of-bounds reads during RGB conversion (issue #1796). Reject the mismatch at + // construction so the inconsistent image cannot be built in the first place. Only + // Cb/Cr are constrained; alpha, depth, disparity and other auxiliary planes may + // legitimately have a size that differs from the colour planes. + if (channel == heif_channel_Cb || channel == heif_channel_Cr) { + uint32_t expected_width, expected_height; + get_subsampled_size(m_width, m_height, channel, m_chroma, &expected_width, &expected_height); + + if (width != expected_width || height != expected_height) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "A Cb/Cr plane must have the chroma-subsampled size of the image."}; + } + } + // for backwards compatibility, allow for 24/32 bits for RGB/RGBA interleaved chromas if (m_chroma == heif_chroma_interleaved_RGB && bit_depth == 24) { @@ -360,15 +418,25 @@ // 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. + // per component while readers/writers access the samples as 16-bit values. A bit depth + // above 16 is no RRGGBB(AA) format either: the components would not be 16-bit words. 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) { + (bit_depth <= 8 || bit_depth > 16)) { return {heif_error_Usage_error, - heif_suberror_Unspecified, - "Cannot create a 16-bit interleaved channel with a bit depth of 8 or less"}; + heif_suberror_Invalid_parameter_value, + "The interleaved RRGGBB formats require a bit depth of 9 to 16"}; + } + + // The interleaved RGB and RGBA formats have 8 bits per component. + if ((m_chroma == heif_chroma_interleaved_RGB || + m_chroma == heif_chroma_interleaved_RGBA) && + bit_depth != 8) { + return {heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "The interleaved RGB and RGBA formats require a bit depth of 8"}; } int num_interleaved_pixels = num_interleaved_components_per_plane(m_chroma); @@ -432,12 +500,7 @@ 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; + int bytes_per_component = bytes_per_sample_for_bit_depth(bit_depth); // 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 @@ -629,6 +692,43 @@ Error HeifPixelImage::extend_to_size_with_zero(uint32_t width, uint32_t height, const heif_security_limits* limits) { + // This function only ever grows the image. A target smaller than the current + // size is out of contract: the per-row right-edge fill below computes its + // memset length as (subsampled_width - old_width), which would underflow to a + // huge value on a shrink request and write past the end of the pixel plane. + // Reject it up front (GHSA-hqc2-cx5m-g6ff). + if (width < m_width || height < m_height) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot extend an image to a size smaller than its current size."}; + } + + // Nothing to do when the target already matches the current size. + if (width == m_width && height == m_height) { + return Error::Ok; + } + + // Preflight: no plane may shrink. Even when the logical target is not smaller + // than the image, get_subsampled_size() can map it to a per-component size that + // is smaller than a plane's current size, e.g. a Cb/Cr plane that was added + // larger than the target's subsampled chroma extent. The per-row right-edge fill + // below would then compute its length as (subsampled_width - old_width), underflow + // uint32_t, and write past the end of the plane. Validate every plane before + // touching any so a rejected request cannot leave the image partially modified + // (GHSA-j2rv-58fh-w8pw, a follow-up to GHSA-hqc2-cx5m-g6ff whose logical-size + // guard above does not cover an individually oversized chroma plane). + for (const auto& component : m_storage) { + uint32_t subsampled_width, subsampled_height; + get_subsampled_size(width, height, component.m_channel, m_chroma, + &subsampled_width, &subsampled_height); + + if (subsampled_width < component.m_width || subsampled_height < component.m_height) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Cannot extend an image to a size smaller than an existing plane."}; + } + } + 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 @@ -876,6 +976,218 @@ } +Error HeifPixelImage::check_plane_layout() const +{ + std::vector colour_planes; + bool separate_alpha_allowed = true; + + auto layout_error = [this](const std::string& what) { + std::stringstream sstr; + sstr << what << " (colorspace " << static_cast(m_colorspace) + << ", chroma " << static_cast(m_chroma) << ")"; + return Error{heif_error_Usage_error, heif_suberror_Invalid_parameter_value, sstr.str()}; + }; + + switch (m_colorspace) { + case heif_colorspace_RGB: + switch (m_chroma) { + case heif_chroma_444: + colour_planes = {heif_channel_R, heif_channel_G, heif_channel_B}; + break; + case heif_chroma_interleaved_RGB: + case heif_chroma_interleaved_RGBA: + case heif_chroma_interleaved_RRGGBB_BE: + case heif_chroma_interleaved_RRGGBB_LE: + case heif_chroma_interleaved_RRGGBBAA_BE: + case heif_chroma_interleaved_RRGGBBAA_LE: + colour_planes = {heif_channel_interleaved}; + separate_alpha_allowed = false; // alpha, if any, is inside the interleaved plane + break; + default: + return layout_error("Chroma format is not valid for an RGB image"); + } + break; + + case heif_colorspace_YCbCr: + switch (m_chroma) { + case heif_chroma_444: + case heif_chroma_422: + case heif_chroma_420: + colour_planes = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}; + break; + case heif_chroma_monochrome: + colour_planes = {heif_channel_Y}; + break; + default: + return layout_error("Chroma format is not valid for a YCbCr image"); + } + break; + + case heif_colorspace_monochrome: + if (m_chroma != heif_chroma_monochrome) { + return layout_error("Chroma format is not valid for a monochrome image"); + } + colour_planes = {heif_channel_Y}; + break; + + case heif_colorspace_filter_array: + if (m_chroma != heif_chroma_planar) { + return layout_error("Chroma format is not valid for a filter-array image"); + } + colour_planes = {heif_channel_filter_array}; + // A filter array with an alpha plane is representable in 'unci', but nothing converts it + // yet. The uncompressed decoder only recognizes the filter-array component on its own; a + // decoded image has both planes when a filter-array item has an alpha auxiliary image. + // Op_bayer_bilinear_to_RGB24_32 carries no alpha, and Op_drop_alpha_plane and + // Op_adjust_alpha_bit_depth decline filter arrays (they only copy the Y/Cb/Cr/R/G/B + // planes). Once those handle it, this may be allowed. + separate_alpha_allowed = false; + break; + + default: + return layout_error("Colorspace has no defined plane layout"); + } + + // Every known plane has to belong to the layout, appear once, and have the size of its channel. + + std::set seen; + + for (const auto& component : m_storage) { + heif_channel channel = component.m_channel; + + if (channel == heif_channel_unknown) { + continue; // multi-component data without colour meaning, tolerated and ignored + } + + bool belongs = (channel == heif_channel_Alpha && separate_alpha_allowed); + for (heif_channel c : colour_planes) { + if (c == channel) { + belongs = true; + } + } + + if (!belongs) { + return layout_error(std::string("Image has a ") + channel_name(channel) + " plane that does not belong to its format"); + } + + if (!seen.insert(channel).second) { + return layout_error(std::string("Image has more than one ") + channel_name(channel) + " plane"); + } + + uint32_t expected_w = channel_width(m_width, m_chroma, channel); + uint32_t expected_h = channel_height(m_height, m_chroma, channel); + if (component.m_width != expected_w || component.m_height != expected_h) { + std::stringstream sstr; + sstr << "The " << channel_name(channel) << " plane has size " << component.m_width << "x" << component.m_height + << ", expected " << expected_w << "x" << expected_h; + return layout_error(sstr.str()); + } + } + + for (heif_channel channel : colour_planes) { + if (seen.count(channel) == 0) { + return layout_error(std::string("Image has no ") + channel_name(channel) + " plane"); + } + } + + return Error::Ok; +} + + +// Returns whether any sample of the plane has one of the bits in 'invalid_bits' set. +template +static bool plane_has_sample_with_bits(const void* mem, size_t stride, size_t samples_per_row, + uint32_t height, T invalid_bits) +{ + for (uint32_t y = 0; y < height; y++) { + const T* row = reinterpret_cast(static_cast(mem) + y * stride); + for (size_t x = 0; x < samples_per_row; x++) { + if (row[x] & invalid_bits) { + return true; + } + } + } + + return false; +} + + +Error HeifPixelImage::check_sample_value_ranges() const +{ + // The interleaved RRGGBB formats store their samples with a fixed byte order, which can + // differ from the byte order of the machine. + bool swapped_byte_order = false; + + if (m_chroma == heif_chroma_interleaved_RRGGBB_BE || + m_chroma == heif_chroma_interleaved_RRGGBBAA_BE) { + swapped_byte_order = (std::endian::native != std::endian::big); + } + else if (m_chroma == heif_chroma_interleaved_RRGGBB_LE || + m_chroma == heif_chroma_interleaved_RRGGBBAA_LE) { + swapped_byte_order = (std::endian::native != std::endian::little); + } + + for (const auto& component : m_storage) { + if (component.m_datatype != heif_component_datatype_unsigned_integer) { + continue; + } + + const int bit_depth = component.m_bit_depth; + const int bytes_per_sample = bytes_per_sample_for_bit_depth(bit_depth); + if (bit_depth == 8 * bytes_per_sample) { + continue; // every value of the storage word is a valid sample + } + + const size_t samples_per_row = static_cast(component.m_width) * component.m_num_interleaved_components; + + // The largest valid value is 2^bit_depth - 1, so a sample is out of range exactly when + // one of the bits above the bit depth is set. + bool out_of_range = false; + + switch (bytes_per_sample) { + case 1: { + auto invalid_bits = static_cast(0xFFu << bit_depth); + out_of_range = plane_has_sample_with_bits(component.mem, component.stride, samples_per_row, + component.m_height, invalid_bits); + break; + } + case 2: { + auto invalid_bits = static_cast(0xFFFFu << bit_depth); + if (swapped_byte_order && component.m_channel == heif_channel_interleaved) { + invalid_bits = static_cast((invalid_bits >> 8) | (invalid_bits << 8)); + } + out_of_range = plane_has_sample_with_bits(component.mem, component.stride, samples_per_row, + component.m_height, invalid_bits); + break; + } + case 4: { + uint32_t invalid_bits = 0xFFFFFFFFu << bit_depth; + out_of_range = plane_has_sample_with_bits(component.mem, component.stride, samples_per_row, + component.m_height, invalid_bits); + break; + } + case 8: { + uint64_t invalid_bits = ~uint64_t{0} << bit_depth; + out_of_range = plane_has_sample_with_bits(component.mem, component.stride, samples_per_row, + component.m_height, invalid_bits); + break; + } + default: + break; + } + + if (out_of_range) { + std::stringstream sstr; + sstr << "The " << channel_name(component.m_channel) << " plane contains sample values that exceed its bit depth of " + << bit_depth << " bits"; + return Error{heif_error_Usage_error, heif_suberror_Invalid_parameter_value, sstr.str()}; + } + } + + return Error::Ok; +} + + std::set HeifPixelImage::get_channel_set() const { std::set channels; @@ -898,9 +1210,11 @@ return 0; } + // The widest pixels are a 128-bit complex sample and four interleaved 16-bit components. + // add_channel() does not let an interleaved plane have wider components. uint32_t bpp = comp->get_bytes_per_pixel() * 8; assert(bpp <= 256); - return static_cast(bpp); + return static_cast(bpp); } @@ -1055,50 +1369,64 @@ } -Error HeifPixelImage::fill_new_channel(heif_channel dst_channel, uint16_t value, int width, int height, int bpp, +Error HeifPixelImage::fill_new_channel(heif_channel dst_channel, uint64_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; + return fill_channel(dst_channel, value); } -void HeifPixelImage::fill_channel(heif_channel dst_channel, uint16_t value) +template +void HeifPixelImage::ComponentStorage::fill(T value) { - int num_interleaved = num_interleaved_components_per_plane(m_chroma); + size_t samples_per_row = static_cast(m_width) * m_num_interleaved_components; + + for (uint32_t y = 0; y < m_height; y++) { + T* row = reinterpret_cast(static_cast(mem) + y * stride); + std::fill_n(row, samples_per_row, value); + } +} - 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); - } +Error HeifPixelImage::fill_channel(heif_channel dst_channel, uint64_t value) +{ + ComponentStorage* component = find_storage_for_channel(dst_channel); + if (!component) { + return Error{heif_error_Usage_error, + heif_suberror_Nonexisting_image_channel_referenced, + "Cannot fill a plane that the image does not have"}; } - 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; - } + + // The value is written with the width of the plane's storage word. A plane with a bit + // depth in between, like 24 or 48 bits, is stored in the next larger word (32 or 64 bits). + + switch (bytes_per_sample_for_bit_depth(component->m_bit_depth)) { + case 1: + component->fill(static_cast(value)); + break; + case 2: + component->fill(static_cast(value)); + break; + case 4: + component->fill(static_cast(value)); + break; + case 8: + component->fill(value); + break; + default: { + std::stringstream sstr; + sstr << "Cannot fill planes with " << component->m_bit_depth << " bits per sample"; + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + sstr.str()}; } } + + return Error::Ok; } @@ -1670,17 +1998,12 @@ ComponentStorage& plane = *comp; - if (plane.m_bit_depth != 8) { + if (plane.m_bit_depth > 16 || plane.m_datatype != heif_component_datatype_unsigned_integer) { return {heif_error_Unsupported_feature, heif_suberror_Unspecified, - "Can currently only fill images with 8 bits per pixel"}; + "Can only fill images with unsigned samples of up to 16 bits"}; } - 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: @@ -1702,23 +2025,14 @@ assert(false); } - auto val8 = static_cast(val16 >> 8U); - + // The values are given with 16 bits. Reduce them to the bit depth of the plane. + auto value = static_cast(val16 >> (16 - plane.m_bit_depth)); - // 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); + if (plane.m_bit_depth <= 8) { + plane.fill(static_cast(value)); } else { - // fill line by line - auto* p = data; - - for (size_t y=0;y(value); } } @@ -1726,15 +2040,17 @@ } -uint32_t negate_negative_int32(int32_t x) -{ - assert(x <= 0); - - if (x == INT32_MIN) { - return static_cast(INT32_MAX) + 1; - } - else { - return static_cast(-x); +// Blends one row of the overlay into the canvas: out = in * alpha + out * (1 - alpha). +template +static void blend_row(T* out, const T* in, const A* alpha, uint32_t n, uint32_t alpha_max) +{ + for (uint32_t x = 0; x < n; x++) { + // An alpha sample above its bit depth would make (alpha_max - a) wrap around. + uint32_t a = std::min(alpha[x], alpha_max); + + // in and out are at most 65535 and the two weights add up to alpha_max (at most 65535), + // so the sum fits into 32 bits. + out[x] = static_cast((uint32_t{in[x]} * a + uint32_t{out[x]} * (alpha_max - a)) / alpha_max); } } @@ -1764,19 +2080,48 @@ // 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. + // All planes of the overlay therefore have to have the same size. A HeifPixelImage + // does not guarantee that: its planes can have any size. Comparing only the Alpha + // plane with the size of the image was not enough, since the color planes can be + // larger than the image, and the loop then read past the end of the Alpha plane. + // check_plane_layout() checks all planes against the size of the image. // 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())) { + if (Error err = overlay->check_plane_layout()) { return {heif_error_Unsupported_feature, heif_suberror_Unspecified, - "Overlay image Alpha plane size does not match the other color planes"}; + "Cannot overlay image: " + err.message}; + } + + // The planes are composed sample by sample, with 8-bit or 16-bit samples. A plane of the + // overlay and the plane of the canvas it is drawn into have to have the same bit depth. + // (They used to be composed byte by byte whatever their bit depth, so that the two bytes + // of a 16-bit sample were blended as if they were two pixels.) + for (heif_channel channel : channels) { + const bool used = (channel == heif_channel_Alpha) || has_channel(channel); + if (!used) { + continue; + } + + if (overlay->get_bits_per_pixel(channel) > 16 || + overlay->get_datatype(channel) != heif_component_datatype_unsigned_integer) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Overlaying images is only implemented for unsigned samples of up to 16 bits"}; + } + + if (has_channel(channel) && + (get_bits_per_pixel(channel) != overlay->get_bits_per_pixel(channel) || + get_datatype(channel) != heif_component_datatype_unsigned_integer)) { + return {heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Overlaying an image onto an image with another bit depth is not implemented"}; + } } + const int alpha_bytes = has_alpha ? bytes_per_sample_for_bit_depth(overlay->get_bits_per_pixel(heif_channel_Alpha)) : 0; + const uint32_t alpha_max = has_alpha ? ((1U << overlay->get_bits_per_pixel(heif_channel_Alpha)) - 1) : 0; + size_t alpha_stride = 0; uint8_t* alpha_p; alpha_p = overlay->get_channel_memory(heif_channel_Alpha, &alpha_stride); @@ -1802,96 +2147,66 @@ uint32_t out_h = get_height(channel); - // --- check whether overlay image overlaps with current image + // --- compute the overlapping area + // + // The overlay covers [dx, dx+in_w) x [dy, dy+in_h) in canvas coordinates and + // may start outside the canvas on any side (ISO/IEC 23008-12 6.6.2.2.3 allows + // negative offsets; pixels outside the canvas are simply not shown). Intersect + // it with the canvas [0, out_w) x [0, out_h). All terms fit into int64_t, so + // this cannot overflow for int32 offsets and uint32 sizes. The copy region is + // then described by its size and by its top-left corner in both images, which + // keeps the loop below free of any "end coordinate vs. count" ambiguity. // 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; - } + const int64_t x0 = std::max(dx, 0); + const int64_t y0 = std::max(dy, 0); + const int64_t x1 = std::min(static_cast(dx) + in_w, out_w); + const int64_t y1 = std::min(static_cast(dy) + in_h, out_h); - if (dy > 0 && static_cast(dy) >= out_h) { - // the overlay image is completely outside the bottom border -> skip overlaying + if (x1 <= x0 || y1 <= y0) { + // the overlay image is completely outside the canvas -> nothing to draw 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 + const uint32_t copy_w = static_cast(x1 - x0); + const uint32_t copy_h = static_cast(y1 - y0); - // 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-left corner of the copied region in the canvas (out_*) and in the overlay (in_*) + const uint32_t out_x0 = static_cast(x0); + const uint32_t out_y0 = static_cast(y0); + const uint32_t in_x0 = static_cast(x0 - dx); + const uint32_t in_y0 = static_cast(y0 - dy); + + // --- composite the overlay in the overlapping area + + const int bytes = bytes_per_sample_for_bit_depth(overlay->get_bits_per_pixel(channel)); // 1 or 2, checked above + + for (uint32_t y = 0; y < copy_h; y++) { + const uint8_t* in_row = in_p + static_cast(in_x0) * bytes + static_cast(in_y0 + y) * in_stride; + uint8_t* out_row = out_p + static_cast(out_x0) * bytes + static_cast(out_y0 + y) * out_stride; - // 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); - } + if (!has_alpha) { + memcpy(out_row, in_row, static_cast(copy_w) * bytes); + continue; + } - // --- computer overlay in overlapping area + const uint8_t* alpha_row = alpha_p + static_cast(in_x0) * alpha_bytes + static_cast(in_y0 + y) * alpha_stride; - 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); + if (bytes == 1 && alpha_bytes == 1) { + blend_row(out_row, in_row, alpha_row, copy_w, alpha_max); + } + else if (bytes == 1) { + blend_row(out_row, in_row, reinterpret_cast(alpha_row), copy_w, alpha_max); + } + else if (alpha_bytes == 1) { + blend_row(reinterpret_cast(out_row), reinterpret_cast(in_row), + alpha_row, copy_w, alpha_max); } 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); - } + blend_row(reinterpret_cast(out_row), reinterpret_cast(in_row), + reinterpret_cast(alpha_row), copy_w, alpha_max); } } } diff -Nru libheif-1.23.4/libheif/image/pixelimage.h libheif-1.23.6/libheif/image/pixelimage.h --- libheif-1.23.4/libheif/image/pixelimage.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image/pixelimage.h 2026-10-05 16:16:00.000000000 +0000 @@ -57,6 +57,35 @@ +// Number of bytes HeifPixelImage uses to store one sample of the given bit depth +// (1, 2, 4, 8 or 16). This is the single source of truth for the sample width: plane +// allocation uses it, and so does every code path that reinterprets plane memory as +// uint8_t/uint16_t samples (color conversion in particular), so that they cannot drift. +// +// A bit depth of 0 is what get_bits_per_pixel() reports for a plane that does not exist. +// It maps to 0 bytes, so that a missing plane can never pass for a valid one-byte plane +// at the call sites that compare the width against sizeof(uint8_t). +inline int bytes_per_sample_for_bit_depth(int bit_depth) +{ + if (bit_depth <= 0) { + return 0; + } + if (bit_depth <= 8) { + return 1; + } + if (bit_depth <= 16) { + return 2; + } + if (bit_depth <= 32) { + return 4; + } + if (bit_depth <= 64) { + return 8; + } + return 16; +} + + class HeifPixelImage : public std::enable_shared_from_this, public ImageDescription, public ErrorBuffer @@ -119,6 +148,32 @@ // undersized single plane. bool has_standard_plane_sizes() const; + // Checks that the stored planes are exactly those of the image's colorspace and chroma + // format: the colour planes of that layout (R/G/B, Y/Cb/Cr, Y alone, the interleaved plane, + // or the filter array), optionally one separate alpha plane on the planar layouts, each + // present exactly once and with the size its channel implies (chroma-subsampled for Cb/Cr, + // the image size otherwise). Planes with channel heif_channel_unknown carry multi-component + // data without a colour meaning (e.g. the padding components of 'unci'); they are tolerated + // and ignored. A custom colorspace has no defined layout and is rejected; a caller that + // accepts such images (the uncompressed encoder) has to test for it first. + // + // This is a check at the entry of the color conversion pipeline and of the encoders, not a + // constraint on HeifPixelImage itself: an image may hold any set of planes, including several + // of the same channel (multi-spectral images consist of several monochrome planes). + // Returns a Usage_error naming the offending plane. + Error check_plane_layout() const; + + // Checks that no sample exceeds the value range of its plane's bit depth. A plane stores + // its samples in whole bytes (e.g. 10-bit samples in 16-bit words), so the memory can hold + // larger values than the bit depth allows, and the bit depth is only a promise of whoever + // filled the plane. Code that derives table sizes or bit counts from the bit depth (third + // party encoders, libsharpyuv) relies on that promise. Only planes with unsigned integer + // samples are checked. + // Like check_plane_layout(), this is a check for the places where pixel data enters from the + // outside, not something HeifPixelImage enforces by itself. + // Returns a Usage_error naming the offending plane. + Error check_sample_value_ranges() const; + heif_chroma get_chroma_format() const { return m_chroma; } heif_colorspace get_colorspace() const { return m_colorspace; } @@ -276,9 +331,14 @@ 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); + // Sets all samples of the plane to 'value'. The plane may have up to 64 bits per sample; + // the value is written with the width of the plane's storage word (8, 16, 32 or 64 bits). + // The datatype of the plane is not considered: the caller has to pass a value that is + // meaningful for it. + // Returns an error if there is no such plane or if it has more than 64 bits per sample. + Error fill_channel(heif_channel dst_channel, uint64_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 fill_new_channel(heif_channel dst_channel, uint64_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, @@ -365,6 +425,8 @@ template void mirror_inplace(heif_transform_mirror_direction); + template void fill(T value); + template void rotate_ccw(int angle_degrees, ComponentStorage& out_plane) const; diff -Nru libheif-1.23.4/libheif/image-items/avc.cc libheif-1.23.6/libheif/image-items/avc.cc --- libheif-1.23.4/libheif/image-items/avc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/avc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -44,7 +44,7 @@ Result> ImageItem_AVC::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } diff -Nru libheif-1.23.4/libheif/image-items/avif.cc libheif-1.23.6/libheif/image-items/avif.cc --- libheif-1.23.4/libheif/image-items/avif.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/avif.cc 2026-10-05 16:16:00.000000000 +0000 @@ -71,13 +71,18 @@ Result> ImageItem_AVIF::read_bitstream_configuration_data() const { - return m_decoder->read_bitstream_configuration_data(); + auto decoderResult = get_decoder(); + if (!decoderResult) { + return decoderResult.error(); + } + + return (*decoderResult)->read_bitstream_configuration_data(); } Result> ImageItem_AVIF::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } diff -Nru libheif-1.23.4/libheif/image-items/grid.cc libheif-1.23.6/libheif/image-items/grid.cc --- libheif-1.23.4/libheif/image-items/grid.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/grid.cc 2026-10-05 16:16:00.000000000 +0000 @@ -21,6 +21,7 @@ #include "grid.h" #include "context.h" #include "file.h" +#include #include #include #include @@ -539,6 +540,11 @@ tile_img = *decodeResult; + // All tiles have to have the same format. The first decoded tile defines it. + if (Error err = check_tile_format(*tile_img)) { + return err; + } + uint32_t w = get_grid_spec().get_width(); uint32_t h = get_grid_spec().get_height(); @@ -561,13 +567,38 @@ } // Fill alpha plane with opaque in case not all tiles have alpha planes + // + // The alpha plane is cloned from the tile, and an 'unci' tile can have an alpha component + // of up to 128 bits and with signed, float or complex samples. The opaque value used to + // be computed as (1UL << alpha_bpp) - 1 for a 16-bit fill, guarded only by an assertion + // that the depth is at most 16, which aborted the process when it was compiled in + // (GHSA-3gwx-cjv4-jr74). if (grid_image->has_channel(heif_channel_Alpha)) { uint16_t alpha_bpp = grid_image->get_bits_per_pixel(heif_channel_Alpha); - assert(alpha_bpp <= 16); - auto alpha_default_value = static_cast((1UL << alpha_bpp) - 1UL); - grid_image->fill_channel(heif_channel_Alpha, alpha_default_value); + if (alpha_bpp <= 64 && + grid_image->get_datatype(heif_channel_Alpha) == heif_component_datatype_unsigned_integer) { + uint64_t alpha_default_value = (alpha_bpp == 64) ? UINT64_MAX : ((uint64_t{1} << alpha_bpp) - 1); + + if (Error fill_err = grid_image->fill_channel(heif_channel_Alpha, alpha_default_value)) { + return fill_err; + } + } + else { + // There is no opaque value that we could fill in for this kind of plane. It stays + // zero, which is what the new image is initialized with. +#if ENABLE_PARALLEL_TILE_DECODING + std::lock_guard warningsLock(warningsMutex); +#endif + warnings->emplace_back( + heif_error_Unsupported_feature, + heif_suberror_Unsupported_data_version, + "The alpha plane of the grid image has a sample type without a defined opaque value " + "(signed, float or complex samples, or more than 64 bits). " + "It is zero where a tile has no alpha plane." + ); + } } grid_image->copy_metadata_from(*tile_img); @@ -586,8 +617,19 @@ "Image tile has different chroma format than combined image"}; } + // The grid may have more rows or columns of tiles than the output image needs. A tile + // that lies completely outside of the image has nothing to contribute. + if (x0 >= inout_image->get_width() || y0 >= inout_image->get_height()) { + return progress_and_return_ok(options, progress_counter); + } - inout_image->copy_image_to(tile_img, x0, y0); + // copy_image_to() refuses a tile whose planes have another bit depth than the canvas. + // Its error used to be discarded: such a tile was left out, its area stayed zero, and + // decoding reported success (GHSA-vv35-6hxg-95x8). check_tile_format() above finds such + // a tile already. + if (Error err = inout_image->copy_image_to(tile_img, x0, y0)) { + return err; + } return progress_and_return_ok(options, progress_counter); } @@ -615,7 +657,78 @@ return error; } - return tile_item->decode_compressed_image(options, false, 0, 0, std::move(decode_state)); + // Decode the tile like the full grid decoding does. This includes the transformations + // of the tile image and its alpha image. With decode_compressed_image(), a tile that + // has an alpha image of its own would be decoded without alpha channel. + auto tileResult = tile_item->decode_image(options, false, 0, 0, std::move(decode_state)); + if (!tileResult) { + return tileResult; + } + + // A single tile is handed out as it is. It has to have the format of the other tiles, + // like the tiles that are copied into the canvas when the whole grid is decoded. + if (Error error = check_tile_format(**tileResult)) { + return error; + } + + return tileResult; +} + + +Error ImageItem_Grid::check_tile_format(const HeifPixelImage& tile_img) const +{ + std::lock_guard lock(m_reference_tile_format_mutex); + + if (!m_reference_tile_format) { + m_reference_tile_format = TileFormat{tile_img.get_colorspace(), + tile_img.get_chroma_format(), + static_cast(tile_img)}; + return Error::Ok; + } + + const TileFormat& reference = *m_reference_tile_format; + + if (tile_img.get_colorspace() != reference.colorspace || + tile_img.get_chroma_format() != reference.chroma) { + return {heif_error_Invalid_input, + heif_suberror_Wrong_tile_image_chroma_format, + "Image tile has different colorspace or chroma format than the other tiles of the grid"}; + } + + // The tiles have to consist of the same components in the same order. This also holds + // for 'unci' tiles, which can have any number of components of any type. The size of the + // components is not compared here: the tile sizes are checked on their own. + + const std::vector& reference_components = reference.description.get_component_descriptions(); + const std::vector& components = tile_img.get_component_descriptions(); + + if (components.size() != reference_components.size()) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_grid_data, + "Image tile has a different number of components than the other tiles of the grid"}; + } + + for (size_t i = 0; i < components.size(); i++) { + const ComponentDescription& c = components[i]; + const ComponentDescription& ref = reference_components[i]; + + if (c.channel != ref.channel || + c.component_type != ref.component_type || + c.datatype != ref.datatype || + c.has_data_plane != ref.has_data_plane) { + return {heif_error_Invalid_input, + heif_suberror_Invalid_grid_data, + "Image tile has different components than the other tiles of the grid"}; + } + + if (c.bit_depth != ref.bit_depth) { + return {heif_error_Invalid_input, + heif_suberror_Wrong_tile_image_pixel_depth, + "Image tile has different bit depth than the other tiles of the grid"}; + } + } + + return Error::Ok; } @@ -826,6 +939,11 @@ const std::shared_ptr& image, heif_encoder* encoder) { + // TODO(v1.24.x): return an error when the tile does not have the format of the tiles that + // were added before (colorspace, chroma format, bit depths and components, e.g. an alpha + // plane that only some of the tiles have). Such a grid is still written here, but the + // decoder refuses it (check_tile_format()). + auto encodingResult = get_context()->encode_image(image, encoder, *m_tile_encoding_options, @@ -886,6 +1004,9 @@ { std::shared_ptr griditem; + // TODO(v1.24.x): return an error when the tiles do not all have the same format (see + // add_image_tile()). + // Create ImageGrid ImageGrid grid; @@ -969,6 +1090,19 @@ return griditem; } +bool ImageItem_Grid::is_coded_in_miaf_profile() const +{ + for (heif_item_id child_id : m_grid_tile_ids) { + auto child = get_context()->get_image(child_id, false); + if (child && !child->is_coded_in_miaf_profile()) { + return false; + } + } + + return true; +} + + heif_brand2 ImageItem_Grid::get_compatible_brand() const { if (m_grid_tile_ids.empty()) { return 0; } diff -Nru libheif-1.23.4/libheif/image-items/grid.h libheif-1.23.6/libheif/image-items/grid.h --- libheif-1.23.4/libheif/image-items/grid.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/grid.h 2026-10-05 16:16:00.000000000 +0000 @@ -22,9 +22,12 @@ #define LIBHEIF_IMAGEITEM_GRID_H #include "image_item.h" +#include "image/image_description.h" #include #include #include +#include +#include #include @@ -138,6 +141,8 @@ heif_brand2 get_compatible_brand() const override; + bool is_coded_in_miaf_profile() const override; + protected: Result> get_decoder() const override; @@ -161,6 +166,51 @@ ImageGrid m_grid_spec; std::vector m_grid_tile_ids; + // --- the format that all tiles of the grid have to share + + // All tiles of a grid have to decode to images of the same format. The first tile that + // is decoded (in whatever order, and through whichever decoding interface) stores its + // description here, and every tile that is decoded afterwards is compared with it. + // + // TODO: move the colorspace and the chroma format into ImageDescription. + // They are members of HeifPixelImage only, although they are the top level of the + // structure that ImageDescription describes: without them, a list of components does + // not tell whether the components are subsampled or interleaved. That is why this + // struct has to carry them next to the description. With both in ImageDescription, + // TileFormat becomes a plain ImageDescription, and check_tile_format() becomes a + // function of ImageDescription that compares two descriptions for having the same + // format. That function can then also compare a decoded image with what its image item + // advertises, of which ImageItem::check_decoded_image_bit_depth() covers only the luma + // and chroma bit depths today (not, e.g., a configuration box that claims another + // chroma format than the bitstream has). + // Things to take care of: + // - ImageDescription::copy_metadata_from() must not copy them. It copies the metadata + // (colour profile, light levels, ...), but not the component list, because the + // target image can have another layout, e.g. after a colour conversion. Colorspace + // and chroma format belong to that structural part. + // - An image item knows two colorspaces: the coded one, and the preferred decoding + // colorspace, which proposes RGB for matrix_coefficients=0 while the decoded image is + // still tagged as YCbCr. The description has to hold the coded one, otherwise it + // does not match the decoded image. + // - Every implementation of populate_component_descriptions() has to set them: + // ImageItem, ImageItem_iden, ImageItem_Grid, ImageItem_uncompressed, ImageItem_Tiled + // and ImageItem_Overlay. Where the format is not known, they stay undefined, and a + // comparison has to treat that as unknown, not as a mismatch. + // - In HeifPixelImage, the two values are tied to the planes of the image and are set + // in create() only. They should not become freely settable through the base class. + struct TileFormat + { + heif_colorspace colorspace = heif_colorspace_undefined; + heif_chroma chroma = heif_chroma_undefined; + ImageDescription description; + }; + + mutable std::mutex m_reference_tile_format_mutex; // tiles are decoded in parallel + mutable std::optional m_reference_tile_format; + + // Stores the format of the first decoded tile, or compares the tile with the stored format. + Error check_tile_format(const HeifPixelImage& tile_img) const; + heif_orientation m_grid_orientation = heif_orientation_normal; heif_encoding_options* m_tile_encoding_options = nullptr; diff -Nru libheif-1.23.4/libheif/image-items/hevc.cc libheif-1.23.6/libheif/image-items/hevc.cc --- libheif-1.23.4/libheif/image-items/hevc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/hevc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -94,21 +94,40 @@ return heif_brand2_heix; } - // TODO: what brand should we use for this case? + // There is no brand for the other profiles of HEVC, like the high throughput profiles or + // the screen content coding profiles: 'heix' is limited to the Main 10 profile and the + // format range extensions profiles (ISO/IEC 23008-12, B.4.1.1). The file does not claim + // a brand for this image, it is a plain 'mif1' file. - return heif_brand2_heix; + return 0; +} + + +bool ImageItem_HEVC::is_coded_in_miaf_profile() const +{ + auto hvcC = get_property(); + if (!hvcC) { + return false; + } + + return hvcC->get_configuration().is_miaf_profile(); } Result> ImageItem_HEVC::read_bitstream_configuration_data() const { - return m_decoder->read_bitstream_configuration_data(); + auto decoderResult = get_decoder(); + if (!decoderResult) { + return decoderResult.error(); + } + + return (*decoderResult)->read_bitstream_configuration_data(); } Result> ImageItem_HEVC::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } @@ -169,36 +188,41 @@ first = false; } else { - std::vector nal_data; - size_t length = start_code_start - (prev_start_code_start + 3); - - nal_data.resize(length); - assert(prev_start_code_start >= 0); - memcpy(nal_data.data(), data.data() + prev_start_code_start + 3, length); - - int nal_type = (nal_data[0] >> 1); - - switch (nal_type) { - case 0x20: - case 0x21: - case 0x22: - hvcC->append_nal_data(nal_data); - break; - - default: { - std::vector nal_data_with_size; - nal_data_with_size.resize(nal_data.size() + 4); - - memcpy(nal_data_with_size.data() + 4, nal_data.data(), nal_data.size()); - nal_data_with_size[0] = ((nal_data.size() >> 24) & 0xFF); - nal_data_with_size[1] = ((nal_data.size() >> 16) & 0xFF); - nal_data_with_size[2] = ((nal_data.size() >> 8) & 0xFF); - nal_data_with_size[3] = ((nal_data.size() >> 0) & 0xFF); + size_t length = start_code_start - (prev_start_code_start + 3); - get_file()->append_iloc_data(get_id(), nal_data_with_size, 0); + // Skip empty NAL units (two consecutive start codes). Besides being invalid, an empty + // NAL unit would read nal_data[0] of an empty vector and pass NULL to memcpy(), which + // is UB even for size 0 (until C2y/N3322), see StreamReader_memory::read(). + if (length > 0) { + std::vector nal_data; + nal_data.resize(length); + + memcpy(nal_data.data(), data.data() + prev_start_code_start + 3, length); + + int nal_type = (nal_data[0] >> 1); + + switch (nal_type) { + case 0x20: + case 0x21: + case 0x22: + hvcC->append_nal_data(nal_data); + break; + + default: { + std::vector nal_data_with_size; + nal_data_with_size.resize(nal_data.size() + 4); + + memcpy(nal_data_with_size.data() + 4, nal_data.data(), nal_data.size()); + nal_data_with_size[0] = ((nal_data.size() >> 24) & 0xFF); + nal_data_with_size[1] = ((nal_data.size() >> 16) & 0xFF); + nal_data_with_size[2] = ((nal_data.size() >> 8) & 0xFF); + nal_data_with_size[3] = ((nal_data.size() >> 0) & 0xFF); + + get_file()->append_iloc_data(get_id(), nal_data_with_size, 0); + } + break; } - break; } } diff -Nru libheif-1.23.4/libheif/image-items/hevc.h libheif-1.23.6/libheif/image-items/hevc.h --- libheif-1.23.4/libheif/image-items/hevc.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/hevc.h 2026-10-05 16:16:00.000000000 +0000 @@ -52,6 +52,8 @@ heif_brand2 get_compatible_brand() const override; + bool is_coded_in_miaf_profile() const override; + // currently not used void set_preencoded_hevc_image(const std::vector& data); diff -Nru libheif-1.23.4/libheif/image-items/iden.cc libheif-1.23.6/libheif/image-items/iden.cc --- libheif-1.23.4/libheif/image-items/iden.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/iden.cc 2026-10-05 16:16:00.000000000 +0000 @@ -93,7 +93,9 @@ return error; } - return imgitem->decode_image(options, decode_tile_only, tile_x0, tile_y0, decode_state); + // An 'iden' image is exposed as a single tile, which is the whole image. Hence, always + // decode the whole image it is derived from, even when that image has tiles. + return imgitem->decode_image(options, false, 0, 0, decode_state); } diff -Nru libheif-1.23.4/libheif/image-items/image_item.cc libheif-1.23.6/libheif/image-items/image_item.cc --- libheif-1.23.4/libheif/image-items/image_item.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/image_item.cc 2026-10-05 16:16:00.000000000 +0000 @@ -480,8 +480,11 @@ void ImageItem::get_tile_size(uint32_t& w, uint32_t& h) const { - w = get_width(); - h = get_height(); + // Without tiles, the single tile is the whole coded image. Like in get_heif_image_tiling(), + // this is the size before the image transformations (which get_width() and get_height() + // include). The size is 0 when it is unknown. + w = get_ispe_width(); + h = get_ispe_height(); } @@ -507,6 +510,19 @@ } +Result> ImageItem::decoder_or_error(std::shared_ptr decoder) +{ + if (!decoder) { + return Error{heif_error_Usage_error, + heif_suberror_Unspecified, + "The image was not read from a file. An image that was added to the context by " + "encoding cannot be decoded and its coded format cannot be queried."}; + } + + return decoder; +} + + Error ImageItem::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const { auto decoderResult = get_decoder(); @@ -1036,41 +1052,47 @@ 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; + // Rank budget passed to this item's own 'dimg' inputs. A coded image is the + // leaf of the derivation chain and has no 'dimg' inputs to walk, but it may + // still carry an auxiliary image, which is checked below. + bool has_derivation_inputs = true; + int child_max_rank = MIAF_RANK_CODED; + bool child_parent_is_iden = false; 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 + has_derivation_inputs = false; // 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; + if (has_derivation_inputs) { + 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. + // chain must independently satisfy MIAF, so check it as a fresh chain. This + // applies to every item of the walk, including coded images (a plain coded + // primary image with an alpha auxiliary is the common case), since the + // auxiliary is decoded as part of this item's decode. if (auto alpha = item->get_alpha_channel()) { if (Error err = check_miaf_derivation_constraints(alpha.get(), MIAF_RANK_OVERLAY, /*parent_is_iden=*/false, verified)) { @@ -1121,7 +1143,8 @@ 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 + DecodeTraversalState decode_state, + bool decode_as_single_tile) const { // Check for cycles before taking m_decode_mutex: a derived item that // (transitively) references itself would otherwise re-enter decode_image() @@ -1161,9 +1184,28 @@ std::lock_guard lock(m_decode_mutex); + // --- check whether the image is exposed as a single tile + + // A tile of this image can only be combined with the same tile of the alpha image. + // When the alpha image has a different tiling, the image is exposed as a single tile, + // which is the whole image (see get_image_tiling_with_alpha()). + if (decode_tile_only && !decode_as_single_tile && has_alpha_with_different_tiling()) { + decode_as_single_tile = true; + } + + // Whether only a tile of the coded image is decoded. A single tile is the whole coded + // image. It is still processed like a tile, which means that it is not cropped. + const bool decode_coded_tile_only = (decode_tile_only && !decode_as_single_tile); + + if (decode_tile_only && decode_as_single_tile && (tile_x0 != 0 || tile_y0 != 0)) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Tile position is outside of the image tiling."}; + } + // --- check whether image size (according to 'ispe') exceeds maximum - if (!decode_tile_only) { + if (!decode_coded_tile_only) { auto ispe = get_property(); if (ispe) { Error err = check_for_valid_image_size(get_context()->get_security_limits(), ispe->get_width(), ispe->get_height()); @@ -1176,15 +1218,31 @@ // --- transform tile position - if (decode_tile_only && options.ignore_transformations == false) { - if (Error error = transform_requested_tile_position_to_original_tile_position(tile_x0, tile_y0)) { - return error; + // The alpha image gets the requested tile position and converts it according to + // its own transformations. + const uint32_t requested_tile_x0 = tile_x0; + const uint32_t requested_tile_y0 = tile_y0; + + if (decode_coded_tile_only) { + if (options.ignore_transformations == false) { + // This also checks that the tile position is within the tiling. + if (Error error = transform_requested_tile_position_to_original_tile_position(tile_x0, tile_y0)) { + return error; + } + } + else { + heif_image_tiling tiling = get_heif_image_tiling(); + if (tile_x0 >= tiling.num_columns || tile_y0 >= tiling.num_rows) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Tile position is outside of the image tiling."}; + } } } // --- decode image - Result> decodingResult = decode_compressed_image(options, decode_tile_only, tile_x0, tile_y0, decode_state); + Result> decodingResult = decode_compressed_image(options, decode_coded_tile_only, tile_x0, tile_y0, decode_state); if (!decodingResult) { return decodingResult.error(); } @@ -1198,7 +1256,13 @@ // --- 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)) { + if (Error err = check_decoded_image_size(*img, decode_coded_tile_only, tile_x0, tile_y0)) { + return err; + } + + // --- validate the decoded image against the signaled bit depth + + if (Error err = check_decoded_image_bit_depth(*img)) { return err; } @@ -1310,7 +1374,10 @@ // 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); + // The alpha image does its own conversion of the tile position. When this image is + // decoded as a single tile, the alpha image is decoded as a whole, too. + auto alphaDecodingResult = alpha_image->decode_image(options, decode_tile_only, requested_tile_x0, requested_tile_y0, + decode_state, decode_as_single_tile); if (!alphaDecodingResult) { return alphaDecodingResult.error(); } @@ -1588,6 +1655,55 @@ } +Error ImageItem::check_decoded_image_bit_depth(const HeifPixelImage& img) const +{ + // The bit depths that the image handle reports (heif_image_handle_get_luma_bits_per_pixel() + // and ..._chroma_...) are taken from the configuration of the item, e.g. the 'hvcC' box, + // or from the first tile of a grid. The decoded image gets its bit depth from the + // bitstream. A file can make the two disagree. An application that reads the planes of + // the decoded image with the bit depth it got from the handle would then read out of + // bounds, so the decoded image must not contradict the handle (GHSA-vv35-6hxg-95x8). + + int luma_bpp = get_luma_bits_per_pixel(); + int chroma_bpp = get_chroma_bits_per_pixel(); + + bool mismatch = false; + + // The handle reports a single luma bit depth. An 'unci' image can have several components + // that are mapped to the Y channel (e.g. several monochrome bands) with different bit + // depths. A single value cannot describe them (the handle reports the largest one), so the + // luma depth is not compared when there are several such components. + int num_luma_components = 0; + for (const ComponentDescription& component : img.get_component_descriptions()) { + if (component.channel == heif_channel_Y && component.has_data_plane) { + num_luma_components++; + } + } + + if (luma_bpp > 0 && num_luma_components <= 1 && img.has_channel(heif_channel_Y) && + img.get_bits_per_pixel(heif_channel_Y) != luma_bpp) { + mismatch = true; + } + + // The handle reports a single bit depth for both chroma planes. An 'unci' image can have + // Cb and Cr planes of different depths, which a single value cannot describe, so the + // chroma depth is only compared when both planes have the same. + if (chroma_bpp > 0 && img.has_channel(heif_channel_Cb) && img.has_channel(heif_channel_Cr) && + img.get_bits_per_pixel(heif_channel_Cb) == img.get_bits_per_pixel(heif_channel_Cr) && + img.get_bits_per_pixel(heif_channel_Cb) != chroma_bpp) { + mismatch = true; + } + + if (mismatch) { + return Error{heif_error_Invalid_input, + heif_suberror_Unspecified, + "Decoded image does not have the bit depth signaled in the file."}; + } + + return Error::Ok; +} + + heif_image_tiling ImageItem::get_heif_image_tiling() const { // --- Return a dummy tiling consisting of only a single tile for the whole image @@ -1627,6 +1743,45 @@ } return tiling; +} + + +bool ImageItem::has_alpha_with_different_tiling() const +{ + const std::shared_ptr& alpha_image = get_alpha_channel(); + if (!alpha_image || alpha_image->get_item_error()) { + return false; + } + + const heif_image_tiling tiling = get_heif_image_tiling(); + const heif_image_tiling alpha_tiling = alpha_image->get_heif_image_tiling(); + + if (tiling.num_columns != alpha_tiling.num_columns || + tiling.num_rows != alpha_tiling.num_rows) { + return true; + } + + // The alpha image may have another resolution, as it is scaled to the size of this + // image. The tiles have to cover the same part of the image, though. + return (static_cast(tiling.tile_width) * alpha_tiling.image_width != + static_cast(alpha_tiling.tile_width) * tiling.image_width || + static_cast(tiling.tile_height) * alpha_tiling.image_height != + static_cast(alpha_tiling.tile_height) * tiling.image_height); +} + + +heif_image_tiling ImageItem::get_image_tiling_with_alpha() const +{ + heif_image_tiling tiling = get_heif_image_tiling(); + + if (has_alpha_with_different_tiling()) { + tiling.num_columns = 1; + tiling.num_rows = 1; + tiling.tile_width = tiling.image_width; + tiling.tile_height = tiling.image_height; + } + + return tiling; } diff -Nru libheif-1.23.4/libheif/image-items/image_item.h libheif-1.23.6/libheif/image-items/image_item.h --- libheif-1.23.4/libheif/image-items/image_item.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/image_item.h 2026-10-05 16:16:00.000000000 +0000 @@ -356,7 +356,12 @@ // 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; } - bool is_miaf_compatible() const { return m_miaf_compatible; } + bool is_miaf_compatible() const { return m_miaf_compatible && is_coded_in_miaf_profile(); } + + // A MIAF image item has to conform to a MIAF codec profile (ISO/IEC 23000-22, 6.3). + // Whether the coded data of this image does. An image that is derived from other + // images answers for them. + virtual bool is_coded_in_miaf_profile() const { return true; } // return 0 if we don't know the brand virtual heif_brand2 get_compatible_brand() const { return 0; } @@ -367,10 +372,14 @@ virtual void set_decoder_input_data() { } + // With 'decode_tile_only', the tile position refers to get_image_tiling_with_alpha(). + // 'decode_as_single_tile' is used for the alpha image of an image that is exposed as + // a single tile: the whole image is decoded, but processed like a tile (not cropped). 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; + DecodeTraversalState decode_state, + bool decode_as_single_tile = false) 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' @@ -400,6 +409,10 @@ bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0) const; + // Checks that the decoded image has the bit depths that the image handle reports + // (get_luma_bits_per_pixel() and get_chroma_bits_per_pixel()). + Error check_decoded_image_bit_depth(const HeifPixelImage& img) const; + Result>> get_properties() const; bool has_essential_property_other_than(const std::set&) const; @@ -451,12 +464,26 @@ const std::vector& get_decoding_warnings() const { return m_decoding_warnings; } + // The tiling of this image item alone. virtual heif_image_tiling get_heif_image_tiling() const; + // The tiling in which the image can be decoded. When a tile is decoded, the same tile + // of the alpha image is decoded and attached to it. This requires that both images + // have the same tiling. Otherwise, the image is exposed as a single tile. + heif_image_tiling get_image_tiling_with_alpha() const; + + bool has_alpha_with_different_tiling() const; + Error process_image_transformations_on_tiling(heif_image_tiling&) const; Error transform_requested_tile_position_to_original_tile_position(uint32_t& tile_x, uint32_t& tile_y) const; + // The decoder of an image item is created when the item is read from a file + // (initialize_decoder()). An item that was added by encoding an image has none. + // This turns a missing decoder into an error, so that the functions that need the + // decoder fail instead of dereferencing a null pointer. + static Result> decoder_or_error(std::shared_ptr decoder); + virtual Result> get_decoder() const { return Error{ @@ -562,7 +589,8 @@ 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 + DecodeTraversalState decode_state, + bool decode_as_single_tile) const override { return m_item_error; } diff -Nru libheif-1.23.4/libheif/image-items/jpeg.cc libheif-1.23.6/libheif/image-items/jpeg.cc --- libheif-1.23.4/libheif/image-items/jpeg.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/jpeg.cc 2026-10-05 16:16:00.000000000 +0000 @@ -44,13 +44,18 @@ Result> ImageItem_JPEG::read_bitstream_configuration_data() const { - return m_decoder->read_bitstream_configuration_data(); + auto decoderResult = get_decoder(); + if (!decoderResult) { + return decoderResult.error(); + } + + return (*decoderResult)->read_bitstream_configuration_data(); } Result> ImageItem_JPEG::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } diff -Nru libheif-1.23.4/libheif/image-items/jpeg2000.cc libheif-1.23.6/libheif/image-items/jpeg2000.cc --- libheif-1.23.4/libheif/image-items/jpeg2000.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/jpeg2000.cc 2026-10-05 16:16:00.000000000 +0000 @@ -63,7 +63,7 @@ Result> ImageItem_JPEG2000::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } diff -Nru libheif-1.23.4/libheif/image-items/mask_image.cc libheif-1.23.6/libheif/image-items/mask_image.cc --- libheif-1.23.4/libheif/image-items/mask_image.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/mask_image.cc 2026-10-05 16:16:00.000000000 +0000 @@ -40,12 +40,11 @@ return range.get_error(); } -std::string Box_mskC::dump(Indent& indent) const +void Box_mskC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "bits_per_pixel: " << ((int)m_bits_per_pixel) << "\n"; - return sstr.str(); + return; } Error Box_mskC::write(StreamWriter& writer) const diff -Nru libheif-1.23.4/libheif/image-items/mask_image.h libheif-1.23.6/libheif/image-items/mask_image.h --- libheif-1.23.4/libheif/image-items/mask_image.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/mask_image.h 2026-10-05 16:16:00.000000000 +0000 @@ -53,7 +53,7 @@ bool is_essential() const override { return true; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; diff -Nru libheif-1.23.4/libheif/image-items/overlay.cc libheif-1.23.6/libheif/image-items/overlay.cc --- libheif-1.23.4/libheif/image-items/overlay.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/overlay.cc 2026-10-05 16:16:00.000000000 +0000 @@ -24,6 +24,7 @@ #include "color-conversion/colorconversion.h" #include "security_limits.h" +#include #include @@ -38,26 +39,27 @@ } +// Read a two's complement field of 'len' (2 or 4) bytes. +// Note: the former implementation complemented all 32 bits of the loaded value, +// which is only correct for 4-byte fields. A negative 2-byte offset such as -640 +// (0xFD80) came out as about -2^31, so the overlay image was placed far outside +// the canvas and silently skipped (conformance files C019 and C021). static int32_t readvec_signed(const std::vector& data, int& ptr, int len) { - const uint32_t high_bit = UINT32_C(0x80) << ((len - 1) * 8); + assert(len == 2 || len == 4); uint32_t val = 0; - while (len--) { + for (int i = 0; i < len; i++) { val <<= 8; val |= data[ptr++]; } - bool negative = (val & high_bit) != 0; - - if (negative) { - return -static_cast((~val) & 0x7fffffff) -1; + if (len == 2) { + return static_cast(val); } else { return static_cast(val); } - - return val; } @@ -333,19 +335,25 @@ return err; } + // The canvas has the bit depth of the first image, which is the bit depth that the image + // handle of the overlay reports. Images with a lower bit depth are brought to that depth + // before they are composed. The samples are composed with 8 or 16 bits. + const int canvas_bit_depth = get_canvas_bit_depth(); + if (canvas_bit_depth > 16) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "Overlay images with more than 16 bits per sample are not supported"}; + } + // TODO: seems we always have to compose this in RGB since the background color is an RGB value img = std::make_shared(); img->create(w, h, heif_colorspace_RGB, heif_chroma_444); - 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_channel(heif_channel_G, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths - return error; - } - if (auto error = img->add_channel(heif_channel_B, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths - return error; + for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B}) { + if (auto error = img->add_channel(channel, w, h, canvas_bit_depth, get_context()->get_security_limits())) { + return error; + } } uint16_t bkg_color[4]; @@ -383,21 +391,31 @@ // process overlay in RGB space + // + // HeifPixelImage::overlay() needs the image as RGB 4:4:4 with the bit depth of the canvas. + // The conversion is requested for every image, also for one that already has this + // format: convert_colorspace() hands such an image through unchanged, but it first checks + // that the image has the planes of its format in the right sizes. + // An image with a higher bit depth than the canvas would have to be reduced, which loses + // accuracy. That is refused. (The images used to be composed byte by byte whatever their + // bit depth, which gave a wrong picture for more than 8 bits.) - 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, - nclx_profile::undefined(), - 0, options.color_conversion_options, options.color_conversion_options_ext, - get_context()->get_security_limits()); - if (!overlay_img_result) { - return overlay_img_result.error(); - } - else { - overlay_img = *overlay_img_result; - } + if (overlay_img->get_visual_image_bits_per_pixel() > canvas_bit_depth) { + return Error{heif_error_Unsupported_feature, + heif_suberror_Unspecified, + "An overlay input image with a higher bit depth than the first input image is not supported"}; + } + + auto overlay_img_result = convert_colorspace(overlay_img, heif_colorspace_RGB, heif_chroma_444, + nclx_profile::undefined(), + canvas_bit_depth, options.color_conversion_options, options.color_conversion_options_ext, + get_context()->get_security_limits()); + if (!overlay_img_result) { + return overlay_img_result.error(); } + overlay_img = *overlay_img_result; + int32_t dx, dy; m_overlay_spec.get_offset(i, &dx, &dy); @@ -417,7 +435,9 @@ } -int ImageItem_Overlay::get_luma_bits_per_pixel() const +// The bit depth of the first coded image that the overlay is composed of, or -1 if it is +// not known (yet: while the file is loaded, the image may not have been read). +int ImageItem_Overlay::get_first_image_bit_depth() const { auto child_result = get_context()->find_first_coded_image_id(get_id()); if (child_result.is_error()) { @@ -425,19 +445,36 @@ } auto image = get_context()->get_image(*child_result, true); + if (!image) { + return -1; + } + return image->get_luma_bits_per_pixel(); } -int ImageItem_Overlay::get_chroma_bits_per_pixel() const +int ImageItem_Overlay::get_canvas_bit_depth() const { - auto child_result = get_context()->find_first_coded_image_id(get_id()); - if (child_result.is_error()) { + // The canvas has at least 8 bits. An image with fewer bits is converted up. + return std::max(get_first_image_bit_depth(), 8); +} + + +// The overlay image is the canvas, so these report the bit depth of the canvas. + +int ImageItem_Overlay::get_luma_bits_per_pixel() const +{ + if (get_first_image_bit_depth() < 0) { return -1; } - auto image = get_context()->get_image(*child_result, true); - return image->get_chroma_bits_per_pixel(); + return get_canvas_bit_depth(); +} + + +int ImageItem_Overlay::get_chroma_bits_per_pixel() const +{ + return get_luma_bits_per_pixel(); } @@ -456,23 +493,32 @@ return; } - // The overlay is always composed in RGB 8-bit 4:4:4 onto the canvas + // The overlay is always composed in RGB 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. + // output format, not the children's formats. The bit depth is the one of the + // canvas, which is taken from the first input image. If that image has not + // been read yet (it comes later in the file than the overlay), the bit depth + // is not known. The overlay is then described in the second pass of + // HeifContext::interpret_heif_file_images(). + if (get_first_image_bit_depth() < 0) { + return; + } + + const auto canvas_bit_depth = static_cast(get_canvas_bit_depth()); 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) { + auto emit = [this, w, h, canvas_bit_depth](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.bit_depth = canvas_bit_depth; d.width = w; d.height = h; d.has_data_plane = true; @@ -538,6 +584,19 @@ return iovl_image; } +bool ImageItem_Overlay::is_coded_in_miaf_profile() const +{ + for (heif_item_id child_id : m_overlay_image_ids) { + auto child = get_context()->get_image(child_id, false); + if (child && !child->is_coded_in_miaf_profile()) { + return false; + } + } + + return true; +} + + heif_brand2 ImageItem_Overlay::get_compatible_brand() const { if (m_overlay_image_ids.empty()) { return 0; } diff -Nru libheif-1.23.4/libheif/image-items/overlay.h libheif-1.23.6/libheif/image-items/overlay.h --- libheif-1.23.4/libheif/image-items/overlay.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/overlay.h 2026-10-05 16:16:00.000000000 +0000 @@ -114,6 +114,8 @@ heif_brand2 get_compatible_brand() const override; + bool is_coded_in_miaf_profile() const override; + Result encode(const std::shared_ptr& image, heif_encoder* encoder, const heif_encoding_options& options, @@ -136,6 +138,11 @@ ImageOverlay m_overlay_spec; std::vector m_overlay_image_ids; + int get_first_image_bit_depth() const; + + // The bit depth of the canvas that the images are composed on. + int get_canvas_bit_depth() const; + Error read_overlay_spec(); Result> decode_overlay_image(const heif_decoding_options& options, diff -Nru libheif-1.23.4/libheif/image-items/tiled.cc libheif-1.23.6/libheif/image-items/tiled.cc --- libheif-1.23.4/libheif/image-items/tiled.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/tiled.cc 2026-10-05 16:16:00.000000000 +0000 @@ -218,11 +218,10 @@ } -std::string Box_tilC::dump(Indent& indent) const +void Box_tilC::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << BoxHeader::dump(indent); + BoxHeader::dump(sstr, indent, full_log); sstr << indent << "version: " << ((int) get_version()) << "\n" //<< indent << "image size: " << m_parameters.image_width << "x" << m_parameters.image_height << "\n" @@ -233,11 +232,8 @@ << indent << "size field length: " << ((int) m_parameters.size_field_length) << " bits\n" << indent << "number of extra dimensions: " << ((int) m_parameters.number_of_extra_dimensions) << "\n"; - sstr << indent << "tile properties:\n" - << dump_children(indent, true); - - return sstr.str(); - + sstr << indent << "tile properties:\n"; + dump_children(sstr, indent, full_log, true); } diff -Nru libheif-1.23.4/libheif/image-items/tiled.h libheif-1.23.6/libheif/image-items/tiled.h --- libheif-1.23.4/libheif/image-items/tiled.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/tiled.h 2026-10-05 16:16:00.000000000 +0000 @@ -69,7 +69,7 @@ Error write(StreamWriter& writer) const override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; std::vector>& get_tile_properties() { return m_children; } diff -Nru libheif-1.23.4/libheif/image-items/unc_image.cc libheif-1.23.6/libheif/image-items/unc_image.cc --- libheif-1.23.4/libheif/image-items/unc_image.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/unc_image.cc 2026-10-05 16:16:00.000000000 +0000 @@ -152,6 +152,25 @@ Error err; if (decode_tile_only) { + uint32_t num_columns, num_rows; + const bool tiles_are_decodable = get_exposed_tiling(num_columns, num_rows); + + // The tile position is not validated on all paths that lead here. + if (tile_x0 >= num_columns || tile_y0 >= num_rows) { + return Error{heif_error_Usage_error, + heif_suberror_Invalid_parameter_value, + "Tile position is outside of the image tiling."}; + } + + // When the uncC tiles cannot be decoded independently, the image is exposed as a + // single tile. That tile is the whole image, which the full image path decodes + // with a single decompression of the item (GHSA-6fqc-p7r8-2g36). + if (!tiles_are_decodable) { + decode_tile_only = false; + } + } + + if (decode_tile_only) { err = UncompressedImageCodec::decode_uncompressed_image_tile(get_context(), get_id(), img, @@ -347,6 +366,12 @@ // TODO: drop alpha } + // Tiles are passed to the 'unci' encoder directly, without going through + // Encoder::convert_colorspace_for_encoding(), so the sample range is checked here. + if (Error err = image->check_sample_value_ranges()) { + return err; + } + Result> codedBitstreamResult = m_unc_encoder->encode_tile(image); if (!codedBitstreamResult) { return codedBitstreamResult.error(); @@ -388,6 +413,32 @@ } +bool ImageItem_uncompressed::get_exposed_tiling(uint32_t& num_columns, uint32_t& num_rows) const +{ + num_columns = 1; + num_rows = 1; + + auto uncC = get_property(); + if (!uncC) { + return true; + } + + // Generic compression with units that span several tiles (e.g. the full item) does not + // give access to the individual tiles: the item has to be decompressed completely to + // get the data of any tile. Such an image is exposed as a single tile, like the images + // of the other codecs that may use tiles internally. Otherwise, decoding the image + // tile by tile would decompress the whole item once for each tile (GHSA-6fqc-p7r8-2g36). + if (UncompressedImageCodec::get_tile_access(uncC, get_property(), get_property()) == + UncompressedImageCodec::TileAccess::whole_item) { + return false; + } + + num_columns = uncC->get_number_of_tile_columns(); + num_rows = uncC->get_number_of_tile_rows(); + return true; +} + + void ImageItem_uncompressed::get_tile_size(uint32_t& w, uint32_t& h) const { auto ispe = get_property(); @@ -397,8 +448,11 @@ w = h = 0; } else { - w = ispe->get_width() / uncC->get_number_of_tile_columns(); - h = ispe->get_height() / uncC->get_number_of_tile_rows(); + uint32_t num_columns, num_rows; + get_exposed_tiling(num_columns, num_rows); + + w = ispe->get_width() / num_columns; + h = ispe->get_height() / num_rows; } } @@ -411,8 +465,7 @@ auto uncC = get_property(); assert(ispe && uncC); - tiling.num_columns = uncC->get_number_of_tile_columns(); - tiling.num_rows = uncC->get_number_of_tile_rows(); + get_exposed_tiling(tiling.num_columns, tiling.num_rows); tiling.tile_width = ispe->get_width() / tiling.num_columns; tiling.tile_height = ispe->get_height() / tiling.num_rows; @@ -426,7 +479,7 @@ Result> ImageItem_uncompressed::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } std::shared_ptr ImageItem_uncompressed::get_encoder() const @@ -483,6 +536,11 @@ bool ImageItem_uncompressed::has_coded_alpha_channel() const { + // An item that was added by encoding has no decoder (see decoder_or_error()). + if (!m_decoder) { + return false; + } + return m_decoder->has_alpha_component(); } diff -Nru libheif-1.23.4/libheif/image-items/unc_image.h libheif-1.23.6/libheif/image-items/unc_image.h --- libheif-1.23.4/libheif/image-items/unc_image.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/unc_image.h 2026-10-05 16:16:00.000000000 +0000 @@ -102,6 +102,11 @@ std::shared_ptr get_encoder() const override; private: + // The tiling that is exposed through the API. This is the uncC tiling, unless its + // tiles cannot be decoded independently because of the generic compression. In that + // case, the image is exposed as a single tile and false is returned. + bool get_exposed_tiling(uint32_t& num_columns, uint32_t& num_rows) const; + std::shared_ptr m_decoder; std::shared_ptr m_encoder; diff -Nru libheif-1.23.4/libheif/image-items/vvc.cc libheif-1.23.6/libheif/image-items/vvc.cc --- libheif-1.23.4/libheif/image-items/vvc.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/image-items/vvc.cc 2026-10-05 16:16:00.000000000 +0000 @@ -65,7 +65,7 @@ Result> ImageItem_VVC::get_decoder() const { - return {m_decoder}; + return decoder_or_error(m_decoder); } diff -Nru libheif-1.23.4/libheif/logging.h libheif-1.23.6/libheif/logging.h --- libheif-1.23.4/libheif/logging.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/logging.h 2026-10-05 16:16:00.000000000 +0000 @@ -29,6 +29,15 @@ #include #include #include +#include + + +// dump() is a debugging aid. Boxes whose content scales with the file size (the +// sample tables, reference lists, ...) can otherwise produce gigabytes of text +// from a small input, which nested containers then copy at every level. Unless +// the caller passes full_log=true, such boxes print at most this many entries +// and then a short note about how many were omitted. +static const size_t MAX_DUMP_ENTRIES = 100; class Indent @@ -59,6 +68,34 @@ return ostr; } + +// dump() writes every box into one shared stream, so a sticky modifier such as +// std::hex or std::setfill left behind by one box would leak into the next. +// Reset the stream to its defaults (decimal, blank fill) between boxes so each +// one starts formatting from a known state. +inline void reset_stream_format(std::ostream& ostr) +{ + ostr << std::dec << std::noboolalpha; + ostr.fill(' '); +} + + +// Helper for dump() loops over a container whose length scales with the file +// size. Call it at the top of the loop body with the current index and the +// total count. When full_log is false and MAX_DUMP_ENTRIES items have already +// been written, it writes a short note about the remaining items and returns +// true, telling the loop to stop. +inline bool dump_reached_entry_limit(std::ostream& ostr, const Indent& indent, + bool full_log, size_t index, size_t total) +{ + if (full_log || index < MAX_DUMP_ENTRIES) { + return false; + } + + ostr << indent << "... (" << (total - index) << " more)\n"; + return true; +} + std::string write_raw_data_as_hex(const uint8_t* data, size_t len, const std::string& firstLineIndent, const std::string& remainingLinesIndent); diff -Nru libheif-1.23.4/libheif/mini.cc libheif-1.23.6/libheif/mini.cc --- libheif-1.23.4/libheif/mini.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/mini.cc 2026-10-05 16:16:00.000000000 +0000 @@ -561,10 +561,9 @@ return range.get_error(); } -std::string Box_mini::dump(Indent &indent) const +void Box_mini::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "version: " << (int)m_version << "\n"; sstr << indent << "explicit_codec_types_flag: " << m_explicit_codec_types_flag << "\n"; @@ -845,7 +844,7 @@ { sstr << "xmp_data offset: " << m_xmp_item_data_offset << ", size: " << m_xmp_data_size << "\n"; } - return sstr.str(); + return; } @@ -1293,7 +1292,8 @@ // 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) + std::shared_ptr* out_box, + const heif_security_limits* limits) { const size_t header_size = 8; if (config_bytes.size() > std::numeric_limits::max() - header_size) { @@ -1322,7 +1322,7 @@ 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()); + return Box::read(range, out_box, limits); } @@ -1412,7 +1412,7 @@ 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)) { + if (auto err = parse_codec_config_box(get_main_item_codec_config(), config_type, &main_item_codec_prop, file->get_security_limits())) { return err; } ipco_box->append_child_box(main_item_codec_prop); // entry 1 @@ -1466,7 +1466,7 @@ 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)) { + if (auto err = parse_codec_config_box(get_alpha_item_codec_config(), config_type, &alpha_item_codec_prop, file->get_security_limits())) { return err; } ipco_box->append_child_box(alpha_item_codec_prop); // entry 6 diff -Nru libheif-1.23.4/libheif/mini.h libheif-1.23.6/libheif/mini.h --- libheif-1.23.4/libheif/mini.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/mini.h 2026-10-05 16:16:00.000000000 +0000 @@ -140,7 +140,7 @@ 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; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; diff -Nru libheif-1.23.4/libheif/nclx.cc libheif-1.23.6/libheif/nclx.cc --- libheif-1.23.4/libheif/nclx.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/nclx.cc 2026-10-05 16:16:00.000000000 +0000 @@ -438,40 +438,37 @@ } -std::string Box_colr::dump(Indent& indent) const +void Box_colr::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); if (m_color_profile) { sstr << indent << "colour_type: " << fourcc_to_string(get_color_profile_type()) << "\n"; - sstr << m_color_profile->dump(indent); + m_color_profile->dump(sstr, indent, full_log); } else { sstr << indent << "colour_type: ---\n"; sstr << "no color profile\n"; } - return sstr.str(); + return; } -std::string color_profile_raw::dump(Indent& indent) const +void color_profile_raw::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; sstr << indent << "profile size: " << m_data.size() << "\n"; - return sstr.str(); + return; } -std::string color_profile_nclx::dump(Indent& indent) const +void color_profile_nclx::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; 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(); + return; } diff -Nru libheif-1.23.4/libheif/nclx.h libheif-1.23.6/libheif/nclx.h --- libheif-1.23.4/libheif/nclx.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/nclx.h 2026-10-05 16:16:00.000000000 +0000 @@ -93,7 +93,7 @@ virtual uint32_t get_type() const = 0; - virtual std::string dump(Indent&) const = 0; + virtual void dump(std::ostream&, Indent&, bool full_log) const = 0; virtual Error write(StreamWriter& writer) const = 0; }; @@ -108,7 +108,7 @@ const std::vector& get_data() const { return m_data; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error write(StreamWriter& writer) const override; @@ -183,7 +183,7 @@ uint32_t get_type() const override { return fourcc("nclx"); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; Error parse(BitstreamRange& range); @@ -206,7 +206,7 @@ set_short_type(fourcc("colr")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; uint32_t get_color_profile_type() const { return m_color_profile->get_type(); } diff -Nru libheif-1.23.4/libheif/omaf_boxes.cc libheif-1.23.6/libheif/omaf_boxes.cc --- libheif-1.23.4/libheif/omaf_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/omaf_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -38,10 +38,9 @@ return range.get_error(); } -std::string Box_prfr::dump(Indent& indent) const +void Box_prfr::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "projection_type: "; switch (m_projection) { case heif_omaf_image_projection_equirectangular: @@ -54,7 +53,7 @@ sstr << "unknown (" << m_projection << ")\n"; break; } - return sstr.str(); + return; } Error Box_prfr::write(StreamWriter& writer) const diff -Nru libheif-1.23.4/libheif/omaf_boxes.h libheif-1.23.6/libheif/omaf_boxes.h --- libheif-1.23.4/libheif/omaf_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/omaf_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -42,7 +42,7 @@ Error set_image_projection(heif_omaf_image_projection projection); - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Projection Format"; } diff -Nru libheif-1.23.4/libheif/plugins/decoder_ffmpeg.cc libheif-1.23.6/libheif/plugins/decoder_ffmpeg.cc --- libheif-1.23.4/libheif/plugins/decoder_ffmpeg.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/plugins/decoder_ffmpeg.cc 2026-10-05 16:16:00.000000000 +0000 @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -42,14 +43,23 @@ } +// Plugins are compiled with LIBHEIF_EXPORTS, so on MSVC a reference to the exported +// data object heif_error_success does not resolve against libheif (issue #1854). +// Use a file-local success object instead. +static const heif_error kSuccess = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; + struct ffmpeg_decoder { // --- input data struct Packet { + // The payload is followed by AV_INPUT_BUFFER_PADDING_SIZE zero bytes, as FFmpeg + // requires for its input buffers. An empty vector is the 'flush' packet. std::vector data; uintptr_t user_data; + + int payload_size() const { return (int) (data.size() - AV_INPUT_BUFFER_PADDING_SIZE); } }; std::deque input_data; @@ -58,9 +68,12 @@ // --- decoder const AVCodec* av_codec = NULL; - AVCodecParserContext* av_codec_parser_context = NULL; AVCodecContext* av_codec_context = NULL; + // Only set when the decoder does not export the colour signalling itself, + // see ffmpeg_decoder_needs_parser_for_colour_signalling(). + AVCodecParserContext* av_codec_parser_context = NULL; + std::string error_message; ~ffmpeg_decoder() @@ -75,12 +88,40 @@ return id == AV_CODEC_ID_H264 || id == AV_CODEC_ID_H265 || id == AV_CODEC_ID_H266; } +// Emulation prevention keeps 00 00 00, 00 00 01 and 00 00 02 out of every valid +// NAL unit. The NAL units are handed to FFmpeg with start codes, so such a byte +// sequence would make FFmpeg split the data differently than libheif did when it +// checked the parameter sets. +static bool contains_start_code_prefix(const uint8_t* data, size_t size) +{ + for (size_t i = 2; i < size; i++) { + if (data[i] <= 2 && data[i - 1] == 0 && data[i - 2] == 0) { + return true; + } + } + return false; +} + // 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; } +// FFmpeg's VVC decoder copies the colour signalling of the VUI into the codec context +// only since FFmpeg 8.0 (libavcodec 62). In FFmpeg 7.x, this is done by the VVC parser +// and without it, all VVC images would be reported with an unspecified colour profile. +// All other decoders export the colour signalling themselves and do not need the parser. +static bool ffmpeg_decoder_needs_parser_for_colour_signalling(AVCodecID id) +{ +#if LIBAVCODEC_VERSION_MAJOR < 62 + return id == AV_CODEC_ID_VVC; +#else + (void) id; + return false; +#endif +} + static const int FFMPEG_DECODER_PLUGIN_PRIORITY = 90; #define MAX_PLUGIN_NAME_LENGTH 80 @@ -113,6 +154,12 @@ { switch(format) { case heif_compression_HEVC: + // FFmpeg's HEVC decoder handles 8, 9, 10 and 12 bits per sample only (FFmpeg 6.1 and 7.1). + // For streams with 11 bits or with 13 to 16 bits, avcodec_send_packet() fails. libde265 + // decodes all bit depths and is the decoder to use for these streams. It has the higher + // priority, so that it is chosen whenever it is available. + // We cannot return 0 for the bit depths that FFmpeg does not handle: the format + // description passed to does_support_format2() holds the compression format only. 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; @@ -182,9 +229,10 @@ 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" }; + if (ffmpeg_decoder_needs_parser_for_colour_signalling(decoder->av_codec->id)) { + // This is NULL when FFmpeg was built without the parser. We can still decode then, + // only the colour signalling of the bitstream is not reported. + decoder->av_codec_parser_context = av_parser_init(decoder->av_codec->id); } decoder->av_codec_context = avcodec_alloc_context3(decoder->av_codec); @@ -192,13 +240,26 @@ return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "avcodec_alloc_context3 returned error" }; } + // Crop exactly as the bitstream signals. Otherwise FFmpeg rounds a left crop + // down to keep the plane pointers aligned, and the decoded image comes out + // wider than the size signaled in the file. + decoder->av_codec_context->flags |= AV_CODEC_FLAG_UNALIGNED; + + // Let FFmpeg refuse pictures larger than libheif's limit for this image + // (ispe plus a coding-unit margin), as the libde265 plugin does. + const heif_security_limits* limits = options->limits ? options->limits : heif_get_global_security_limits(); + if (limits->max_image_size_pixels > 0 && + limits->max_image_size_pixels <= static_cast(std::numeric_limits::max())) { + decoder->av_codec_context->max_pixels = static_cast(limits->max_image_size_pixels); + } + /* 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; + return kSuccess; } static heif_error ffmpeg_new_decoder(void** dec) @@ -318,6 +379,14 @@ }; } + if (contains_start_code_prefix(cdata + ptr, nal_size)) { + return { + heif_error_Decoder_plugin_error, + heif_suberror_Unspecified, + "NAL unit contains a start code prefix" + }; + } + pkt.data.push_back(0); pkt.data.push_back(0); pkt.data.push_back(1); @@ -329,11 +398,12 @@ pkt.data.insert(pkt.data.end(), cdata, cdata + size); } + pkt.data.resize(pkt.data.size() + AV_INPUT_BUFFER_PADDING_SIZE, 0); pkt.user_data = user_data; decoder->input_data.emplace_back(std::move(pkt)); - return heif_error_success; + return kSuccess; } static heif_error ffmpeg_push_data(void *decoder_raw, const void *data, size_t size) @@ -349,6 +419,7 @@ static heif_chroma ffmpeg_get_chroma_format(AVPixelFormat pix_fmt) { switch (pix_fmt) { case AV_PIX_FMT_GRAY8: + case AV_PIX_FMT_GRAY9: case AV_PIX_FMT_GRAY10: case AV_PIX_FMT_GRAY12: case AV_PIX_FMT_GRAY14: @@ -357,6 +428,7 @@ case AV_PIX_FMT_YUV420P: case AV_PIX_FMT_YUVJ420P: + case AV_PIX_FMT_YUV420P9: case AV_PIX_FMT_YUV420P10: case AV_PIX_FMT_YUV420P12: case AV_PIX_FMT_YUV420P14: @@ -364,6 +436,8 @@ return heif_chroma_420; case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: + case AV_PIX_FMT_YUV422P9: case AV_PIX_FMT_YUV422P10: case AV_PIX_FMT_YUV422P12: case AV_PIX_FMT_YUV422P14: @@ -371,12 +445,27 @@ return heif_chroma_422; case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUVJ444P: + case AV_PIX_FMT_YUV444P9: case AV_PIX_FMT_YUV444P10: case AV_PIX_FMT_YUV444P12: case AV_PIX_FMT_YUV444P14: case AV_PIX_FMT_YUV444P16: return heif_chroma_444; + // FFmpeg's HEVC and AVC decoders output planar GBR for 4:4:4 streams signaling + // matrix_coefficients 0. Its G, B, R planes map onto Y, Cb, Cr, which is + // exactly how libheif stores images with the identity matrix. + // HEVC is decoded up to 12 bits, AVC (High 4:4:4 Predictive) up to 14 bits. + // The JPEG decoder outputs GBRP for a JPEG that was coded without colour transform. + // That one carries no colour signalling, see ffmpeg_is_planar_gbr(). + case AV_PIX_FMT_GBRP: + case AV_PIX_FMT_GBRP9: + case AV_PIX_FMT_GBRP10: + case AV_PIX_FMT_GBRP12: + case AV_PIX_FMT_GBRP14: + return heif_chroma_444; + default: // Unsupported pix_fmt return heif_chroma_undefined; @@ -422,22 +511,34 @@ case AV_PIX_FMT_YUV420P: case AV_PIX_FMT_YUVJ420P: case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUVJ444P: + case AV_PIX_FMT_GBRP: return 8; + case AV_PIX_FMT_GRAY9: + case AV_PIX_FMT_YUV420P9: + case AV_PIX_FMT_YUV422P9: + case AV_PIX_FMT_YUV444P9: + case AV_PIX_FMT_GBRP9: + return 9; case AV_PIX_FMT_GRAY10: case AV_PIX_FMT_YUV420P10: case AV_PIX_FMT_YUV422P10: case AV_PIX_FMT_YUV444P10: + case AV_PIX_FMT_GBRP10: return 10; case AV_PIX_FMT_GRAY12: case AV_PIX_FMT_YUV420P12: case AV_PIX_FMT_YUV422P12: case AV_PIX_FMT_YUV444P12: + case AV_PIX_FMT_GBRP12: return 12; case AV_PIX_FMT_GRAY14: case AV_PIX_FMT_YUV420P14: case AV_PIX_FMT_YUV422P14: case AV_PIX_FMT_YUV444P14: + case AV_PIX_FMT_GBRP14: return 14; case AV_PIX_FMT_GRAY16: case AV_PIX_FMT_YUV420P16: @@ -478,6 +579,17 @@ } +// Whether the planes of a format accepted by ffmpeg_get_chroma_format() hold G, B, R instead +// of Y, Cb, Cr. Packed RGB is not meant here: the JPEG 2000 decoder returns sYCC as RGB24/RGB48. +static bool ffmpeg_is_planar_gbr(AVPixelFormat pix_fmt) +{ + const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(pix_fmt); + const uint64_t planar_rgb = AV_PIX_FMT_FLAG_PLANAR | AV_PIX_FMT_FLAG_RGB; + + return desc && (desc->flags & planar_rgb) == planar_rgb; +} + + // 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, @@ -607,7 +719,7 @@ av_frame->width, av_frame->height, shift, byte_swap); } - return heif_error_success; + return kSuccess; } heif_chroma chroma = ffmpeg_get_chroma_format(pix_fmt); @@ -680,7 +792,7 @@ w, h, shift, byte_swap); } - return heif_error_success; + return kSuccess; } else { const char* fmt_name = av_get_pix_fmt_name(static_cast(av_frame->format)); @@ -707,12 +819,9 @@ heif_color_profile_nclx* nclx = NULL; int ret = 0; - heif_error err = heif_error_success; + heif_error err = kSuccess; if (!decoder->input_data.empty()) { - uint8_t* parse_av_data = NULL; - int parse_av_data_size = 0; - ffmpeg_decoder::Packet& first_pkt = decoder->input_data.front(); if (first_pkt.data.empty()) { @@ -732,49 +841,28 @@ return { heif_error_Memory_allocation_error, heif_suberror_Unspecified, "av_packet_alloc returned error" }; } - parse_av_data = first_pkt.data.data(); - parse_av_data_size = (int) first_pkt.data.size(); - size_t n_bytes_consumed = 0; + // Every pushed chunk is a complete access unit, so it goes to the decoder + // as one packet. + av_pkt->data = first_pkt.data.data(); + av_pkt->size = first_pkt.payload_size(); + av_pkt->pts = first_pkt.user_data; + + if (decoder->av_codec_parser_context) { + // We run the parser only to let it copy the colour signalling into the codec context. + // It does not split the input and we send the whole access unit to the decoder. + uint8_t* parsed_data = NULL; + int parsed_size = 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"}; - } - - // std::cout << "decode packet of size: " << ret << "\n"; - - 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; - } + av_parser_parse2(decoder->av_codec_parser_context, decoder->av_codec_context, &parsed_data, &parsed_size, + av_pkt->data, av_pkt->size, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); } - 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); - } + ret = avcodec_send_packet(decoder->av_codec_context, av_pkt); + decoder->input_data.pop_front(); + if (ret < 0) { + return {heif_error_Decoder_plugin_error, heif_suberror_Unspecified, "Error in avcodec_send_packet"}; } } } @@ -813,6 +901,21 @@ // 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)) { + + // The exception are planes that hold G, B, R, which the JPEG decoder returns for a JPEG + // coded without colour transform. Without the identity matrix, libheif would convert + // them like YCbCr. The colour primaries and the transfer curve remain unspecified. + if (ffmpeg_is_planar_gbr(static_cast(av_frame->format))) { + 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, heif_color_primaries_unspecified); + heif_nclx_color_profile_set_transfer_characteristics(nclx, heif_transfer_characteristic_unspecified); + heif_nclx_color_profile_set_matrix_coefficients(nclx, heif_matrix_coefficients_RGB_GBR); + nclx->full_range_flag = true; + heif_image_set_nclx_color_profile(*out_img, nclx); + } + return heif_error_ok; } diff -Nru libheif-1.23.4/libheif/plugins/decoder_openjpeg.cc libheif-1.23.6/libheif/plugins/decoder_openjpeg.cc --- libheif-1.23.4/libheif/plugins/decoder_openjpeg.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/plugins/decoder_openjpeg.cc 2026-10-05 16:16:00.000000000 +0000 @@ -24,6 +24,8 @@ #include "decoder_openjpeg.h" #include "common_utils.h" #include +#include +#include #include #include @@ -277,6 +279,105 @@ //************************************************************************** +// Memory that OpenJPEG allocates while it reads the SIZ marker segment: the coding +// parameters (opj_tcp_t) and the codestream index of each tile, and the coding parameters +// of each component of each tile (opj_tccp_t). These are the sizes of OpenJPEG 2.5 on a +// 64-bit platform, rounded up. +static const uint64_t OPENJPEG_HEADER_BYTES_PER_TILE = 8192; +static const uint64_t OPENJPEG_HEADER_BYTES_PER_TILE_COMPONENT = 1088; + +// Each tile has at least one tile-part, which consists at least of an SOT marker segment +// (12 bytes) and an SOD marker (2 bytes). +static const uint64_t JPEG2000_MIN_BYTES_PER_TILE = 14; + + +static uint32_t read_uint32_be(const uint8_t* p) +{ + return (uint32_t{p[0]} << 24) | (uint32_t{p[1]} << 16) | (uint32_t{p[2]} << 8) | uint32_t{p[3]}; +} + + +// OpenJPEG allocates memory for every tile and for every component of every tile already +// when it reads the SIZ marker segment in opj_read_header(), and there is no way to limit +// that. A SIZ marker segment of less than 100 bytes can declare 65535 tiles of one pixel +// with up to 16384 components each, for which OpenJPEG allocates many gigabytes before we +// get to see the image header. Hence, we read the SIZ marker segment ourselves and check +// the number of tiles and components before we call OpenJPEG (GHSA-h4h8-qgvc-m7r2). +static heif_error openjpeg_check_siz_marker_segment(const std::vector& data, + const heif_security_limits* limits) +{ + // SOC marker, SIZ marker, Lsiz, Rsiz, eight 32-bit sizes and offsets, Csiz + const size_t fixed_part_size = 2 + 2 + 2 + 2 + 8 * 4 + 2; + + // The SIZ marker segment has to follow the SOC marker directly. OpenJPEG does not insist + // on this, it skips over other data until it finds an SIZ marker. We do insist, because + // this is the only way to know which SIZ marker segment OpenJPEG is going to use. + if (data.size() < fixed_part_size || + data[0] != 0xFF || data[1] != 0x4F || + data[2] != 0xFF || data[3] != 0x51) { + return {heif_error_Invalid_input, heif_suberror_Invalid_J2K_codestream, + "JPEG 2000 codestream does not start with an SOC marker and an SIZ marker segment"}; + } + + const uint64_t xsiz = read_uint32_be(&data[8]); + const uint64_t ysiz = read_uint32_be(&data[12]); + const uint64_t xtsiz = read_uint32_be(&data[24]); + const uint64_t ytsiz = read_uint32_be(&data[28]); + const uint64_t xtosiz = read_uint32_be(&data[32]); + const uint64_t ytosiz = read_uint32_be(&data[36]); + const uint32_t csiz = (uint32_t{data[40]} << 8) | uint32_t{data[41]}; + + if (xtsiz == 0 || ytsiz == 0 || xtosiz >= xsiz || ytosiz >= ysiz) { + return {heif_error_Invalid_input, heif_suberror_Invalid_J2K_codestream, + "Invalid tile geometry in JPEG 2000 codestream"}; + } + + // This plugin handles the image size as 'int'. Moreover, OpenJPEG versions before 2.5.1 + // compute the number of tiles with signed 32-bit integers. With a reference grid of + // more than INT32_MAX in one direction, they can get to 65535 tiles where we compute + // a single one here. + if (xsiz > INT32_MAX || ysiz > INT32_MAX) { + return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, + "JPEG 2000 reference grid is too large"}; + } + + if (limits->max_components > 0 && csiz > limits->max_components) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG 2000 image exceeds the maximum number of components"}; + } + + if (csiz != 3 && csiz != 1) { + //TODO - Handle other numbers of components + return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Number of components must be 3 or 1"}; + } + + // Both factors are below 2^31, thus there is no overflow in the product. + const uint64_t num_tiles = ((xsiz - xtosiz + xtsiz - 1) / xtsiz) * ((ysiz - ytosiz + ytsiz - 1) / ytsiz); + + if (limits->max_number_of_tiles > 0 && num_tiles > limits->max_number_of_tiles) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG 2000 image exceeds the maximum number of tiles"}; + } + + // A codestream that is shorter than the minimum size of its tiles cannot be complete. + // This check ties the memory that OpenJPEG allocates for the tiles to the input size. + if (num_tiles > data.size() / JPEG2000_MIN_BYTES_PER_TILE) { + return {heif_error_Invalid_input, heif_suberror_Invalid_J2K_codestream, + "JPEG 2000 codestream is too short for its number of tiles"}; + } + + // num_tiles is bounded by the input size here and csiz is at most 3, no overflow. + const uint64_t header_memory = num_tiles * (OPENJPEG_HEADER_BYTES_PER_TILE + + csiz * OPENJPEG_HEADER_BYTES_PER_TILE_COMPONENT); + if (limits->max_memory_block_size > 0 && header_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 for its tiles"}; + } + + return heif_error_ok; +} + + // 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 @@ -319,6 +420,15 @@ } + heif_error siz_error = openjpeg_check_siz_marker_segment(decoder->encoded_data, limits); + if (siz_error.code) { + return siz_error; + } + + // OpenJPEG has to read the data from its start, as we did in the check above. + decoder->read_position = 0; + + OPJ_BOOL success; opj_dparameters_t decompression_parameters; std::unique_ptr l_codec(opj_create_decompress(OPJ_CODEC_J2K), @@ -365,15 +475,25 @@ "JPEG 2000 image exceeds maximum allowed image size"}; } + // The visible image is only the window (x1-x0, y1-y0), but OpenJPEG performs + // its tile and coefficient arithmetic over the full JPEG 2000 reference grid + // (Xsiz=x1, Ysiz=y1). A tiny window placed at very large absolute coordinates + // therefore reaches internal decode paths with pathological geometry, which + // has caused out-of-bounds writes inside OpenJPEG (see GHSA-q492-cfcm-895h, + // CVE-2020-6851). Bound the reference-grid area, not just the window span. + // (x1,y1 are uint32, so the product fits in uint64 without overflow.) + uint64_t grid_pixels = uint64_t(image->x1) * uint64_t(image->y1); + if (limits->max_image_size_pixels > 0 && grid_pixels > limits->max_image_size_pixels) { + return {heif_error_Memory_allocation_error, heif_suberror_Security_limit_exceeded, + "JPEG 2000 reference grid 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 return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, "Number of components must be 3 or 1"}; @@ -450,6 +570,18 @@ } } + // A JPEG 2000 codestream can declare its components as signed. OpenJPEG then returns + // negative sample values, but the planes created below hold unsigned samples. The cast + // in the copy loop turned a negative value into a sample far above the bit depth of the + // plane (e.g. -1 into 65535 in a 10-bit plane), and code that relies on the bit depth + // then read out of bounds (GHSA-q7mw-2fmm-5q94). + for (size_t c = 0; c < image->numcomps; c++) { + if (image->comps[c].sgnd) { + return {heif_error_Unsupported_feature, heif_suberror_Unsupported_data_version, + "JPEG 2000 images with signed components are not supported"}; + } + } + heif_error error = heif_image_create(width, height, colorspace, chroma, out_img); if (error.code) { return error; @@ -480,10 +612,15 @@ // TODO: a SIMD implementation to convert int32 to uint8 would speed this up // https://stackoverflow.com/questions/63774643/how-to-convert-uint32-to-uint8-using-simd-but-not-avx512 + // OpenJPEG clamps the decoded values of an unsigned component to the range of its + // precision. We clamp them nevertheless: a sample outside of the range of the plane's + // bit depth must not leave the plugin, whatever the library does. + const OPJ_INT32 max_value = (bit_depth < 31) ? ((OPJ_INT32{1} << bit_depth) - 1) : INT32_MAX; + if (bit_depth <= 8) { for (int y = 0; y < cheight; y++) { for (int x = 0; x < cwidth; x++) { - p[y * stride + x] = (uint8_t) opj_comp.data[y * cwidth + x]; + p[y * stride + x] = (uint8_t) std::clamp(opj_comp.data[y * cwidth + x], OPJ_INT32{0}, max_value); } } } @@ -491,7 +628,7 @@ uint16_t* p16 = (uint16_t*)p; for (int y = 0; y < cheight; y++) { for (int x = 0; x < cwidth; x++) { - p16[y * stride/2 + x] = (uint16_t) opj_comp.data[y * cwidth + x]; + p16[y * stride/2 + x] = (uint16_t) std::clamp(opj_comp.data[y * cwidth + x], OPJ_INT32{0}, max_value); } } } diff -Nru libheif-1.23.4/libheif/plugins/encoder_aom.cc libheif-1.23.6/libheif/plugins/encoder_aom.cc --- libheif-1.23.4/libheif/plugins/encoder_aom.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/plugins/encoder_aom.cc 2026-10-05 16:16:00.000000000 +0000 @@ -202,6 +202,10 @@ }; #if defined(AOM_HAVE_TUNE_IQ) +// libaom's handle_tuning() enables chroma delta-q for AOM_TUNE_IQ, and validate_config() +// then rejects that combination with lossless coding. Tracked upstream as +// https://aomedia.g-issues.chromium.org/issues/383595066 (still present in libaom v3.15.0). +// Once libaom lifts the restriction, this workaround can be dropped for those versions. static heif_error heif_error_lossless_with_tune_iq = { heif_error_Usage_error, heif_suberror_Invalid_parameter_value, @@ -1112,6 +1116,7 @@ // 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. + // See https://aomedia.g-issues.chromium.org/issues/383595066 if (!is_identity_matrix && !is_lossless && (cfg.g_usage == AOM_USAGE_ALL_INTRA || iq_supports_inter) && aom_version >= aom_version_3_13_0) { diff -Nru libheif-1.23.4/libheif/plugins/encoder_kvazaar.cc libheif-1.23.6/libheif/plugins/encoder_kvazaar.cc --- libheif-1.23.4/libheif/plugins/encoder_kvazaar.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/plugins/encoder_kvazaar.cc 2026-10-05 16:16:00.000000000 +0000 @@ -525,22 +525,39 @@ config->framerate_num = framerate_num; config->framerate_denom = framerate_denom; + // Set the input format through config_parse() instead of writing config->input_format directly. + // kvazaar git master (since July 2026) keeps the encoder chroma format in separate kvz_config + // fields that default to 4:2:0 and are only updated by the "input-format" parser. A direct + // assignment leaves them at 4:2:0, and the encoder then dereferences the NULL chroma planes of + // a 4:0:0 input picture. Released kvazaar versions derive the chroma format from input_format, + // so the parser works for them as well. The parser also rejects the chroma formats a kvazaar + // build does not support (4:2:2 and 4:4:4 in released versions), which becomes an error here + // instead of an encoder running in a mode it was not built for. + const char* input_format; if (isGreyscale) { - config->input_format = KVZ_FORMAT_P400; + input_format = "P400"; } else if (chroma == heif_chroma_420) { - config->input_format = KVZ_FORMAT_P420; + input_format = "P420"; } else if (chroma == heif_chroma_422) { - config->input_format = KVZ_FORMAT_P422; + input_format = "P422"; } else if (chroma == heif_chroma_444) { - config->input_format = KVZ_FORMAT_P444; + input_format = "P444"; } else { return heif_error{ heif_error_Encoder_plugin_error, heif_suberror_Unsupported_image_type, + kError_unsupported_chroma + }; + } + + if (!api->config_parse(config, "input-format", input_format)) { + return heif_error{ + heif_error_Encoder_plugin_error, + heif_suberror_Unsupported_image_type, kError_unsupported_chroma }; } diff -Nru libheif-1.23.4/libheif/plugins/nalu_utils.cc libheif-1.23.6/libheif/plugins/nalu_utils.cc --- libheif-1.23.4/libheif/plugins/nalu_utils.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/plugins/nalu_utils.cc 2026-10-05 16:16:00.000000000 +0000 @@ -23,6 +23,11 @@ #include #include "nalu_utils.h" +// Plugins are compiled with LIBHEIF_EXPORTS, so on MSVC a reference to the exported +// data object heif_error_success does not resolve against libheif (issue #1854). +// Use a file-local success object instead. +static const heif_error kSuccess = {heif_error_Ok, heif_suberror_Unspecified, "Success"}; + NalUnit::NalUnit() { nal_data_ptr = NULL; @@ -91,7 +96,7 @@ ptr += nal_size; } - return heif_error_success; + return kSuccess; } void NalMap::clear() { map.clear(); } \ No newline at end of file diff -Nru libheif-1.23.4/libheif/security_limits.cc libheif-1.23.6/libheif/security_limits.cc --- libheif-1.23.4/libheif/security_limits.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/security_limits.cc 2026-10-05 16:16:00.000000000 +0000 @@ -79,8 +79,13 @@ 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) + // The coding units of AVC (macroblock) and JPEG (4:2:0 MCU) are 16 pixels. Their margin + // is larger because the tightened limit is also given to the decoder plugin as its + // maximum picture size, and FFmpeg compares that with the picture width rounded up to + // its stride alignment (up to 64 pixels). With a margin of 16, FFmpeg refused images + // like 720x1280, since 768*1280 is more than (720+16)*(1280+16). + case heif_compression_AVC: return 64; + case heif_compression_JPEG: return 64; case heif_compression_JPEG2000: return 64; case heif_compression_HTJ2K: return 64; default: return 0; diff -Nru libheif-1.23.4/libheif/security_limits.h libheif-1.23.6/libheif/security_limits.h --- libheif-1.23.4/libheif/security_limits.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/security_limits.h 2026-10-05 16:16:00.000000000 +0000 @@ -105,6 +105,8 @@ // 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. +// For AVC and JPEG, this is 64 instead of their coding-unit size of 16, so that +// the margin also covers the stride alignment of the decoder (see the implementation). // Returns 0 for codecs without coding-unit padding (e.g. uncompressed). uint32_t max_coding_unit_size_for_codec(heif_compression_format format); @@ -115,7 +117,7 @@ // 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 +// (e.g. 128 for AV1/VVC, 64 for HEVC), see max_coding_unit_size_for_codec(). 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. // diff -Nru libheif-1.23.4/libheif/sequences/seq_boxes.cc libheif-1.23.6/libheif/sequences/seq_boxes.cc --- libheif-1.23.4/libheif/sequences/seq_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/sequences/seq_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -32,13 +32,12 @@ } -std::string Box_container::dump(Indent& indent) const +void Box_container::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); - sstr << dump_children(indent); + Box::dump(sstr, indent, full_log); + dump_children(sstr, indent, full_log); - return sstr.str(); + return; } @@ -91,10 +90,9 @@ } -std::string Box_mvhd::dump(Indent& indent) const +void Box_mvhd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "creation time: " << m_creation_time << "\n" << indent << "modification time: " << m_modification_time << "\n" << indent << "timescale: " << m_timescale << "\n" @@ -111,7 +109,7 @@ } sstr << indent << "next_track_ID: " << m_next_track_ID << "\n"; - return sstr.str(); + return; } @@ -216,10 +214,9 @@ } -std::string Box_tkhd::dump(Indent& indent) const +void Box_tkhd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); 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" @@ -243,7 +240,7 @@ sstr << indent << "width: " << get_width() << "\n" << indent << "height: " << get_height() << "\n"; - return sstr.str(); + return; } @@ -332,17 +329,16 @@ } -std::string Box_mdhd::dump(Indent& indent) const +void Box_mdhd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); 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(); + return; } @@ -407,10 +403,9 @@ } -std::string Box_vmhd::dump(Indent& indent) const +void Box_vmhd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "graphics mode: " << m_graphics_mode; if (m_graphics_mode == 0) { sstr << " (copy)"; @@ -418,7 +413,7 @@ sstr << "\n" << indent << "op color: " << m_op_color[0] << "; " << m_op_color[1] << "; " << m_op_color[2] << "\n"; - return sstr.str(); + return; } @@ -449,12 +444,11 @@ } -std::string Box_nmhd::dump(Indent& indent) const +void Box_nmhd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); - return sstr.str(); + return; } @@ -513,18 +507,17 @@ } -std::string Box_stsd::dump(Indent& indent) const +void Box_stsd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_sample_entries.size(); i++) { sstr << indent << "[" << i << "]\n"; indent++; - sstr << m_sample_entries[i]->dump(indent); + m_sample_entries[i]->dump(sstr, indent, full_log); indent--; } - return sstr.str(); + return; } @@ -625,15 +618,15 @@ } -std::string Box_stts::dump(Indent& indent) const +void Box_stts::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_entries.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break; sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", delta=" << m_entries[i].sample_delta << "\n"; } - return sstr.str(); + return; } @@ -819,15 +812,15 @@ } -std::string Box_ctts::dump(Indent& indent) const +void Box_ctts::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_entries.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break; sstr << indent << "[" << i << "] : cnt=" << m_entries[i].sample_count << ", offset=" << m_entries[i].sample_offset << "\n"; } - return sstr.str(); + return; } @@ -989,18 +982,18 @@ } -std::string Box_stsc::dump(Indent& indent) const +void Box_stsc::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_entries.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break; 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(); + return; } @@ -1091,15 +1084,15 @@ } -std::string Box_stco::dump(Indent& indent) const +void Box_stco::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_offsets.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_offsets.size())) break; sstr << indent << "[" << i << "] : 0x" << std::hex << m_offsets[i] << std::dec << "\n"; } - return sstr.str(); + return; } @@ -1193,13 +1186,13 @@ } -std::string Box_stsz::dump(Indent& indent) const +void Box_stsz::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); 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++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_sizes.size())) break; sstr << indent << "[" << i << "] : " << m_sample_sizes[i] << "\n"; } } @@ -1207,7 +1200,7 @@ sstr << indent << "fixed sample size: " << m_fixed_sample_size << "\n"; } - return sstr.str(); + return; } @@ -1288,15 +1281,15 @@ } -std::string Box_stss::dump(Indent& indent) const +void Box_stss::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); for (size_t i = 0; i < m_sync_samples.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sync_samples.size())) break; sstr << indent << "[" << i << "] : " << m_sync_samples[i] << "\n"; } - return sstr.str(); + return; } @@ -1382,9 +1375,8 @@ } -std::string VisualSampleEntry::dump(Indent& indent) const +void VisualSampleEntry::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; sstr << indent << "data reference index: " << data_reference_index << "\n" << indent << "width: " << width << "\n" << indent << "height: " << height << "\n" @@ -1394,7 +1386,7 @@ << indent << "compressorname: " << compressorname << "\n" << indent << "depth: " << depth << "\n"; - return sstr.str(); + return; } @@ -1414,13 +1406,12 @@ } -std::string Box_URIMetaSampleEntry::dump(Indent& indent) const +void Box_URIMetaSampleEntry::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); sstr << indent << "data reference index: " << data_reference_index << "\n"; - sstr << dump_children(indent); - return sstr.str(); + dump_children(sstr, indent, full_log); + return; } @@ -1452,13 +1443,12 @@ } -std::string Box_uri::dump(Indent& indent) const +void Box_uri::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "uri: " << m_uri << "\n"; - return sstr.str(); + return; } @@ -1495,17 +1485,16 @@ } -std::string Box_ccst::dump(Indent& indent) const +void Box_ccst::dump(std::ostream& sstr, Indent& indent, bool full_log) const { const auto& constraints = m_codingConstraints; - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "all ref pics intra: " << std::boolalpha < MAX_DUMP_ENTRIES) { + sstr << indent << "... (" << (m_entries.size() - MAX_DUMP_ENTRIES) << " more)\n"; + } sstr << indent << "total samples: " << total_samples << "\n"; - return sstr.str(); + return; } @@ -1757,10 +1748,9 @@ } -std::string Box_sgpd::dump(Indent& indent) const +void Box_sgpd::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "grouping_type: " << fourcc_to_string(m_grouping_type) << "\n"; if (m_default_length) { @@ -1771,6 +1761,7 @@ } for (size_t i=0; idump() << "\n"; @@ -1780,7 +1771,7 @@ } } - return sstr.str(); + return; } @@ -1863,16 +1854,15 @@ } -std::string Box_btrt::dump(Indent& indent) const +void Box_btrt::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); 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(); + return; } @@ -1961,10 +1951,9 @@ } -std::string Box_saiz::dump(Indent& indent) const +void Box_saiz::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "aux_info_type: "; if (m_aux_info_type == 0) { @@ -1992,11 +1981,12 @@ if (m_default_sample_info_size == 0) { for (size_t i = 0; i < m_sample_sizes.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_sizes.size())) break; sstr << indent << "[" << i << "] : " << ((int) m_sample_sizes[i]) << "\n"; } } - return sstr.str(); + return; } @@ -2106,10 +2096,9 @@ } -std::string Box_saio::dump(Indent& indent) const +void Box_saio::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "aux_info_type: "; if (m_aux_info_type == 0) { @@ -2128,10 +2117,11 @@ } for (size_t i = 0; i < m_chunk_offset.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_chunk_offset.size())) break; sstr << indent << "[" << i << "] : 0x" << std::hex << m_chunk_offset[i] << "\n"; } - return sstr.str(); + return; } @@ -2235,14 +2225,16 @@ } -std::string Box_sdtp::dump(Indent& indent) const +void Box_sdtp::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::stringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); assert(m_sample_information.size() <= UINT32_MAX); for (uint32_t i = 0; i < static_cast(m_sample_information.size()); i++) { + + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_sample_information.size())) break; + const char* spaces = " "; int nSpaces = 6; int k = i; @@ -2259,7 +2251,7 @@ << indent << spaces << "has_redundancy=" << (int) get_has_redundancy(i) << "\n"; } - return sstr.str(); + return; } @@ -2271,10 +2263,22 @@ // in the standard. Instead, we read until the end of the box. size_t nSamples = range.get_remaining_bytes(); + if (limits && limits->max_sequence_frames > 0 && nSamples > limits->max_sequence_frames) { + return { + heif_error_Memory_allocation_error, + heif_suberror_Security_limit_exceeded, + "Number of 'sdtp' samples exceeds the maximum number of sequence frames." + }; + } + + if (auto err = m_memory_handle.alloc(nSamples, limits, "the 'sdtp' table")) { + return err; + } + m_sample_information.resize(nSamples); range.read(m_sample_information.data(), nSamples); - return Error::Ok; + return range.get_error(); } @@ -2331,43 +2335,16 @@ m_references.push_back(ref); } - - // --- check for duplicate references - - if (auto error = check_for_double_references()) { - return error; - } + // Note: a track ID may appear several times within one reference type box. + // ISO/IEC 14496-12 does not forbid it and no consumer depends on uniqueness, + // so a redundant entry is tolerated instead of rejecting the whole file. 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) { @@ -2388,10 +2365,9 @@ } -std::string Box_tref::dump(Indent& indent) const +void Box_tref::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << Box::dump(indent); + Box::dump(sstr, indent, full_log); for (const auto& ref : m_references) { sstr << indent << "reference with type '" << fourcc_to_string(ref.reference_type) << "'" @@ -2402,7 +2378,7 @@ sstr << "\n"; } - return sstr.str(); + return; } @@ -2539,21 +2515,22 @@ } -std::string Box_elst::dump(Indent& indent) const +void Box_elst::dump(std::ostream& sstr, Indent& indent, bool full_log) const { - std::ostringstream sstr; - sstr << FullBox::dump(indent); + FullBox::dump(sstr, indent, full_log); sstr << indent << "repeat list: " << ((get_flags() & Flags::Repeat_EditList) ? "yes" : "no") << "\n"; - for (const auto& entry : m_entries) { + for (size_t i = 0; i < m_entries.size(); i++) { + if (dump_reached_entry_limit(sstr, indent, full_log, i, m_entries.size())) break; + const auto& entry = m_entries[i]; 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(); + return; } void Box_elst::derive_box_version() diff -Nru libheif-1.23.4/libheif/sequences/seq_boxes.h libheif-1.23.6/libheif/sequences/seq_boxes.h --- libheif-1.23.4/libheif/sequences/seq_boxes.h 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/sequences/seq_boxes.h 2026-10-05 16:16:00.000000000 +0000 @@ -38,7 +38,7 @@ set_short_type(fourcc(type)); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; protected: Error parse(BitstreamRange& range, const heif_security_limits*) override; @@ -62,7 +62,7 @@ set_short_type(fourcc("mvhd")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Movie Header"; } @@ -142,7 +142,7 @@ Track_size_is_aspect_ratio = 0x08 }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Track Header"; } @@ -209,7 +209,7 @@ set_short_type(fourcc("mdhd")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Media Header"; } @@ -258,7 +258,7 @@ set_flags(1); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Video Media Header"; } @@ -282,7 +282,7 @@ set_flags(1); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Null Media Header"; } @@ -310,7 +310,7 @@ set_short_type(fourcc("stsd")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Description"; } @@ -348,7 +348,7 @@ set_short_type(fourcc("stts")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Decoding Time to Sample"; } @@ -394,7 +394,7 @@ set_short_type(fourcc("ctts")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Composition Time to Sample"; } @@ -434,7 +434,7 @@ set_short_type(fourcc("stsc")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample to Chunk"; } @@ -478,7 +478,7 @@ set_short_type(fourcc("stco")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Offset"; } @@ -511,7 +511,7 @@ set_short_type(fourcc("stsz")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Size"; } @@ -546,7 +546,7 @@ set_short_type(fourcc("stss")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sync Sample"; } @@ -585,7 +585,7 @@ set_short_type(fourcc("ccst")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Coding Constraints"; } @@ -610,7 +610,7 @@ set_short_type(fourcc("auxi")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Auxiliary Info Type"; } @@ -659,7 +659,7 @@ Error write(StreamWriter& writer) const; - std::string dump(Indent&) const; + void dump(std::ostream&, Indent&, bool full_log) const; }; @@ -667,7 +667,7 @@ public: Error write(StreamWriter& writer) const override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const VisualSampleEntry& get_VisualSampleEntry_const() const { return m_visualSampleEntry; } @@ -690,7 +690,7 @@ Error write(StreamWriter& writer) const override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "URI Meta Sample Entry"; } @@ -717,7 +717,7 @@ Error write(StreamWriter& writer) const override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "URI"; } @@ -739,7 +739,7 @@ void derive_box_version() override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample to Group"; } @@ -800,7 +800,7 @@ void derive_box_version() override; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Group Description"; } @@ -831,7 +831,7 @@ set_short_type(fourcc("btrt")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Bitrate"; } @@ -870,7 +870,7 @@ uint32_t get_num_samples() const { return m_num_samples; } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Auxiliary Information Sizes"; } @@ -910,7 +910,7 @@ uint64_t get_chunk_offset(uint32_t idx) const; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Sample Auxiliary Information Offsets"; } @@ -942,7 +942,7 @@ set_short_type(fourcc("sdtp")); } - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Independent and Disposable Samples"; } @@ -961,6 +961,7 @@ private: std::vector m_sample_information; + MemoryHandle m_memory_handle; }; @@ -977,7 +978,7 @@ std::vector to_track_id; }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Track Reference"; } @@ -996,8 +997,6 @@ Error write(StreamWriter& writer) const override; - Error check_for_double_references() const; - private: std::vector m_references; }; @@ -1024,7 +1023,7 @@ Repeat_EditList = 0x01 }; - std::string dump(Indent&) const override; + void dump(std::ostream&, Indent&, bool full_log) const override; const char* debug_box_name() const override { return "Edit List"; } diff -Nru libheif-1.23.4/libheif/sequences/track_visual.cc libheif-1.23.6/libheif/sequences/track_visual.cc --- libheif-1.23.4/libheif/sequences/track_visual.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/libheif/sequences/track_visual.cc 2026-10-05 16:16:00.000000000 +0000 @@ -53,6 +53,25 @@ return parentLoadError; } + // Every chunk of a visual track must have a decoder attached. Track::load() + // attaches one only when the chunk's sample description is a VisualSampleEntry. + // A 'pict'/'vide'/'auxv' track whose 'stsd' also carries a non-visual entry (for + // example an 'mp4a' audio entry) and maps a chunk to it via 'stsc' produces a + // decoder-less chunk. The decode loop (decode_next_image_sample) guarded that + // pointer only with assert(), so on an NDEBUG build it was a NULL-pointer + // dereference through a virtual call (GHSA-8p2c-9wv4-rx4p). Reject the file here, + // before any decoding. This is scoped to the visual track and does not affect + // metadata tracks, which legitimately have non-visual sample entries. + for (const auto& chunk : m_chunks) { + if (!chunk->get_decoder()) { + return { + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Image sequence track has a chunk that references a non-visual sample description." + }; + } + } + const std::vector& chunk_offsets = m_stco->get_offsets(); // Find sequence resolution @@ -205,7 +224,17 @@ const std::shared_ptr& chunk = m_chunks[chunk_idx]; auto decoder = chunk->get_decoder(); - assert(decoder); + if (!decoder) { + // Should not happen: Track_Visual::load() rejects any visual track that has a + // decoder-less chunk. Guard anyway so that a future regression surfaces as a + // clean error instead of a NULL-pointer dereference through the virtual call + // below (GHSA-8p2c-9wv4-rx4p). + return Error{ + heif_error_Invalid_input, + heif_suberror_Unspecified, + "Sequence chunk has no decoder." + }; + } // avoid calling get_decoded_frame() before starting the decoder. if (m_next_sample_to_be_decoded != 0) { @@ -286,7 +315,11 @@ // Postprocess decoded image, attach metadata. if (m_stts) { - image->set_sample_duration(m_stts->get_sample_duration(sample_idx_in_chunk)); + // 'sample_idx_in_chunk' follows m_next_sample_to_be_decoded, which runs ahead of the output + // by the decoder latency. Look the duration up by the output position instead, like + // Track::get_next_sample_raw_data() does. + const SampleTiming& outputTiming = m_presentation_timeline[m_next_sample_to_be_output % m_presentation_timeline.size()]; + image->set_sample_duration(m_stts->get_sample_duration(outputTiming.sampleIdx)); } // --- assign alpha if we have an assigned alpha track @@ -651,9 +684,10 @@ { auto encoder = m_chunks.back()->get_encoder(); - std::optional encodingResult = encoder->encode_sequence_get_data(); + std::optional encodingResult = encoder->encode_sequence_extract_data(); if (!encodingResult) { - return {}; + // nothing pending in the encoder + return {false}; } const Encoder::CodedImageData& data = *encodingResult; @@ -754,9 +788,14 @@ config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_MainStillPicture)) { return heif_brand2_hevc; } - else { + + if (config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_Main10) || + config.is_profile_compatible(HEVCDecoderConfigurationRecord::Profile_RExt)) { return heif_brand2_hevx; } + + // There is no brand for the other profiles of HEVC (ISO/IEC 23008-12, B.4.2.2). + return 0; } case fourcc("avc1"): diff -Nru libheif-1.23.4/scripts/run-ci.sh libheif-1.23.6/scripts/run-ci.sh --- libheif-1.23.4/scripts/run-ci.sh 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/scripts/run-ci.sh 2026-10-05 16:16:00.000000000 +0000 @@ -156,6 +156,20 @@ # compilation mode CMAKE_OPTIONS="$CMAKE_OPTIONS -DCMAKE_BUILD_TYPE=Release" +# Run the test suite with the libstdc++ hardening assertions (container bounds checks, access +# to a disengaged std::optional, ...). They are independent of NDEBUG, so they also fire in +# this Release build. Distributions like Fedora build with this flag; that is how the crash +# fixed in #1907 was found. +# With the assertions enabled, GCC 13 at -O3 reports bogus -Wstringop-overflow errors for +# std::vector range inserts all over the tree ("writing N bytes into a region of size 0" in +# stl_algobase.h). The same build is warning-free without the assertions, so keep that +# diagnostic visible but non-fatal here. +# Only the test job gets the flag: the other jobs run more compiler versions, which would each +# need to be checked for further false positives under -Werror. +if [ ! -z "$TESTS" ]; then + export CXXFLAGS="${CXXFLAGS:+$CXXFLAGS }-D_GLIBCXX_ASSERTIONS -Wno-error=stringop-overflow" +fi + if [ ! -z "$FUZZER" ] && [ "$CURRENT_OS" = "linux" ]; then export ASAN_SYMBOLIZER="$BUILD_ROOT/clang/bin/llvm-symbolizer" diff -Nru libheif-1.23.4/tests/CMakeLists.txt libheif-1.23.6/tests/CMakeLists.txt --- libheif-1.23.4/tests/CMakeLists.txt 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/CMakeLists.txt 2026-10-05 16:16:00.000000000 +0000 @@ -43,19 +43,30 @@ message(WARNING "Several unit tests have been disabled because they can only be compiled with full symbol visibility (WITH_REDUCED_VISIBILITY=OFF)") else() add_libheif_test(bitstream_tests) + add_libheif_test(nal_split) 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(conversion_operator_contract) + add_libheif_test(plane_layout) + add_libheif_test(fill_channel) add_libheif_test(duplicate_alpha_channel) add_libheif_test(idat) +add_libheif_test(box_dump_limit) +add_libheif_test(sdtp_sample_limit) add_libheif_test(scale_plane_checks) add_libheif_test(crop_plane_checks) add_libheif_test(extract_area_plane_checks) + add_libheif_test(extend_to_size_checks) + add_libheif_test(pixelimage_overlay) + add_libheif_test(overlay_spec_parse) + add_libheif_test(add_channel_checks) add_libheif_test(jpeg2000) add_libheif_test(avc_box) add_libheif_test(hevc_sps) + add_libheif_test(hevc_brands) add_libheif_test(file_layout) add_libheif_test(image_description_metadata) add_libheif_test(exception_guard) @@ -75,6 +86,11 @@ if (NOT WITH_REDUCED_VISIBILITY AND WITH_UNCOMPRESSED_CODEC) add_libheif_test(uncompressed_box) + + # Companion of uncompressed_interleaved_alpha_plane below. It drives the + # uncompressed encoder factory with a HeifPixelImage assembled through + # internal calls, so it needs full symbol visibility. + add_libheif_test(uncompressed_interleaved_alpha_plane_internal) endif() # --- tests that only access the public API @@ -82,15 +98,26 @@ add_libheif_test(clap_decode) add_libheif_test(encode) add_libheif_test(encode_grid) +add_libheif_test(encode_hevc_bitdepth) add_libheif_test(entity_groups) add_libheif_test(extended_type) +add_libheif_test(zero_length_memcpy) add_libheif_test(grid_tile_missing) add_libheif_test(iden_declared_size) +add_libheif_test(inband_coded_size_limit) +add_libheif_test(jpeg2000_openjpeg_grid_limit) +add_libheif_test(jpeg2000_signed_components) add_libheif_test(item_properties) add_libheif_test(item_writing) add_libheif_test(overlay_amplification) +add_libheif_test(overlay_offsets) add_libheif_test(error_item_decode) add_libheif_test(alpha_cycle_deadlock) +add_libheif_test(alpha_composite_decode) +add_libheif_test(encode_plane_layout) +add_libheif_test(encode_handle_queries) +add_libheif_test(decoded_bit_depth_mismatch) +add_libheif_test(write_to_file) add_libheif_test(parallel_grid_deadlock) # The deadlock regression tests decode on a worker thread guarded by a timeout. find_package(Threads REQUIRED) @@ -101,7 +128,10 @@ add_libheif_test(sequence_no_track) add_libheif_test(sequence_null_options) add_libheif_test(sequence_timing_overflow) +add_libheif_test(sequence_vfr_durations) +add_libheif_test(track_reference_duplicates) add_libheif_test(sequence_raw_sample_errors) +add_libheif_test(sequence_nonvisual_sample_entry) add_libheif_test(tai) add_libheif_test(text) add_libheif_test(cxx_wrapper) @@ -124,6 +154,7 @@ add_libheif_test(mini_box) endif() add_libheif_test(mini_decode) +add_libheif_test(mini_security_limits) if (NOT WITH_REDUCED_VISIBILITY) add_libheif_test(omaf_boxes) @@ -141,13 +172,22 @@ add_libheif_test(uncompressed_decode_ycbcr420) add_libheif_test(uncompressed_decode_ycbcr422) add_libheif_test(uncompressed_sequence_odd_chroma) + add_libheif_test(uncompressed_sequence_tiles) add_libheif_test(uncompressed_mixed_chroma_depth_overflow) + add_libheif_test(uncompressed_mixed_interleave_bit_depth) + add_libheif_test(uncompressed_grid_wide_alpha) + add_libheif_test(uncompressed_grid_tile_mismatch) add_libheif_test(uncompressed_mixed_chroma_depth_colorconv) + add_libheif_test(uncompressed_alpha_composite_depth_mismatch) + add_libheif_test(uncompressed_mixed_rgb_depth_to_sdr) + add_libheif_test(uncompressed_bayer_depth_conversion) 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) + add_libheif_test(uncompressed_interleaved_alpha_plane) + add_libheif_test(tiling_combined_images) if (ENABLE_EXPERIMENTAL_FEATURES) add_libheif_test(uncompressed_encode_multicomponent) @@ -156,6 +196,8 @@ if (ZLIB_FOUND) add_libheif_test(uncompressed_decode_generic_compression) add_libheif_test(uncompressed_tile_range_overflow) + add_libheif_test(uncompressed_tile_data_fetch) + add_libheif_test(uncompressed_generic_compression_tiles) else() message(WARNING "Generic compress tests of the 'uncompressed codec' are not compiled because zlib was not found") endif () diff -Nru libheif-1.23.4/tests/add_channel_checks.cc libheif-1.23.6/tests/add_channel_checks.cc --- libheif-1.23.4/tests/add_channel_checks.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/add_channel_checks.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,195 @@ +/* + 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 the Cb/Cr size check in HeifPixelImage::add_channel(). +// +// A Cb/Cr plane must have exactly the chroma-subsampled size of the logical image +// (round_up). Building one at a different size makes the plane's allocation disagree +// with the size other code derives from the logical dimensions. An oversized plane +// let extend_to_size_with_zero() underflow its per-row fill length and write ~4 GB +// past the plane (GHSA-j2rv-58fh-w8pw); an undersized plane caused out-of-bounds +// reads during RGB conversion (issue #1796). add_channel() now rejects the mismatch +// at construction. Only Cb/Cr are constrained; alpha/depth/... may differ. + +TEST_CASE("add_channel rejects an oversized Cb plane (GHSA-j2rv-58fh-w8pw)") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(12, 8, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 12, 8, 8, limits).error_code == heif_error_Ok); + + // The subsampled Cb size of a 12x8 4:2:0 image is 6x4; 32x8 is far too large. + Error err = image->add_channel(heif_channel_Cb, 32, 8, 8, limits); + REQUIRE(err.error_code == heif_error_Usage_error); + REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value); +} + +TEST_CASE("add_channel rejects an undersized (round-down) chroma plane") { + auto* limits = heif_get_global_security_limits(); + + // Odd dimensions: the round_up subsampled size is 7x5, so a floor-division 6x4 + // plane (as an out-of-spec producer might compute) must be rejected. + auto image = std::make_shared(); + image->create(13, 9, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 13, 9, 8, limits).error_code == heif_error_Ok); + + REQUIRE(image->add_channel(heif_channel_Cb, 6, 4, 8, limits).error_code == heif_error_Usage_error); + REQUIRE(image->add_channel(heif_channel_Cr, 6, 4, 8, limits).error_code == heif_error_Usage_error); +} + +TEST_CASE("add_channel accepts correctly-subsampled chroma planes") { + auto* limits = heif_get_global_security_limits(); + + SECTION("4:2:0, even dimensions") { + auto image = std::make_shared(); + image->create(12, 8, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 12, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 6, 4, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 6, 4, 8, limits).error_code == heif_error_Ok); + } + + SECTION("4:2:0, odd dimensions round up") { + auto image = std::make_shared(); + image->create(13, 9, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 13, 9, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 7, 5, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 7, 5, 8, limits).error_code == heif_error_Ok); + } + + SECTION("4:2:2 subsamples width only") { + auto image = std::make_shared(); + image->create(12, 8, heif_colorspace_YCbCr, heif_chroma_422); + REQUIRE(image->add_channel(heif_channel_Y, 12, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 6, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 6, 8, 8, limits).error_code == heif_error_Ok); + } + + SECTION("4:4:4 chroma is full size") { + auto image = std::make_shared(); + image->create(12, 8, heif_colorspace_YCbCr, heif_chroma_444); + REQUIRE(image->add_channel(heif_channel_Y, 12, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 12, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 12, 8, 8, limits).error_code == heif_error_Ok); + } +} + +TEST_CASE("add_channel does not constrain non-chroma auxiliary planes") { + auto* limits = heif_get_global_security_limits(); + + // The size rule is Cb/Cr only. An auxiliary plane such as depth may legitimately + // have a size unrelated to the colour planes, so add_channel must still accept it. + auto image = std::make_shared(); + image->create(12, 8, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(image->add_channel(heif_channel_Y, 12, 8, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cb, 6, 4, 8, limits).error_code == heif_error_Ok); + REQUIRE(image->add_channel(heif_channel_Cr, 6, 4, 8, limits).error_code == heif_error_Ok); + + REQUIRE(image->add_channel(heif_channel_depth, 5, 5, 8, limits).error_code == heif_error_Ok); +} + +// The interleaved chroma formats define the bit depth of their components: 8 bits for RGB +// and RGBA, 9 to 16 bits (stored as 16-bit words) for the RRGGBB formats. add_channel() +// accepted any bit depth for them. An interleaved plane with 64-bit or 128-bit components +// has pixels of more than 255 bits, for which get_storage_bits_per_pixel() ran into its +// assertion, or, without assertions, reported a truncated size (384 bits as 128, and 256 +// bits as 0, which the API turned into "no such channel"). +TEST_CASE("add_channel checks the bit depth of interleaved formats") { + auto* limits = heif_get_global_security_limits(); + + auto add = [limits](heif_chroma chroma, int bit_depth, std::shared_ptr* out_image = nullptr) { + auto image = std::make_shared(); + image->create(16, 16, heif_colorspace_RGB, chroma); + Error err = image->add_channel(heif_channel_interleaved, 16, 16, bit_depth, limits); + if (out_image) { + *out_image = image; + } + return err; + }; + + SECTION("RGB and RGBA have 8 bits per component") { + for (heif_chroma chroma : {heif_chroma_interleaved_RGB, heif_chroma_interleaved_RGBA}) { + const int num_components = (chroma == heif_chroma_interleaved_RGB) ? 3 : 4; + INFO("chroma " << chroma); + + std::shared_ptr image; + REQUIRE(!add(chroma, 8, &image)); + CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == 8); + CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 8 * num_components); + + // for backwards compatibility, the size of the whole pixel is accepted as well + REQUIRE(!add(chroma, 8 * num_components, &image)); + CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == 8); + CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 8 * num_components); + + for (int bit_depth : {1, 7, 9, 10, 16, 64, 128}) { + INFO("bit depth " << bit_depth); + Error err = add(chroma, bit_depth); + CHECK(err.error_code == heif_error_Usage_error); + CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value); + } + } + } + + SECTION("the RRGGBB formats have 9 to 16 bits per component") { + for (heif_chroma chroma : {heif_chroma_interleaved_RRGGBB_LE, heif_chroma_interleaved_RRGGBB_BE, + heif_chroma_interleaved_RRGGBBAA_LE, heif_chroma_interleaved_RRGGBBAA_BE}) { + const int num_components = (chroma == heif_chroma_interleaved_RRGGBB_LE || + chroma == heif_chroma_interleaved_RRGGBB_BE) ? 3 : 4; + INFO("chroma " << chroma); + + for (int bit_depth : {9, 10, 12, 16}) { + INFO("bit depth " << bit_depth); + std::shared_ptr image; + REQUIRE(!add(chroma, bit_depth, &image)); + CHECK(image->get_bits_per_pixel(heif_channel_interleaved) == bit_depth); + CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 16 * num_components); + } + + for (int bit_depth : {1, 8, 17, 32, 64, 128}) { + INFO("bit depth " << bit_depth); + Error err = add(chroma, bit_depth); + CHECK(err.error_code == heif_error_Usage_error); + CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value); + } + } + } + + SECTION("planes of other formats keep their wide components") { + auto image = std::make_shared(); + image->create(16, 16, heif_colorspace_custom, heif_chroma_planar); + REQUIRE(!image->add_channel(heif_channel_Y, 16, 16, 128, limits, heif_component_datatype_complex_number)); + CHECK(image->get_storage_bits_per_pixel(heif_channel_Y) == 128); + } + + SECTION("a channel that does not exist") { + auto image = std::make_shared(); + image->create(16, 16, heif_colorspace_RGB, heif_chroma_interleaved_RGB); + CHECK(image->get_storage_bits_per_pixel(heif_channel_interleaved) == 0); + } +} diff -Nru libheif-1.23.4/tests/alpha_composite_decode.cc libheif-1.23.6/tests/alpha_composite_decode.cc --- libheif-1.23.4/tests/alpha_composite_decode.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/alpha_composite_decode.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,127 @@ +/* + 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: decoding an image that has an alpha channel into a YCbCr target +// while compositing the alpha onto a background (what heif-dec does when it writes +// JPEG) crashed with a null-pointer write in Op_flatten_alpha_plane. The operator +// allocated its composited R/G/B planes with the depth recorded for those channels +// in the target ColorState, which is 0 for a YCbCr target. Decoding to an RGB +// target was unaffected because the planner converts to RGB before it flattens. +// +// The companion test in conversion.cc drives the operator directly on synthesized +// images. This one goes through the public decoding API on the shipped alpha samples +// (HEVC and AV1), so it also covers the planner route a real decode takes. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +namespace { + +struct Sample +{ + const char* filename; + heif_compression_format codec; +}; + +const Sample samples[] = { + {"simple_osm_tile_alpha.avif", heif_compression_AV1}, + {"with-alpha-512x512.heic", heif_compression_HEVC}, +}; + +void decode_composited(const char* filename, + heif_colorspace colorspace, heif_chroma chroma, + heif_alpha_composition_mode mode) +{ + INFO(filename << " -> colorspace " << colorspace << ", chroma " << chroma << ", mode " << mode); + + heif_context* ctx = get_context_for_test_file(filename); + heif_image_handle* handle = get_primary_image_handle(ctx); + REQUIRE(heif_image_handle_has_alpha_channel(handle)); + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + + heif_color_conversion_options_ext* ext = heif_color_conversion_options_ext_alloc(); + REQUIRE(ext != nullptr); + ext->alpha_composition_mode = mode; + ext->background_red = 0xFFFF; + ext->background_green = 0xFFFF; + ext->background_blue = 0xFFFF; + ext->secondary_background_red = 0x8000; + ext->secondary_background_green = 0x8000; + ext->secondary_background_blue = 0x8000; + ext->checkerboard_square_size = 16; + options->color_conversion_options_ext = ext; + + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, colorspace, chroma, options); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + CHECK(heif_image_get_colorspace(img) == colorspace); + CHECK(heif_image_get_chroma_format(img) == chroma); + + // The alpha has been composited onto the colour planes, so none is left. + CHECK(!heif_image_has_channel(img, heif_channel_Alpha)); + + heif_channel size_channel = (colorspace == heif_colorspace_YCbCr) ? heif_channel_Y : heif_channel_interleaved; + CHECK(heif_image_get_width(img, size_channel) == heif_image_handle_get_width(handle)); + CHECK(heif_image_get_height(img, size_channel) == heif_image_handle_get_height(handle)); + + heif_image_release(img); + heif_color_conversion_options_ext_free(ext); + heif_decoding_options_free(options); + heif_image_handle_release(handle); + heif_context_free(ctx); +} + +} // namespace + +TEST_CASE("decode with alpha compositing into a YCbCr target") +{ + int samples_tested = 0; + + for (const Sample& sample : samples) { + if (!heif_have_decoder_for_format(sample.codec)) { + continue; + } + samples_tested++; + + // heif-dec's default when the output format cannot store alpha. + decode_composited(sample.filename, heif_colorspace_YCbCr, heif_chroma_420, heif_alpha_composition_mode_checkerboard); + decode_composited(sample.filename, heif_colorspace_YCbCr, heif_chroma_420, heif_alpha_composition_mode_solid_color); + decode_composited(sample.filename, heif_colorspace_YCbCr, heif_chroma_444, heif_alpha_composition_mode_solid_color); + + // An RGB target takes the other planner route (convert first, flatten on RGB). + decode_composited(sample.filename, heif_colorspace_RGB, heif_chroma_interleaved_RGB, heif_alpha_composition_mode_checkerboard); + } + + if (samples_tested == 0) { + SKIP("neither an AV1 nor a HEVC decoder is available"); + } +} diff -Nru libheif-1.23.4/tests/avc_box.cc libheif-1.23.6/tests/avc_box.cc --- libheif-1.23.4/tests/avc_box.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/avc_box.cc 2026-10-05 16:16:00.000000000 +0000 @@ -63,7 +63,7 @@ REQUIRE(avcC->getPictureParameterSets().size() == 1); REQUIRE(avcC->getPictureParameterSets()[0].size() == 7); Indent indent; - std::string dumpResult = box->dump(indent); + std::string dumpResult = box->dump_to_string(indent); REQUIRE(dumpResult == "Box: avcC -----\n" "size: 52 (header size: 8)\n" "configuration_version: 1\n" diff -Nru libheif-1.23.4/tests/box_dump_limit.cc libheif-1.23.6/tests/box_dump_limit.cc --- libheif-1.23.4/tests/box_dump_limit.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/box_dump_limit.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,110 @@ +/* + 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. +*/ + +// Box::dump() is a debugging aid. Boxes whose content scales with the file size +// (here the 'sdtp' sample-dependency table, one entry per byte) print four lines +// per entry, and a 'j2kH' container copies that text at every nesting level. A +// ~1 MB file therefore produced a ~200 MB dump string, which timed out the +// box_fuzzer (OSS-Fuzz, GHSA-h4h8-qgvc-m7r2). +// +// dump() now (a) writes into a single stream instead of returning and +// re-concatenating a string at every level, and (b) prints at most +// MAX_DUMP_ENTRIES entries per box unless full_log is set. This test builds a +// 'j2kH' holding an 'sdtp' with many entries and checks that the default dump is +// small while the full dump still contains every entry. + +#include "catch_amalgamated.hpp" +#include "box.h" +#include "logging.h" + +#include +#include +#include + +namespace { + +void append_u32(std::vector& v, uint32_t x) +{ + v.push_back(uint8_t(x >> 24)); + v.push_back(uint8_t(x >> 16)); + v.push_back(uint8_t(x >> 8)); + v.push_back(uint8_t(x)); +} + +// A 'j2kH' box containing an 'sdtp' box with `num_samples` one-byte entries. +std::vector build_j2kH_with_sdtp(uint32_t num_samples) +{ + std::vector sdtp; + append_u32(sdtp, 8 + 4 + num_samples); // box size + for (char c : std::string("sdtp")) sdtp.push_back(uint8_t(c)); + append_u32(sdtp, 0); // FullBox version/flags + sdtp.insert(sdtp.end(), num_samples, 0); // one byte per sample + + std::vector j2kH; + append_u32(j2kH, 8 + uint32_t(sdtp.size())); // box size + for (char c : std::string("j2kH")) j2kH.push_back(uint8_t(c)); + j2kH.insert(j2kH.end(), sdtp.begin(), sdtp.end()); + + return j2kH; +} + +std::shared_ptr read_first_box(const std::vector& data) +{ + auto reader = std::make_shared(data.data(), data.size(), false); + BitstreamRange range(reader, data.size()); + + std::shared_ptr box; + Error err = Box::read(range, &box, heif_get_global_security_limits()); + REQUIRE(err == Error::Ok); + REQUIRE(box); + return box; +} + +} // namespace + + +TEST_CASE("box dump: verbose boxes are bounded unless full_log is set") +{ + const uint32_t num_samples = 50000; // would be 4 lines each in the dump + std::shared_ptr box = read_first_box(build_j2kH_with_sdtp(num_samples)); + + Indent indent_default; + std::string dflt = box->dump_to_string(indent_default); + + Indent indent_full; + std::string full = box->dump_to_string(indent_full, true); + + // The default dump stops after MAX_DUMP_ENTRIES entries, so it stays small + // regardless of the number of samples, and shows the truncation note. + REQUIRE(dflt.size() < 16 * 1024); + REQUIRE(dflt.find("more") != std::string::npos); + + // The full dump contains an entry for every sample and is therefore much + // larger. It must not contain the truncation note. + REQUIRE(full.size() > num_samples); + REQUIRE(full.find("more") == std::string::npos); + REQUIRE(full.find("[" + std::to_string(num_samples - 1) + "]") != std::string::npos); +} diff -Nru libheif-1.23.4/tests/conversion.cc libheif-1.23.6/tests/conversion.cc --- libheif-1.23.4/tests/conversion.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/conversion.cc 2026-10-05 16:16:00.000000000 +0000 @@ -28,8 +28,12 @@ #include "catch_amalgamated.hpp" #include "color-conversion/colorconversion.h" #include "color-conversion/hdr_sdr.h" +#include "color-conversion/monochrome.h" +#include "color-conversion/rgb2yuv.h" +#include "color-conversion/yuv2rgb.h" #include "image/pixelimage.h" #include +#include // Enable for more verbose test output. constexpr bool kEnableDebugOutput = false; @@ -172,17 +176,17 @@ std::vector planes; if (state.colorspace == heif_colorspace_monochrome) { if (state.chroma != heif_chroma_monochrome) return {}; - planes.push_back({heif_channel_Y, width, height, state.bits_per_pixel}); - if (state.has_alpha) { + planes.push_back({heif_channel_Y, width, height, state.bits_per_pixel_Y}); + if (state.has_alpha()) { planes.push_back( - {heif_channel_Alpha, width, height, state.bits_per_pixel}); + {heif_channel_Alpha, width, height, state.bits_per_pixel_alpha}); } } else if (state.colorspace == heif_colorspace_YCbCr) { if (state.chroma != heif_chroma_444 && state.chroma != heif_chroma_422 && state.chroma != heif_chroma_420 && state.chroma != heif_chroma_monochrome) { return {}; } - planes.push_back({heif_channel_Y, width, height, state.bits_per_pixel}); + planes.push_back({heif_channel_Y, width, height, state.bits_per_pixel_Y}); if (state.chroma != heif_chroma_monochrome) { int chroma_width = state.chroma == heif_chroma_444 ? width : width / 2; int chroma_height = @@ -190,13 +194,13 @@ ? height : height / 2; planes.push_back( - {heif_channel_Cb, chroma_width, chroma_height, state.bits_per_pixel}); + {heif_channel_Cb, chroma_width, chroma_height, state.bits_per_pixel_Cb}); planes.push_back( - {heif_channel_Cr, chroma_width, chroma_height, state.bits_per_pixel}); + {heif_channel_Cr, chroma_width, chroma_height, state.bits_per_pixel_Cr}); } - if (state.has_alpha) { + if (state.has_alpha()) { planes.push_back( - {heif_channel_Alpha, width, height, state.bits_per_pixel}); + {heif_channel_Alpha, width, height, state.bits_per_pixel_alpha}); } } else if (state.colorspace == heif_colorspace_RGB) { // heif_chroma_planar is a synonym for heif_chroma_444 in the RGB @@ -212,16 +216,17 @@ return {}; } 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}); - if (state.has_alpha) { + planes.push_back({heif_channel_R, width, height, state.bits_per_pixel_R}); + planes.push_back({heif_channel_G, width, height, state.bits_per_pixel_G}); + planes.push_back({heif_channel_B, width, height, state.bits_per_pixel_B}); + if (state.has_alpha()) { planes.push_back( - {heif_channel_Alpha, width, height, state.bits_per_pixel}); + {heif_channel_Alpha, width, height, state.bits_per_pixel_alpha}); } } else { + // Interleaved formats are represented by their per-component depth in R/G/B. planes.push_back( - {heif_channel_interleaved, width, height, state.bits_per_pixel}); + {heif_channel_interleaved, width, height, state.bits_per_pixel_R}); } } else { return {}; // Unsupported colorspace. @@ -321,16 +326,15 @@ REQUIRE(out_image != nullptr); CHECK(out_image->get_colorspace() == target_state.colorspace); CHECK(out_image->get_chroma_format() == target_state.chroma); - CHECK(out_image->has_alpha() == target_state.has_alpha); + CHECK(out_image->has_alpha() == target_state.has_alpha()); for (const Plane& plane : GetPlanes(target_state, width, height)) { INFO("Channel: " << plane.channel); size_t stride; CHECK(out_image->get_channel_memory(plane.channel, &stride) != nullptr); - CHECK(out_image->get_bits_per_pixel(plane.channel) == - target_state.bits_per_pixel); + CHECK(out_image->get_bits_per_pixel(plane.channel) == plane.bit_depth); // If an alpha plane was created from nothing, check that it's filled // with the max alpha value. - if (plane.channel == heif_channel_Alpha && !input_state.has_alpha) { + if (plane.channel == heif_channel_Alpha && !input_state.has_alpha()) { double alpha_psnr = GetPsnr(*out_image, *out_image, heif_channel_Alpha, /*expect_alpha_max=*/true); REQUIRE(alpha_psnr == 100.f); @@ -347,10 +351,10 @@ std::shared_ptr recovered_image = *recovered_image_result; // If the alpha plane was lost in the target state, it should come back // as the max value for the given bpp, i.e. (1<& img, heif_channel channel, int w, int h, const std::vector& pixels) +#ifdef HAVE_LIBSHARPYUV +// libsharpyuv uses the RGB sample values as indices into its gamma tables, which are sized +// for the bit depth it is told, and it does not check the range. A 10-bit or 12-bit plane is +// stored in 16-bit words, so nothing keeps an application (or a decoder that hands through +// padding bits) from storing a larger value. The operator has to refuse such an image +// instead of passing it on: the table lookup went far out of bounds otherwise. +TEST_CASE("Sharp yuv refuses sample values above the bit depth", "[heif_image]") { - auto error = img->add_channel(channel, w, h, 8, nullptr); + 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; + + nclx_profile target_nclx = nclx_profile::defaults(); + target_nclx.set_matrix_coefficients(heif_matrix_coefficients_ITU_R_BT_601_6); + + const uint16_t probe = 1; + const bool big_endian = (*reinterpret_cast(&probe) == 0); + const heif_chroma native_rrggbb = big_endian ? heif_chroma_interleaved_RRGGBB_BE + : heif_chroma_interleaved_RRGGBB_LE; + + for (bool interleaved : {false, true}) { + for (int bpp : {10, 12, 16}) { + const uint16_t max_value = static_cast((1 << bpp) - 1); + + // The largest valid value, the first invalid one, and the value of the original report. + for (uint16_t value : {max_value, static_cast(max_value + 1), uint16_t{0x5a5a}}) { + // one sample out of range is enough, wherever it is + for (bool whole_plane : {true, false}) { + INFO("interleaved=" << interleaved << " bpp=" << bpp << " value=" << value + << " whole_plane=" << whole_plane); + + auto img = std::make_shared(); + heif_channel last_channel; + + if (interleaved) { + img->create(width, height, heif_colorspace_RGB, native_rrggbb); + REQUIRE(!img->fill_new_channel(heif_channel_interleaved, whole_plane ? value : 0, + width, height, bpp, nullptr)); + last_channel = heif_channel_interleaved; + } + else { + img->create(width, height, heif_colorspace_RGB, heif_chroma_444); + for (heif_channel c : {heif_channel_R, heif_channel_G, heif_channel_B}) { + REQUIRE(!img->fill_new_channel(c, whole_plane ? value : 0, width, height, bpp, nullptr)); + } + last_channel = heif_channel_B; + } + + if (!whole_plane) { + // only the very last sample of the last plane + size_t stride; + uint8_t* p = img->get_channel_memory(last_channel, &stride); + size_t samples_per_row = static_cast(width) * (interleaved ? 3 : 1); + reinterpret_cast(p + (height - 1) * stride)[samples_per_row - 1] = value; + } + + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + target_nclx, 8, sharp_options, nullptr, + heif_get_disabled_security_limits()); + + if (value <= max_value) { + REQUIRE(result); + CHECK((*result)->get_chroma_format() == heif_chroma_420); + } + else { + REQUIRE(!result); + CHECK(result.error().error_code == heif_error_Invalid_input); + } + } + } + } + } +} +#endif + + +static void fill_plane(std::shared_ptr& img, heif_channel channel, int w, int h, const std::vector& pixels, int bit_depth = 8) +{ + auto error = img->add_channel(channel, w, h, bit_depth, nullptr); REQUIRE(!error); size_t stride; @@ -658,26 +742,33 @@ for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - p[y * stride + x] = pixels[y * w + x]; + if (bit_depth <= 8) { + p[y * stride + x] = static_cast(pixels[y * w + x]); + } + else { + reinterpret_cast(p + y * stride)[x] = pixels[y * w + x]; + } } } } -static void assert_plane(std::shared_ptr& img, heif_channel channel, const std::vector& pixels) +static void assert_plane(std::shared_ptr& img, heif_channel channel, const std::vector& pixels) { INFO("channel: " << channel); uint32_t w = img->get_width(channel); uint32_t h = img->get_height(channel); + int bit_depth = img->get_bits_per_pixel(channel); size_t stride; - uint8_t* p = img->get_channel_memory(channel, &stride); + const uint8_t* p = img->get_channel_memory(channel, &stride); for (uint32_t y = 0; y < h; y++) { INFO("row: " << y); for (uint32_t x = 0; x < w; x++) { INFO("column: " << x); - REQUIRE((int)p[y * stride + x] == (int)pixels[y * w + x]); + int value = (bit_depth <= 8) ? p[y * stride + x] : reinterpret_cast(p + y * stride)[x]; + REQUIRE(value == (int) pixels[y * w + x]); } } } @@ -690,17 +781,19 @@ .only_use_preferred_chroma_algorithm = true}; std::shared_ptr img = std::make_shared(); - img->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420); + img->create(6, 6, heif_colorspace_YCbCr, heif_chroma_420); - auto error = img->fill_new_channel(heif_channel_Y, 128, 4,4, 8, nullptr); + auto error = img->fill_new_channel(heif_channel_Y, 128, 6, 6, 8, nullptr); REQUIRE(!error); - fill_plane(img, heif_channel_Cb, 2,2, - {10, 40, - 100, 240}); - fill_plane(img, heif_channel_Cr, 2, 2, - {255, 200, - 50, 0}); + fill_plane(img, heif_channel_Cb, 3, 3, + {10, 40, 80, + 100, 240, 160, + 180, 200, 220}); + fill_plane(img, heif_channel_Cr, 3, 3, + {255, 200, 160, + 50, 0, 80, + 100, 40, 20}); auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, nclx_profile::defaults(), 8, options, nullptr, heif_get_disabled_security_limits()); @@ -709,22 +802,201 @@ assert_plane(out, heif_channel_Cb, { - 10, 18, 33, 40, - 33, 47, 76, 90, - 78, 106, 162, 190, - 100, 135, 205, 240 + 10, 18, 33, 50, 70, 80, + 33, 47, 76, 93, 98, 100, + 78, 106, 162, 178, 153, 140, + 120, 148, 203, 216, 189, 175, + 160, 173, 198, 209, 206, 205, + 180, 185, 195, 205, 215, 220 }); + assert_plane(out, heif_channel_Cr, + { + 255, 241, 214, 190, 170, 160, + 204, 190, 163, 148, 143, 140, + 101, 88, 63, 63, 88, 100, + 63, 49, 23, 24, 51, 65, + 88, 73, 44, 31, 34, 35, + 100, 85, 55, 35, 25, 20 + }); +} + + +// The right and bottom borders of an odd-sized image are not filled by the dedicated border loops. +// The last column and row have to be covered by the inner loop and the top/left border loops instead. +TEST_CASE("Bilinear upsampling of odd-sized image", "[heif_image]") +{ + heif_color_conversion_options options = { + .preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear, + .only_use_preferred_chroma_algorithm = true}; + + std::shared_ptr img = std::make_shared(); + img->create(5, 5, heif_colorspace_YCbCr, heif_chroma_420); + + auto error = img->fill_new_channel(heif_channel_Y, 128, 5, 5, 8, nullptr); + REQUIRE(!error); + + // Same chroma planes as in the 6x6 test above. The 5x5 output is the top-left crop of the 6x6 output. + fill_plane(img, heif_channel_Cb, 3, 3, + {10, 40, 80, + 100, 240, 160, + 180, 200, 220}); + fill_plane(img, heif_channel_Cr, 3, 3, + {255, 200, 160, + 50, 0, 80, + 100, 40, 20}); + + 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; + + assert_plane(out, heif_channel_Cb, + { + 10, 18, 33, 50, 70, + 33, 47, 76, 93, 98, + 78, 106, 162, 178, 153, + 120, 148, 203, 216, 189, + 160, 173, 198, 209, 206 + }); assert_plane(out, heif_channel_Cr, { - 255, 241, 214, 200, - 204, 190, 163, 150, - 101, 88, 63, 50, - 50, 38, 13, 0 + 255, 241, 214, 190, 170, + 204, 190, 163, 148, 143, + 101, 88, 63, 63, 88, + 63, 49, 23, 24, 51, + 88, 73, 44, 31, 34 }); } + +TEST_CASE("Bilinear upsampling 12 bit", "[heif_image]") +{ + heif_color_conversion_options options = { + .preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear, + .only_use_preferred_chroma_algorithm = true}; + + std::shared_ptr img = std::make_shared(); + img->create(4, 4, heif_colorspace_YCbCr, heif_chroma_420); + + auto error = img->fill_new_channel(heif_channel_Y, 2048, 4, 4, 12, nullptr); + REQUIRE(!error); + + fill_plane(img, heif_channel_Cb, 2, 2, + {0, 4095, + 4095, 0}, 12); + fill_plane(img, heif_channel_Cr, 2, 2, + {4095, 1000, + 2000, 3000}, 12); + + auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile::defaults(), 12, options, nullptr, heif_get_disabled_security_limits()); + REQUIRE(conversionResult); + std::shared_ptr out = *conversionResult; + + REQUIRE(out->get_bits_per_pixel(heif_channel_Cb) == 12); + + assert_plane(out, heif_channel_Cb, + { + 0, 1024, 3071, 4095, + 1024, 1536, 2559, 3071, + 3071, 2559, 1536, 1024, + 4095, 3071, 1024, 0 + }); + + assert_plane(out, heif_channel_Cr, + { + 4095, 3321, 1774, 1000, + 3571, 3053, 2018, 1500, + 2524, 2518, 2506, 2500, + 2000, 2250, 2750, 3000 + }); +} + + +// Reference implementation of 4:2:0 bilinear chroma upsampling. +// Chroma samples are located in the center of each 2x2 luma block. Each output pixel is interpolated +// from its nearest chroma sample (weight 3/4) and the next nearest one (weight 1/4) in each direction. +// At the image border, the missing neighbor is replaced by the nearest sample, i.e. the chroma plane is clamped. +static uint16_t reference_bilinear_chroma_sample(const std::vector& chroma, uint32_t chroma_w, uint32_t chroma_h, + uint32_t x, uint32_t y) +{ + auto taps = [](uint32_t pos, uint32_t chroma_size, uint32_t& near, uint32_t& far) { + near = pos / 2; + int f = (pos % 2 == 0) ? (int) near - 1 : (int) near + 1; + far = (uint32_t) std::clamp(f, 0, (int) chroma_size - 1); + }; + + uint32_t nx, fx, ny, fy; + taps(x, chroma_w, nx, fx); + taps(y, chroma_h, ny, fy); + + auto c = [&](uint32_t cx, uint32_t cy) { return (int) chroma[cy * chroma_w + cx]; }; + + int sum = 9 * c(nx, ny) + 3 * c(fx, ny) + 3 * c(nx, fy) + c(fx, fy); + return (uint16_t) ((sum + 8) / 16); +} + + +TEST_CASE("Bilinear upsampling matches reference for all small image sizes", "[heif_image]") +{ + heif_color_conversion_options options = { + .preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear, + .only_use_preferred_chroma_algorithm = true}; + + uint32_t seed = 12345; + auto next_random = [&seed](int bit_depth) { + seed = seed * 1103515245u + 12345u; + return (uint16_t) ((seed >> 8) & ((1u << bit_depth) - 1)); + }; + + for (int bit_depth: {8, 12}) { + for (uint32_t height = 1; height <= 9; height++) { + for (uint32_t width = 1; width <= 9; width++) { + INFO("bit depth: " << bit_depth << ", size: " << width << "x" << height); + + const uint32_t chroma_w = (width + 1) / 2; + const uint32_t chroma_h = (height + 1) / 2; + + std::vector cb(chroma_w * chroma_h), cr(chroma_w * chroma_h); + for (auto& v: cb) v = next_random(bit_depth); + for (auto& v: cr) v = next_random(bit_depth); + + std::shared_ptr img = std::make_shared(); + img->create(width, height, heif_colorspace_YCbCr, heif_chroma_420); + + auto error = img->fill_new_channel(heif_channel_Y, 1 << (bit_depth - 1), width, height, bit_depth, nullptr); + REQUIRE(!error); + + fill_plane(img, heif_channel_Cb, chroma_w, chroma_h, cb, bit_depth); + fill_plane(img, heif_channel_Cr, chroma_w, chroma_h, cr, bit_depth); + + auto conversionResult = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, + nclx_profile::defaults(), bit_depth, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(conversionResult); + std::shared_ptr out = *conversionResult; + + REQUIRE(out->get_width(heif_channel_Cb) == width); + REQUIRE(out->get_height(heif_channel_Cb) == height); + + std::vector expected_cb(width * height), expected_cr(width * height); + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + expected_cb[y * width + x] = reference_bilinear_chroma_sample(cb, chroma_w, chroma_h, x, y); + expected_cr[y * width + x] = reference_bilinear_chroma_sample(cr, chroma_w, chroma_h, x, y); + } + } + + assert_plane(out, heif_channel_Cb, expected_cb); + assert_plane(out, heif_channel_Cr, expected_cr); + } + } + } +} + + TEST_CASE("RGB 5-6-5 to RGB") { heif_color_conversion_options options = {}; @@ -777,7 +1049,7 @@ 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; + input_state.bits_per_pixel_alpha = 8; nclx_default_if_undefined(input_state); ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8); @@ -790,7 +1062,7 @@ 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; + input_state.bits_per_pixel_alpha = 10; nclx_default_if_undefined(input_state); ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8); @@ -803,11 +1075,11 @@ 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; + input_state.bits_per_pixel_alpha = 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; + target_state.bits_per_pixel_alpha = 10; ColorConversionPipeline pipeline; bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext); @@ -958,6 +1230,48 @@ } +// An 'unci' component may be 32, 64 or 128 bits wide. HeifPixelImage stores such planes with +// 4, 8 or 16 bytes per sample, but every conversion operator reads samples through uint8_t +// or uint16_t pointers. An operator must therefore decline such input in +// state_after_conversion() based on the sample width (not on an 8-bit SDR/HDR split, which +// would treat a 64-bit plane like a 16-bit one), so that no pipeline is ever built for it. +TEST_CASE("Conversion operators decline planes wider than 16 bits", "[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 rgb8(heif_colorspace_RGB, heif_chroma_444, false, 8); + ColorState rrggbb16(heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE, false, 16); + ColorState ycbcr8(heif_colorspace_YCbCr, heif_chroma_444, false, 8); + nclx_default_if_undefined(ycbcr8); + + for (int bits : {32, 64, 128}) { + INFO("bits=" << bits); + + ColorState ycbcr(heif_colorspace_YCbCr, heif_chroma_444, false, bits); + nclx_default_if_undefined(ycbcr); + ColorState rgb(heif_colorspace_RGB, heif_chroma_444, false, bits); + ColorState mono(heif_colorspace_monochrome, heif_chroma_monochrome, false, bits); + + CHECK(ycbcr.get_bytes_per_sample(heif_channel_Y) > 2); + CHECK_FALSE(ycbcr.color_channels_have_bytes_per_sample(2)); + + // The individual operators must not offer themselves ... + CHECK(Op_YCbCr_to_RGB().state_after_conversion(ycbcr, rgb, options, *options_ext).empty()); + CHECK(Op_RGB_to_YCbCr().state_after_conversion(rgb, ycbcr, options, *options_ext).empty()); + CHECK(Op_mono_to_YCbCr420().state_after_conversion(mono, ycbcr, options, *options_ext).empty()); + CHECK(Op_to_sdr_planes().state_after_conversion(rgb, rgb8, options, *options_ext).empty()); + + // ... and consequently no pipeline can be built from such an input. + ColorConversionPipeline pipeline; + CHECK_FALSE(pipeline.construct_pipeline(ycbcr, rgb8, options, *options_ext)); + CHECK_FALSE(pipeline.construct_pipeline(rgb, rrggbb16, options, *options_ext)); + CHECK_FALSE(pipeline.construct_pipeline(mono, ycbcr8, options, *options_ext)); + } +} + + // 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"), @@ -1090,3 +1404,478 @@ REQUIRE(big_y[static_cast(h - 1) * big_stride + (w - 1)] == small_y[small_stride]); REQUIRE(small_y[0] != small_y[small_stride]); } + + +// Regression test for a null-pointer write introduced together with the per-plane +// ColorState (bits_per_pixel_R/G/B/Y/Cb/Cr/alpha). +// +// Op_flatten_alpha_plane composites the alpha plane onto the colour planes when the output +// cannot carry alpha. It converts its input to planar RGB, composites, and converts the +// result back to the input's colorspace. The composited R/G/B planes were allocated with +// target_state.get_bits_per_pixel(channel). The operation keeps the source colorspace, so +// for a YCbCr (or monochrome) source the R/G/B fields of target_state are 0 (plane absent): +// add_channel() refused the zero depth, the returned error was dropped, and the composite +// loop wrote through a null plane pointer. Every compositing decode whose target is YCbCr +// crashed (heif-dec writing JPEG from any alpha HEIC or AVIF), while an RGB target was +// unaffected because the planner converts to RGB before it flattens. The planes are now +// allocated with the depth of the RGB-converted input and the add_channel() error is +// propagated. + +static std::shared_ptr make_ycbcr_alpha_image(heif_chroma chroma, + uint32_t width, uint32_t height, + int bpp, uint16_t luma, uint16_t alpha, + const nclx_profile& nclx) +{ + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_YCbCr, chroma); + + uint32_t chroma_width = (chroma == heif_chroma_444) ? width : (width + 1) / 2; + uint32_t chroma_height = (chroma == heif_chroma_420) ? (height + 1) / 2 : height; + uint16_t neutral = static_cast(1 << (bpp - 1)); + + img->fill_new_channel(heif_channel_Y, luma, width, height, bpp, nullptr); + img->fill_new_channel(heif_channel_Cb, neutral, chroma_width, chroma_height, bpp, nullptr); + img->fill_new_channel(heif_channel_Cr, neutral, chroma_width, chroma_height, bpp, nullptr); + img->fill_new_channel(heif_channel_Alpha, alpha, width, height, bpp, nullptr); + img->set_color_profile_nclx(nclx); + return img; +} + +static std::shared_ptr make_mono_alpha_image(uint32_t width, uint32_t height, + int bpp, uint16_t luma, uint16_t alpha, + const nclx_profile& nclx) +{ + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_monochrome, heif_chroma_monochrome); + img->fill_new_channel(heif_channel_Y, luma, width, height, bpp, nullptr); + img->fill_new_channel(heif_channel_Alpha, alpha, width, height, bpp, nullptr); + img->set_color_profile_nclx(nclx); + return img; +} + +// The operator computes (p * a + bkg * (alpha_max - a)) >> bpp_alpha. With neutral chroma +// and a full-range BT.601 matrix, the RGB gray level equals the luma, so the composited +// luma is predictable up to the rounding of the two matrix conversions. +static int expected_composited_luma(int luma, int alpha, int background, int bpp) +{ + int alpha_max = (1 << bpp) - 1; + return (luma * alpha + background * (alpha_max - alpha)) >> bpp; +} + +TEST_CASE("Alpha compositing into a YCbCr or monochrome target", "[heif_image]") +{ + const uint32_t width = 16; + const uint32_t height = 8; + + heif_color_conversion_options options{}; + + heif_color_conversion_options_ext options_ext{}; + options_ext.version = 1; + options_ext.alpha_composition_mode = heif_alpha_composition_mode_solid_color; + options_ext.background_red = 0xFFFF; + options_ext.background_green = 0xFFFF; + options_ext.background_blue = 0xFFFF; + + nclx_profile nclx = nclx_profile::defaults(); + nclx.set_matrix_coefficients(heif_matrix_coefficients_ITU_R_BT_601_6); + nclx.set_full_range_flag(true); + + SECTION("8-bit YCbCr 4:2:0 -> YCbCr 4:2:0 with a solid background (heif-dec JPEG output)") { + auto img = make_ycbcr_alpha_image(heif_chroma_420, width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + nclx, 0, options, &options_ext, + 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); + CHECK(!out->has_channel(heif_channel_Alpha)); + REQUIRE(out->has_channel(heif_channel_Y)); + CHECK(out->get_bits_per_pixel(heif_channel_Y) == 8); + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + + int expected = expected_composited_luma(100, 128, 255, 8); + CHECK(std::abs(p_y[0] - expected) <= 2); + CHECK(std::abs(p_y[(height - 1) * stride + (width - 1)] - expected) <= 2); + } + + SECTION("10-bit YCbCr 4:2:0 -> YCbCr 4:2:0 with a solid background (16-bit operator)") { + auto img = make_ycbcr_alpha_image(heif_chroma_420, width, height, 10, 400, 512, nclx); + + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + nclx, 0, options, &options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_chroma_format() == heif_chroma_420); + CHECK(!out->has_channel(heif_channel_Alpha)); + REQUIRE(out->has_channel(heif_channel_Y)); + CHECK(out->get_bits_per_pixel(heif_channel_Y) == 10); + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + const uint16_t* y16 = reinterpret_cast(p_y); + + int expected = expected_composited_luma(400, 512, 1023, 10); + CHECK(std::abs(y16[0] - expected) <= 3); + CHECK(std::abs(y16[(height - 1) * (stride / 2) + (width - 1)] - expected) <= 3); + } + + SECTION("8-bit YCbCr 4:4:4 -> YCbCr 4:4:4 with a checkerboard") { + heif_color_conversion_options_ext checker = options_ext; + checker.alpha_composition_mode = heif_alpha_composition_mode_checkerboard; + checker.secondary_background_red = 0; + checker.secondary_background_green = 0; + checker.secondary_background_blue = 0; + checker.checkerboard_square_size = 8; + + auto img = make_ycbcr_alpha_image(heif_chroma_444, width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_444, + nclx, 0, options, &checker, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_chroma_format() == heif_chroma_444); + CHECK(!out->has_channel(heif_channel_Alpha)); + REQUIRE(out->has_channel(heif_channel_Y)); + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + + // parity = (x/8 + y/8) % 2 selects the secondary (black) square at (0,0) and the + // primary (white) square at (8,0). + int expected_black = expected_composited_luma(100, 128, 0, 8); + int expected_white = expected_composited_luma(100, 128, 255, 8); + CHECK(std::abs(p_y[0] - expected_black) <= 2); + CHECK(std::abs(p_y[8] - expected_white) <= 2); + } + + SECTION("8-bit YCbCr 4:2:0 -> interleaved RGB (RGB target, was not affected)") { + auto img = make_ycbcr_alpha_image(heif_chroma_420, width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx, 0, options, &options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGB); + + size_t stride; + const uint8_t* p = out->get_channel_memory(heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + int expected = expected_composited_luma(100, 128, 255, 8); + CHECK(std::abs(p[0] - expected) <= 2); + CHECK(std::abs(p[1] - expected) <= 2); + CHECK(std::abs(p[2] - expected) <= 2); + } + + // A luma-only image is composited directly on its Y plane against the BT.601 luma of the + // background colour (there is no RGB to monochrome operator for the round trip the colour + // path takes). Nothing but integer arithmetic is involved, so the results are exact. + + SECTION("8-bit monochrome -> monochrome with a solid background") { + auto img = make_mono_alpha_image(width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_monochrome, heif_chroma_monochrome, + nclx, 0, options, &options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_colorspace() == heif_colorspace_monochrome); + CHECK(!out->has_channel(heif_channel_Alpha)); + REQUIRE(out->has_channel(heif_channel_Y)); + CHECK(out->get_bits_per_pixel(heif_channel_Y) == 8); + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + + int expected = expected_composited_luma(100, 128, 255, 8); + CHECK(p_y[0] == expected); + CHECK(p_y[(height - 1) * stride + (width - 1)] == expected); + } + + SECTION("10-bit monochrome -> monochrome (16-bit operator)") { + auto img = make_mono_alpha_image(width, height, 10, 400, 512, nclx); + + auto result = convert_colorspace(img, heif_colorspace_monochrome, heif_chroma_monochrome, + nclx, 0, options, &options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(!out->has_channel(heif_channel_Alpha)); + REQUIRE(out->has_channel(heif_channel_Y)); + CHECK(out->get_bits_per_pixel(heif_channel_Y) == 10); + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + const uint16_t* y16 = reinterpret_cast(p_y); + + int expected = expected_composited_luma(400, 512, 1023, 10); + CHECK(y16[0] == expected); + CHECK(y16[(height - 1) * (stride / 2) + (width - 1)] == expected); + } + + SECTION("8-bit monochrome -> interleaved RGB") { + // The planner flattens the monochrome image first (cheapest) and converts afterwards. + auto img = make_mono_alpha_image(width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx, 0, options, &options_ext, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGB); + + size_t stride; + const uint8_t* p = out->get_channel_memory(heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + int expected = expected_composited_luma(100, 128, 255, 8); + CHECK(p[0] == expected); + CHECK(p[1] == expected); + CHECK(p[2] == expected); + } + + SECTION("8-bit monochrome with a checkerboard") { + heif_color_conversion_options_ext checker = options_ext; + checker.alpha_composition_mode = heif_alpha_composition_mode_checkerboard; + checker.secondary_background_red = 0; + checker.secondary_background_green = 0; + checker.secondary_background_blue = 0; + checker.checkerboard_square_size = 8; + + auto img = make_mono_alpha_image(width, height, 8, 100, 128, nclx); + + auto result = convert_colorspace(img, heif_colorspace_monochrome, heif_chroma_monochrome, + nclx, 0, options, &checker, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + + // parity = (x/8 + y/8) % 2 selects the secondary (black) square at (0,0) and the + // primary (white) square at (8,0). + CHECK(p_y[0] == expected_composited_luma(100, 128, 0, 8)); + CHECK(p_y[8] == expected_composited_luma(100, 128, 255, 8)); + } + + SECTION("8-bit monochrome with a coloured background uses its BT.601 luma") { + heif_color_conversion_options_ext red = options_ext; + red.background_red = 0xFFFF; + red.background_green = 0; + red.background_blue = 0; + + // Fully transparent, so the output is the background's luma: 0.299 * 65535 = 19595 + // as a 16-bit value, 76 at 8 bits. + auto img = make_mono_alpha_image(width, height, 8, 100, 0, nclx); + + auto result = convert_colorspace(img, heif_colorspace_monochrome, heif_chroma_monochrome, + nclx, 0, options, &red, + heif_get_disabled_security_limits()); + REQUIRE(result); + auto out = *result; + + size_t stride; + const uint8_t* p_y = out->get_channel_memory(heif_channel_Y, &stride); + REQUIRE(p_y != nullptr); + CHECK(p_y[0] == expected_composited_luma(100, 0, 76, 8)); + } +} + + +// ColorState only reports one colour depth when all colour planes agree. The former accessor +// returned the depth of the first colour plane, which made planning decisions depend on the +// order of the planes in an image with mixed depths ('unci' declares a depth per component). + +static const int kMixedRgbDepths[3][3] = {{8, 8, 16}, {16, 8, 8}, {8, 16, 8}}; + +TEST_CASE("ColorState uniform bit depth accessors", "[heif_image]") +{ + SECTION("uniform planes report their depth") { + ColorState rgb(heif_colorspace_RGB, heif_chroma_444, true, 10); + CHECK(rgb.get_uniform_color_bits_per_pixel() == 10); + CHECK(rgb.get_uniform_bits_per_pixel() == 10); + CHECK(rgb.get_max_color_bits_per_pixel() == 10); + CHECK(rgb.color_channels_have_same_bpp()); + CHECK(rgb.all_channels_have_same_bpp()); + + ColorState ycc(heif_colorspace_YCbCr, heif_chroma_420, false, 12); + CHECK(ycc.get_uniform_color_bits_per_pixel() == 12); + CHECK(ycc.get_uniform_bits_per_pixel() == 12); + + ColorState mono(heif_colorspace_monochrome, heif_chroma_monochrome, false, 8); + CHECK(mono.get_uniform_color_bits_per_pixel() == 8); + } + + SECTION("mixed colour planes report 0 in every plane order") { + for (const int* d : kMixedRgbDepths) { + INFO("R/G/B = " << d[0] << "/" << d[1] << "/" << d[2]); + ColorState s; + s.colorspace = heif_colorspace_RGB; + s.chroma = heif_chroma_444; + s.bits_per_pixel_R = d[0]; + s.bits_per_pixel_G = d[1]; + s.bits_per_pixel_B = d[2]; + CHECK(s.get_uniform_color_bits_per_pixel() == 0); + CHECK(s.get_uniform_bits_per_pixel() == 0); + CHECK(s.get_max_color_bits_per_pixel() == 16); + CHECK_FALSE(s.color_channels_have_same_bpp()); + CHECK_FALSE(s.all_channels_have_same_bpp()); + } + + ColorState ycc(heif_colorspace_YCbCr, heif_chroma_444, false, 12); + ycc.bits_per_pixel_Cr = 8; + CHECK(ycc.get_uniform_color_bits_per_pixel() == 0); + CHECK(ycc.get_max_color_bits_per_pixel() == 12); + } + + SECTION("a differing alpha plane only affects the all-planes variant") { + ColorState s(heif_colorspace_RGB, heif_chroma_444, true, 8); + s.bits_per_pixel_alpha = 16; + CHECK(s.get_uniform_color_bits_per_pixel() == 8); + CHECK(s.get_uniform_bits_per_pixel() == 0); + CHECK(s.get_max_color_bits_per_pixel() == 8); + CHECK(s.get_max_bits_per_pixel() == 16); + CHECK(s.color_channels_have_same_bpp()); + CHECK_FALSE(s.all_channels_have_same_bpp()); + } + + SECTION("no colour plane") { + ColorState s; + CHECK(s.get_uniform_color_bits_per_pixel() == 0); + CHECK(s.get_uniform_bits_per_pixel() == 0); + CHECK(s.get_max_color_bits_per_pixel() == 0); + } +} + + +// Op_to_sdr_planes lowers every plane to 8 bits on its own, so it can equalize an image whose +// colour planes have different depths. It used to be offered only when the first colour plane +// was wider than 8 bits, so 8/8/16 failed with "unsupported color conversion" while the same +// planes in the order 16/8/8 converted fine. + +// 100 at 8 bits; at wider depths the same value with a few extra low bits, so that the shift +// down to 8 bits has something to drop. +static uint16_t sample_at_depth(int bits) +{ + return static_cast((100u << (bits - 8)) | (bits > 8 ? 0x5u : 0u)); +} + +static std::shared_ptr make_rgb_planar(uint32_t width, uint32_t height, + int r_bits, int g_bits, int b_bits) +{ + auto img = std::make_shared(); + img->create(width, height, heif_colorspace_RGB, heif_chroma_444); + img->fill_new_channel(heif_channel_R, sample_at_depth(r_bits), width, height, r_bits, nullptr); + img->fill_new_channel(heif_channel_G, sample_at_depth(g_bits), width, height, g_bits, nullptr); + img->fill_new_channel(heif_channel_B, sample_at_depth(b_bits), width, height, b_bits, nullptr); + return img; +} + +TEST_CASE("Op_to_sdr_planes equalizes mixed colour depths", "[heif_image]") +{ + heif_color_conversion_options options{}; + const uint32_t width = 8; + const uint32_t height = 4; + + SECTION("mixed planes -> 8-bit interleaved RGB, in every plane order") { + for (const int* d : kMixedRgbDepths) { + INFO("R/G/B = " << d[0] << "/" << d[1] << "/" << d[2]); + auto img = make_rgb_planar(width, height, d[0], d[1], d[2]); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx_profile::defaults(), 0, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + + size_t stride; + const uint8_t* p = (*result)->get_channel_memory(heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == 100); + CHECK(p[1] == 100); + CHECK(p[2] == 100); + CHECK(p[(height - 1) * stride + (width - 1) * 3 + 2] == 100); + } + } + + SECTION("mixed planes -> 8-bit planar RGB (requested depth 8)") { + auto img = make_rgb_planar(width, height, 8, 8, 16); + + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_444, + nclx_profile::defaults(), 8, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + CHECK((*result)->get_bits_per_pixel(ch) == 8); + size_t stride; + const uint8_t* p = (*result)->get_channel_memory(ch, &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == 100); + } + } + + SECTION("offered for mixed input, declined for uniform 8-bit input") { + std::unique_ptr + options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free); + + Op_to_sdr_planes op; + ColorState target(heif_colorspace_RGB, heif_chroma_444, false, 8); + + ColorState mixed; + mixed.colorspace = heif_colorspace_RGB; + mixed.chroma = heif_chroma_444; + mixed.bits_per_pixel_R = 8; + mixed.bits_per_pixel_G = 8; + mixed.bits_per_pixel_B = 16; + + auto states = op.state_after_conversion(mixed, target, options, *options_ext); + REQUIRE(states.size() == 1); + CHECK(states[0].color_state.get_uniform_color_bits_per_pixel() == 8); + + ColorState uniform8(heif_colorspace_RGB, heif_chroma_444, false, 8); + CHECK(op.state_after_conversion(uniform8, target, options, *options_ext).empty()); + } +} + + +// bytes_per_sample_for_bit_depth() is the single source of truth for the storage width of a +// plane. A depth of 0 is what get_bits_per_pixel() reports for a plane that does not exist; +// it must not map to the width of a valid one-byte plane. +TEST_CASE("bytes_per_sample_for_bit_depth", "[heif_image]") +{ + CHECK(bytes_per_sample_for_bit_depth(0) == 0); + CHECK(bytes_per_sample_for_bit_depth(-1) == 0); + + for (int bits = 1; bits <= 128; bits++) { + int expected = (bits <= 8) ? 1 : (bits <= 16) ? 2 : (bits <= 32) ? 4 : (bits <= 64) ? 8 : 16; + INFO("bits = " << bits); + CHECK(bytes_per_sample_for_bit_depth(bits) == expected); + } + + // The ColorState wrappers agree, including for a missing plane. + ColorState s(heif_colorspace_RGB, heif_chroma_444, false, 12); + CHECK(s.get_bytes_per_sample(heif_channel_R) == 2); + CHECK(s.get_bytes_per_sample(heif_channel_Alpha) == 0); + CHECK(s.get_max_bytes_per_sample() == 2); +} diff -Nru libheif-1.23.4/tests/conversion_operator_contract.cc libheif-1.23.6/tests/conversion_operator_contract.cc --- libheif-1.23.4/tests/conversion_operator_contract.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/conversion_operator_contract.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,686 @@ +/* + 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. +*/ + +// A color conversion operator declares in state_after_conversion() which image it is going +// to return, and the conversion pipeline is planned with these declarations: the next +// operator reads the planes that its input state lists, with the bit depths given there. +// An operator that returns an image with other planes than it declared makes the next +// operator read planes that do not exist or that have another sample size. +// +// Op_RGB_to_YCbCr copied the chroma format of the target state into the state it declared +// without checking that it is a chroma format of planar YCbCr. For a monochrome target it +// declared a YCbCr state with a Y plane only and returned an image with Y, Cb and Cr planes. +// For an interleaved RGB target it declared a YCbCr state with R, G and B. +// +// The tests run every operator against every state that an image can have and against a +// large set of target states, and compare each returned image with the declaration. They +// also check the place where the pipeline now verifies this for every conversion step. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "api_structs.h" +#include "color-conversion/colorconversion.h" +#include "color-conversion/rgb2yuv.h" +#include "image/pixelimage.h" +#include "nclx.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using OptionsExt = std::unique_ptr; + +OptionsExt make_options_ext(heif_alpha_composition_mode mode) +{ + OptionsExt ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free); + ext->alpha_composition_mode = mode; + ext->checkerboard_square_size = 4; + return ext; +} + + +const heif_chroma kChromas[] = {heif_chroma_monochrome, heif_chroma_420, heif_chroma_422, 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}; + +// Bit depths below 8, with 8 bits, with 16-bit samples and with samples that no operator reads. +const int kDepths[] = {4, 8, 10, 16, 32}; + + +bool is_interleaved(heif_chroma chroma) +{ + return num_interleaved_components_per_plane(chroma) > 1; +} + + +nclx_profile make_nclx(int matrix_coefficients, bool full_range) +{ + nclx_profile nclx = nclx_profile::defaults(); + nclx.set_matrix_coefficients(static_cast(matrix_coefficients)); + nclx.set_full_range_flag(full_range); + return nclx; +} + +// The default, GBR, BT.709 with limited range and YCgCo. +std::vector nclx_variants() +{ + return {nclx_profile::defaults(), make_nclx(0, true), make_nclx(1, false), make_nclx(8, true)}; +} + + +std::string describe(const ColorState& state) +{ + std::ostringstream ostr; + ostr << state; + return ostr.str(); +} + + +// Whether an image can have this state: the planes that the colorspace and the chroma format +// imply, and no others. +bool is_image_state(const ColorState& s) +{ + const bool ycbcr_planes = s.bits_per_pixel_Y || s.bits_per_pixel_Cb || s.bits_per_pixel_Cr; + const bool rgb_planes = s.bits_per_pixel_R || s.bits_per_pixel_G || s.bits_per_pixel_B; + + if (s.colorspace == heif_colorspace_filter_array) { + return s.chroma == heif_chroma_planar && s.bits_per_pixel_filter_array && !ycbcr_planes && !rgb_planes && + !s.bits_per_pixel_alpha; + } + + if (s.bits_per_pixel_filter_array) { + return false; + } + + switch (s.colorspace) { + case heif_colorspace_YCbCr: + return (s.chroma == heif_chroma_420 || s.chroma == heif_chroma_422 || s.chroma == heif_chroma_444) && + !rgb_planes && s.bits_per_pixel_Y && s.bits_per_pixel_Cb && s.bits_per_pixel_Cr; + + case heif_colorspace_monochrome: + return s.chroma == heif_chroma_monochrome && !rgb_planes && + s.bits_per_pixel_Y && !s.bits_per_pixel_Cb && !s.bits_per_pixel_Cr; + + case heif_colorspace_RGB: { + if (ycbcr_planes || !s.bits_per_pixel_R || !s.bits_per_pixel_G || !s.bits_per_pixel_B) { + return false; + } + if (s.chroma == heif_chroma_444) { + return true; + } + if (!is_interleaved(s.chroma)) { + return false; + } + + const int bpp = s.bits_per_pixel_R; + if (s.bits_per_pixel_G != bpp || s.bits_per_pixel_B != bpp) { + return false; + } + if (s.bits_per_pixel_alpha != (is_interleaved_with_alpha(s.chroma) ? bpp : 0)) { + return false; + } + if (s.chroma == heif_chroma_interleaved_RGB || s.chroma == heif_chroma_interleaved_RGBA) { + return bpp == 8; + } + return bpp >= 9 && bpp <= 16; + } + + default: + return false; + } +} + + +uint32_t g_random_state = 1; + +uint32_t next_random() +{ + g_random_state = g_random_state * 1664525u + 1013904223u; + return g_random_state >> 8; +} + +// Fills a plane with samples that are within the range of the bit depth. +void fill_plane(uint8_t* mem, size_t stride, uint32_t samples_per_row, uint32_t rows, int bits, bool big_endian) +{ + const int bytes = bytes_per_sample_for_bit_depth(bits); + + for (uint32_t y = 0; y < rows; y++) { + for (uint32_t x = 0; x < samples_per_row; x++) { + uint64_t value = next_random(); + if (bits < 24) { + value &= (uint64_t{1} << bits) - 1; + } + + uint8_t* p = mem + y * stride + static_cast(x) * bytes; + if (bytes == 2 && big_endian) { + p[0] = static_cast(value >> 8); + p[1] = static_cast(value); + } + else if (bytes == 2) { + auto v16 = static_cast(value); + memcpy(p, &v16, 2); + } + else { + memcpy(p, &value, bytes < 8 ? bytes : 8); + } + } + } +} + + +std::shared_ptr make_bayer_image(const ColorState& state, uint32_t w, uint32_t h) +{ + const int bits = state.bits_per_pixel_filter_array; + + heif_image* image = nullptr; + REQUIRE(heif_image_create(static_cast(w), static_cast(h), + heif_colorspace_filter_array, heif_chroma_planar, &image).code == heif_error_Ok); + + uint32_t filter_array_id = 0; + REQUIRE(heif_image_add_component(image, w, h, heif_cmpd_component_type_filter_array, + heif_component_datatype_unsigned_integer, bits, &filter_array_id).code == heif_error_Ok); + + uint32_t r_id = 0, g_id = 0, b_id = 0; + REQUIRE(heif_image_add_bayer_component(image, heif_cmpd_component_type_red, &r_id).code == heif_error_Ok); + REQUIRE(heif_image_add_bayer_component(image, heif_cmpd_component_type_green, &g_id).code == heif_error_Ok); + REQUIRE(heif_image_add_bayer_component(image, heif_cmpd_component_type_blue, &b_id).code == heif_error_Ok); + + heif_bayer_pattern_pixel pattern[4] = {{r_id, 1.0f}, {g_id, 1.0f}, {g_id, 1.0f}, {b_id, 1.0f}}; + REQUIRE(heif_image_set_bayer_pattern(image, filter_array_id, 2, 2, pattern).code == heif_error_Ok); + + std::shared_ptr img = image->image; + heif_image_release(image); + + size_t stride = 0; + uint8_t* mem = img->get_channel_memory(heif_channel_filter_array, &stride); + REQUIRE(mem != nullptr); + fill_plane(mem, stride, w, h, bits, false); + + if (state.bits_per_pixel_alpha) { + REQUIRE(!img->add_channel(heif_channel_Alpha, w, h, state.bits_per_pixel_alpha, heif_get_disabled_security_limits())); + + mem = img->get_channel_memory(heif_channel_Alpha, &stride); + REQUIRE(mem != nullptr); + fill_plane(mem, stride, w, h, state.bits_per_pixel_alpha, std::endian::native == std::endian::big); + } + + return img; +} + + +// An image with the planes of 'state'. Returns null if HeifPixelImage refuses to create it. +std::shared_ptr make_image(const ColorState& state, uint32_t w, uint32_t h) +{ + if (state.colorspace == heif_colorspace_filter_array) { + return make_bayer_image(state, w, h); + } + + const heif_security_limits* limits = heif_get_disabled_security_limits(); + + auto img = std::make_shared(); + img->create(w, h, state.colorspace, state.chroma); + + auto add_plane = [&](heif_channel channel, uint32_t plane_w, uint32_t plane_h, int bits, int components) { + if (img->add_channel(channel, plane_w, plane_h, bits, limits)) { + return false; + } + + const bool big_endian = (state.chroma == heif_chroma_interleaved_RRGGBB_BE || + state.chroma == heif_chroma_interleaved_RRGGBBAA_BE); + // Planar 16-bit samples are in the byte order of the machine. + const bool store_big_endian = (channel == heif_channel_interleaved) ? big_endian : (std::endian::native == std::endian::big); + + size_t stride = 0; + uint8_t* mem = img->get_channel_memory(channel, &stride); + fill_plane(mem, stride, plane_w * components, plane_h, bits, store_big_endian); + return true; + }; + + if (is_interleaved(state.chroma)) { + if (!add_plane(heif_channel_interleaved, w, h, state.bits_per_pixel_R, num_interleaved_components_per_plane(state.chroma))) { + return nullptr; + } + } + else { + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, + heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}) { + const int bits = state.get_bits_per_pixel(channel); + if (bits == 0) { + continue; + } + + if (!add_plane(channel, channel_width(w, state.chroma, channel), channel_height(h, state.chroma, channel), bits, 1)) { + return nullptr; + } + } + } + + img->set_color_profile_nclx(state.nclx); + return img; +} + + +// colorspace x chroma format x bit depth x alpha x nclx, whether an image can have that +// combination or not. A target state can be any of these. +std::vector all_states() +{ + std::vector states; + + for (heif_colorspace colorspace : {heif_colorspace_YCbCr, heif_colorspace_RGB, heif_colorspace_monochrome, + heif_colorspace_filter_array, heif_colorspace_undefined}) { + for (heif_chroma chroma : kChromas) { + for (int bpp : kDepths) { + for (int alpha_bpp : {0, bpp, bpp == 8 ? 10 : 8}) { + for (const nclx_profile& nclx : nclx_variants()) { + ColorState state; + state.colorspace = colorspace; + state.chroma = chroma; + state.set_color_bits_per_pixel(bpp); + state.bits_per_pixel_alpha = alpha_bpp; + state.nclx = nclx; + states.push_back(state); + } + } + } + } + } + + return states; +} + + +// The states that an image can have. +std::vector image_states() +{ + const nclx_profile default_nclx = nclx_profile::defaults(); + + std::vector states; + + for (const ColorState& state : all_states()) { + if (!is_image_state(state)) { + continue; + } + + // The nclx of a state is only looked at for YCbCr. + if (state.colorspace != heif_colorspace_YCbCr && + (state.nclx.get_matrix_coefficients() != default_nclx.get_matrix_coefficients() || + state.nclx.get_full_range_flag() != default_nclx.get_full_range_flag())) { + continue; + } + + states.push_back(state); + } + + // planes of different bit depths, as an 'unci' image can have them + for (heif_chroma chroma : {heif_chroma_420, heif_chroma_444}) { + ColorState state; + state.colorspace = heif_colorspace_YCbCr; + state.chroma = chroma; + state.nclx = default_nclx; + + state.bits_per_pixel_Y = 10; + state.bits_per_pixel_Cb = 8; + state.bits_per_pixel_Cr = 8; + states.push_back(state); + + state.bits_per_pixel_Y = 8; + state.bits_per_pixel_Cb = 12; + state.bits_per_pixel_Cr = 10; + states.push_back(state); + } + + { + ColorState state; + state.colorspace = heif_colorspace_RGB; + state.chroma = heif_chroma_444; + state.nclx = default_nclx; + + state.bits_per_pixel_R = 5; + state.bits_per_pixel_G = 6; + state.bits_per_pixel_B = 5; + states.push_back(state); + + state.bits_per_pixel_R = 8; + state.bits_per_pixel_G = 10; + state.bits_per_pixel_B = 8; + state.bits_per_pixel_alpha = 8; + states.push_back(state); + } + + return states; +} + + +// States that convert_colorspace() refuses at its entry (HeifPixelImage::check_plane_layout()), +// but that an image can be built with, because HeifPixelImage accepts any set of planes: a +// filter array with an alpha plane. What an operator declares for such an input has to hold as +// well. Op_drop_alpha_plane and Op_adjust_alpha_bit_depth took the chroma format of a filter +// array (planar, the same value as monochrome) for a monochrome image, declared the input +// state with another alpha plane and returned an image without the filter-array plane. +std::vector refused_input_states() +{ + std::vector states; + + for (int bpp : {8, 10, 16}) { + for (int alpha_bpp : {8, 10, 16}) { + ColorState state; + state.colorspace = heif_colorspace_filter_array; + state.chroma = heif_chroma_planar; + state.bits_per_pixel_filter_array = bpp; + state.bits_per_pixel_alpha = alpha_bpp; + state.nclx = nclx_profile::defaults(); + states.push_back(state); + } + } + + return states; +} + + +struct Options +{ + heif_color_conversion_options options; + heif_alpha_composition_mode alpha_mode; +}; + +std::vector option_sets() +{ + std::vector sets; + + heif_color_conversion_options options; + heif_color_conversion_options_set_defaults(&options); + + // only the preferred chroma algorithms, alpha is kept + options.only_use_preferred_chroma_algorithm = true; + sets.push_back({options, heif_alpha_composition_mode_none}); + + // any chroma algorithm, alpha is composited onto a background + options.only_use_preferred_chroma_algorithm = false; + sets.push_back({options, heif_alpha_composition_mode_solid_color}); + + return sets; +} + +} // namespace + + +TEST_CASE("conversion operators return the image that they declared") +{ + const std::vector targets = all_states(); + const heif_security_limits* limits = heif_get_disabled_security_limits(); + + std::vector inputs = image_states(); + for (const ColorState& state : refused_input_states()) { + inputs.push_back(state); + } + + size_t num_operators_used = 0; + size_t num_conversions = 0; + size_t num_contradictions = 0; + std::string examples; + + for (const auto& op : ColorConversionPipeline::get_operations()) { + const auto& op_ref = *op; + const std::string op_name = typeid(op_ref).name(); + size_t num_conversions_of_operator = 0; + + for (const Options& opt : option_sets()) { + OptionsExt ext = make_options_ext(opt.alpha_mode); + + for (const ColorState& input_state : inputs) { + // The same output state is declared for many target states. Run each conversion once. + std::set converted; + + for (const ColorState& target_state : targets) { + for (const ColorStateWithCost& declared : op->state_after_conversion(input_state, target_state, opt.options, *ext)) { + if (!converted.insert(describe(declared.color_state)).second) { + continue; + } + + // an even and an odd image size + for (auto size : {std::pair{18, 10}, std::pair{7, 5}}) { + auto image = make_image(input_state, size.first, size.second); + REQUIRE(image != nullptr); + + auto result = op->convert_colorspace(image, input_state, declared.color_state, opt.options, *ext, limits); + if (!result) { + // Refusing the conversion does not contradict the declaration. + continue; + } + + num_conversions++; + num_conversions_of_operator++; + + if (ColorConversionPipeline::check_operation_output(*result, declared.color_state)) { + num_contradictions++; + if (num_contradictions <= 5) { + examples += op_name + "\n input: " + describe(input_state) + + "\n declared: " + describe(declared.color_state) + + "\n returned: " + describe(ColorState::from_image_planes(**result)) + "\n"; + } + } + else { + CHECK((*result)->get_width() == size.first); + CHECK((*result)->get_height() == size.second); + } + } + } + } + } + } + + if (num_conversions_of_operator > 0) { + num_operators_used++; + } + } + + INFO("first contradictions:\n" << examples); + CHECK(num_contradictions == 0); + + // The test is of no use if the operators decline everything they are offered. + CHECK(num_operators_used >= 25); + CHECK(num_conversions >= 2000); +} + + +TEST_CASE("Op_RGB_to_YCbCr only declares planar YCbCr chroma formats") +{ + heif_color_conversion_options options; + heif_color_conversion_options_set_defaults(&options); + options.only_use_preferred_chroma_algorithm = false; + OptionsExt ext = make_options_ext(heif_alpha_composition_mode_none); + + for (int bpp : {8, 10}) { + ColorState input(heif_colorspace_RGB, heif_chroma_444, false, bpp); + input.nclx = nclx_profile::defaults(); + + auto declared_states = [&](heif_colorspace colorspace, heif_chroma chroma) { + ColorState target(colorspace, chroma, false, bpp); + target.nclx = nclx_profile::defaults(); + + if (bpp == 8) { + return Op_RGB_to_YCbCr().state_after_conversion(input, target, options, *ext); + } + else { + return Op_RGB_to_YCbCr().state_after_conversion(input, target, options, *ext); + } + }; + + for (heif_chroma chroma : {heif_chroma_420, heif_chroma_422, heif_chroma_444}) { + auto states = declared_states(heif_colorspace_YCbCr, chroma); + REQUIRE(states.size() == 1); + CHECK(states[0].color_state.colorspace == heif_colorspace_YCbCr); + CHECK(states[0].color_state.chroma == chroma); + CHECK(is_image_state(states[0].color_state)); + } + + // It used to declare a YCbCr state with the chroma format of these targets. + CHECK(declared_states(heif_colorspace_monochrome, heif_chroma_monochrome).empty()); + CHECK(declared_states(heif_colorspace_RGB, heif_chroma_interleaved_RGB).empty()); + CHECK(declared_states(heif_colorspace_RGB, heif_chroma_interleaved_RGBA).empty()); + CHECK(declared_states(heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE).empty()); + CHECK(declared_states(heif_colorspace_RGB, heif_chroma_interleaved_RRGGBBAA_BE).empty()); + } +} + + +TEST_CASE("the pipeline refuses an operator result that contradicts the declared state") +{ + const heif_security_limits* limits = heif_get_disabled_security_limits(); + const uint32_t w = 18, h = 10; + + // An image with exactly these planes. + auto image_with_planes = [&](heif_colorspace colorspace, heif_chroma chroma, + std::initializer_list> planes, + uint32_t chroma_w, uint32_t chroma_h) { + auto img = std::make_shared(); + img->create(w, h, colorspace, chroma); + for (const auto& plane : planes) { + const bool is_chroma = (plane.first == heif_channel_Cb || plane.first == heif_channel_Cr); + REQUIRE(!img->add_channel(plane.first, is_chroma ? chroma_w : w, is_chroma ? chroma_h : h, plane.second, limits)); + } + return img; + }; + + const ColorState ycbcr420(heif_colorspace_YCbCr, heif_chroma_420, false, 8); + const ColorState ycbcr420_alpha(heif_colorspace_YCbCr, heif_chroma_420, true, 8); + + auto good = image_with_planes(heif_colorspace_YCbCr, heif_chroma_420, + {{heif_channel_Y, 8}, {heif_channel_Cb, 8}, {heif_channel_Cr, 8}}, 9, 5); + + SECTION("an image with the declared planes is accepted") { + CHECK(!ColorConversionPipeline::check_operation_output(good, ycbcr420)); + } + + SECTION("no image") { + CHECK(ColorConversionPipeline::check_operation_output(nullptr, ycbcr420)); + } + + SECTION("what Op_RGB_to_YCbCr returned for a monochrome target") { + // declared: YCbCr with the monochrome chroma format and a Y plane only + ColorState declared; + declared.colorspace = heif_colorspace_YCbCr; + declared.chroma = heif_chroma_monochrome; + declared.set_color_bits_per_pixel(8); + REQUIRE(declared.bits_per_pixel_Cb == 0); + + auto returned = image_with_planes(heif_colorspace_YCbCr, heif_chroma_monochrome, + {{heif_channel_Y, 8}, {heif_channel_Cb, 8}, {heif_channel_Cr, 8}}, w, h); + CHECK(ColorConversionPipeline::check_operation_output(returned, declared)); + } + + SECTION("a plane that was not declared") { + auto with_alpha = image_with_planes(heif_colorspace_YCbCr, heif_chroma_420, + {{heif_channel_Y, 8}, {heif_channel_Cb, 8}, {heif_channel_Cr, 8}, {heif_channel_Alpha, 8}}, 9, 5); + CHECK(ColorConversionPipeline::check_operation_output(with_alpha, ycbcr420)); + CHECK(!ColorConversionPipeline::check_operation_output(with_alpha, ycbcr420_alpha)); + } + + SECTION("a declared plane is missing") { + CHECK(ColorConversionPipeline::check_operation_output(good, ycbcr420_alpha)); + } + + SECTION("another bit depth") { + // A plane with 8-bit samples that the next operator would read with 16-bit samples. + CHECK(ColorConversionPipeline::check_operation_output(good, ColorState(heif_colorspace_YCbCr, heif_chroma_420, false, 10))); + + auto mixed = image_with_planes(heif_colorspace_YCbCr, heif_chroma_420, + {{heif_channel_Y, 10}, {heif_channel_Cb, 8}, {heif_channel_Cr, 8}}, 9, 5); + CHECK(ColorConversionPipeline::check_operation_output(mixed, ColorState(heif_colorspace_YCbCr, heif_chroma_420, false, 10))); + } + + SECTION("another chroma format or colorspace") { + CHECK(ColorConversionPipeline::check_operation_output(good, ColorState(heif_colorspace_YCbCr, heif_chroma_422, false, 8))); + CHECK(ColorConversionPipeline::check_operation_output(good, ColorState(heif_colorspace_RGB, heif_chroma_444, false, 8))); + } + + SECTION("planes that do not have the size of the image") { + // The planes and their bit depths are the declared ones, but one plane is larger. + auto img = std::make_shared(); + img->create(w, h, heif_colorspace_RGB, heif_chroma_444); + REQUIRE(!img->add_channel(heif_channel_R, w, h, 8, limits)); + REQUIRE(!img->add_channel(heif_channel_G, w, h, 8, limits)); + REQUIRE(!img->add_channel(heif_channel_B, 2 * w, 2 * h, 8, limits)); + + CHECK(ColorConversionPipeline::check_operation_output(img, ColorState(heif_colorspace_RGB, heif_chroma_444, false, 8))); + } +} + + +TEST_CASE("a successful color conversion returns the requested format") +{ + const heif_security_limits* limits = heif_get_disabled_security_limits(); + + size_t num_converted = 0; + + for (const Options& opt : option_sets()) { + OptionsExt ext = make_options_ext(opt.alpha_mode); + + for (const ColorState& input_state : image_states()) { + auto image = make_image(input_state, 18, 10); + REQUIRE(image != nullptr); + + // Any combination of colorspace and chroma format can be requested, also those that + // no image can have. + for (heif_colorspace colorspace : {heif_colorspace_YCbCr, heif_colorspace_RGB, heif_colorspace_monochrome}) { + for (heif_chroma chroma : kChromas) { + for (int output_bpp : {0, 8, 10}) { + auto result = convert_colorspace(image, colorspace, chroma, nclx_profile::undefined(), output_bpp, + opt.options, ext.get(), limits); + if (!result) { + // An unsupported conversion is refused, but no conversion step may have + // returned something else than it declared. + if (result.error().message.find("did not return the image format it declared") != std::string::npos) { + INFO("input: " << describe(input_state) << " requested colorspace " << colorspace << " chroma " << chroma); + FAIL_CHECK(result.error().message); + } + continue; + } + + num_converted++; + + const HeifPixelImage& out = **result; + if (out.get_colorspace() != colorspace || out.get_chroma_format() != chroma || out.check_plane_layout()) { + INFO("input: " << describe(input_state) << " requested colorspace " << colorspace << " chroma " << chroma); + FAIL_CHECK("the converted image does not have the requested format"); + } + } + } + } + } + } + + CHECK(num_converted >= 2000); +} Binary files /srv/release.debian.org/tmp/Xg5l_5F0C7/libheif-1.23.4/tests/data/pclr_zero_columns.heic and /srv/release.debian.org/tmp/wZiwfh6Znx/libheif-1.23.6/tests/data/pclr_zero_columns.heic differ diff -Nru libheif-1.23.4/tests/decoded_bit_depth_mismatch.cc libheif-1.23.6/tests/decoded_bit_depth_mismatch.cc --- libheif-1.23.4/tests/decoded_bit_depth_mismatch.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/decoded_bit_depth_mismatch.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,225 @@ +/* + 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. +*/ + +// The bit depth that heif_image_handle_get_luma_bits_per_pixel() reports is taken from the +// configuration box of the image item ('hvcC', 'av1C'). The decoded image gets its bit depth +// from the bitstream. A file in which the two disagree was decoded without an error, and the +// decoded image had another bit depth than the handle reported. An application that reads +// the planes of the decoded image with the bit depth from the handle (16-bit samples from a +// plane with 8-bit samples) reads out of bounds (GHSA-vv35-6hxg-95x8). Such a file is +// refused now, like a file whose decoded image does not have the signaled size. +// +// The test encodes an 8-bit image and changes the configuration box so that it claims 10 bit. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" + +#include +#include +#include +#include +#include + +namespace { + +constexpr int W = 64; +constexpr int H = 64; + +heif_error write_to_vector(heif_context*, const void* data, size_t size, void* userdata) +{ + auto* out = static_cast*>(userdata); + const auto* bytes = static_cast(data); + out->insert(out->end(), bytes, bytes + size); + return heif_error{heif_error_Ok, heif_suberror_Unspecified, "Success"}; +} + + +std::vector encode_8bit_image(heif_compression_format format) +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(W, H, heif_colorspace_YCbCr, heif_chroma_420, &img); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + int w = (channel == heif_channel_Y) ? W : W / 2; + int h = (channel == heif_channel_Y) ? H : H / 2; + err = heif_image_add_plane(img, channel, w, h, 8); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + for (int y = 0; y < h; y++) { + memset(p + y * stride, 0x60, w); + } + } + + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + err = heif_context_get_encoder_for_format(ctx, format, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr); + INFO("encode: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + std::vector file; + heif_writer writer; + writer.writer_api_version = 1; + writer.write = write_to_vector; + err = heif_context_write(ctx, &writer, &file); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(img); + + return file; +} + + +// Makes the configuration box claim a bit depth of 10 for luma and chroma. +void claim_10_bit(std::vector& file, heif_compression_format format) +{ + const char* fourcc = (format == heif_compression_HEVC) ? "hvcC" : "av1C"; + + for (size_t i = 0; i + 4 + 19 < file.size(); i++) { + if (memcmp(&file[i], fourcc, 4) != 0) { + continue; + } + + uint8_t* config = &file[i + 4]; + + if (format == heif_compression_HEVC) { + // HEVCDecoderConfigurationRecord: bitDepthLumaMinus8 and bitDepthChromaMinus8 are the + // lower three bits of the bytes 17 and 18. + config[17] = static_cast((config[17] & 0xF8) | 2); + config[18] = static_cast((config[18] & 0xF8) | 2); + } + else { + // AV1CodecConfigurationRecord: high_bitdepth is the second bit of the third byte. + config[2] = static_cast(config[2] | 0x40); + } + return; + } + + FAIL("configuration box not found"); +} + + +struct Decoded +{ + int handle_luma_bits = 0; + heif_error_code code = heif_error_Ok; + std::string message; + bool has_image = false; + int image_luma_bits = 0; +}; + +Decoded decode(const std::vector& file, heif_colorspace colorspace, heif_chroma chroma) +{ + Decoded result; + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + INFO("read: " << err.message); + 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); + + result.handle_luma_bits = heif_image_handle_get_luma_bits_per_pixel(handle); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, colorspace, chroma, nullptr); + result.code = err.code; + result.message = err.message ? err.message : ""; + result.has_image = (img != nullptr); + + if (img) { + if (heif_image_has_channel(img, heif_channel_Y)) { + result.image_luma_bits = heif_image_get_bits_per_pixel_range(img, heif_channel_Y); + } + heif_image_release(img); + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + return result; +} + +} // namespace + + +TEST_CASE("an image whose decoded bit depth contradicts the image handle is refused") +{ + int num_formats_tested = 0; + + for (heif_compression_format format : {heif_compression_AV1, heif_compression_HEVC}) { + if (!heif_have_encoder_for_format(format) || !heif_have_decoder_for_format(format)) { + continue; + } + num_formats_tested++; + + INFO("format: " << (format == heif_compression_HEVC ? "HEVC" : "AV1")); + + std::vector file = encode_8bit_image(format); + + // --- control: the unmodified file + + Decoded control = decode(file, heif_colorspace_undefined, heif_chroma_undefined); + INFO("control: " << control.message); + REQUIRE(control.code == heif_error_Ok); + CHECK(control.handle_luma_bits == 8); + CHECK(control.image_luma_bits == 8); + + // --- the configuration box claims 10 bit, the bitstream has 8 bit + + claim_10_bit(file, format); + + Decoded native = decode(file, heif_colorspace_undefined, heif_chroma_undefined); + INFO("decoding without conversion: " << native.message); + CHECK(native.handle_luma_bits == 10); + + // The decoded image must never have another bit depth than the handle reports. + if (native.has_image) { + CHECK(native.image_luma_bits == native.handle_luma_bits); + } + CHECK(native.code != heif_error_Ok); + CHECK(!native.has_image); + + // A conversion does not hide the contradiction. + Decoded converted = decode(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB); + INFO("decoding to RGB: " << converted.message); + CHECK(converted.code != heif_error_Ok); + CHECK(!converted.has_image); + } + + if (num_formats_tested == 0) { + SKIP("neither an AV1 nor an HEVC encoder and decoder available"); + } +} diff -Nru libheif-1.23.4/tests/encode.cc libheif-1.23.6/tests/encode.cc --- libheif-1.23.4/tests/encode.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/encode.cc 2026-10-05 16:16:00.000000000 +0000 @@ -142,13 +142,22 @@ } -TEST_CASE( "ispe odd size", "[heif_context]" ) { +TEST_CASE( "HEVC ispe odd size", "[heif_context]" ) { + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("HEVC encoder not available, skipping test"); + } // HEVC encoders typically encode with even dimensions only test_ispe_size(heif_compression_HEVC, heif_orientation_normal, 121,99, 122,100); test_ispe_size(heif_compression_HEVC, heif_orientation_rotate_180, 121,99, 122,100); test_ispe_size(heif_compression_HEVC, heif_orientation_rotate_90_cw, 121,99, 122,100); test_ispe_size(heif_compression_HEVC, heif_orientation_rotate_90_cw, 120,100, 120,100); +} + +TEST_CASE( "AVIF ispe odd size", "[heif_context]" ) { + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + SKIP("AV1 encoder not available, skipping test"); + } // AVIF encoders typically encode with odd dimensions test_ispe_size(heif_compression_AV1, heif_orientation_normal, 121,99, 121,99); @@ -196,4 +205,52 @@ heif_image_release(img); heif_encoder_release(enc); heif_context_free(ctx); +} + + +TEST_CASE("kvazaar encodes monochrome images", "[heif_encoder]") { + // Regression test for PR #1915. kvazaar git master keeps the encoder chroma format in + // kvz_config fields that only the "input-format" parser updates. Setting input_format + // directly left the encoder at 4:2:0 while the 4:0:0 input picture had no chroma planes, + // which crashed every monochrome encode, including every alpha auxiliary image. + + const heif_encoder_descriptor* descriptor = nullptr; + int n = heif_get_encoder_descriptors(heif_compression_HEVC, "kvazaar", &descriptor, 1); + if (n == 0) { + SKIP("kvazaar 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); + + const int w = 128; + const int h = 128; + 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); + + size_t stride; + uint8_t* plane = heif_image_get_plane2(img, heif_channel_Y, &stride); + REQUIRE(plane != nullptr); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + plane[y * stride + x] = (uint8_t) (x + y); + } + } + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(handle != nullptr); + REQUIRE(heif_image_handle_get_width(handle) == w); + REQUIRE(heif_image_handle_get_height(handle) == h); + + heif_image_handle_release(handle); + heif_image_release(img); + heif_encoder_release(enc); + heif_context_free(ctx); } diff -Nru libheif-1.23.4/tests/encode_handle_queries.cc libheif-1.23.6/tests/encode_handle_queries.cc --- libheif-1.23.4/tests/encode_handle_queries.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/encode_handle_queries.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,178 @@ +/* + 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. +*/ + +// A heif_context is used either for reading or for writing. The decoder object of an image +// item is only created when the item is read from a file, so an image that was added to the +// context by encoding has none. Decoding such an image, and also the plain queries for its +// bit depth, colorspace and alpha channel on the handle that heif_context_encode_image() +// returns, dereferenced the missing decoder and crashed. This is not a supported use of the +// API, but it has to fail with an error. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" + +#include +#include +#include +#include + +namespace { + +constexpr int W = 64; +constexpr int H = 64; + +heif_image* create_rgb_image() +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(W, H, heif_colorspace_RGB, heif_chroma_444, &img); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B}) { + err = heif_image_add_plane(img, channel, W, H, 8); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + for (int y = 0; y < H; y++) { + memset(p + y * stride, 0x40, W); + } + } + + return img; +} + +const char* format_name(heif_compression_format format) +{ + switch (format) { + case heif_compression_HEVC: return "HEVC"; + case heif_compression_AVC: return "AVC"; + case heif_compression_JPEG: return "JPEG"; + case heif_compression_AV1: return "AV1"; + case heif_compression_VVC: return "VVC"; + case heif_compression_JPEG2000: return "JPEG 2000"; + case heif_compression_uncompressed: return "uncompressed"; + case heif_compression_HTJ2K: return "HT-J2K"; + default: return "?"; + } +} + +} // namespace + + +TEST_CASE("queries and decoding on the handle of an encoded image fail without crashing") +{ + int num_formats_tested = 0; + + for (heif_compression_format format : {heif_compression_HEVC, heif_compression_AVC, heif_compression_JPEG, + heif_compression_AV1, heif_compression_VVC, heif_compression_JPEG2000, + heif_compression_uncompressed, heif_compression_HTJ2K}) { + if (!heif_have_encoder_for_format(format)) { + continue; + } + + INFO("format: " << format_name(format)); + + heif_image* img = create_rgb_image(); + heif_context* ctx = heif_context_alloc(); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, format, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, img, encoder, nullptr, &handle); + if (err.code != heif_error_Ok) { + // an encoder that does not take this kind of image is not what is tested here + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(img); + continue; + } + REQUIRE(handle != nullptr); + num_formats_tested++; + + // --- these work on a handle of an image that is being written + + CHECK(heif_image_handle_get_width(handle) == W); + CHECK(heif_image_handle_get_height(handle) == H); + + // --- these need the decoder of the image item, which an encoded image does not have + + // The bit depth is unknown (-1). + CHECK(heif_image_handle_get_luma_bits_per_pixel(handle) == -1); + CHECK(heif_image_handle_get_chroma_bits_per_pixel(handle) == -1); + + heif_colorspace colorspace; + heif_chroma chroma; + err = heif_image_handle_get_preferred_decoding_colorspace(handle, &colorspace, &chroma); + CHECK(err.code != heif_error_Ok); + + CHECK(heif_image_handle_has_alpha_channel(handle) == 0); + + // The uncompressed codec happens to decode from an encoding context. That is not + // required. What is required is that no format crashes and that a failure is + // reported together with a null image. + heif_image* decoded = nullptr; + err = heif_decode_image(handle, &decoded, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + if (format != heif_compression_uncompressed) { + CHECK(err.code != heif_error_Ok); + } + CHECK((err.code == heif_error_Ok) == (decoded != nullptr)); + if (decoded) { + heif_image_release(decoded); + } + + decoded = nullptr; + err = heif_image_handle_decode_image_tile(handle, &decoded, heif_colorspace_undefined, heif_chroma_undefined, + nullptr, 0, 0); + if (format != heif_compression_uncompressed) { + CHECK(err.code != heif_error_Ok); + } + CHECK((err.code == heif_error_Ok) == (decoded != nullptr)); + if (decoded) { + heif_image_release(decoded); + } + + // --- the same through the primary image handle of the context + + heif_image_handle* primary = nullptr; + err = heif_context_get_primary_image_handle(ctx, &primary); + REQUIRE(err.code == heif_error_Ok); + CHECK(heif_image_handle_get_luma_bits_per_pixel(primary) == -1); + CHECK(heif_image_handle_has_alpha_channel(primary) == 0); + heif_image_handle_release(primary); + + heif_image_handle_release(handle); + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(img); + } + + if (num_formats_tested == 0) { + SKIP("no encoder available"); + } +} diff -Nru libheif-1.23.4/tests/encode_hevc_bitdepth.cc libheif-1.23.6/tests/encode_hevc_bitdepth.cc --- libheif-1.23.4/tests/encode_hevc_bitdepth.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/encode_hevc_bitdepth.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,185 @@ +/* + 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. +*/ + +// The 'hvcC' box stores the bit depths in 3 bit fields and can signal at most 15 bits per +// sample. An HEVC image with 16 bits would be written with a bit depth of 8 in its 'hvcC' +// box. libheif refuses to encode such an image, and it does so itself, before the image +// reaches the encoder plugin: the error has to come with heif_error_Unsupported_feature, +// not with the heif_error_Encoder_plugin_error of a plugin that happens to reject the +// bit depth as well. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +constexpr uint32_t W = 64; +constexpr uint32_t H = 64; + +void add_plane(heif_image* img, heif_channel channel, uint32_t w, uint32_t h, int bit_depth) +{ + heif_error err = heif_image_add_plane(img, channel, w, h, bit_depth); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + + const uint16_t value = static_cast(1u << (bit_depth - 1)); + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + if (bit_depth > 8) { + memcpy(p + y * stride + 2 * x, &value, 2); + } + else { + p[y * stride + x] = static_cast(value); + } + } + } +} + +heif_image* create_image(heif_colorspace colorspace, heif_chroma chroma, int bit_depth) +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(W, H, colorspace, chroma, &img); + REQUIRE(err.code == heif_error_Ok); + + if (colorspace == heif_colorspace_RGB) { + for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B}) { + add_plane(img, channel, W, H, bit_depth); + } + } + else { + add_plane(img, heif_channel_Y, W, H, bit_depth); + if (chroma == heif_chroma_420) { + add_plane(img, heif_channel_Cb, W / 2, H / 2, bit_depth); + add_plane(img, heif_channel_Cr, W / 2, H / 2, bit_depth); + } + } + return img; +} + +// heif_error::message may point into the context that reported the error, so it has to be +// copied before the context is freed. +struct EncodeResult +{ + heif_error_code code; + heif_suberror_code subcode; + std::string message; +}; + +EncodeResult encode_image(heif_image* img) +{ + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_HEVC, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr); + EncodeResult result{err.code, err.subcode, err.message}; + + heif_encoder_release(encoder); + heif_context_free(ctx); + return result; +} + +EncodeResult encode_sequence_frame(heif_image* img) +{ + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_HEVC, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_track* track = nullptr; + err = heif_context_add_visual_sequence_track(ctx, static_cast(W), static_cast(H), + heif_track_type_video, nullptr, nullptr, &track); + REQUIRE(err.code == heif_error_Ok); + + heif_image_set_duration(img, 1); + err = heif_track_encode_sequence_image(track, img, encoder, nullptr); + EncodeResult result{err.code, err.subcode, err.message}; + + heif_track_release(track); + heif_encoder_release(encoder); + heif_context_free(ctx); + return result; +} + +void check_refused_by_libheif(const EncodeResult& result) +{ + INFO("encode error (" << result.code << "/" << result.subcode << "): " << result.message); + CHECK(result.code == heif_error_Unsupported_feature); + CHECK(result.subcode == heif_suberror_Unsupported_bit_depth); + CHECK(result.message.find("hvcC") != std::string::npos); +} + +} // namespace + + +TEST_CASE("HEVC images with 16 bits per sample are refused") +{ + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("no HEVC encoder available"); + } + + SECTION("control: an 8 bit image encodes") { + heif_image* img = create_image(heif_colorspace_YCbCr, heif_chroma_420, 8); + EncodeResult result = encode_image(img); + INFO("encode error (" << result.code << "/" << result.subcode << "): " << result.message); + CHECK(result.code == heif_error_Ok); + heif_image_release(img); + } + + SECTION("YCbCr 4:2:0") { + heif_image* img = create_image(heif_colorspace_YCbCr, heif_chroma_420, 16); + check_refused_by_libheif(encode_image(img)); + heif_image_release(img); + } + + SECTION("monochrome") { + heif_image* img = create_image(heif_colorspace_monochrome, heif_chroma_monochrome, 16); + check_refused_by_libheif(encode_image(img)); + heif_image_release(img); + } + + SECTION("RGB, which is converted to YCbCr for the encoder") { + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444, 16); + check_refused_by_libheif(encode_image(img)); + heif_image_release(img); + } + + SECTION("frame of an image sequence") { + heif_image* img = create_image(heif_colorspace_YCbCr, heif_chroma_420, 16); + check_refused_by_libheif(encode_sequence_frame(img)); + heif_image_release(img); + } +} diff -Nru libheif-1.23.4/tests/encode_plane_layout.cc libheif-1.23.6/tests/encode_plane_layout.cc --- libheif-1.23.4/tests/encode_plane_layout.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/encode_plane_layout.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,267 @@ +/* + 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 image handed to heif_context_encode_image() has to have exactly the planes of its +// colorspace and chroma format. Before this check, an RGB image without its B plane failed +// deep inside a conversion operator, a YCbCr 4:2:0 image without its Cr plane passed the +// pipeline as a no-op and aborted inside the x265 plugin (a release build would have read a +// null plane), and foreign or duplicate planes were silently dropped or ignored. The check is +// made on the libheif side in Encoder::check_input_image_layout(), before the image is +// converted for, or handed unchanged to, the plugin. Planes with channel heif_channel_unknown +// (custom multi-component data) are tolerated. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include + +namespace { + +constexpr uint32_t W = 32; +constexpr uint32_t H = 32; + +heif_compression_format pick_encoder_format() +{ + for (heif_compression_format format : {heif_compression_HEVC, heif_compression_AV1, heif_compression_uncompressed}) { + if (heif_have_encoder_for_format(format)) { + return format; + } + } + return heif_compression_undefined; +} + +heif_image* create_image(heif_colorspace cs, heif_chroma chroma) +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(W, H, cs, chroma, &img); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + return img; +} + +void add_plane(heif_image* img, heif_channel channel, uint32_t w, uint32_t h) +{ + heif_error err = heif_image_add_plane(img, channel, w, h, 8); + INFO("add_plane error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + for (uint32_t y = 0; y < h; y++) { + memset(p + y * stride, 0x80, w); + } +} + +// heif_error::message may point into the context that reported the error, so it has to be +// copied before the context is freed. +struct EncodeResult +{ + heif_error_code code; + heif_suberror_code subcode; + std::string message; +}; + +EncodeResult encode(heif_image* img, heif_compression_format format) +{ + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, format, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr); + EncodeResult result{err.code, err.subcode, err.message ? err.message : ""}; + + heif_encoder_release(encoder); + heif_context_free(ctx); + return result; +} + +void expect_refused(heif_image* img, heif_compression_format format) +{ + EncodeResult err = encode(img, format); + INFO("encode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code != heif_error_Ok); + CHECK(err.code == heif_error_Usage_error); + heif_image_release(img); +} + +} // namespace + + +TEST_CASE("encoding refuses images with a non-canonical plane layout") +{ + heif_compression_format format = pick_encoder_format(); + if (format == heif_compression_undefined) { + SKIP("no HEVC, AV1 or uncompressed encoder available"); + } + + SECTION("control: a complete RGB image encodes") { + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444); + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + add_plane(img, ch, W, H); + } + EncodeResult err = encode(img, format); + INFO("encode error (" << err.code << "/" << err.subcode << "): " << err.message); + CHECK(err.code == heif_error_Ok); + heif_image_release(img); + } + + SECTION("RGB without its B plane") { + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444); + add_plane(img, heif_channel_R, W, H); + add_plane(img, heif_channel_G, W, H); + expect_refused(img, format); + } + + SECTION("YCbCr 4:2:0 without its Cr plane (reached the x265 plugin before)") { + heif_image* img = create_image(heif_colorspace_YCbCr, heif_chroma_420); + add_plane(img, heif_channel_Y, W, H); + add_plane(img, heif_channel_Cb, W / 2, H / 2); + expect_refused(img, format); + } + + SECTION("RGB with a depth plane") { + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444); + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_depth}) { + add_plane(img, ch, W, H); + } + expect_refused(img, format); + } + + SECTION("monochrome with a stray Cb plane") { + heif_image* img = create_image(heif_colorspace_monochrome, heif_chroma_monochrome); + add_plane(img, heif_channel_Y, W, H); + add_plane(img, heif_channel_Cb, W, H); + expect_refused(img, format); + } + + SECTION("duplicate colour plane (R twice)") { + // heif_image_add_plane() allows this (multi-spectral images consist of several monochrome + // planes); the encoder entry is where it is refused. + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444); + for (heif_channel ch : {heif_channel_R, heif_channel_R, heif_channel_G, heif_channel_B}) { + add_plane(img, ch, W, H); + } + expect_refused(img, format); + } + + SECTION("a component without colour meaning next to RGB is tolerated") { + heif_image* img = create_image(heif_colorspace_RGB, heif_chroma_444); + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + add_plane(img, ch, W, H); + } + uint32_t id = 0; + heif_error err = heif_image_add_component(img, W, H, heif_cmpd_component_type_padded, + heif_component_datatype_unsigned_integer, 8, &id); + REQUIRE(err.code == heif_error_Ok); + + EncodeResult result = encode(img, format); + INFO("encode error (" << result.code << "/" << result.subcode << "): " << result.message); + CHECK(result.code == heif_error_Ok); + heif_image_release(img); + } +} + + + +// A plane with a bit depth of 9 to 15 bits stores its samples in 16-bit words, so an +// application can fill it with values that exceed the bit depth it declared. The conversion +// operators and the encoder libraries assume that the samples are in range (libsharpyuv uses +// them as table indices, x264 aborts on an internal assertion), so the image is refused when +// it is passed to the encoder, whatever the encoder is and whether or not it is converted first. +TEST_CASE("encoding refuses images with sample values above their bit depth") +{ + heif_compression_format format = pick_encoder_format(); + if (format == heif_compression_undefined) { + SKIP("no HEVC, AV1 or uncompressed encoder available"); + } + + struct Layout + { + const char* name; + heif_colorspace colorspace; + heif_chroma chroma; + std::initializer_list channels; + }; + + const Layout layouts[] = { + {"YCbCr 4:2:0", heif_colorspace_YCbCr, heif_chroma_420, {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}}, + {"RGB", heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}}, + {"monochrome", heif_colorspace_monochrome, heif_chroma_monochrome, {heif_channel_Y}}, + }; + + for (const Layout& layout : layouts) { + for (int bit_depth : {10, 12}) { + for (bool in_range : {true, false}) { + INFO(layout.name << ", " << bit_depth << " bit, in_range=" << in_range); + + const uint16_t max_value = static_cast((1 << bit_depth) - 1); + const uint16_t value = in_range ? max_value : static_cast(max_value + 1); + + heif_image* img = create_image(layout.colorspace, layout.chroma); + for (heif_channel channel : layout.channels) { + bool subsampled = (layout.chroma == heif_chroma_420 && channel != heif_channel_Y); + uint32_t w = subsampled ? W / 2 : W; + uint32_t h = subsampled ? H / 2 : H; + + heif_error err = heif_image_add_plane(img, channel, w, h, bit_depth); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + for (uint32_t y = 0; y < h; y++) { + uint16_t* row = reinterpret_cast(p + y * stride); + for (uint32_t x = 0; x < w; x++) { + row[x] = value; + } + } + } + + EncodeResult result = encode(img, format); + INFO("encode error (" << result.code << "/" << result.subcode << "): " << result.message); + + const bool refused_for_range = (result.message.find("exceed its bit depth") != std::string::npos); + if (in_range) { + // Whether the encoder supports this bit depth is up to the encoder, but the image + // must not be refused because of its sample values. + CHECK(!refused_for_range); + } + else { + CHECK(result.code == heif_error_Usage_error); + CHECK(refused_for_range); + } + + heif_image_release(img); + } + } + } +} diff -Nru libheif-1.23.4/tests/extend_to_size_checks.cc libheif-1.23.6/tests/extend_to_size_checks.cc --- libheif-1.23.4/tests/extend_to_size_checks.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/extend_to_size_checks.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,115 @@ +/* + 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::extend_to_size_with_zero(). +// +// This function only grows an image. The per-row right-edge fill computes its +// memset length as (subsampled_width - old_width); on a shrink request that +// unsigned subtraction underflows to a huge value and the memset writes far +// past the end of the pixel plane (GHSA-hqc2-cx5m-g6ff, reachable through the +// public heif_image_extend_to_size_fill_with_zero() API). The function now +// rejects any target smaller than the current size before touching a plane. + +TEST_CASE("extend_to_size_with_zero rejects a smaller width") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(64, 8, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 64, 8, 8, limits).error_code == heif_error_Ok); + + // Requesting width 1 < 64 previously underflowed the fill length -> heap OOB write. + Error err = image->extend_to_size_with_zero(1, 8, limits); + REQUIRE(err.error_code == heif_error_Usage_error); + REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value); + + // The image must be left unchanged on the rejected call. + REQUIRE(image->get_width() == 64); + REQUIRE(image->get_height() == 8); +} + +TEST_CASE("extend_to_size_with_zero rejects a smaller height") { + auto* limits = heif_get_global_security_limits(); + + auto image = std::make_shared(); + image->create(8, 64, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(image->add_channel(heif_channel_Y, 8, 64, 8, limits).error_code == heif_error_Ok); + + Error err = image->extend_to_size_with_zero(8, 1, limits); + REQUIRE(err.error_code == heif_error_Usage_error); + REQUIRE(err.sub_error_code == heif_suberror_Invalid_parameter_value); + + REQUIRE(image->get_width() == 8); + REQUIRE(image->get_height() == 64); +} + +TEST_CASE("extend_to_size_with_zero still grows an image") { + 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); + + // Fill the visible area with a marker so we can check the padding is zero. + size_t stride; + uint8_t* p = image->get_channel_memory(heif_channel_Y, &stride); + for (uint32_t y = 0; y < 4; y++) + for (uint32_t x = 0; x < 4; x++) + p[y * stride + x] = 0xAB; + + Error err = image->extend_to_size_with_zero(8, 8, limits); + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(image->get_width() == 8); + REQUIRE(image->get_height() == 8); + + p = image->get_channel_memory(heif_channel_Y, &stride); + + // Original pixels are preserved. + for (uint32_t y = 0; y < 4; y++) + for (uint32_t x = 0; x < 4; x++) + REQUIRE(p[y * stride + x] == 0xAB); + + // Right and bottom borders are zero-filled. + for (uint32_t y = 0; y < 8; y++) + for (uint32_t x = 0; x < 8; x++) + if (x >= 4 || y >= 4) + REQUIRE(p[y * stride + x] == 0); +} + +TEST_CASE("extend_to_size_with_zero accepts an unchanged size") { + 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); + + Error err = image->extend_to_size_with_zero(8, 8, limits); + REQUIRE(err.error_code == heif_error_Ok); + REQUIRE(image->get_width() == 8); + REQUIRE(image->get_height() == 8); +} diff -Nru libheif-1.23.4/tests/file_layout.cc libheif-1.23.6/tests/file_layout.cc --- libheif-1.23.4/tests/file_layout.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/file_layout.cc 2026-10-05 16:16:00.000000000 +0000 @@ -142,8 +142,17 @@ REQUIRE(err.error_code == heif_error_Ok); REQUIRE(file.get_moov_box() != nullptr); +} + + +TEST_CASE("moov box with size 0 yields a readable sequence track") { + // The frames of the file are stored in an 'uncv' track. libheif only knows this sample + // entry when it is built with the uncompressed codec (WITH_UNCOMPRESSED_CODEC), and + // refuses the track otherwise. + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("the uncompressed codec is not compiled in"); + } - // 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); diff -Nru libheif-1.23.4/tests/fill_channel.cc libheif-1.23.6/tests/fill_channel.cc --- libheif-1.23.4/tests/fill_channel.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/fill_channel.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,96 @@ +/* + 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 "image/pixelimage.h" + +#include + +// fill_channel() writes the value with the width of the plane's storage word. It used to +// support only 8-bit and 16-bit planes: a wider plane was written as 16-bit words, which +// filled only a part of each row. +TEST_CASE( "fill_channel for all plane widths" ) +{ + auto* limits = heif_get_global_security_limits(); + const uint32_t w = 5; + const uint32_t h = 3; + + struct Case { int bit_depth; uint64_t value; }; + + // 24 and 48 bits are stored in the next larger word (32 and 64 bits). + for (const Case& c : {Case{8, 0xA5}, Case{10, 1023}, Case{16, 0xFFFF}, Case{24, 0xABCDEF}, + Case{32, 0xFFFFFFFFu}, Case{48, 0xABCDEF012345ull}, + Case{64, 0xFFFFFFFFFFFFFFFFull}, Case{64, 0x0123456789ABCDEFull}}) { + INFO("bit depth " << c.bit_depth); + + HeifPixelImage image; + image.create(w, h, heif_colorspace_custom, heif_chroma_planar); + REQUIRE(!image.add_channel(heif_channel_Y, w, h, c.bit_depth, limits, heif_component_datatype_unsigned_integer)); + REQUIRE(!image.fill_channel(heif_channel_Y, c.value)); + + size_t stride; + const uint8_t* data = image.get_channel_memory(heif_channel_Y, &stride); + const size_t bytes_per_sample = image.get_storage_bits_per_pixel(heif_channel_Y) / 8; + + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + uint64_t sample = 0; + switch (bytes_per_sample) { + case 1: sample = data[y * stride + x]; break; + case 2: sample = reinterpret_cast(data + y * stride)[x]; break; + case 4: sample = reinterpret_cast(data + y * stride)[x]; break; + case 8: sample = reinterpret_cast(data + y * stride)[x]; break; + default: FAIL("unexpected sample size"); + } + REQUIRE(sample == c.value); + } + } + } + + SECTION("all components of an interleaved plane") { + HeifPixelImage image; + image.create(w, h, heif_colorspace_RGB, heif_chroma_interleaved_RGB); + REQUIRE(!image.fill_new_channel(heif_channel_interleaved, 0x7F, w, h, 8, limits)); + + size_t stride; + const uint8_t* data = image.get_channel_memory(heif_channel_interleaved, &stride); + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < 3 * w; x++) { + REQUIRE(data[y * stride + x] == 0x7F); + } + } + } + + SECTION("planes with more than 64 bits and missing planes are refused") { + HeifPixelImage image; + image.create(w, h, heif_colorspace_custom, heif_chroma_planar); + REQUIRE(!image.add_channel(heif_channel_Y, w, h, 128, limits, heif_component_datatype_complex_number)); + + CHECK(image.fill_channel(heif_channel_Y, 0)); + CHECK(image.fill_channel(heif_channel_Alpha, 0)); + } +} diff -Nru libheif-1.23.4/tests/hevc_brands.cc libheif-1.23.6/tests/hevc_brands.cc --- libheif-1.23.4/tests/hevc_brands.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/hevc_brands.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,369 @@ +/* + 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. +*/ + +// ISO/IEC 23008-12 has brands for HEVC images in the Main and Main Still Picture profiles +// ('heic', for sequences 'hevc') and in the Main 10 and format range extensions profiles +// ('heix', 'hevx'). It has none for the other profiles of HEVC, like the high throughput +// profiles and the screen content coding profiles. libheif wrote 'heix' and 'hevx' for them. +// +// The 'miaf' brand also depends on the profile. A MIAF image item has to conform to a MIAF +// codec profile (ISO/IEC 23000-22, 6.3), and the MIAF profiles for HEVC (A.3 to A.5) list the +// HEVC profiles of their images, which end at 10 bits. libheif wrote 'miaf' for all HEVC +// images. The limits of MIAF for the tier and the level are not checked. +// +// No encoder of libheif may be able to write these profiles, and the brands only depend +// on the profile in the 'hvcC' box. So the tests change the profile in the 'hvcC' box of a +// file and compute the brands that libheif would write for it. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "libheif/heif_tiling.h" +#include "brands.h" +#include "context.h" +#include "test_utils.h" +#include "test-config.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +const int PROFILE_MAIN = 1; +const int PROFILE_MAIN_10 = 2; +const int PROFILE_MAIN_STILL_PICTURE = 3; +const int PROFILE_FORMAT_RANGE_EXTENSIONS = 4; +const int PROFILE_HIGH_THROUGHPUT = 5; +const int PROFILE_SCREEN_CONTENT_CODING = 9; +const int PROFILE_HIGH_THROUGHPUT_SCREEN_CONTENT_CODING = 11; + +struct hevc_profile +{ + const char* name; + int profile_idc; + + // The constraint flags that tell the format range extensions profiles apart (Table A.2 of + // ITU-T H.265), from general_max_12bit_constraint_flag to + // general_lower_bit_rate_constraint_flag. + const char* constraint_flags; + + heif_brand2 image_brand; // 0: there is no brand for this profile + heif_brand2 sequence_brand; + bool is_miaf_profile; +}; + +const char* const NO_FLAGS = "000000000"; + +const heif_brand2 heic = heif_brand2_heic; +const heif_brand2 heix = heif_brand2_heix; +const heif_brand2 hevc = heif_brand2_hevc; +const heif_brand2 hevx = heif_brand2_hevx; + +const hevc_profile all_profiles[] = { + // --- MIAF HEVC Basic profile + {"Main", PROFILE_MAIN, NO_FLAGS, heic, hevc, true}, + {"Main Still Picture", PROFILE_MAIN_STILL_PICTURE, NO_FLAGS, heic, hevc, true}, + + // --- MIAF HEVC Advanced profile + {"Main 10", PROFILE_MAIN_10, NO_FLAGS, heix, hevx, true}, + {"Main Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "111110100", heix, hevx, true}, + {"Main 10 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "110110101", heix, hevx, true}, + {"Main 4:2:2 10 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "110100100", heix, hevx, true}, + + // --- MIAF HEVC Extended profile + {"Main 4:4:4", PROFILE_FORMAT_RANGE_EXTENSIONS, "111000001", heix, hevx, true}, + {"Main 4:4:4 10", PROFILE_FORMAT_RANGE_EXTENSIONS, "110000001", heix, hevx, true}, + {"Main 4:4:4 10 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "110000100", heix, hevx, true}, + {"Main 4:4:4 Still Picture", PROFILE_FORMAT_RANGE_EXTENSIONS, "111000110", heix, hevx, true}, + {"Monochrome", PROFILE_FORMAT_RANGE_EXTENSIONS, "111111001", heix, hevx, true}, + {"Monochrome 10", PROFILE_FORMAT_RANGE_EXTENSIONS, "110111001", heix, hevx, true}, + + // --- not listed by MIAF, but subsets of a profile that is + {"Main 4:4:4 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "111000101", heix, hevx, true}, + {"Main 4:2:2 10", PROFILE_FORMAT_RANGE_EXTENSIONS, "110100001", heix, hevx, true}, + + // --- no MIAF profile: more than 10 bits + {"Main 12", PROFILE_FORMAT_RANGE_EXTENSIONS, "100110001", heix, hevx, false}, + {"Main 12 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "100110100", heix, hevx, false}, + {"Main 4:2:2 12", PROFILE_FORMAT_RANGE_EXTENSIONS, "100100001", heix, hevx, false}, + {"Main 4:4:4 12 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "100000100", heix, hevx, false}, + {"Main 4:4:4 16 Intra", PROFILE_FORMAT_RANGE_EXTENSIONS, "000000100", heix, hevx, false}, + {"Main 4:4:4 16 Still Picture", PROFILE_FORMAT_RANGE_EXTENSIONS, "000000110", heix, hevx, false}, + {"Monochrome 12", PROFILE_FORMAT_RANGE_EXTENSIONS, "100111001", heix, hevx, false}, + {"Monochrome 16", PROFILE_FORMAT_RANGE_EXTENSIONS, "000111001", heix, hevx, false}, + + // --- profiles without a brand of their own and without a MIAF profile + {"High Throughput 4:4:4 16 Intra", PROFILE_HIGH_THROUGHPUT, "000000100", 0, 0, false}, + {"Screen-Extended Main", PROFILE_SCREEN_CONTENT_CODING, "111110001", 0, 0, false}, + {"Screen-Extended Main 4:4:4 10", PROFILE_SCREEN_CONTENT_CODING, "110000001", 0, 0, false}, + {"Screen-Extended High Throughput 4:4:4", PROFILE_HIGH_THROUGHPUT_SCREEN_CONTENT_CODING, "111000001", 0, 0, false} +}; + + +// Overwrites the profile in all 'hvcC' boxes of the file. The compatibility flag of the +// profile is the only one that is set. +void set_hevc_profile(std::vector& file, const hevc_profile& profile) +{ + static const uint8_t fourcc[4] = {'h', 'v', 'c', 'C'}; + + int num_boxes = 0; + + auto box = std::search(file.begin(), file.end(), fourcc, fourcc + 4); + while (box != file.end()) { + REQUIRE(file.end() - box > 17); + + // HEVCDecoderConfigurationRecord: configurationVersion, then profile_tier_level + auto record = box + 4; + record[1] = static_cast((record[1] & 0xE0) | profile.profile_idc); + + const uint32_t compatibility_flags = uint32_t{1} << (31 - profile.profile_idc); + for (int i = 0; i < 4; i++) { + record[2 + i] = static_cast(compatibility_flags >> (24 - 8 * i)); + } + + // The 48 constraint flags. The first four describe the source and are kept, the + // next nine are the flags of the format range extensions. + record[6] &= 0xF0; + for (int i = 7; i < 12; i++) { + record[i] = 0; + } + + for (int i = 0; i < 9; i++) { + if (profile.constraint_flags[i] == '1') { + const int bit = 4 + i; + record[6 + bit / 8] |= static_cast(0x80 >> (bit % 8)); + } + } + + num_boxes++; + box = std::search(box + 4, file.end(), fourcc, fourcc + 4); + } + + REQUIRE(num_boxes > 0); +} + + +struct Brands +{ + heif_brand2 main_brand = 0; + std::vector compatible_brands; + + bool has(heif_brand2 brand) const + { + return std::find(compatible_brands.begin(), compatible_brands.end(), brand) != compatible_brands.end(); + } +}; + +Brands brands_for(const std::vector& file) +{ + HeifContext ctx; + Error err = ctx.read_from_memory(file.data(), file.size(), false); + INFO("read error: " << err.message); + REQUIRE(!err); + + Brands brands; + brands.compatible_brands = compute_compatible_brands(&ctx, &brands.main_brand); + return brands; +} + + +heif_error write_to_vector(heif_context*, const void* data, size_t size, void* userdata) +{ + auto* file = static_cast*>(userdata); + const uint8_t* bytes = static_cast(data); + file->insert(file->end(), bytes, bytes + size); + return heif_error{heif_error_Ok, heif_suberror_Unspecified, nullptr}; +} + +std::vector write_to_memory(heif_context* ctx) +{ + std::vector file; + heif_writer writer{}; + writer.writer_api_version = 1; + writer.write = write_to_vector; + heif_error err = heif_context_write(ctx, &writer, &file); + REQUIRE(err.code == heif_error_Ok); + + return file; +} + +heif_image* create_gray_image(uint32_t size, uint8_t value) +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(size, size, heif_colorspace_YCbCr, heif_chroma_420, &img); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + const uint32_t plane_size = (channel == heif_channel_Y ? size : size / 2); + err = heif_image_add_plane(img, channel, plane_size, plane_size, 8); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + for (uint32_t y = 0; y < plane_size; y++) { + std::fill(p + y * stride, p + y * stride + plane_size, value); + } + } + + return img; +} + +// Two tiles next to each other. +std::vector encode_hevc_grid() +{ + heif_context* ctx = heif_context_alloc(); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_HEVC, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_image* tiles[2] = {create_gray_image(64, 100), create_gray_image(64, 150)}; + + heif_image_handle* handle = nullptr; + err = heif_context_encode_grid(ctx, tiles, 1, 2, encoder, nullptr, &handle); + INFO("encoding error: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + + std::vector file = write_to_memory(ctx); + + heif_image_handle_release(handle); + heif_image_release(tiles[0]); + heif_image_release(tiles[1]); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return file; +} + +std::vector encode_hevc_sequence() +{ + const uint16_t size = 64; + + heif_context* ctx = heif_context_alloc(); + + heif_encoder* encoder = nullptr; + heif_error err = heif_context_get_encoder_for_format(ctx, heif_compression_HEVC, &encoder); + REQUIRE(err.code == heif_error_Ok); + + heif_track* track = nullptr; + err = heif_context_add_visual_sequence_track(ctx, size, size, heif_track_type_image_sequence, + nullptr, nullptr, &track); + REQUIRE(err.code == heif_error_Ok); + + for (int frame = 0; frame < 2; frame++) { + heif_image* img = create_gray_image(size, static_cast(100 + 50 * frame)); + heif_image_set_duration(img, 1); + + err = heif_track_encode_sequence_image(track, img, encoder, nullptr); + INFO("encoding error: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + + heif_image_release(img); + } + + err = heif_track_encode_end_of_sequence(track, encoder); + REQUIRE(err.code == heif_error_Ok); + + std::vector file = write_to_memory(ctx); + + heif_track_release(track); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return file; +} + + +void check_image_brands(const std::vector& original) +{ + for (const hevc_profile& profile : all_profiles) { + INFO("profile: " << profile.name); + + std::vector file = original; + set_hevc_profile(file, profile); + + const Brands brands = brands_for(file); + + CHECK(brands.has(heif_brand2_mif1)); + CHECK(brands.has(heif_brand2_heic) == (profile.image_brand == heif_brand2_heic)); + CHECK(brands.has(heif_brand2_heix) == (profile.image_brand == heif_brand2_heix)); + CHECK(brands.has(heif_brand2_miaf) == profile.is_miaf_profile); + + // Without a brand for the image, what remains is the structural brand. + CHECK(brands.main_brand == (profile.image_brand ? profile.image_brand : heif_brand2_mif1)); + } +} + +} // namespace + + +TEST_CASE("brands of an HEVC image follow its profile") +{ + std::ifstream istr(tests_data_directory + "/rainbow-451x461.heic", std::ios::binary); + REQUIRE(istr.good()); + const std::vector file{std::istreambuf_iterator(istr), std::istreambuf_iterator()}; + + check_image_brands(file); +} + + +TEST_CASE("brands of an HEVC grid image follow the profile of its tiles") +{ + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("no HEVC encoder available"); + } + + check_image_brands(encode_hevc_grid()); +} + + +TEST_CASE("brand of an HEVC image sequence follows its profile") +{ + if (!heif_have_encoder_for_format(heif_compression_HEVC)) { + SKIP("no HEVC encoder available"); + } + + const std::vector original = encode_hevc_sequence(); + + for (const hevc_profile& profile : all_profiles) { + INFO("profile: " << profile.name); + + std::vector file = original; + set_hevc_profile(file, profile); + + const Brands brands = brands_for(file); + + CHECK(brands.has(heif_brand2_msf1)); + CHECK(brands.has(heif_brand2_hevc) == (profile.sequence_brand == heif_brand2_hevc)); + CHECK(brands.has(heif_brand2_hevx) == (profile.sequence_brand == heif_brand2_hevx)); + + CHECK(brands.main_brand == (profile.sequence_brand ? profile.sequence_brand : heif_brand2_msf1)); + } +} diff -Nru libheif-1.23.4/tests/hevc_sps.cc libheif-1.23.6/tests/hevc_sps.cc --- libheif-1.23.4/tests/hevc_sps.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/hevc_sps.cc 2026-10-05 16:16:00.000000000 +0000 @@ -26,9 +26,13 @@ #include "catch_amalgamated.hpp" #include "codecs/hevc_boxes.h" +#include "codecs/hevc_dec.h" #include "codecs/decoder.h" +#include "bitstream.h" #include "error.h" +#include "libheif/heif.h" #include +#include #include // SPS NAL unit (with emulation prevention bytes) taken from the hvcC box of @@ -40,6 +44,99 @@ 0x96, 0xea, 0x49, 0x29, 0xae, 0x6e, 0x02, 0x1a, 0x0c, 0x08, 0x00, 0x00, 0x03, 0x00, 0xc8, 0x00, 0x00, 0x03, 0x00, 0x08, 0x40}; +static void write_uvlc(BitWriter& writer, uint32_t value) +{ + uint32_t code = value + 1; + int bits = 0; + for (uint32_t n = code; n != 0; n >>= 1) { + bits++; + } + writer.write_bits(0, bits - 1); + writer.write_bits(code, bits); +} + + +static std::vector sps_with_sub_layer_profile() +{ + BitWriter writer; + writer.write_bits8(0x42, 8); // SPS NAL header + writer.write_bits8(0x01, 8); + writer.write_bits(0, 4); // sps_video_parameter_set_id + writer.write_bits(1, 3); // sps_max_sub_layers_minus1 + writer.write_flag(true); // sps_temporal_id_nesting_flag + + writer.write_bits(1, 8); // general profile space, tier, and idc + writer.write_bits32(0, 32); // general compatibility flags + writer.write_bits16(0, 16); + writer.write_bits16(0, 16); + writer.write_bits16(0, 16); // 48 general source and constraint flags + writer.write_bits8(120, 8); // general_level_idc + + writer.write_flag(true); // sub_layer_profile_present_flag[0] + writer.write_flag(true); // sub_layer_level_present_flag[0] + for (int i = 1; i < 8; i++) { + writer.write_bits(0, 2); // reserved_zero_2bits + } + writer.write_bits(1, 8); // sub-layer profile space, tier, and idc + writer.write_bits32(0, 32); // sub-layer compatibility flags + writer.write_bits16(0, 16); + writer.write_bits16(0, 16); + writer.write_bits16(0, 16); // 48 sub-layer source and constraint flags + writer.write_bits8(120, 8); // sub_layer_level_idc[0] + + write_uvlc(writer, 0); // sps_seq_parameter_set_id + write_uvlc(writer, 1); // chroma_format_idc (4:2:0) + write_uvlc(writer, 64); // pic_width_in_luma_samples + write_uvlc(writer, 48); // pic_height_in_luma_samples + writer.write_flag(false); // conformance_window_flag + write_uvlc(writer, 0); // bit_depth_luma_minus8 + write_uvlc(writer, 0); // bit_depth_chroma_minus8 + return writer.get_data(); +} + + +TEST_CASE("SPS sub-layer profile consumes all 88 bits") +{ + std::vector sps = sps_with_sub_layer_profile(); + 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(width == 64); + CHECK(height == 48); + CHECK(coded.width == 64); + CHECK(coded.height == 48); +} + + +TEST_CASE("HEVC coded-size scan rejects a malformed SPS after a valid one") +{ + std::vector invalid_sps = rainbow_sps; + REQUIRE((invalid_sps[18] & 0xF0) == 0xA0); + invalid_sps[18] = 0x94 | (invalid_sps[18] & 0x03); // chroma_format_idc = 4 + + std::vector data; + auto append_nal = [&data](const std::vector& nal) { + uint32_t size = static_cast(nal.size()); + data.push_back(static_cast(size >> 24)); + data.push_back(static_cast(size >> 16)); + data.push_back(static_cast(size >> 8)); + data.push_back(static_cast(size)); + data.insert(data.end(), nal.begin(), nal.end()); + }; + append_nal(rainbow_sps); + append_nal(invalid_sps); + + auto box = std::make_shared(); + Decoder_HEVC decoder(box); + auto result = decoder.get_max_coded_image_size(data); + REQUIRE(result.is_error()); + CHECK(result.error().error_code == heif_error_Invalid_input); +} + TEST_CASE("SPS conformance window yields visible and coded size") { @@ -62,6 +159,56 @@ } +TEST_CASE("SPS constraint flags are copied to hvcC") +{ + 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); + + // The SPS has general_progressive_source_flag and general_frame_only_constraint_flag + // set: the constraint flags are 0x90 and five zero bytes. + + CHECK(config.general_profile_idc == HEVCDecoderConfigurationRecord::Profile_MainStillPicture); + + for (int i = 0; i < HEVCDecoderConfigurationRecord::NUM_CONSTRAINT_INDICATOR_FLAGS; i++) { + INFO("constraint flag " << i); + CHECK(config.general_constraint_indicator_flags[i] == (i == 0 || i == 3)); + } + + // They are written in the same order, the first flag in the most significant bit. + + config.configuration_version = 1; + + // a flag in each of the bytes, among them the first and the last bit of a byte + config.general_constraint_indicator_flags[8] = true; + config.general_constraint_indicator_flags[23] = true; + config.general_constraint_indicator_flags[47] = true; + + StreamWriter writer; + err = config.write(writer); + REQUIRE(!err); + + const std::vector data = writer.get_data(); + REQUIRE(data.size() >= 12); + + const std::vector constraint_flags(data.begin() + 6, data.begin() + 12); + CHECK(constraint_flags == std::vector{0x90, 0x80, 0x01, 0x00, 0x00, 0x01}); + + // ... and read back into the same flags. + + BitstreamRange range(std::make_shared(data.data(), data.size(), false), data.size()); + + HEVCDecoderConfigurationRecord parsed{}; + err = parsed.parse(range, heif_get_global_security_limits()); + REQUIRE(!err); + CHECK(parsed.general_constraint_indicator_flags == config.general_constraint_indicator_flags); +} + + TEST_CASE("SPS chroma_format_idc out of range is rejected") { // Same SPS with chroma_format_idc changed from 1 (uvlc '010') to 4 diff -Nru libheif-1.23.4/tests/idat.cc libheif-1.23.6/tests/idat.cc --- libheif-1.23.4/tests/idat.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/idat.cc 2026-10-05 16:16:00.000000000 +0000 @@ -46,6 +46,6 @@ box->get_type(); box->get_type_string(); Indent indent; - box->dump(indent); + box->dump_to_string(indent); } } diff -Nru libheif-1.23.4/tests/inband_coded_size_limit.cc libheif-1.23.6/tests/inband_coded_size_limit.cc --- libheif-1.23.4/tests/inband_coded_size_limit.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/inband_coded_size_limit.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,168 @@ +/* + 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 the in-band coded-size security gate. +// +// The container 'ispe' can declare a small image while the actual coded size +// lives in the bitstream: an AV1 sequence header OBU, an SPS NAL for AVC/HEVC/VVC, +// or the SOF marker for JPEG. That coded size is what the decoder allocates. For the NAL codecs the +// SPS may sit in the item data rather than only in the avcC/hvcC/vvcC record, +// and for JPEG the SOF is always in the bitstream. libheif now scans the whole +// combined config+bitstream buffer for the largest coded size and rejects it +// against the (ispe-tightened) security limits before any decoder plugin runs. +// +// Each file below carries a 64x64 (HEVC: 320x240) 'ispe' but an oversized coded +// size in the bitstream, and must be rejected with a security-limit error. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" + +#include +#include +#include + +namespace { + +std::vector base64_decode(const std::string& in) { + auto val = [](char c) -> int { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; + }; + std::vector out; + int bits = 0; + uint32_t acc = 0; + for (char c : in) { + if (c == '=' || c == '\n' || c == '\r') continue; + int v = val(c); + if (v < 0) continue; + acc = (acc << 6) | uint32_t(v); + bits += 6; + if (bits >= 8) { bits -= 8; out.push_back(uint8_t((acc >> bits) & 0xFF)); } + } + return out; +} + +// Decode `b64` and assert it is rejected with a security-limit error before the +// decoder allocates. Returns without asserting if no decoder for `format` is built. +void expect_security_reject(heif_compression_format format, const char* b64) { + if (!heif_have_decoder_for_format(format)) { + SKIP("no decoder for this format built, skipping"); + } + + std::vector data = base64_decode(b64); + + 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_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + + if (img) heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} + +// HEVC item, 320x240 ispe, with an extra SPS (declaring 2000x2000) injected into +// the item data. The hvcC SPS (320x240) passes the tightened limit, so only the +// in-band 2000x2000 SPS can trip the gate. +const char* kHevcInbandB64 = + "AAAAHGZ0eXBoZWljAAAAAG1pZjFoZWljbWlhZgAAAVZtZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAAAAA" + "ACJpbG9jAAAAAERAAAEAAQAAAAABegABAAAAAAAAAKQAAAAjaWluZgAAAAAAAQAAABVpbmZlAgAAAAABAABodmMxAAAAAA5waXRt" + "AAAAAAABAAAA1mlwcnAAAAC3aXBjbwAAAHhodmNDAQNwAAAAAAAAAAAAPPAA/P34+AAADwNgAAEAGEABDAH//wNwAAADAJAAAAMA" + "AAMAPLoCQGEAAQArQgEBA3AAAAMAkAAAAwAAAwA8oAoIDxZbqSSmubgIaDAgAAADAyAAAAMAIWIAAQAHRAHBcrBiQAAAABNjb2xy" + "bmNseAABAA0ABoAAAAAUaXNwZQAAAAAAAAFAAAAA8AAAABBwaXhpAAAAAAMICAgAAAAXaXBtYQAAAAAAAAABAAEEgQIDBAAAAKxt" + "ZGF0AAAALUIBAQNwAAADAJAAAAMAAAMAlqAD6IAfRZbqSSmubgIaDAgAAAMAyAAAAwAIQAAAAG8oAa8E8hpVEoBBMmnb/xh///+y" + "jH//RNiT0cYM7GDOTinQihDqlAAAAwAAAwB5QABlUvVKAAADAAADAAADAAADAAAacAAAAwAAAwAAAwAAAwAAAwAAX0AAAAMAAAMA" + "AAMAAAMAAAMAAAMAAAMAAx4="; + +// AVC item, 64x64 ispe, avcC carrying no SPS (only a PPS); the SPS (declaring +// 1024x512) is in the item data. The old config-record-only check saw no SPS. +const char* kAvcInbandB64 = + "AAAAGGZ0eXBtaWYxAAAAAG1pZjFoZWljAAAA0W1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QAAAAAAAAAAAAAAAAAAAAADnBp" + "dG0AAAAAAAEAAAAjaWluZgAAAAAAAQAAABVpbmZlAgAAAAABAABhdmMxAAAAAFFpcHJwAAAANGlwY28AAAAYYXZjQwFkACj/4AEA" + "B2joQ4OSyLAAAAAUaXNwZQAAAAAAAABAAAAAQAAAABVpcG1hAAAAAAAAAAEAAQKBAgAAACJpbG9jAAAAAERAAAEAAQAAAAAA8QAB" + "AAAAAAAAAB4AAAAmbWRhdAAAABpnZAAorHIEQEAEGhAAAAMAEAAAAwMg8YMYRg=="; + +// JPEG item, 64x64 ispe, with a SOF marker declaring 8000x8000. +const char* kJpegSofB64 = + "AAAAGGZ0eXBtaWYxAAAAAG1pZjFoZWljAAAAuG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QAAAAAAAAAAAAAAAAAAAAADnBp" + "dG0AAAAAAAEAAAAjaWluZgAAAAAAAQAAABVpbmZlAgAAAAABAABqcGVnAAAAADhpcHJwAAAAHGlwY28AAAAUaXNwZQAAAAAAAABA" + "AAAAQAAAABRpcG1hAAAAAAAAAAEAAQGBAAAAImlsb2MAAAAAREAAAQABAAAAAADYAAEAAAAAAAAChAAAAoxtZGF0/9j/4AAQSkZJ" + "RgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0o" + "MCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/" + "wAARCB9AH0ADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQID" + "AAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZn" + "aGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx" + "8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJB" + "UQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3" + "eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6" + "/9oADAMBAAIRAxEAPwCKiiivjz74KKKKACiiigAooooA/9k="; + + +// AVIF item, 64x64 ispe, with an in-band AV1 Sequence Header OBU coding 8192x8192. +// This is the original advisory reproducer (GHSA-v8qw-hwjv-44hw, +// poc_av1_huge-frame-8192.avif). +const char* kAvifPocB64 = + "AAAAHGZ0eXBhdmlmAAAAAG1pZjFhdmlmbWlhZgAAAM9tZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAA" + "AAAAAA5waXRtAAAAAAABAAAAI2lpbmYAAAAAAAEAAAAVaW5mZQIAAAAAAQAAYXYwMQAAAABRaXBycAAAADRpcGNvAAAAFGlz" + "cGUAAAAAAAAAQAAAAEAAAAAYYXYxQ4ERDAAKCgAAAAKv/4lfIAgAAAAVaXBtYQAAAAAAAAABAAECgYIAAAAgaWxvYwEAAABE" + "AAABAAEAAAAAAAEAAADzAAAAbAAAAHRtZGF0EgAKDQAAAIP8f/x//Er5AEAyWRAAkLQAggQQAACAACUAKPPXDGWYHeUNJr+m" + "mLGOwbZyMlmA0easG5s7ljg1k8Hjl6lwngAo89cMZZgd5Q0mv6aYsY7BtnIyWYDR5qwbmzuWODWTweOXqXCe" +; + +} // namespace + + +TEST_CASE("avif: reject in-band AV1 sequence header exceeding the security limit") { + expect_security_reject(heif_compression_AV1, kAvifPocB64); +} + +TEST_CASE("hevc: reject in-band SPS exceeding the security limit") { + expect_security_reject(heif_compression_HEVC, kHevcInbandB64); +} + +TEST_CASE("avc: reject in-band SPS exceeding the security limit") { + expect_security_reject(heif_compression_AVC, kAvcInbandB64); +} + +TEST_CASE("jpeg: reject oversized SOF exceeding the security limit") { + expect_security_reject(heif_compression_JPEG, kJpegSofB64); +} diff -Nru libheif-1.23.4/tests/jpeg2000.cc libheif-1.23.6/tests/jpeg2000.cc --- libheif-1.23.4/tests/jpeg2000.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/jpeg2000.cc 2026-10-05 16:16:00.000000000 +0000 @@ -27,8 +27,13 @@ #include "catch_amalgamated.hpp" #include "libheif/heif.h" #include "codecs/jpeg2000_boxes.h" +#include "security_limits.h" +#include "test-config.h" #include #include +#include +#include +#include TEST_CASE( "cdef" ) @@ -53,7 +58,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = cdef->dump(indent); + std::string dump_output = cdef->dump_to_string(indent); REQUIRE(dump_output == "Box: cdef -----\nsize: 0 (header size: 0)\nchannel_index: 1, channel_type: 2, channel_association: 0\n"); } @@ -87,7 +92,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = cmap->dump(indent); + std::string dump_output = cmap->dump_to_string(indent); REQUIRE(dump_output == "Box: cmap -----\nsize: 0 (header size: 0)\ncomponent_index: 2, mapping_type: 1, palette_colour: 3\ncomponent_index: 4, mapping_type: 0, palette_colour: 0\n"); } @@ -100,7 +105,7 @@ REQUIRE(pclr->get_bit_depths().size() == 0); Indent indent; - std::string dump_output = pclr->dump(indent); + std::string dump_output = pclr->dump_to_string(indent); REQUIRE(dump_output == "Box: pclr -----\nsize: 0 (header size: 0)\nNE: 0, NPC: 0, B: \n"); StreamWriter writer; @@ -131,10 +136,10 @@ Error err = pclr->write(writer); REQUIRE(err.error_code == heif_error_Ok); const std::vector bytes = writer.get_data(); - std::vector expected = {0x00, 0x00, 0x00, 0x14, 'p', 'c', 'l', 'r', 0x00, 0x02, 0x03, 0x08, 0x08, 0x08, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD}; + std::vector expected = {0x00, 0x00, 0x00, 0x14, 'p', 'c', 'l', 'r', 0x00, 0x02, 0x03, 0x07, 0x07, 0x07, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD}; REQUIRE(bytes == expected); Indent indent; - std::string dump_output = pclr->dump(indent); + std::string dump_output = pclr->dump_to_string(indent); REQUIRE(dump_output == "Box: pclr -----\nsize: 0 (header size: 0)\nNE: 2, NPC: 3, B: 8, 8, 8, \n"); } @@ -159,10 +164,10 @@ Error err = pclr->write(writer); REQUIRE(err.error_code == heif_error_Ok); const std::vector bytes = writer.get_data(); - std::vector expected = {0x00, 0x00, 0x00, 0x1A, 'p', 'c', 'l', 'r', 0x00, 0x02, 0x03, 0x0C, 0x0C, 0x0C, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x0F, 0xFF, 0x0F, 0xFE, 0x0F, 0xFD}; + std::vector expected = {0x00, 0x00, 0x00, 0x1A, 'p', 'c', 'l', 'r', 0x00, 0x02, 0x03, 0x0B, 0x0B, 0x0B, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x0F, 0xFF, 0x0F, 0xFE, 0x0F, 0xFD}; REQUIRE(bytes == expected); Indent indent; - std::string dump_output = pclr->dump(indent); + std::string dump_output = pclr->dump_to_string(indent); REQUIRE(dump_output == "Box: pclr -----\nsize: 0 (header size: 0)\nNE: 2, NPC: 3, B: 12, 12, 12, \n"); } @@ -188,7 +193,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = j2kL->dump(indent); + std::string dump_output = j2kL->dump_to_string(indent); REQUIRE(dump_output == "Box: j2kL -----\nsize: 0 (header size: 0)\nlayer_id: 1, discard_levels: 2, decode_layers: 3\n"); } @@ -727,3 +732,228 @@ REQUIRE(uut.get_precision(0) == 8); REQUIRE(uut.hasHighThroughputExtension() == false); } + + +// --- 'pclr' parsing (GHSA-9c75-9g8r-4728) ---------------------------------- +// +// A 'pclr' box with zero palette columns declares no entry data, so the entry +// count was not bounded by the box size and each declared entry allocated an +// empty vector. Nested 'j2kH' containers repeated this hundreds of times from +// a few KB of input. The parser now rejects zero columns, bounds the entry +// count by the box payload, and charges the palette to the memory limits. + +// Parses a single box from raw bytes, as it would be encountered inside a +// 'j2kH' container. +static Error parse_box(const std::vector& bytes, + const heif_security_limits* limits, + std::shared_ptr* box) +{ + auto reader = std::make_shared(bytes.data(), bytes.size(), false); + BitstreamRange range(reader, bytes.size()); + return Box::read(range, box, limits); +} + +// Box header (size, 'pclr') followed by the payload. +static std::vector pclr_box(std::vector payload) +{ + uint32_t size = static_cast(8 + payload.size()); + std::vector bytes{static_cast(size >> 24), static_cast(size >> 16), + static_cast(size >> 8), static_cast(size), + 'p', 'c', 'l', 'r'}; + bytes.insert(bytes.end(), payload.begin(), payload.end()); + return bytes; +} + + +TEST_CASE("pclr parse") +{ + // NE=2, NPC=2, B={7,11} (8 and 12 bits), entries (0x10, 0x0123) and (0x20, 0x0456) + auto bytes = pclr_box({0x00, 0x02, 0x02, 0x07, 0x0b, + 0x10, 0x01, 0x23, + 0x20, 0x04, 0x56}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err == Error::Ok); + + auto pclr = std::dynamic_pointer_cast(box); + REQUIRE(pclr); + REQUIRE(pclr->get_bit_depths() == std::vector{8, 12}); + REQUIRE(pclr->get_entries().size() == 2); + REQUIRE(pclr->get_entries()[0].columns == std::vector{0x10, 0x0123}); + REQUIRE(pclr->get_entries()[1].columns == std::vector{0x20, 0x0456}); +} + + +TEST_CASE("pclr bit depth field is precision minus one") +{ + // Table I.13: B_i = 0 is a 1-bit column stored in one byte, B_i = 15 is a + // 16-bit column stored in two bytes. NE=1, NPC=2, entry (1, 0xFFFF). + auto bytes = pclr_box({0x00, 0x01, 0x02, 0x00, 0x0f, + 0x01, 0xff, 0xff}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err == Error::Ok); + + auto pclr = std::dynamic_pointer_cast(box); + REQUIRE(pclr); + REQUIRE(pclr->get_bit_depths() == std::vector{1, 16}); + REQUIRE(pclr->get_entries().size() == 1); + REQUIRE(pclr->get_entries()[0].columns == std::vector{1, 0xffff}); +} + + +TEST_CASE("pclr rejects unsupported bit depths") +{ + SECTION("17 bits does not fit the 16-bit entry storage") { + // NE=1, NPC=1, B=16 (17 bits), entry 0x000001 + auto bytes = pclr_box({0x00, 0x01, 0x01, 0x10, 0x00, 0x00, 0x01}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err.error_code == heif_error_Unsupported_feature); + } + + SECTION("signed columns") { + // NE=1, NPC=1, B=0x87 (signed 8 bits), entry 0x01 + auto bytes = pclr_box({0x00, 0x01, 0x01, 0x87, 0x01}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err.error_code == heif_error_Unsupported_feature); + } +} + + +TEST_CASE("pclr one-bit columns still bound the entry count") +{ + // The smallest possible column (B_i = 0, 1 bit) still occupies one byte per + // entry, so the byte bound must reject NE=65535 with no entry data. This is + // the case a naive ceil(B_i / 8) would let through. + auto bytes = pclr_box({0xff, 0xff, 0x01, 0x00}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err.error_code == heif_error_Invalid_input); + REQUIRE(err.sub_error_code == heif_suberror_End_of_data); +} + + +TEST_CASE("pclr write and parse round trip") +{ + auto pclr = std::make_shared(); + pclr->set_columns(3, 12); + Box_pclr::PaletteEntry entry0; + entry0.columns = {0x001, 0x002, 0xfff}; + pclr->add_entry(entry0); + Box_pclr::PaletteEntry entry1; + entry1.columns = {0x800, 0x000, 0x7ff}; + pclr->add_entry(entry1); + + StreamWriter writer; + REQUIRE(pclr->write(writer) == Error::Ok); + const std::vector bytes = writer.get_data(); + + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err == Error::Ok); + + auto parsed = std::dynamic_pointer_cast(box); + REQUIRE(parsed); + REQUIRE(parsed->get_bit_depths() == pclr->get_bit_depths()); + REQUIRE(parsed->get_entries().size() == 2); + REQUIRE(parsed->get_entries()[0].columns == entry0.columns); + REQUIRE(parsed->get_entries()[1].columns == entry1.columns); +} + + +TEST_CASE("pclr more than 255 entries round trip") +{ + // NE is a 16-bit field; get_num_entries() used to return uint8_t, so the + // writer emitted a truncated count for palettes above 255 entries. + auto pclr = std::make_shared(); + pclr->set_columns(1, 8); + for (int i = 0; i < 300; i++) { + Box_pclr::PaletteEntry entry; + entry.columns = {static_cast(i & 0xff)}; + pclr->add_entry(entry); + } + REQUIRE(pclr->get_num_entries() == 300); + + StreamWriter writer; + REQUIRE(pclr->write(writer) == Error::Ok); + const std::vector bytes = writer.get_data(); + REQUIRE(bytes.size() == 8 + 2 + 1 + 1 + 300); + REQUIRE(bytes[8] == 0x01); // NE = 0x012c + REQUIRE(bytes[9] == 0x2c); + + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err == Error::Ok); + auto parsed = std::dynamic_pointer_cast(box); + REQUIRE(parsed); + REQUIRE(parsed->get_num_entries() == 300); + REQUIRE(parsed->get_entries()[299].columns == std::vector{299 & 0xff}); +} + + +TEST_CASE("pclr zero columns rejected") +{ + // NE=65535, NPC=0: the 11-byte box from the advisory. + auto bytes = pclr_box({0xff, 0xff, 0x00}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err.error_code == heif_error_Invalid_input); + REQUIRE(std::dynamic_pointer_cast(box) == nullptr); +} + + +TEST_CASE("pclr entry count bounded by box payload") +{ + // NE=65535, NPC=1, B=7 (8 bits), but no entry data at all. + auto bytes = pclr_box({0xff, 0xff, 0x01, 0x07}); + std::shared_ptr box; + Error err = parse_box(bytes, heif_get_global_security_limits(), &box); + REQUIRE(err.error_code == heif_error_Invalid_input); + REQUIRE(err.sub_error_code == heif_suberror_End_of_data); +} + + +TEST_CASE("pclr palette charged to memory limits") +{ + // NE=256, NPC=1, B=7 (8 bits), with all 256 entry bytes present. This is a + // valid palette, but it costs 256 PaletteEntry vectors in memory. + std::vector payload{0x01, 0x00, 0x01, 0x07}; + for (int i = 0; i < 256; i++) { + payload.push_back(static_cast(i)); + } + auto bytes = pclr_box(payload); + + heif_security_limits limits = *heif_get_global_security_limits(); + TotalMemoryTracker tracker(&limits); + + SECTION("accepted within the budget") { + std::shared_ptr box; + Error err = parse_box(bytes, &limits, &box); + REQUIRE(err == Error::Ok); + auto pclr = std::dynamic_pointer_cast(box); + REQUIRE(pclr); + REQUIRE(pclr->get_entries().size() == 256); + } + + SECTION("rejected when it exceeds max_total_memory") { + limits.max_total_memory = 1024; + std::shared_ptr box; + Error err = parse_box(bytes, &limits, &box); + REQUIRE(err.error_code == heif_error_Memory_allocation_error); + REQUIRE(err.sub_error_code == heif_suberror_Security_limit_exceeded); + } +} + + +TEST_CASE("pclr zero columns in nested j2kH rejects the file") +{ + // The advisory's reproducer: 200 zero-column 'pclr' boxes spread over + // nested 'j2kH' properties. It used to parse successfully at ~330 MB RSS. + heif_context* ctx = heif_context_alloc(); + std::string path = tests_data_directory + "/pclr_zero_columns.heic"; + heif_error err = heif_context_read_from_file(ctx, path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Invalid_input); + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/tests/jpeg2000_openjpeg_grid_limit.cc libheif-1.23.6/tests/jpeg2000_openjpeg_grid_limit.cc --- libheif-1.23.4/tests/jpeg2000_openjpeg_grid_limit.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/jpeg2000_openjpeg_grid_limit.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,385 @@ +/* + 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-q492-cfcm-895h: the OpenJPEG decoder plugin's +// pre-decode resource-limit gate only bounded the JPEG 2000 *window* span +// (x1-x0) * (y1-y0), not the absolute SIZ coordinates. A crafted codestream +// can declare a tiny window (e.g. 17 pixels) anchored at coordinates close to +// the 32-bit boundary; OpenJPEG's tile/coefficient arithmetic operates over +// the full reference grid (Xsiz=x1, Ysiz=y1), not just the window, so such a +// file passed libheif's span-based checks and reached opj_decode() with +// pathological geometry (observed as a heap-buffer-overflow write inside +// OpenJPEG 2.3.1; corresponds to the sink class of CVE-2020-6851). +// +// The fix additionally bounds the reference-grid area x1*y1 against +// max_image_size_pixels, so the file below must now be rejected with a +// Security_limit_exceeded error *before* opj_decode() is ever called. +// +// The test file bytes below are exactly the trigger.heif PoC from the +// advisory (ISO base media container bytes + a real JPEG 2000 codestream +// with SIZ: x0=2147483631, x1=2147483648, y0=0, y1=1 -> window span = 17 +// pixels, reference grid area = 2147483648 pixels). + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" + +#include +#include +#include + +namespace { + +// Minimal base64 decoder (no external dependency needed for this test). +std::vector base64_decode(const std::string& in) { + auto val = [](char c) -> int { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; + }; + + std::vector out; + int bits = 0; + uint32_t acc = 0; + for (char c : in) { + if (c == '=' || c == '\n' || c == '\r') { + continue; + } + int v = val(c); + if (v < 0) { + continue; + } + acc = (acc << 6) | uint32_t(v); + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push_back(uint8_t((acc >> bits) & 0xFF)); + } + } + return out; +} + + +// ftyp + meta for a single j2k1 item; iloc extent starts right after this +// header, at file offset 284. Taken verbatim from the advisory's PoC. +const char* kHeaderB64 = + "AAAAHGZ0eXBqMmtpAAAAAG1pZjFqMmtpbWlhZgAAAQBtZXRhAAAAAAAAACFoZGxyAAAAAAAAAABw" + "aWN0AAAAAAAAAAAAAAAAAAAAACJpbG9jAAAAAERAAAEAAQAAAAABJAABAAAAAAAABCkAAAAjaWlu" + "ZgAAAAAAAQAAABVpbmZlAgAAAAABAABqMmsxAAAAAA5waXRtAAAAAAABAAAAgGlwcnAAAABhaXBj" + "bwAAACRqMmtIAAAAHGNkZWYAAwAAAAAAAQABAAAAAgACAAAAAwAAABNjb2xybmNseAABAA0ABoAA" + "AAAUaXNwZQAAAAAAAAAgAAAAIAAAAA5waXhpAAAAAAEIAAAAF2lwbWEAAAAAAAAAAQABBIECAwQ="; + +// Raw JPEG 2000 codestream (1065 bytes). SIZ: huge absolute offsets, window +// span == 17 pixels, reference grid area == 2147483648 pixels. +const char* kJ2kB64 = + "/0//UQApAACAAAAAAAAAAX///+8AAAAAAAAAEAAAAAF////vAAAAAAABBwEB/1IADAAAAAEAAAQE" + "AAH/XAAEQED/ZAAlAAFDcmVhdGVkIGJ5IE9wZW5KUEVHIHZlcnNpb24gMi4zLjH/kAAKAAAAAAAf" + "AAH/k9+AcAc2AD6Ey0mHIVs2Icqr/5AACgABAAAAHgAB/5PPtDQJXfai5z0KYbomZ1SF/5AACgAC" + "AAAAHgAB/5PPtDQJWaoxlpMOQrZsQ5VX/5AACgADAAAAHgAB/5PPtDQJY0vTznoUw3RMzqkL/5AA" + "CgAEAAAAHwAB/5PPtDgHNgA+enwM/2bxxDlVf/+QAAoABQAAAB0AAf+Tx9QYCVmqHPgZ/03jiHKq" + "/5AACgAGAAAAHAAB/5PH1BYHNgA6dOTCJ3lUzP+QAAoABwAAABoAAf+Tw+cSBzXV2/Un/O3v/5AA" + "CgAIAAAAGQAB/5PB8ggZze1KnSkB0f+QAAoACQAAABwAAf+Tw+cWAqnKfq5737yqZj//kAAKAAoA" + "AAAdAAH/k8fUGAKq6UAZ/03jiHKqf/+QAAoACwAAAB0AAf+Tx9QYAqWeshn/TeOIcqp//5AACgAM" + "AAAAHQAB/5PPtDACqwf7AF0LWbEOVU//kAAKAA0AAAAeAAH/k8+0NAKrjMbgakU4qm2dUhP/kAAK" + "AA4AAAAdAAH/k8+0MAKlvW0AXQtZsQ5VT/+QAAoADwAAAB4AAf+Tz7Q0AqY5a42auO6JmdUhP/+Q" + "AAoAEAAAAB8AAf+T34BwBzYAPoTLSYchWzYhyqv/kAAKABEAAAAeAAH/k8+0NAld9qLnPQphuiZn" + "VIX/kAAKABIAAAAeAAH/k8+0NAlZqjGWkw5CtmxDlVf/kAAKABMAAAAeAAH/k8+0NAljS9POehTD" + "dEzOqQv/kAAKABQAAAAfAAH/k8+0OAc2AD56fAz/ZvHEOVV//5AACgAVAAAAHQAB/5PH1BgJWaoc" + "+Bn/TeOIcqr/kAAKABYAAAAcAAH/k8fUFgc2ADp05MIneVTM/5AACgAXAAAAGgAB/5PD5xIHNdXb" + "9Sf87e//kAAKABgAAAAZAAH/k8HyCBnN7UqdKQHR/5AACgAZAAAAHAAB/5PD5xYCqcp+rnvfvKpm" + "P/+QAAoAGgAAAB0AAf+Tx9QYAqrpQBn/TeOIcqp//5AACgAbAAAAHQAB/5PH1BgCpZ6yGf9N44hy" + "qn//kAAKABwAAAAdAAH/k8+0MAKrB/sAXQtZsQ5VT/+QAAoAHQAAAB4AAf+Tz7Q0AquMxuBqRTiq" + "bZ1SE/+QAAoAHgAAAB0AAf+Tz7QwAqW9bQBdC1mxDlVP/5AACgAfAAAAHgAB/5PPtDQCpjlrjZq4" + "7omZ1SE//5AACgAgAAAAHwAB/5PfgHAHPcWfV5c9In/nmcSVV//Z"; + + +// Appends the codestream as 'mdat' to the header above. The item location in the header +// is fixed, hence the codestream has to have the size of the advisory's PoC. +std::vector wrap_codestream_in_heif(const std::vector& j2k) { + std::vector header = base64_decode(kHeaderB64); + REQUIRE(header.size() == 284); + REQUIRE(j2k.size() == 1065); + + std::vector file = header; + + uint32_t mdat_size = uint32_t(8 + j2k.size()); + file.push_back(uint8_t((mdat_size >> 24) & 0xFF)); + file.push_back(uint8_t((mdat_size >> 16) & 0xFF)); + file.push_back(uint8_t((mdat_size >> 8) & 0xFF)); + file.push_back(uint8_t(mdat_size & 0xFF)); + file.push_back('m'); + file.push_back('d'); + file.push_back('a'); + file.push_back('t'); + file.insert(file.end(), j2k.begin(), j2k.end()); + + return file; +} + + +std::vector build_trigger_heif() { + return wrap_codestream_in_heif(base64_decode(kJ2kB64)); +} + + +void append_be(std::vector& data, uint32_t value, int num_bytes) { + for (int i = num_bytes - 1; i >= 0; i--) { + data.push_back(uint8_t((value >> (i * 8)) & 0xFF)); + } +} + + +// A codestream that consists of the SOC marker, optionally an unknown marker segment, and +// an SIZ marker segment for three 8-bit components. It is padded with zeros to the size +// of the item. Nothing more is needed, because the codestream has to be rejected based on +// its SIZ marker segment. +std::vector build_codestream(uint32_t width, uint32_t height, + uint32_t tile_width, uint32_t tile_height, + bool data_before_siz = false) { + const uint16_t num_components = 3; + + std::vector j2k = {0xFF, 0x4F}; // SOC + + if (data_before_siz) { + // A marker that does not exist in JPEG 2000, with a segment length of 4 and two bytes + // of data. OpenJPEG and FFmpeg both skip it and continue with the SIZ marker segment. + j2k.insert(j2k.end(), {0xFF, 0xD8, 0x00, 0x04, 0x12, 0x34}); + } + + j2k.insert(j2k.end(), {0xFF, 0x51}); // SIZ + append_be(j2k, 38 + 3 * num_components, 2); // Lsiz + append_be(j2k, 0, 2); // Rsiz + append_be(j2k, width, 4); // Xsiz + append_be(j2k, height, 4); // Ysiz + append_be(j2k, 0, 4); // XOsiz + append_be(j2k, 0, 4); // YOsiz + append_be(j2k, tile_width, 4); // XTsiz + append_be(j2k, tile_height, 4); // YTsiz + append_be(j2k, 0, 4); // XTOsiz + append_be(j2k, 0, 4); // YTOsiz + append_be(j2k, num_components, 2); // Csiz + for (int c = 0; c < num_components; c++) { + j2k.insert(j2k.end(), {0x07, 0x01, 0x01}); // Ssiz, XRsiz, YRsiz + } + + j2k.resize(1065); + return j2k; +} + + +std::vector jpeg2000_decoder_ids() { + const heif_decoder_descriptor* descriptors[10]; + int n = heif_get_decoder_descriptors(heif_compression_JPEG2000, descriptors, 10); + + std::vector ids; + for (int i = 0; i < n && i < 10; i++) { + ids.emplace_back(heif_decoder_descriptor_get_id_name(descriptors[i])); + } + return ids; +} + + +bool have_openjpeg_decoder() { + for (const std::string& id : jpeg2000_decoder_ids()) { + if (id == "openjpeg") { + return true; + } + } + return false; +} + + +struct DecodeResult { + heif_error_code code; + heif_suberror_code subcode; +}; + +// Decodes the codestream as the primary image of a HEIF file. When 'decoder_id' is not +// NULL, this decoder is used. The limits are the defaults, optionally with a lower +// maximum number of tiles and maximum memory block size. +DecodeResult decode_codestream(const std::vector& j2k, const char* decoder_id, + uint64_t max_number_of_tiles = 0, uint64_t max_memory_block_size = 0) { + std::vector data = wrap_codestream_in_heif(j2k); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_security_limits* limits = heif_context_get_security_limits(ctx); + if (max_number_of_tiles) { + limits->max_number_of_tiles = max_number_of_tiles; + } + if (max_memory_block_size) { + limits->max_memory_block_size = max_memory_block_size; + } + + 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(); + options->decoder_id = decoder_id; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, options); + DecodeResult result{err.code, err.subcode}; + + if (img) { + heif_image_release(img); + } + heif_decoding_options_free(options); + heif_image_handle_release(handle); + heif_context_free(ctx); + + return result; +} + +} // namespace + + +TEST_CASE("jpeg2000: OpenJPEG plugin rejects huge reference-grid coordinates with a tiny window") +{ + if (!heif_have_decoder_for_format(heif_compression_JPEG2000)) { + SKIP("JPEG 2000 (OpenJPEG) decoder not available, skipping test"); + } + + std::vector data = build_trigger_heif(); + + 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); + + // Before the fix, this pathological geometry passed the span-based + // pre-decode gate (window span = 17 pixels) and reached opj_decode(), + // which either crashes the underlying codec (OpenJPEG <= 2.3.x) or fails + // with a generic decoder error (OpenJPEG >= 2.4, observed as + // heif_error_Decoder_plugin_error / "opj_decode()" on this build). Either + // way, opj_decode() was called on the pathological input. + // + // After the fix, the reference-grid-area check must reject the input + // before opj_decode() is invoked, surfacing as a security-limit error. + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + + if (img) { + heif_image_release(img); + } + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +// Regression test for GHSA-h4h8-qgvc-m7r2 (found by OSS-Fuzz as an out-of-memory error +// of the sequence_fuzzer in opj_j2k_read_siz()). +// +// OpenJPEG allocates its coding parameters for each tile and for each component of each +// tile while it reads the SIZ marker segment, i.e. within opj_read_header(), before the +// plugin could check anything. A codestream of some hundred bytes made it allocate 2 GB +// (65388 tiles with 24 components) and it could have been much more. The plugin now reads +// the SIZ marker segment itself and checks it before it calls OpenJPEG. + +TEST_CASE("jpeg2000: OpenJPEG plugin checks the number of tiles before it reads the header") +{ + if (!have_openjpeg_decoder()) { + SKIP("OpenJPEG decoder not available, skipping test"); + } + + // In all cases, we got heif_error_Decoder_plugin_error before the fix, because + // opj_read_header() had read the SIZ marker segment and failed on the data after it. + + SECTION("more tiles than the codestream can hold") { + // 65025 tiles of one pixel. The reference grid is small enough to pass the image size + // limit. OpenJPEG allocated 500 MB for it (2 GB with 24 components). + DecodeResult result = decode_codestream(build_codestream(255, 255, 1, 1), "openjpeg"); + REQUIRE(result.code == heif_error_Invalid_input); + REQUIRE(result.subcode == heif_suberror_Invalid_J2K_codestream); + } + + SECTION("more tiles than the security limit allows") { + DecodeResult result = decode_codestream(build_codestream(4, 2, 1, 1), "openjpeg", 4); + REQUIRE(result.code == heif_error_Memory_allocation_error); + REQUIRE(result.subcode == heif_suberror_Security_limit_exceeded); + } + + SECTION("tiles need more memory than the security limit allows") { + // OpenJPEG allocates about 11 kB for each tile with three components. + DecodeResult result = decode_codestream(build_codestream(4, 2, 1, 1), "openjpeg", 0, 64 * 1024); + REQUIRE(result.code == heif_error_Memory_allocation_error); + REQUIRE(result.subcode == heif_suberror_Security_limit_exceeded); + } + + SECTION("the limits do not get in the way of the same tiles with the default limits") { + // The 8 tiles pass our checks. OpenJPEG fails later, as the codestream ends after + // the SIZ marker segment. + DecodeResult result = decode_codestream(build_codestream(4, 2, 1, 1), "openjpeg"); + REQUIRE(result.code == heif_error_Decoder_plugin_error); + } +} + + + +// libheif checks the reference grid of a JPEG 2000 codestream against the image size limit +// before it calls a decoder plugin. This check was skipped when libheif could not read the +// SIZ marker segment, which it expects directly after the SOC marker. The decoder +// libraries are more lenient. They skip what comes between the SOC marker and the SIZ +// marker segment, so they got to work with a reference grid that libheif had not checked. +// The file that OSS-Fuzz found for GHSA-h4h8-qgvc-m7r2 came through this way. + +TEST_CASE("jpeg2000: codestream with data between SOC and SIZ is not passed to the decoder") +{ + std::vector decoder_ids = jpeg2000_decoder_ids(); + if (decoder_ids.empty()) { + SKIP("JPEG 2000 decoder not available, skipping test"); + } + + // The reference grid is far above the image size limit. + std::vector j2k = build_codestream(12000, 12000, 12000, 12000, true); + + for (const std::string& id : decoder_ids) { + INFO("decoder: " << id); + DecodeResult result = decode_codestream(j2k, id.c_str()); + + // This is the error of libheif. Before the fix, the plugin got the codestream and + // the error was the one of the plugin. + REQUIRE(result.code == heif_error_Invalid_input); + REQUIRE(result.subcode == heif_suberror_Invalid_J2K_codestream); + } +} diff -Nru libheif-1.23.4/tests/jpeg2000_signed_components.cc libheif-1.23.6/tests/jpeg2000_signed_components.cc --- libheif-1.23.4/tests/jpeg2000_signed_components.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/jpeg2000_signed_components.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,225 @@ +/* + 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. +*/ + +// A JPEG 2000 codestream declares in its SIZ marker, per component, whether the samples are +// signed. For a signed component OpenJPEG returns negative values. The OpenJPEG decoder +// plugin copied them into the unsigned samples of the image planes with a plain cast, so +// that e.g. -1 became 65535 in a plane with a bit depth of 10. Code that relies on the bit +// depth of a plane (libsharpyuv uses the samples as table indices) then read out of bounds. +// The plugin now refuses codestreams with signed components. +// +// The test encodes a small 10-bit image, sets the sign bit of every component in the SIZ +// marker of the written file, and decodes it again. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" + +#include +#include +#include +#include + +namespace { + +constexpr int W = 32; +constexpr int H = 32; +constexpr int BIT_DEPTH = 10; + +heif_error write_to_vector(heif_context*, const void* data, size_t size, void* userdata) +{ + auto* out = static_cast*>(userdata); + const auto* bytes = static_cast(data); + out->insert(out->end(), bytes, bytes + size); + return heif_error{heif_error_Ok, heif_suberror_Unspecified, "Success"}; +} + + +std::vector encode_jpeg2000_image() +{ + heif_image* img = nullptr; + heif_error err = heif_image_create(W, H, heif_colorspace_RGB, heif_chroma_444, &img); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B}) { + err = heif_image_add_plane(img, channel, W, H, BIT_DEPTH); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + REQUIRE(p != nullptr); + for (int y = 0; y < H; y++) { + uint16_t* row = reinterpret_cast(p + y * stride); + for (int x = 0; x < W; x++) { + row[x] = static_cast((x * 31 + y * 17 + channel * 5) & ((1 << BIT_DEPTH) - 1)); + } + } + } + + heif_context* ctx = heif_context_alloc(); + heif_encoder* encoder = nullptr; + err = heif_context_get_encoder_for_format(ctx, heif_compression_JPEG2000, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr); + INFO("encode: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + std::vector file; + heif_writer writer; + writer.writer_api_version = 1; + writer.write = write_to_vector; + err = heif_context_write(ctx, &writer, &file); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_context_free(ctx); + heif_image_release(img); + + return file; +} + + +// Sets the sign bit in the Ssiz field of every component in the SIZ marker segment. +// Returns the number of components that were changed. +int make_components_signed(std::vector& file) +{ + // SOC marker (FF4F), directly followed by the SIZ marker (FF51). + const uint8_t start[4] = {0xFF, 0x4F, 0xFF, 0x51}; + + for (size_t i = 0; i + 4 + 38 < file.size(); i++) { + if (memcmp(&file[i], start, 4) != 0) { + continue; + } + + // After the SIZ marker: Lsiz(2) Rsiz(2) Xsiz(4) Ysiz(4) XOsiz(4) YOsiz(4) XTsiz(4) + // YTsiz(4) XTOsiz(4) YTOsiz(4) Csiz(2), then Ssiz(1) XRsiz(1) YRsiz(1) per component. + size_t siz = i + 4; + int num_components = (file[siz + 36] << 8) | file[siz + 37]; + if (siz + 38 + 3 * static_cast(num_components) > file.size()) { + return 0; + } + + for (int c = 0; c < num_components; c++) { + file[siz + 38 + 3 * c] |= 0x80; + } + return num_components; + } + + return 0; +} + + +struct DecodeResult +{ + heif_error_code code; + std::string message; + bool has_image; + uint32_t largest_sample; +}; + +DecodeResult decode(const std::vector& file) +{ + heif_context* ctx = heif_context_alloc(); + 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); + + // Decode without conversion, so that the planes come back as the plugin filled them. + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + + // The message may point into the context, so copy it before the context is freed. + DecodeResult result{err.code, err.message ? err.message : "", img != nullptr, 0}; + + if (img) { + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, + heif_channel_R, heif_channel_G, heif_channel_B}) { + if (!heif_image_has_channel(img, channel)) { + continue; + } + + REQUIRE(heif_image_get_bits_per_pixel_range(img, channel) == BIT_DEPTH); + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, channel, &stride); + REQUIRE(p != nullptr); + int w = heif_image_get_width(img, channel); + int h = heif_image_get_height(img, channel); + for (int y = 0; y < h; y++) { + const uint16_t* row = reinterpret_cast(p + y * stride); + for (int x = 0; x < w; x++) { + if (row[x] > result.largest_sample) { + result.largest_sample = row[x]; + } + } + } + } + + heif_image_release(img); + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + return result; +} + +} // namespace + + +TEST_CASE("JPEG 2000 codestreams with signed components are refused") +{ + if (!heif_have_encoder_for_format(heif_compression_JPEG2000) || + !heif_have_decoder_for_format(heif_compression_JPEG2000)) { + SKIP("no JPEG 2000 encoder or decoder available"); + } + + std::vector file = encode_jpeg2000_image(); + + SECTION("control: the unmodified file decodes, with all samples within the bit depth") { + DecodeResult result = decode(file); + INFO("decode: " << result.message); + REQUIRE(result.code == heif_error_Ok); + REQUIRE(result.has_image); + CHECK(result.largest_sample <= (1u << BIT_DEPTH) - 1); + } + + SECTION("signed components") { + REQUIRE(make_components_signed(file) > 0); + + DecodeResult result = decode(file); + INFO("decode: " << result.message); + + // Whatever the decoder does with such a file, it must not hand out samples above the + // bit depth of the plane. + CHECK(result.largest_sample <= (1u << BIT_DEPTH) - 1); + + CHECK(result.code == heif_error_Unsupported_feature); + CHECK(!result.has_image); + } +} diff -Nru libheif-1.23.4/tests/mini_box.cc libheif-1.23.6/tests/mini_box.cc --- libheif-1.23.4/tests/mini_box.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/mini_box.cc 2026-10-05 16:16:00.000000000 +0000 @@ -70,7 +70,7 @@ REQUIRE(ftyp->get_minor_version() == fourcc("avif")); REQUIRE(ftyp->list_brands().size() == 0); Indent indent; - std::string dumpResult = box->dump(indent); + std::string dumpResult = box->dump_to_string(indent); REQUIRE(dumpResult == "Box: ftyp ----- (File Type)\n" "size: 16 (header size: 8)\n" "major brand: mif3\n" @@ -97,7 +97,7 @@ REQUIRE((int)(mini->get_main_item_codec_config().data()[1]) == 0x20); REQUIRE((int)(mini->get_main_item_codec_config().data()[2]) == 0x00); REQUIRE((int)(mini->get_main_item_codec_config().data()[3]) == 0x00); - dumpResult = box->dump(indent); + dumpResult = box->dump_to_string(indent); REQUIRE(dumpResult == "Box: mini -----\n" "size: 74 (header size: 8)\n" "version: 0\n" @@ -404,7 +404,7 @@ REQUIRE((int)(mini->get_main_item_codec_config().data()[2]) == 0x00); REQUIRE((int)(mini->get_main_item_codec_config().data()[3]) == 0x00); Indent indent; - std::string dumpResult = mini->dump(indent); + std::string dumpResult = mini->dump_to_string(indent); REQUIRE(dumpResult == "Box: mini -----\n" "size: 1923 (header size: 8)\n" "version: 0\n" @@ -456,7 +456,7 @@ REQUIRE((int)(mini->get_main_item_codec_config().data()[2]) == 0x00); REQUIRE((int)(mini->get_main_item_codec_config().data()[3]) == 0x00); Indent indent; - std::string dumpResult = mini->dump(indent); + std::string dumpResult = mini->dump_to_string(indent); REQUIRE(dumpResult == "Box: mini -----\n" "size: 6294 (header size: 8)\n" "version: 0\n" @@ -504,7 +504,7 @@ REQUIRE(mini->get_height() == 256); REQUIRE(mini->get_main_item_codec_config().size() == 113); Indent indent; - std::string dumpResult = mini->dump(indent); + std::string dumpResult = mini->dump_to_string(indent); REQUIRE(dumpResult == "Box: mini -----\n" "size: 19229 (header size: 8)\n" "version: 0\n" diff -Nru libheif-1.23.4/tests/mini_security_limits.cc libheif-1.23.6/tests/mini_security_limits.cc --- libheif-1.23.4/tests/mini_security_limits.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/mini_security_limits.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,140 @@ +/* + 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-7pwf-qh74-p35w. +// +// FileLayout::read forwarded the caller's security limits to the ftyp, meta +// and moov parsers but parsed a MinimizedImageBox ('mini') file under the +// global default limits, and Box_mini re-parsed the embedded av1C/hvcC blob +// the same way. An application that tightened heif_security_limits on its +// context therefore got no enforcement of its budget on mini-format input. +// +// Box_mini::parse copies the whole box payload into memory through +// MemoryHandle, so a max_memory_block_size (or max_total_memory) below the +// payload size must reject the file, and a value above it must accept it. +// The fixtures' payloads lie between 1.9 KB and 19.3 KB. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test-config.h" +#include +#include + +#define MINI_FILES \ + "lightning_mini.heif", "simple_osm_tile_meta.avif", "simple_osm_tile_alpha.avif", "mini_size_zero.avif" + +static const uint64_t kLimitBelowAllPayloads = 1024; +static const uint64_t kBlockLimitAboveAllPayloads = 64 * 1024; +static const uint64_t kTotalLimitAboveAllPayloads = 256 * 1024; + + +// Reads a fixture on a context whose limits were tightened before the read. +// A zero argument keeps the default for that limit. +static heif_context* read_with_limits(const char* filename, + uint64_t max_memory_block_size, + uint64_t max_total_memory, + heif_error* out_err) +{ + heif_context* ctx = heif_context_alloc(); + + heif_security_limits limits = *heif_context_get_security_limits(ctx); + if (max_memory_block_size) { + limits.max_memory_block_size = max_memory_block_size; + } + if (max_total_memory) { + limits.max_total_memory = max_total_memory; + } + heif_context_set_security_limits(ctx, &limits); + + std::string path = tests_data_directory + "/" + filename; + *out_err = heif_context_read_from_file(ctx, path.c_str(), nullptr); + return ctx; +} + + +// The error must come from the mini payload allocation, not from some other +// limit check that happens to fire first. +static bool mentions_mini_payload(const heif_error& err) +{ + return err.message != nullptr && + std::string(err.message).find("MinimizedImageBox") != std::string::npos; +} + + +TEST_CASE("mini payload is rejected by a tightened max_memory_block_size") +{ + auto file = GENERATE(MINI_FILES); + INFO("file name: " << file); + + heif_error err; + heif_context* ctx = read_with_limits(file, kLimitBelowAllPayloads, 0, &err); + INFO("error: " << (err.message ? err.message : "(null)")); + + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + REQUIRE(mentions_mini_payload(err)); + + heif_context_free(ctx); +} + + +TEST_CASE("mini payload is rejected by a tightened max_total_memory") +{ + auto file = GENERATE(MINI_FILES); + INFO("file name: " << file); + + heif_error err; + heif_context* ctx = read_with_limits(file, 0, kLimitBelowAllPayloads, &err); + INFO("error: " << (err.message ? err.message : "(null)")); + + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + REQUIRE(mentions_mini_payload(err)); + + heif_context_free(ctx); +} + + +TEST_CASE("mini file is accepted when the tightened limits cover the payload") +{ + auto file = GENERATE(MINI_FILES); + INFO("file name: " << file); + + heif_error err; + heif_context* ctx = read_with_limits(file, kBlockLimitAboveAllPayloads, kTotalLimitAboveAllPayloads, &err); + INFO("error: " << (err.message ? err.message : "(null)")); + + REQUIRE(err.code == heif_error_Ok); + + // The expanded boxes must have been built from the mini box. + 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_handle_release(handle); + + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/tests/nal_split.cc libheif-1.23.6/tests/nal_split.cc --- libheif-1.23.4/tests/nal_split.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/nal_split.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,75 @@ +/* + libheif unit tests - NAL unit splitter + + 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/decoder.h" + +#include +#include + +TEST_CASE("split_nal_units: two well-formed NAL units") { + std::vector buf{ + 0x00, 0x00, 0x00, 0x03, 0xAA, 0xBB, 0xCC, // NAL of 3 bytes + 0x00, 0x00, 0x00, 0x02, 0xDD, 0xEE}; // NAL of 2 bytes + auto units = split_nal_units_4byte_length_prefixed(buf.data(), buf.size()); + REQUIRE(units.size() == 2); + REQUIRE(units[0].second == 3); + REQUIRE(units[0].first[0] == 0xAA); + REQUIRE(units[1].second == 2); + REQUIRE(units[1].first[0] == 0xDD); +} + +TEST_CASE("split_nal_units: length running past the end stops cleanly") { + // First NAL is fine; second declares 100 bytes but only 2 remain. + std::vector buf{ + 0x00, 0x00, 0x00, 0x01, 0xAA, + 0x00, 0x00, 0x00, 0x64, 0xBB, 0xCC}; + auto units = split_nal_units_4byte_length_prefixed(buf.data(), buf.size()); + REQUIRE(units.size() == 1); + REQUIRE(units[0].second == 1); + REQUIRE(units[0].first[0] == 0xAA); +} + +TEST_CASE("split_nal_units: zero-length NAL is skipped, parsing continues") { + std::vector buf{ + 0x00, 0x00, 0x00, 0x00, // zero-length NAL + 0x00, 0x00, 0x00, 0x01, 0x7E}; // 1-byte NAL + auto units = split_nal_units_4byte_length_prefixed(buf.data(), buf.size()); + REQUIRE(units.size() == 1); + REQUIRE(units[0].second == 1); + REQUIRE(units[0].first[0] == 0x7E); +} + +TEST_CASE("split_nal_units: a lone truncated length prefix yields nothing") { + std::vector buf{0x00, 0x00, 0x00}; // fewer than 4 bytes + auto units = split_nal_units_4byte_length_prefixed(buf.data(), buf.size()); + REQUIRE(units.empty()); +} + +TEST_CASE("split_nal_units: empty input yields nothing") { + auto units = split_nal_units_4byte_length_prefixed(nullptr, 0); + REQUIRE(units.empty()); +} diff -Nru libheif-1.23.4/tests/omaf_boxes.cc libheif-1.23.6/tests/omaf_boxes.cc --- libheif-1.23.4/tests/omaf_boxes.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/omaf_boxes.cc 2026-10-05 16:16:00.000000000 +0000 @@ -49,7 +49,7 @@ 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); + std::string dumpResult = box->dump_to_string(indent); REQUIRE(dumpResult == "Box: prfr ----- (Projection Format)\n" "size: 13 (header size: 12)\n" "projection_type: cube-map\n"); diff -Nru libheif-1.23.4/tests/overlay_amplification.cc libheif-1.23.6/tests/overlay_amplification.cc --- libheif-1.23.4/tests/overlay_amplification.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/overlay_amplification.cc 2026-10-05 16:16:00.000000000 +0000 @@ -202,8 +202,9 @@ // 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. +// (listing the same item twice in one 'dimg' entry is legal too, but the two +// items keep the gadget independent of that). 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 diff -Nru libheif-1.23.4/tests/overlay_offsets.cc libheif-1.23.6/tests/overlay_offsets.cc --- libheif-1.23.4/tests/overlay_offsets.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/overlay_offsets.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,769 @@ +/* + 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 three defects found with the HEIF conformance file C021, +// an 'iovl' that composites the same coded image twice: at (0,0) and, on top of +// it, at (-640,-360) on a canvas of half the image size. +// +// 1. Box_iref rejected every 'dimg' entry that lists the same item twice +// ("'iref' has double references"). Neither ISO/IEC 14496-12 nor 23008-12 +// forbids this, and an overlay needs it to place one image at two positions, +// because the offsets are paired with the references. +// 2. ImageOverlay::parse() sign-extended 16-bit offsets incorrectly, so every +// negative offset in the common 16-bit form placed the image far outside the +// canvas, where it was silently skipped. +// 3. HeifPixelImage::overlay() mis-clipped input images with negative offsets: +// a layer whose negative offset was at least half its size was not drawn at +// all, and smaller negative offsets were truncated. +// +// The files are built by hand from an 'mski' base image, which needs no codec, +// so the tests run in every build configuration. Each test decodes the base +// image and the overlay through the public API and compares the overlay with a +// straightforward reference composition of the decoded base. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include + +namespace { + +const uint32_t BASE_W = 8; +const uint32_t BASE_H = 8; + +const heif_item_id BASE_ID = 1; +const heif_item_id IOVL_ID = 2; +const heif_item_id BASE2_ID = 3; // an optional second base image with another bit depth + +struct Layer { + int32_t x; + int32_t y; + bool second_base = false; // place the second base image instead of the first one +}; + + +// ImageOverlay payload (ISO/IEC 23008-12 6.6.2.2): version, flags (bit 0 +// selects 32-bit instead of 16-bit size and offset fields), background RGBA +// (opaque white), canvas size, and one signed (x,y) offset per input image. +std::vector make_overlay_spec(uint16_t canvas_w, uint16_t canvas_h, const std::vector& layers, + bool long_fields) +{ + auto put_field = [long_fields](std::vector& out, int32_t v) { + if (long_fields) { + put_u32_be(out, static_cast(v)); + } + else { + put_u16_be(out, static_cast(static_cast(v))); + } + }; + + std::vector s; + s.push_back(0); + s.push_back(long_fields ? 1 : 0); + for (int i = 0; i < 4; i++) { put_u16_be(s, 0xFFFF); } + put_field(s, canvas_w); + put_field(s, canvas_h); + for (const auto& l : layers) { + put_field(s, l.x); + put_field(s, l.y); + } + return s; +} + + +// A file with two items: item 1 is an 8x8 'mski' base image whose pixel value is +// 8*y+x, item 2 is the primary 'iovl' that references the base once per layer. +// With two or more layers, the 'dimg' entry therefore lists item 1 repeatedly. +// The pixel value of a base image at pixel index i (= 8*y+x). +uint16_t base_sample(uint8_t bits_per_pixel, uint32_t i, bool second_base) +{ + if (bits_per_pixel == 8) { + return static_cast(second_base ? 255 - 2 * i : i); + } + + // 16 bits, with different upper and lower bytes + return static_cast(second_base ? 65000 - i * 700 : i * 1000 + 300); +} + +std::vector base_image_data(uint8_t bits_per_pixel, bool second_base) +{ + std::vector data; + for (uint32_t i = 0; i < BASE_W * BASE_H; i++) { + uint16_t value = base_sample(bits_per_pixel, i, second_base); + if (bits_per_pixel == 8) { + data.push_back(static_cast(value)); + } + else { + // 16-bit samples in the byte order of the machine + uint8_t bytes[2]; + memcpy(bytes, &value, 2); + data.push_back(bytes[0]); + data.push_back(bytes[1]); + } + } + return data; +} + +// 'second_base_bits_per_pixel' = 0: there is no second base image. +std::vector build_file(uint16_t canvas_w, uint16_t canvas_h, const std::vector& layers, bool long_fields, + uint8_t base_bits_per_pixel = 8, uint8_t second_base_bits_per_pixel = 0) +{ + const bool has_second_base = (second_base_bits_per_pixel != 0); + + 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, static_cast(IOVL_ID)); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // iinf + std::vector> items = {{BASE_ID, "mski"}, {IOVL_ID, "iovl"}}; + if (has_second_base) { + items.emplace_back(BASE2_ID, "mski"); + } + + std::vector iinf_payload; + put_u16_be(iinf_payload, static_cast(items.size())); + for (const auto& item : items) { + std::vector infe_payload; + put_u16_be(infe_payload, static_cast(item.first)); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, item.second); + 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: property 1 = ispe of the base, 2 = mskC, 3 = ispe of the canvas, 4 = mskC of the second base + std::vector ispe_base_payload; + put_u32_be(ispe_base_payload, BASE_W); + put_u32_be(ispe_base_payload, BASE_H); + auto ispe_base = make_box("ispe", ispe_base_payload, /*full=*/true); + + std::vector mskC_payload; + mskC_payload.push_back(base_bits_per_pixel); + auto mskC = make_box("mskC", mskC_payload, /*full=*/true); + + std::vector ispe_canvas_payload; + put_u32_be(ispe_canvas_payload, canvas_w); + put_u32_be(ispe_canvas_payload, canvas_h); + auto ispe_canvas = make_box("ispe", ispe_canvas_payload, /*full=*/true); + + std::vector mskC2_payload; + mskC2_payload.push_back(second_base_bits_per_pixel); + auto mskC2 = make_box("mskC", mskC2_payload, /*full=*/true); + + std::vector ipco_payload; + append(ipco_payload, ispe_base); + append(ipco_payload, mskC); + append(ipco_payload, ispe_canvas); + if (has_second_base) { + append(ipco_payload, mskC2); + } + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, has_second_base ? 3 : 2); // entry_count + put_u16_be(ipma_payload, static_cast(BASE_ID)); + ipma_payload.push_back(2); // association_count + ipma_payload.push_back(0x80 | 1); // essential, ispe (base) + ipma_payload.push_back(0x80 | 2); // essential, mskC + put_u16_be(ipma_payload, static_cast(IOVL_ID)); + ipma_payload.push_back(1); + ipma_payload.push_back(0x80 | 3); // essential, ispe (canvas) + if (has_second_base) { + put_u16_be(ipma_payload, static_cast(BASE2_ID)); + ipma_payload.push_back(2); + ipma_payload.push_back(0x80 | 1); // essential, ispe (same size as the base) + ipma_payload.push_back(0x80 | 4); // essential, mskC of the second base + } + 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: base pixels, the pixels of the second base, then the overlay spec + std::vector base_data = base_image_data(base_bits_per_pixel, false); + std::vector base2_data; + if (has_second_base) { + base2_data = base_image_data(second_base_bits_per_pixel, true); + } + auto spec = make_overlay_spec(canvas_w, canvas_h, layers, long_fields); + + std::vector idat_payload; + append(idat_payload, base_data); + append(idat_payload, base2_data); + append(idat_payload, spec); + auto idat = make_box("idat", idat_payload); + + struct Extent { heif_item_id id; uint32_t off; uint32_t len; }; + std::vector extents = { + Extent{BASE_ID, 0, static_cast(base_data.size())}, + Extent{IOVL_ID, static_cast(base_data.size() + base2_data.size()), static_cast(spec.size())}}; + if (has_second_base) { + extents.push_back(Extent{BASE2_ID, static_cast(base_data.size()), static_cast(base2_data.size())}); + } + + 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 Extent& e : extents) { + put_u16_be(iloc_payload, static_cast(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); + put_u32_be(iloc_payload, e.len); + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // iref: one 'dimg' entry listing the base once per layer + std::vector dimg_payload; + put_u16_be(dimg_payload, static_cast(IOVL_ID)); + put_u16_be(dimg_payload, static_cast(layers.size())); + for (const Layer& layer : layers) { + put_u16_be(dimg_payload, static_cast(layer.second_base ? BASE2_ID : BASE_ID)); + } + auto iref = make_box("iref", make_box("dimg", dimg_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; +} + + +struct Pixels { + uint32_t w = 0; + uint32_t h = 0; + std::vector rgb; // interleaved, tightly packed +}; + +Pixels decode_rgb(heif_image_handle* handle) +{ + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, nullptr); + REQUIRE(err.code == heif_error_Ok); + + Pixels px; + px.w = static_cast(heif_image_get_width(img, heif_channel_interleaved)); + px.h = static_cast(heif_image_get_height(img, heif_channel_interleaved)); + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + px.rgb.resize(static_cast(px.w) * px.h * 3); + for (uint32_t y = 0; y < px.h; y++) { + memcpy(&px.rgb[static_cast(y) * px.w * 3], p + y * stride, static_cast(px.w) * 3); + } + + heif_image_release(img); + return px; +} + +Pixels decode_item(heif_context* ctx, heif_item_id id) +{ + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_image_handle(ctx, id, &handle).code == heif_error_Ok); + Pixels px = decode_rgb(handle); + heif_image_handle_release(handle); + return px; +} + +Pixels decode_primary(heif_context* ctx) +{ + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok); + Pixels px = decode_rgb(handle); + heif_image_handle_release(handle); + return px; +} + + +// Reference composition: an opaque white canvas with the base image painted at +// every layer offset in order, pixels outside the canvas dropped. +std::vector composite(const Pixels& base, uint32_t canvas_w, uint32_t canvas_h, const std::vector& layers) +{ + std::vector out(static_cast(canvas_w) * canvas_h * 3, 255); + + for (const auto& l : layers) { + for (uint32_t y = 0; y < base.h; y++) { + for (uint32_t x = 0; x < base.w; x++) { + int64_t cx = static_cast(l.x) + x; + int64_t cy = static_cast(l.y) + y; + if (cx < 0 || cy < 0 || cx >= canvas_w || cy >= canvas_h) { + continue; + } + + memcpy(&out[(static_cast(cy) * canvas_w + static_cast(cx)) * 3], + &base.rgb[(static_cast(y) * base.w + x) * 3], + 3); + } + } + } + + return out; +} + + +// Compare pixel by pixel. (A REQUIRE on the whole vectors would print hundreds +// of bytes on failure, and the first mismatch is what matters.) +void require_same_pixels(const Pixels& actual, const std::vector& expected) +{ + REQUIRE(actual.rgb.size() == expected.size()); + + for (size_t i = 0; i < expected.size(); i++) { + if (actual.rgb[i] != expected[i]) { + size_t pixel = i / 3; + uint32_t x = static_cast(pixel % actual.w); + uint32_t y = static_cast(pixel / actual.w); + + std::string actual_row, expected_row; + for (uint32_t xx = 0; xx < actual.w; xx++) { + size_t idx = (static_cast(y) * actual.w + xx) * 3; + actual_row += std::to_string(actual.rgb[idx]) + " "; + expected_row += std::to_string(expected[idx]) + " "; + } + + INFO("first mismatch at pixel (" << x << "," << y << "), channel " << (i % 3)); + INFO("decoded row " << y << ": " << actual_row); + INFO("expected row " << y << ": " << expected_row); + REQUIRE(static_cast(actual.rgb[i]) == static_cast(expected[i])); + } + } +} + + +void check_composition_with_field_size(uint16_t canvas_w, uint16_t canvas_h, const std::vector& layers, + bool long_fields, uint8_t base_bits_per_pixel = 8) +{ + INFO("canvas " << canvas_w << "x" << canvas_h << ", " << layers.size() << " layer(s), first offset (" + << layers[0].x << "," << layers[0].y << "), " << (long_fields ? 32 : 16) << "-bit fields, " + << static_cast(base_bits_per_pixel) << "-bit base image"); + + auto data = build_file(canvas_w, canvas_h, layers, long_fields, base_bits_per_pixel); + + 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); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + Pixels base = decode_item(ctx, BASE_ID); + REQUIRE(base.w == BASE_W); + REQUIRE(base.h == BASE_H); + + Pixels canvas = decode_primary(ctx); + REQUIRE(canvas.w == canvas_w); + REQUIRE(canvas.h == canvas_h); + require_same_pixels(canvas, composite(base, canvas_w, canvas_h, layers)); + + heif_context_free(ctx); +} + +// Check the composition with both overlay field sizes. Negative 16-bit offsets +// were mis-read as huge negative values (the sign extension was only correct +// for 32-bit fields), which also made the image disappear from the canvas. +void check_composition(uint16_t canvas_w, uint16_t canvas_h, const std::vector& layers) +{ + check_composition_with_field_size(canvas_w, canvas_h, layers, false); + check_composition_with_field_size(canvas_w, canvas_h, layers, true); +} + +} // namespace + + +TEST_CASE("overlay offsets: negative offset of at least half the image size") { + // Used to be dropped entirely. + check_composition(8, 8, {{-4, -4}}); + check_composition(8, 8, {{-6, -2}}); + check_composition(8, 8, {{0, -5}}); +} + +TEST_CASE("overlay offsets: small negative offset") { + // Used to be truncated. + check_composition(8, 8, {{-2, -3}}); + check_composition(8, 8, {{-1, 0}}); +} + +TEST_CASE("overlay offsets: positive offset partially outside the canvas") { + check_composition(8, 8, {{5, 6}}); + check_composition(8, 8, {{7, 7}}); +} + +TEST_CASE("overlay offsets: input image completely outside the canvas") { + check_composition(8, 8, {{-8, 0}}); + check_composition(8, 8, {{0, 8}}); + check_composition(4, 4, {{-8, -8}}); + check_composition(4, 4, {{4, 0}}); +} + +TEST_CASE("overlay offsets: canvas smaller than the input image") { + check_composition(4, 4, {{0, 0}}); + check_composition(4, 4, {{-2, -2}}); + check_composition(4, 4, {{-4, -4}}); +} + +TEST_CASE("overlay offsets: the same input image placed twice (C021 structure)") { + // The 'dimg' entry lists item 1 twice. These files were rejected while + // parsing with "'iref' has double references". + check_composition(8, 8, {{0, 0}, {-4, -4}}); + check_composition(4, 4, {{0, 0}, {-4, -4}}); + check_composition(8, 8, {{-4, -4}, {0, 0}}); + check_composition(8, 8, {{0, 0}, {2, 2}, {-6, -6}}); +} + +TEST_CASE("overlay offsets: overlay written through the API may reference one image twice") { + heif_encoder* encoder = get_encoder_or_skip_test(heif_compression_mask); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_image* base = nullptr; + REQUIRE(heif_image_create(BASE_W, BASE_H, heif_colorspace_monochrome, heif_chroma_monochrome, &base).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(base, heif_channel_Y, BASE_W, BASE_H, 8).code == heif_error_Ok); + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(base, heif_channel_Y, &stride); + for (uint32_t y = 0; y < BASE_H; y++) { + for (uint32_t x = 0; x < BASE_W; x++) { + p[y * stride + x] = static_cast(BASE_W * y + x); + } + } + + heif_image_handle* base_handle = nullptr; + REQUIRE(heif_context_encode_image(ctx, base, encoder, nullptr, &base_handle).code == heif_error_Ok); + heif_item_id base_id = heif_image_handle_get_item_id(base_handle); + + const std::vector layers = {{0, 0}, {-4, -4}}; + heif_item_id ids[2] = {base_id, base_id}; + int32_t offsets[4] = {0, 0, -4, -4}; + uint16_t background[4] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF}; + + heif_image_handle* iovl_handle = nullptr; + REQUIRE(heif_context_add_overlay_image(ctx, 8, 8, 2, ids, offsets, background, &iovl_handle).code == heif_error_Ok); + REQUIRE(heif_context_set_primary_image(ctx, iovl_handle).code == heif_error_Ok); + + // Writing used to fail here with "'iref' has double references". + std::string path = get_tests_output_file_path("overlay_same_image_twice.heif"); + heif_error err = heif_context_write_to_file(ctx, path.c_str()); + INFO("write error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(iovl_handle); + heif_image_handle_release(base_handle); + heif_image_release(base); + heif_encoder_release(encoder); + heif_context_free(ctx); + + // Read the file back and check the composition. + ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + REQUIRE(heif_context_read_from_file(ctx, path.c_str(), nullptr).code == heif_error_Ok); + + Pixels base_px = decode_item(ctx, base_id); + REQUIRE(base_px.w == BASE_W); + REQUIRE(base_px.h == BASE_H); + + Pixels canvas = decode_primary(ctx); + REQUIRE(canvas.w == 8); + REQUIRE(canvas.h == 8); + require_same_pixels(canvas, composite(base_px, 8, 8, layers)); + + heif_context_free(ctx); +} + + +// --- input images with more than 8 bits per sample +// +// The canvas of an overlay had 8 bits per sample, and HeifPixelImage::overlay() composed the +// planes byte by byte. An input image with more than 8 bits was handed to it with its +// 16-bit samples, so each sample was treated as two pixels and the overlay showed a +// stretched, wrong picture of the left half of the image. +// The canvas now has the bit depth of the first input image, and the samples are composed +// with 8 or 16 bits. + +namespace { + +struct Planes { + uint32_t w = 0; + uint32_t h = 0; + int bits = 0; + std::vector rgb[3]; +}; + +// Decodes to planar RGB, which keeps the bit depth of the image. +Planes decode_planar_rgb(heif_image_handle* handle) +{ + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_444, nullptr); + INFO("decode: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + Planes px; + px.w = static_cast(heif_image_get_width(img, heif_channel_R)); + px.h = static_cast(heif_image_get_height(img, heif_channel_R)); + px.bits = heif_image_get_bits_per_pixel_range(img, heif_channel_R); + + const heif_channel channels[3] = {heif_channel_R, heif_channel_G, heif_channel_B}; + for (int c = 0; c < 3; c++) { + REQUIRE(heif_image_get_bits_per_pixel_range(img, channels[c]) == px.bits); + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, channels[c], &stride); + REQUIRE(p != nullptr); + + for (uint32_t y = 0; y < px.h; y++) { + for (uint32_t x = 0; x < px.w; x++) { + if (px.bits <= 8) { + px.rgb[c].push_back(p[y * stride + x]); + } + else { + px.rgb[c].push_back(reinterpret_cast(p + y * stride)[x]); + } + } + } + } + + heif_image_release(img); + return px; +} + +Planes decode_planar_rgb(heif_context* ctx, heif_item_id id) +{ + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_image_handle(ctx, id, &handle).code == heif_error_Ok); + Planes px = decode_planar_rgb(handle); + heif_image_handle_release(handle); + return px; +} + +// The image handle of an overlay describes the canvas: R, G, B with its bit depth. +void require_canvas_description(const heif_image_handle* handle, int bits) +{ + CHECK(heif_image_handle_get_luma_bits_per_pixel(handle) == bits); + CHECK(heif_image_handle_get_chroma_bits_per_pixel(handle) == bits); + + REQUIRE(heif_image_handle_get_number_of_components(handle) == 3); + + uint32_t ids[3]; + heif_image_handle_get_used_component_ids(handle, ids); + for (uint32_t id : ids) { + CHECK(heif_image_handle_get_component_bits_per_pixel(handle, id) == bits); + } +} + +// Brings a sample to a higher bit depth the way the color conversion does it. +uint16_t to_bit_depth(uint16_t value, int from_bits, int to_bits) +{ + if (from_bits == to_bits) { + return value; + } + REQUIRE(from_bits == 8); + REQUIRE(to_bits == 16); + return static_cast((value << 8) | value); +} + +} // namespace + + +TEST_CASE("overlay offsets: input image with 16 bits per sample") { + const std::vector> layer_sets = {{{0, 0}}, {{3, 2}}, {{-3, -2}}, {{2, 2}, {-4, 5}}}; + + for (const auto& layers : layer_sets) { + for (bool long_fields : {false, true}) { + INFO("first offset (" << layers[0].x << "," << layers[0].y << "), " << layers.size() << " layer(s), " + << (long_fields ? 32 : 16) << "-bit fields"); + + const uint32_t canvas_w = 10, canvas_h = 9; + auto data = build_file(canvas_w, canvas_h, layers, long_fields, 16); + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + // The handle of the overlay reports the bit depth of the canvas. + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok); + require_canvas_description(handle, 16); + heif_image_handle_release(handle); + + Planes base = decode_planar_rgb(ctx, BASE_ID); + REQUIRE(base.bits == 16); + REQUIRE(base.rgb[0][1] == base_sample(16, 1, false)); // the samples are not reduced to 8 bits + + Planes canvas = decode_planar_rgb(ctx, IOVL_ID); + REQUIRE(canvas.w == canvas_w); + REQUIRE(canvas.h == canvas_h); + REQUIRE(canvas.bits == 16); + + for (int c = 0; c < 3; c++) { + // reference: an opaque white canvas with the base painted at every offset in order + std::vector expected(static_cast(canvas_w) * canvas_h, 0xFFFF); + for (const Layer& l : layers) { + for (uint32_t y = 0; y < base.h; y++) { + for (uint32_t x = 0; x < base.w; x++) { + int64_t cx = static_cast(l.x) + x; + int64_t cy = static_cast(l.y) + y; + if (cx >= 0 && cy >= 0 && cx < canvas_w && cy < canvas_h) { + expected[static_cast(cy) * canvas_w + static_cast(cx)] = base.rgb[c][y * base.w + x]; + } + } + } + } + + for (size_t i = 0; i < expected.size(); i++) { + INFO("channel " << c << ", pixel (" << i % canvas_w << "," << i / canvas_w << ")"); + REQUIRE(canvas.rgb[c][i] == expected[i]); + } + } + + heif_context_free(ctx); + } + } +} + + +TEST_CASE("overlay offsets: input images with different bit depths") { + SECTION("an image with fewer bits than the first image is brought to its bit depth") { + // first the 16-bit base, then the 8-bit base on top of its right half + auto data = build_file(8, 8, {{0, 0}, {4, 0, true}}, false, 16, 8); + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + Planes canvas = decode_planar_rgb(ctx, IOVL_ID); + REQUIRE(canvas.bits == 16); + + for (uint32_t y = 0; y < 8; y++) { + for (uint32_t x = 0; x < 8; x++) { + INFO("pixel (" << x << "," << y << ")"); + uint16_t expected; + if (x < 4) { + expected = base_sample(16, 8 * y + x, false); + } + else { + expected = to_bit_depth(base_sample(8, 8 * y + (x - 4), true), 8, 16); + } + REQUIRE(canvas.rgb[0][y * 8 + x] == expected); + } + } + + heif_context_free(ctx); + } + + SECTION("the first input image comes later in the file than the overlay") { + // The second base image (16 bits) has a higher item ID than the overlay. It is the first + // input image, so it sets the bit depth of the canvas, although it is not known yet when + // the overlay item is read. The 8-bit base is then drawn onto the right half. + auto data = build_file(8, 8, {{0, 0, true}, {4, 0}}, false, 8, 16); + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok); + require_canvas_description(handle, 16); + heif_image_handle_release(handle); + + Planes canvas = decode_planar_rgb(ctx, IOVL_ID); + REQUIRE(canvas.bits == 16); + + for (uint32_t y = 0; y < 8; y++) { + for (uint32_t x = 0; x < 8; x++) { + INFO("pixel (" << x << "," << y << ")"); + uint16_t expected; + if (x < 4) { + expected = base_sample(16, 8 * y + x, true); + } + else { + expected = to_bit_depth(base_sample(8, 8 * y + (x - 4), false), 8, 16); + } + REQUIRE(canvas.rgb[0][y * 8 + x] == expected); + } + } + + heif_context_free(ctx); + } + + SECTION("an image with more bits than the first image is refused") { + // first the 8-bit base, then the 16-bit base: it would have to be reduced to 8 bits + auto data = build_file(8, 8, {{0, 0}, {4, 0, true}}, false, 8, 16); + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, data.data(), data.size(), nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = nullptr; + REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, nullptr); + INFO("decode: " << err.message); + CHECK(err.code == heif_error_Unsupported_feature); + CHECK(img == nullptr); + + heif_image_handle_release(handle); + heif_context_free(ctx); + } +} diff -Nru libheif-1.23.4/tests/overlay_spec_parse.cc libheif-1.23.6/tests/overlay_spec_parse.cc --- libheif-1.23.4/tests/overlay_spec_parse.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/overlay_spec_parse.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,218 @@ +/* + 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. +*/ + +// ImageOverlay::parse() reads the canvas size and the per-image offsets from +// 2-byte fields, or from 4-byte fields when bit 0 of the flags byte is set +// (ISO/IEC 23008-12 6.6.2.2). The offsets are signed two's complement values. +// +// A regression in v1.19.2 (ab0565a6) sign-extended the 2-byte offsets as if +// they were 4 bytes wide, so every negative 16-bit offset came out as about +// -2^31 and the image was silently dropped from the canvas. That went unnoticed +// for a long time because the sign extension was only checked indirectly and +// only for a few values. tests/overlay_offsets.cc covers a handful of offsets +// through the full decode path. This file checks the field decoding itself over +// the complete 16-bit range and a broad sample of the 32-bit range, plus the +// write()/parse() round trip including the automatic field-size selection. + +#include "image-items/overlay.h" +#include "catch_amalgamated.hpp" + +#include +#include +#include + + +namespace { + +void put_be(std::vector& out, uint32_t v, int len) +{ + for (int i = len - 1; i >= 0; i--) { + out.push_back(static_cast(v >> (8 * i))); + } +} + +// ImageOverlay payload: version 0, flags, black background, a 1x1 canvas and +// then the raw offset fields in file order (x0, y0, x1, y1, ...). +std::vector make_spec(bool long_fields, const std::vector& raw_offset_fields) +{ + const int len = long_fields ? 4 : 2; + + std::vector s; + s.push_back(0); // version + s.push_back(long_fields ? 1 : 0); // flags + for (int i = 0; i < 4; i++) { + put_be(s, 0, 2); // background RGBA + } + put_be(s, 1, len); // canvas width + put_be(s, 1, len); // canvas height + for (uint32_t f : raw_offset_fields) { + put_be(s, f, len); + } + + return s; +} + +// Two's complement value of an 'nbits' wide field, computed with plain 64-bit +// arithmetic. The implementation relies on the modular unsigned-to-signed +// conversion that C++20 [conv.integral]/3 guarantees; the expectation here +// deliberately does not, so a compiler that got that rule wrong would fail the +// test instead of agreeing with the implementation. +int64_t twos_complement(uint32_t v, int nbits) +{ + const int64_t range = int64_t{1} << nbits; + return (v >= range / 2) ? static_cast(v) - range : static_cast(v); +} + +// Parse one overlay holding all 'values' as x offsets and their bitwise +// complements as y offsets, and compare each decoded pair with the expected +// two's complement interpretation. Using the complement for y makes a wrong +// field order or a wrong read-pointer advance visible as well. +void check_offset_fields(bool long_fields, const std::vector& values) +{ + std::vector fields; + fields.reserve(2 * values.size()); + + const uint32_t field_mask = long_fields ? 0xFFFFFFFFu : 0xFFFFu; + + for (uint32_t v : values) { + fields.push_back(v); + fields.push_back(~v & field_mask); + } + + ImageOverlay ovl; + Error err = ovl.parse(values.size(), make_spec(long_fields, fields)); + INFO("parse error: " << err.message); + REQUIRE(!err); + REQUIRE(ovl.get_num_offsets() == values.size()); + + for (size_t i = 0; i < values.size(); i++) { + const uint32_t v = values[i]; + const uint32_t v_compl = ~v & field_mask; + + const int nbits = long_fields ? 32 : 16; + const int64_t expected_x = twos_complement(v, nbits); + const int64_t expected_y = twos_complement(v_compl, nbits); + + int32_t x = 0, y = 0; + ovl.get_offset(i, &x, &y); + + // A plain REQUIRE per value would work but bloats the assertion count. + if (x != expected_x || y != expected_y) { + FAIL((long_fields ? 32 : 16) << "-bit field 0x" << std::hex << v << std::dec + << " decoded as (" << x << "," << y << "), expected (" + << expected_x << "," << expected_y << ")"); + } + } + + SUCCEED(); +} + +} // namespace + + +TEST_CASE("overlay spec: every 16-bit offset value") +{ + std::vector values; + values.reserve(0x10000); + for (uint32_t v = 0; v <= 0xFFFF; v++) { + values.push_back(v); + } + + check_offset_fields(false, values); +} + + +TEST_CASE("overlay spec: 32-bit offsets over the edges and a spread sample of the range") +{ + std::vector values = { + 0, 1, 0x7FFF, 0x8000, 0xFFFF, 0x10000, + 0x7FFFFFFF, 0x80000000, 0x80000001, + 0xFFFF0000, 0xFFFF7FFF, 0xFFFF8000, + 0xFFFFFD80, // -640, the C021 offset + 0xFFFFFFFE, 0xFFFFFFFF, + }; + + // Multiples of an odd constant wrap around 2^32 and spread evenly over the + // range, so every high byte and both signs are visited many times. + uint32_t v = 0; + for (int i = 0; i < 0x10000; i++) { + v += 0x9E3779B1u; + values.push_back(v); + } + + check_offset_fields(true, values); +} + + +TEST_CASE("overlay spec: write() and parse() round trip with automatic field size") +{ + struct Case + { + uint32_t canvas_w, canvas_h; + int32_t x, y; + bool expect_long_fields; + }; + + const Case cases[] = { + {640, 360, 0, 0, false}, + {640, 360, -640, -360, false}, // C021 + {640, 360, 32767, -32768, false}, // 16-bit extremes still fit + {640, 360, 32768, 0, true}, // one past the positive 16-bit limit + {640, 360, 0, -32769, true}, // one past the negative 16-bit limit + {640, 360, INT32_MAX, INT32_MIN, true}, + {0xFFFF, 0xFFFF, -1, -1, false}, // largest 16-bit canvas + {0x10000, 1, -1, -1, true}, // canvas forces long fields + }; + + for (const Case& c : cases) { + INFO("canvas " << c.canvas_w << "x" << c.canvas_h << ", offset (" << c.x << "," << c.y << ")"); + + ImageOverlay ovl; + ovl.set_canvas_size(c.canvas_w, c.canvas_h); + ovl.add_image_on_top(1, c.x, c.y); + + std::vector data = ovl.write(); + + const size_t field_len = c.expect_long_fields ? 4 : 2; + REQUIRE(data.size() == 2 + 4 * 2 + field_len * 4); + REQUIRE(data[0] == 0); + REQUIRE(data[1] == (c.expect_long_fields ? 1 : 0)); + + ImageOverlay parsed; + Error err = parsed.parse(1, data); + INFO("parse error: " << err.message); + REQUIRE(!err); + + REQUIRE(parsed.get_canvas_width() == c.canvas_w); + REQUIRE(parsed.get_canvas_height() == c.canvas_h); + REQUIRE(parsed.get_num_offsets() == 1); + + int32_t x = 0, y = 0; + parsed.get_offset(0, &x, &y); + REQUIRE(x == c.x); + REQUIRE(y == c.y); + } +} diff -Nru libheif-1.23.4/tests/parallel_grid_deadlock.cc libheif-1.23.6/tests/parallel_grid_deadlock.cc --- libheif-1.23.4/tests/parallel_grid_deadlock.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/parallel_grid_deadlock.cc 2026-10-05 16:16:00.000000000 +0000 @@ -466,3 +466,67 @@ REQUIRE(completed); REQUIRE(err.code == heif_error_Ok); } + + +// An auxiliary (alpha) image is a separate image whose own derivation chain +// must satisfy MIAF independently of the master image. Here the alpha is a +// nested grid (invalid under 7.3.11), attached with 'auxl' to a plain coded +// primary image. The primary's own chain is trivially valid, so the validator +// has to continue from the coded leaf into the auxiliary chain to notice. +// (Previously the coded-image branch returned before the auxiliary check, so +// this was only caught when the primary was itself a grid or overlay; GitHub +// issue #1929.) +static std::vector coded_primary_with_nested_grid_alpha_items() { + const std::vector pixels(64 * 64, 0x7F); + std::vector items; + // id type w h alpha dimg auxl data + items.push_back({1, "mski", 64, 64, false, {}, {}, pixels}); // coded primary + items.push_back({2, "grid", 64, 64, true, {3}, {1}, image_grid(1, 1, 64, 64)}); // its alpha: grid over ... + items.push_back({3, "grid", 64, 64, false, {4}, {}, image_grid(1, 1, 64, 64)}); // ... a grid (invalid) + items.push_back({4, "mski", 64, 64, false, {}, {}, pixels}); // coded base of the alpha + return items; +} + +TEST_CASE("MIAF: an invalid auxiliary chain on a coded primary is rejected with the 'miaf' brand") { + auto data = build_file(coded_primary_with_nested_grid_alpha_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); +} + +// Without the brand, the auxiliary chain is (like the primary's own chain) not +// subject to the MIAF constraints and the nested-grid alpha decodes. +TEST_CASE("MIAF: without the 'miaf' brand an invalid auxiliary chain on a coded primary is accepted") { + auto data = build_file(coded_primary_with_nested_grid_alpha_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); +} + +// The same invalid auxiliary chain attached to a grid primary was already +// rejected before; this pins the behavior as the counterpart of the coded case. +TEST_CASE("MIAF: an invalid auxiliary chain on a grid primary is rejected with the 'miaf' brand") { + 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, {5}, {}, image_grid(1, 1, 64, 64)}); // grid primary (valid) + items.push_back({2, "grid", 64, 64, true, {3}, {1}, image_grid(1, 1, 64, 64)}); // its alpha: grid over ... + items.push_back({3, "grid", 64, 64, false, {4}, {}, image_grid(1, 1, 64, 64)}); // ... a grid (invalid) + items.push_back({4, "mski", 64, 64, false, {}, {}, pixels}); // coded base of the alpha + items.push_back({5, "mski", 64, 64, false, {}, {}, pixels}); // coded tile of the primary + auto data = build_file(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); +} diff -Nru libheif-1.23.4/tests/pixelimage_overlay.cc libheif-1.23.6/tests/pixelimage_overlay.cc --- libheif-1.23.4/tests/pixelimage_overlay.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/pixelimage_overlay.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,593 @@ +/* + 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" + +#include +#include +#include +#include +#include + +// Regression tests for HeifPixelImage::overlay() with the overlay image placed +// partially or completely outside of the canvas. +// +// The former overlap computation clipped the overlay against the right and +// bottom canvas borders by shrinking in_w/in_h to an end coordinate, then +// against the left and top borders by converting them into a count, but the +// copy loops kept using them as end coordinates. An overlay with a negative +// offset of at least half its size was therefore not drawn at all, and smaller +// negative offsets drew a truncated part. The alpha path additionally added the +// source x offset twice and wrote to the wrong destination column. +// +// Every test composites a small overlay onto a uniformly filled canvas at some +// offset and compares each canvas pixel with a straightforward reference +// computation. + +namespace { + +const uint32_t CANVAS = 8; + +// Non-square and not a divisor of the canvas size, to catch swapped axes. +const uint32_t OVL_W = 5; +const uint32_t OVL_H = 4; + +const uint8_t BACKGROUND = 100; + + +// Overlay pixel values. Every (channel, x, y) combination is unique and none of +// the color values equals BACKGROUND. +uint8_t alpha_value(uint32_t x, uint32_t y) +{ + switch ((x + y) % 3) { + case 0: return 0; + case 1: return 255; + default: return 128; + } +} + +uint8_t overlay_value(heif_channel ch, uint32_t x, uint32_t y) +{ + uint8_t idx = static_cast(16 * y + x); + switch (ch) { + case heif_channel_R: return static_cast(1 + idx); + case heif_channel_G: return static_cast(200 - idx); + case heif_channel_B: return static_cast(128 + idx); + case heif_channel_Alpha: return alpha_value(x, y); + default: return 0; + } +} + + +std::shared_ptr make_canvas() +{ + auto img = std::make_shared(); + img->create(CANVAS, CANVAS, heif_colorspace_RGB, heif_chroma_444); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + REQUIRE(img->add_channel(ch, CANVAS, CANVAS, 8, heif_get_global_security_limits()).error_code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = img->get_channel_memory(ch, &stride); + for (uint32_t y = 0; y < CANVAS; y++) { + memset(p + y * stride, BACKGROUND, CANVAS); + } + } + + return img; +} + + +std::shared_ptr make_overlay(bool with_alpha) +{ + auto img = std::make_shared(); + img->create(OVL_W, OVL_H, heif_colorspace_RGB, heif_chroma_444); + + std::vector channels = {heif_channel_R, heif_channel_G, heif_channel_B}; + if (with_alpha) { + channels.push_back(heif_channel_Alpha); + } + + for (heif_channel ch : channels) { + REQUIRE(img->add_channel(ch, OVL_W, OVL_H, 8, heif_get_global_security_limits()).error_code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = img->get_channel_memory(ch, &stride); + for (uint32_t y = 0; y < OVL_H; y++) { + for (uint32_t x = 0; x < OVL_W; x++) { + p[y * stride + x] = overlay_value(ch, x, y); + } + } + } + + return img; +} + + +// The value that canvas pixel (cx,cy) must have after compositing the overlay at (dx,dy). +uint8_t expected_value(heif_channel ch, uint32_t cx, uint32_t cy, int32_t dx, int32_t dy, bool with_alpha) +{ + int64_t ox = static_cast(cx) - dx; + int64_t oy = static_cast(cy) - dy; + + if (ox < 0 || oy < 0 || ox >= OVL_W || oy >= OVL_H) { + return BACKGROUND; + } + + uint8_t in = overlay_value(ch, static_cast(ox), static_cast(oy)); + if (!with_alpha) { + return in; + } + + int a = alpha_value(static_cast(ox), static_cast(oy)); + return static_cast((in * a + BACKGROUND * (255 - a)) / 255); +} + + +void check_overlay_at(int32_t dx, int32_t dy, bool with_alpha) +{ + INFO("offset (" << dx << "," << dy << "), alpha=" << with_alpha); + + auto canvas = make_canvas(); + auto overlay = make_overlay(with_alpha); + + REQUIRE(canvas->overlay(overlay, dx, dy).error_code == heif_error_Ok); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + size_t stride = 0; + const uint8_t* p = canvas->get_channel_memory(ch, &stride); + + for (uint32_t cy = 0; cy < CANVAS; cy++) { + for (uint32_t cx = 0; cx < CANVAS; cx++) { + INFO("channel " << static_cast(ch) << ", canvas pixel (" << cx << "," << cy << ")"); + REQUIRE(p[cy * stride + cx] == expected_value(ch, cx, cy, dx, dy, with_alpha)); + } + } + } +} + + +struct Offset { + int32_t dx; + int32_t dy; +}; + +// Offsets that leave part of the overlay on the canvas. +const std::vector partially_visible_offsets = { + {2, 3}, // completely inside + {0, 0}, // aligned with the top-left corner + {-1, -1}, // slightly outside on the top-left; used to draw a truncated part + {-3, -2}, // outside by at least half its size on both axes; used to be dropped + {-4, 0}, // one column left + {0, -3}, // one row left + {-4, 3}, // negative x only + {3, -3}, // negative y only + {6, 5}, // partially outside on the right and bottom + {7, 7}, // one pixel visible at the bottom-right corner + {-2, 6}, // outside on the left and bottom + {5, -1}, // outside on the right and top +}; + +// Offsets that place the overlay completely outside of the canvas, including +// the extreme values, which must neither draw anything nor overflow. +const std::vector invisible_offsets = { + {-5, 0}, {0, -4}, {8, 0}, {0, 8}, {-5, -4}, {8, 8}, + {INT32_MIN, 0}, {0, INT32_MIN}, {INT32_MIN, INT32_MIN}, + {INT32_MAX, 0}, {0, INT32_MAX}, {INT32_MAX, INT32_MAX}, + {INT32_MIN, INT32_MAX}, +}; + +} // namespace + + +TEST_CASE("overlay without alpha at offsets partially outside the canvas") { + for (const auto& o : partially_visible_offsets) { + check_overlay_at(o.dx, o.dy, false); + } +} + +TEST_CASE("overlay with alpha at offsets partially outside the canvas") { + for (const auto& o : partially_visible_offsets) { + check_overlay_at(o.dx, o.dy, true); + } +} + +TEST_CASE("overlay completely outside the canvas leaves it unchanged") { + for (const auto& o : invisible_offsets) { + check_overlay_at(o.dx, o.dy, false); + check_overlay_at(o.dx, o.dy, true); + } +} + + +// overlay() reads all planes of the overlay with the coordinates of the color planes and +// blends them byte by byte. HeifPixelImage itself puts no constraint on the size or the +// bit depth of R, G, B and alpha planes, so overlay() has to refuse an image that does not +// have the form it needs. It only compared the alpha plane with the size of the image: +// with color planes that are larger than the image, the alpha plane passed that check and +// the blend loop read past its end. + +namespace { + +// An overlay whose planes can have sizes and bit depths that do not fit the image. +std::shared_ptr make_irregular_overlay(uint32_t image_size, + uint32_t color_plane_size, int color_bits, + uint32_t alpha_plane_size, int alpha_bits) +{ + auto* limits = heif_get_global_security_limits(); + + auto img = std::make_shared(); + img->create(image_size, image_size, heif_colorspace_RGB, heif_chroma_444); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + REQUIRE(img->fill_new_channel(ch, 100, color_plane_size, color_plane_size, color_bits, limits).error_code == heif_error_Ok); + } + + if (alpha_plane_size > 0) { + REQUIRE(img->fill_new_channel(heif_channel_Alpha, 100, alpha_plane_size, alpha_plane_size, alpha_bits, limits).error_code == heif_error_Ok); + } + + return img; +} + +void require_canvas_unchanged(const std::shared_ptr& canvas) +{ + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + size_t stride = 0; + const uint8_t* p = canvas->get_channel_memory(ch, &stride); + for (uint32_t y = 0; y < CANVAS; y++) { + for (uint32_t x = 0; x < CANVAS; x++) { + REQUIRE(p[y * stride + x] == BACKGROUND); + } + } + } +} + +} // namespace + + +TEST_CASE("overlay refuses an image whose planes do not have the size of the image") { + auto canvas = make_canvas(); + + SECTION("control: regular overlay with alpha") { + auto overlay = make_irregular_overlay(4, 4, 8, 4, 8); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Ok); + } + + SECTION("color planes larger than the image, alpha plane of the image size") { + // 64x64 color planes on a 4x4 image: the blend loop used to read 64 rows and columns of + // an alpha plane that has 4. + auto overlay = make_irregular_overlay(4, 64, 8, 4, 8); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_canvas_unchanged(canvas); + } + + SECTION("color planes larger than the image, no alpha plane") { + auto overlay = make_irregular_overlay(4, 64, 8, 0, 8); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_canvas_unchanged(canvas); + } + + SECTION("alpha plane smaller than the image") { + auto overlay = make_irregular_overlay(4, 4, 8, 2, 8); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_canvas_unchanged(canvas); + } + + SECTION("alpha plane larger than the image") { + auto overlay = make_irregular_overlay(4, 4, 8, 64, 8); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_canvas_unchanged(canvas); + } +} + + +// --- samples with more than 8 bits +// +// overlay() composed the planes byte by byte whatever their bit depth. The two bytes of a +// 16-bit sample were blended like two pixels, and only the left half of each row was +// drawn. The samples are now composed with their own width (8 or 16 bits), the alpha plane +// may have another bit depth than the color planes, and what cannot be composed (planes +// with more than 16 bits, samples that are not unsigned integers, an overlay with another +// bit depth than the canvas) is refused. + +namespace { + +uint32_t max_sample(int bits) +{ + return (uint32_t{1} << bits) - 1; +} + +// The values use the whole range of the bit depth, with different upper and lower bytes. +uint16_t wide_background(int bits) +{ + return static_cast((BACKGROUND * 257 + 77) >> (16 - bits)); +} + +uint16_t wide_alpha_value(uint32_t x, uint32_t y, int bits) +{ + switch ((x + y) % 3) { + case 0: return 0; + case 1: return static_cast(max_sample(bits)); + default: return static_cast(max_sample(bits) / 3); + } +} + +uint16_t wide_overlay_value(heif_channel ch, uint32_t x, uint32_t y, int bits) +{ + if (ch == heif_channel_Alpha) { + return wide_alpha_value(x, y, bits); + } + + return static_cast(((overlay_value(ch, x, y) * 257) ^ 0x5A) >> (16 - bits)); +} + +void write_sample(const std::shared_ptr& img, heif_channel ch, uint32_t x, uint32_t y, uint16_t value) +{ + size_t stride = 0; + uint8_t* p = img->get_channel_memory(ch, &stride); + + if (img->get_bits_per_pixel(ch) <= 8) { + p[y * stride + x] = static_cast(value); + } + else { + memcpy(p + y * stride + 2 * x, &value, 2); + } +} + +uint16_t read_sample(const std::shared_ptr& img, heif_channel ch, uint32_t x, uint32_t y) +{ + size_t stride = 0; + const uint8_t* p = img->get_channel_memory(ch, &stride); + + if (img->get_bits_per_pixel(ch) <= 8) { + return p[y * stride + x]; + } + + uint16_t value; + memcpy(&value, p + y * stride + 2 * x, 2); + return value; +} + +std::shared_ptr make_wide_canvas(int bits, + heif_component_datatype datatype = heif_component_datatype_unsigned_integer) +{ + auto img = std::make_shared(); + img->create(CANVAS, CANVAS, heif_colorspace_RGB, heif_chroma_444); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + REQUIRE(img->add_channel(ch, CANVAS, CANVAS, bits, heif_get_global_security_limits(), datatype).error_code == heif_error_Ok); + + if (bits <= 16) { + for (uint32_t y = 0; y < CANVAS; y++) { + for (uint32_t x = 0; x < CANVAS; x++) { + write_sample(img, ch, x, y, wide_background(bits)); + } + } + } + } + + return img; +} + +// 'alpha_bits' = 0: no alpha plane. +std::shared_ptr make_wide_overlay(int color_bits, int alpha_bits, + heif_component_datatype color_datatype = heif_component_datatype_unsigned_integer, + heif_component_datatype alpha_datatype = heif_component_datatype_unsigned_integer) +{ + auto img = std::make_shared(); + img->create(OVL_W, OVL_H, heif_colorspace_RGB, heif_chroma_444); + + std::vector channels = {heif_channel_R, heif_channel_G, heif_channel_B}; + if (alpha_bits > 0) { + channels.push_back(heif_channel_Alpha); + } + + for (heif_channel ch : channels) { + const bool is_alpha = (ch == heif_channel_Alpha); + const int bits = is_alpha ? alpha_bits : color_bits; + + REQUIRE(img->add_channel(ch, OVL_W, OVL_H, bits, heif_get_global_security_limits(), + is_alpha ? alpha_datatype : color_datatype).error_code == heif_error_Ok); + + if (bits <= 16) { + for (uint32_t y = 0; y < OVL_H; y++) { + for (uint32_t x = 0; x < OVL_W; x++) { + write_sample(img, ch, x, y, wide_overlay_value(ch, x, y, bits)); + } + } + } + } + + return img; +} + +void require_wide_canvas_unchanged(const std::shared_ptr& canvas, int bits) +{ + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + for (uint32_t y = 0; y < CANVAS; y++) { + for (uint32_t x = 0; x < CANVAS; x++) { + REQUIRE(read_sample(canvas, ch, x, y) == wide_background(bits)); + } + } + } +} + +void check_wide_overlay_at(int32_t dx, int32_t dy, int color_bits, int alpha_bits) +{ + INFO("offset (" << dx << "," << dy << "), " << color_bits << "-bit color, " << alpha_bits << "-bit alpha"); + + auto canvas = make_wide_canvas(color_bits); + auto overlay = make_wide_overlay(color_bits, alpha_bits); + + Error err = canvas->overlay(overlay, dx, dy); + INFO("error: " << err.message); + REQUIRE(err.error_code == heif_error_Ok); + + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + REQUIRE(canvas->get_bits_per_pixel(ch) == color_bits); + + for (uint32_t cy = 0; cy < CANVAS; cy++) { + for (uint32_t cx = 0; cx < CANVAS; cx++) { + INFO("channel " << static_cast(ch) << ", canvas pixel (" << cx << "," << cy << ")"); + + int64_t ox = static_cast(cx) - dx; + int64_t oy = static_cast(cy) - dy; + + uint64_t expected = wide_background(color_bits); + if (ox >= 0 && oy >= 0 && ox < OVL_W && oy < OVL_H) { + uint64_t in = wide_overlay_value(ch, static_cast(ox), static_cast(oy), color_bits); + + if (alpha_bits == 0) { + expected = in; + } + else { + uint64_t a = wide_alpha_value(static_cast(ox), static_cast(oy), alpha_bits); + uint64_t a_max = max_sample(alpha_bits); + expected = (in * a + expected * (a_max - a)) / a_max; + } + } + + REQUIRE(read_sample(canvas, ch, cx, cy) == expected); + } + } + } +} + +} // namespace + + +TEST_CASE("overlay of images with more than 8 bits per sample") { + // {color bits, alpha bits}; 0 = no alpha plane + const std::vector> formats = { + {10, 0}, {12, 0}, {16, 0}, // no alpha + {10, 10}, {12, 12}, {16, 16}, // alpha with the bit depth of the color planes + {10, 8}, {16, 8}, // 8-bit alpha on wide color planes + {8, 16}, {8, 10}, // wide alpha on 8-bit color planes + {12, 16}, {16, 9}, // different wide bit depths + {8, 1}, {16, 1}, // binary alpha + }; + + for (const auto& format : formats) { + for (const auto& o : partially_visible_offsets) { + check_wide_overlay_at(o.dx, o.dy, format.first, format.second); + } + + for (const auto& o : invisible_offsets) { + check_wide_overlay_at(o.dx, o.dy, format.first, format.second); + } + } +} + + +TEST_CASE("overlay limits alpha samples to the range of their bit depth") { + // Nothing guarantees that the samples of a 10-bit plane are below 1024. With an alpha + // sample above the maximum, the weight of the canvas (maximum - alpha) would wrap around. + for (int color_bits : {8, 16}) { + auto canvas = make_wide_canvas(color_bits); + auto overlay = make_wide_overlay(color_bits, 10); + + for (uint32_t y = 0; y < OVL_H; y++) { + for (uint32_t x = 0; x < OVL_W; x++) { + write_sample(overlay, heif_channel_Alpha, x, y, 0xFFFF); + } + } + + REQUIRE(canvas->overlay(overlay, 0, 0).error_code == heif_error_Ok); + + // treated as an opaque pixel + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + for (uint32_t y = 0; y < OVL_H; y++) { + for (uint32_t x = 0; x < OVL_W; x++) { + REQUIRE(read_sample(canvas, ch, x, y) == wide_overlay_value(ch, x, y, color_bits)); + } + } + } + } +} + + +TEST_CASE("overlay refuses sample formats that it cannot compose") { + SECTION("overlay with more bits than the canvas") { + for (int bits : {10, 16}) { + auto canvas = make_wide_canvas(8); + auto overlay = make_wide_overlay(bits, 0); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_wide_canvas_unchanged(canvas, 8); + } + } + + SECTION("overlay with fewer bits than the canvas") { + for (int alpha_bits : {0, 8}) { + auto canvas = make_wide_canvas(16); + auto overlay = make_wide_overlay(8, alpha_bits); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_wide_canvas_unchanged(canvas, 16); + } + } + + SECTION("different bit depths with the same sample size") { + auto canvas = make_wide_canvas(12); + auto overlay = make_wide_overlay(10, 0); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_wide_canvas_unchanged(canvas, 12); + } + + SECTION("color planes with more than 16 bits") { + auto canvas = make_wide_canvas(32); + auto overlay = make_wide_overlay(32, 0); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + } + + SECTION("alpha plane with more than 16 bits") { + auto canvas = make_wide_canvas(8); + auto overlay = make_wide_overlay(8, 32); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_wide_canvas_unchanged(canvas, 8); + } + + SECTION("samples that are not unsigned integers") { + { + auto canvas = make_wide_canvas(16, heif_component_datatype_signed_integer); + auto overlay = make_wide_overlay(16, 0, heif_component_datatype_signed_integer); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + } + + { + auto canvas = make_wide_canvas(16); + auto overlay = make_wide_overlay(16, 16, heif_component_datatype_unsigned_integer, heif_component_datatype_signed_integer); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + require_wide_canvas_unchanged(canvas, 16); + } + + { + auto canvas = make_wide_canvas(16, heif_component_datatype_signed_integer); + auto overlay = make_wide_overlay(16, 0); + CHECK(canvas->overlay(overlay, 0, 0).error_code == heif_error_Unsupported_feature); + } + } +} diff -Nru libheif-1.23.4/tests/plane_layout.cc libheif-1.23.6/tests/plane_layout.cc --- libheif-1.23.4/tests/plane_layout.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/plane_layout.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,390 @@ +/* + 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. +*/ + +// HeifPixelImage::check_plane_layout() is the gate through which images enter the color +// conversion pipeline and the encoders. The operators read the planes their colorspace and +// chroma format imply, at the sizes they imply; an image with a missing, duplicate, foreign or +// undersized plane used to fail somewhere inside an operator, or not at all (a YCbCr image +// without a Cr plane passed the pipeline as a no-op and reached the x265 plugin). Planes with +// channel heif_channel_unknown carry multi-component data without a colour meaning (the +// padding components of 'unci') and are tolerated and ignored. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "image/pixelimage.h" +#include "color-conversion/colorconversion.h" + +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t W = 16; +constexpr uint32_t H = 8; + +std::shared_ptr make_image(heif_colorspace cs, heif_chroma chroma, + std::initializer_list channels, int bpp = 8) +{ + auto img = std::make_shared(); + img->create(W, H, cs, chroma); + for (heif_channel ch : channels) { + Error err = img->add_channel(ch, channel_width(W, chroma, ch), channel_height(H, chroma, ch), bpp, nullptr); + INFO("add_channel: " << err.message); + REQUIRE(!err); + } + return img; +} + +void add_unknown_components(const std::shared_ptr& img, int count) +{ + for (int i = 0; i < count; i++) { + auto result = img->add_component(W, H, heif_cmpd_component_type_padded, + heif_component_datatype_unsigned_integer, 8, nullptr); + REQUIRE(result); + } + REQUIRE(img->has_channel(heif_channel_unknown)); +} + +bool mentions(const Error& err, const char* text) +{ + return err.message.find(text) != std::string::npos; +} + +} // namespace + + +TEST_CASE("check_plane_layout accepts the canonical layouts") +{ + struct Layout + { + const char* name; + heif_colorspace cs; + heif_chroma chroma; + std::vector planes; + int bpp; + }; + + const Layout layouts[] = { + {"RGB 4:4:4", heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}, 8}, + {"RGB 4:4:4 + alpha", heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}, 8}, + {"interleaved RGB", heif_colorspace_RGB, heif_chroma_interleaved_RGB, {heif_channel_interleaved}, 8}, + {"interleaved RGBA", heif_colorspace_RGB, heif_chroma_interleaved_RGBA, {heif_channel_interleaved}, 8}, + {"interleaved RRGGBB_LE", heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE, {heif_channel_interleaved}, 10}, + {"interleaved RRGGBBAA_BE", heif_colorspace_RGB, heif_chroma_interleaved_RRGGBBAA_BE, {heif_channel_interleaved}, 12}, + {"YCbCr 4:4:4", heif_colorspace_YCbCr, heif_chroma_444, {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}, 8}, + {"YCbCr 4:2:2 + alpha", heif_colorspace_YCbCr, heif_chroma_422, {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, heif_channel_Alpha}, 8}, + {"YCbCr 4:2:0", heif_colorspace_YCbCr, heif_chroma_420, {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}, 8}, + {"YCbCr 4:2:0 10-bit + alpha", heif_colorspace_YCbCr, heif_chroma_420, {heif_channel_Y, heif_channel_Cb, heif_channel_Cr, heif_channel_Alpha}, 10}, + {"YCbCr luma only", heif_colorspace_YCbCr, heif_chroma_monochrome, {heif_channel_Y}, 8}, + {"monochrome", heif_colorspace_monochrome, heif_chroma_monochrome, {heif_channel_Y}, 8}, + {"monochrome + alpha", heif_colorspace_monochrome, heif_chroma_monochrome, {heif_channel_Y, heif_channel_Alpha}, 8}, + {"filter array", heif_colorspace_filter_array, heif_chroma_planar, {heif_channel_filter_array}, 12}, + }; + + for (const Layout& l : layouts) { + INFO(l.name); + auto img = std::make_shared(); + img->create(W, H, l.cs, l.chroma); + for (heif_channel ch : l.planes) { + REQUIRE(!img->add_channel(ch, channel_width(W, l.chroma, ch), channel_height(H, l.chroma, ch), l.bpp, nullptr)); + } + Error err = img->check_plane_layout(); + INFO(err.message); + CHECK(!err); + } + + SECTION("planes with channel unknown are tolerated") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}); + add_unknown_components(img, 2); + CHECK(!img->check_plane_layout()); + } +} + + +TEST_CASE("check_plane_layout rejects non-canonical layouts") +{ + SECTION("missing colour plane") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G}); + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(err.error_code == heif_error_Usage_error); + CHECK(mentions(err, "no B plane")); + } + + SECTION("missing chroma plane") { + auto img = make_image(heif_colorspace_YCbCr, heif_chroma_420, {heif_channel_Y, heif_channel_Cb}); + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(mentions(err, "no Cr plane")); + } + + SECTION("foreign plane") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_depth}); + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(mentions(err, "depth plane")); + } + + SECTION("stray chroma plane on a monochrome image") { + auto img = make_image(heif_colorspace_monochrome, heif_chroma_monochrome, {heif_channel_Y, heif_channel_Cb}); + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(mentions(err, "Cb plane")); + } + + SECTION("duplicate colour plane") { + // HeifPixelImage itself allows this (multi-spectral images consist of several monochrome + // planes); the layout check is what refuses it for colour conversion and encoding. + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}); + REQUIRE(!img->add_channel(heif_channel_R, W, H, 8, nullptr)); + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(mentions(err, "more than one R plane")); + } + + SECTION("chroma plane with the wrong size") { + // add_channel() now refuses to build a Cb/Cr plane whose size is not the + // chroma-subsampled size of the image, so the inconsistent state that + // check_plane_layout()'s "Cb plane has size" branch used to catch can no + // longer be constructed. Verify the rejection happens at construction. + auto img = std::make_shared(); + img->create(W, H, heif_colorspace_YCbCr, heif_chroma_420); + REQUIRE(!img->add_channel(heif_channel_Y, W, H, 8, nullptr)); + + Error err = img->add_channel(heif_channel_Cb, W, H, 8, nullptr); // should be W/2 x H/2 + REQUIRE(err); + CHECK(err.error_code == heif_error_Usage_error); + + // The correctly-sized planes are still accepted. + REQUIRE(!img->add_channel(heif_channel_Cb, W / 2, H / 2, 8, nullptr)); + REQUIRE(!img->add_channel(heif_channel_Cr, W / 2, H / 2, 8, nullptr)); + CHECK(!img->check_plane_layout()); + } + + SECTION("alpha next to a filter array") { + auto img = make_image(heif_colorspace_filter_array, heif_chroma_planar, {heif_channel_filter_array, heif_channel_Alpha}); + CHECK(img->check_plane_layout()); + } + + SECTION("colorspace and chroma do not fit") { + auto rgb420 = make_image(heif_colorspace_RGB, heif_chroma_420, {}); + CHECK(rgb420->check_plane_layout()); + + auto ycc_interleaved = make_image(heif_colorspace_YCbCr, heif_chroma_interleaved_RGB, {}); + CHECK(ycc_interleaved->check_plane_layout()); + } + + SECTION("custom colorspace has no layout") { + auto img = std::make_shared(); + img->create(W, H, heif_colorspace_custom, heif_chroma_planar); + for (int i = 0; i < 3; i++) { + REQUIRE(img->add_component(W, H, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, 8, nullptr)); + } + Error err = img->check_plane_layout(); + REQUIRE(err); + CHECK(mentions(err, "no defined plane layout")); + } +} + + +TEST_CASE("convert_colorspace refuses images with a non-canonical plane layout") +{ + heif_color_conversion_options options{}; + + SECTION("RGB without B") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G}); + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx_profile::defaults(), 0, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(!result); + CHECK(result.error().error_code == heif_error_Unsupported_feature); + CHECK(result.error().sub_error_code == heif_suberror_Unsupported_image_type); + } + + SECTION("YCbCr 4:2:0 without Cr, same-layout target (used to be a no-op)") { + auto img = make_image(heif_colorspace_YCbCr, heif_chroma_420, {heif_channel_Y, heif_channel_Cb}); + auto result = convert_colorspace(img, heif_colorspace_YCbCr, heif_chroma_420, + nclx_profile::defaults(), 0, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(!result); + CHECK(result.error().sub_error_code == heif_suberror_Unsupported_image_type); + } + + SECTION("custom colorspace") { + auto img = std::make_shared(); + img->create(W, H, heif_colorspace_custom, heif_chroma_planar); + REQUIRE(img->add_component(W, H, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, 8, nullptr)); + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx_profile::defaults(), 0, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(!result); + CHECK(result.error().sub_error_code == heif_suberror_Unsupported_image_type); + } + + SECTION("unknown planes are tolerated and not carried into the output") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}); + add_unknown_components(img, 2); + auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, + nclx_profile::defaults(), 0, options, nullptr, + heif_get_disabled_security_limits()); + REQUIRE(result); + CHECK((*result)->has_channel(heif_channel_interleaved)); + CHECK(!(*result)->has_channel(heif_channel_unknown)); + } +} + + +// A plane stores its samples in whole bytes, so a plane with a bit depth of, say, 10 bits can +// hold larger values in its 16-bit words. HeifPixelImage::check_sample_value_ranges() is the +// gate for that: the encoder entry uses it for images coming from the application, and the +// sharp-yuv operator uses it before it hands the samples to libsharpyuv, which uses them as +// table indices. +TEST_CASE("check_sample_value_ranges") +{ + auto set_sample16 = [](const std::shared_ptr& img, heif_channel ch, size_t idx_in_last_row, uint16_t value) { + size_t stride; + uint8_t* p = img->get_channel_memory(ch, &stride); + REQUIRE(p != nullptr); + reinterpret_cast(p + (img->get_height(ch) - 1) * stride)[idx_in_last_row] = value; + }; + + SECTION("planar planes with 9 to 15 bits") { + for (int bpp : {9, 10, 12, 15}) { + INFO("bpp=" << bpp); + const uint16_t max_value = static_cast((1 << bpp) - 1); + + auto img = make_image(heif_colorspace_YCbCr, heif_chroma_420, + {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}, bpp); + for (heif_channel ch : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + img->fill_channel(ch, max_value); + } + CHECK(!img->check_sample_value_ranges()); + + // a single sample, the last one of the last plane + set_sample16(img, heif_channel_Cr, img->get_width(heif_channel_Cr) - 1, static_cast(max_value + 1)); + Error err = img->check_sample_value_ranges(); + REQUIRE(err); + CHECK(err.error_code == heif_error_Usage_error); + CHECK(mentions(err, "Cr")); + } + } + + SECTION("8 and 16 bit planes cannot be out of range") { + auto img8 = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}, 8); + auto img16 = make_image(heif_colorspace_RGB, heif_chroma_444, {heif_channel_R, heif_channel_G, heif_channel_B}, 16); + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B}) { + img8->fill_channel(ch, 0xFF); + img16->fill_channel(ch, 0xFFFF); + } + CHECK(!img8->check_sample_value_ranges()); + CHECK(!img16->check_sample_value_ranges()); + } + + SECTION("planes with less than 8 bits") { + auto img = make_image(heif_colorspace_monochrome, heif_chroma_monochrome, {heif_channel_Y}, 4); + img->fill_channel(heif_channel_Y, 15); + CHECK(!img->check_sample_value_ranges()); + img->fill_channel(heif_channel_Y, 16); + CHECK(img->check_sample_value_ranges()); + } + + SECTION("the alpha plane is checked, too") { + auto img = make_image(heif_colorspace_RGB, heif_chroma_444, + {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}, 10); + for (heif_channel ch : {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}) { + img->fill_channel(ch, 1023); + } + CHECK(!img->check_sample_value_ranges()); + set_sample16(img, heif_channel_Alpha, 0, 1024); + Error err = img->check_sample_value_ranges(); + REQUIRE(err); + CHECK(mentions(err, "alpha")); + } + + SECTION("interleaved planes: all components, in the byte order of the format") { + for (heif_chroma chroma : {heif_chroma_interleaved_RRGGBB_LE, heif_chroma_interleaved_RRGGBB_BE, + heif_chroma_interleaved_RRGGBBAA_LE, heif_chroma_interleaved_RRGGBBAA_BE}) { + const bool big_endian = (chroma == heif_chroma_interleaved_RRGGBB_BE || + chroma == heif_chroma_interleaved_RRGGBBAA_BE); + const int num_components = (chroma == heif_chroma_interleaved_RRGGBBAA_LE || + chroma == heif_chroma_interleaved_RRGGBBAA_BE) ? 4 : 3; + INFO("chroma=" << chroma); + + // writes one sample in the byte order of the format + auto set_sample = [&](const std::shared_ptr& img, size_t idx, uint16_t value) { + size_t stride; + uint8_t* p = img->get_channel_memory(heif_channel_interleaved, &stride) + (H - 1) * stride + 2 * idx; + p[big_endian ? 0 : 1] = static_cast(value >> 8); + p[big_endian ? 1 : 0] = static_cast(value & 0xFF); + }; + + auto img = make_image(heif_colorspace_RGB, chroma, {heif_channel_interleaved}, 10); + img->fill_channel(heif_channel_interleaved, 0); + + // 0x03FF is the largest 10-bit value. Read with the wrong byte order it would be 0xFF03. + for (size_t idx = 0; idx < static_cast(W) * num_components; idx++) { + set_sample(img, idx, 0x03FF); + } + CHECK(!img->check_sample_value_ranges()); + + // 0x0400 is out of range. Read with the wrong byte order it would be the valid value 4. + set_sample(img, static_cast(W) * num_components - 1, 0x0400); + CHECK(img->check_sample_value_ranges()); + } + } + + SECTION("only unsigned integer planes are checked") { + // A signed 12-bit sample of -1 has all bits of its 16-bit word set. + auto img = std::make_shared(); + img->create(W, H, heif_colorspace_custom, heif_chroma_planar); + auto id = img->add_component(W, H, heif_cmpd_component_type_monochrome, + heif_component_datatype_signed_integer, 12, nullptr); + REQUIRE(id); + size_t stride; + uint8_t* p = img->get_component(*id, &stride); + REQUIRE(p != nullptr); + for (uint32_t y = 0; y < H; y++) { + memset(p + y * stride, 0xFF, 2 * W); + } + CHECK(!img->check_sample_value_ranges()); + + // the same bits in an unsigned plane are out of range + auto id2 = img->add_component(W, H, heif_cmpd_component_type_monochrome, + heif_component_datatype_unsigned_integer, 12, nullptr); + REQUIRE(id2); + p = img->get_component(*id2, &stride); + REQUIRE(p != nullptr); + for (uint32_t y = 0; y < H; y++) { + memset(p + y * stride, 0xFF, 2 * W); + } + CHECK(img->check_sample_value_ranges()); + } +} diff -Nru libheif-1.23.4/tests/sdtp_sample_limit.cc libheif-1.23.6/tests/sdtp_sample_limit.cc --- libheif-1.23.4/tests/sdtp_sample_limit.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/sdtp_sample_limit.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,88 @@ +/* + 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. +*/ + +// 'sdtp' stores one byte per sample and reads to the end of the box, so its +// sample count scales with the file size. Like the other sample tables it is +// now bounded by max_sequence_frames at parse time. + +#include "catch_amalgamated.hpp" +#include "box.h" +#include "security_limits.h" + +#include +#include +#include + +namespace { + +std::vector sdtp_box(uint32_t num_samples) +{ + std::vector v; + uint32_t size = 8 + 4 + num_samples; + v.push_back(uint8_t(size >> 24)); + v.push_back(uint8_t(size >> 16)); + v.push_back(uint8_t(size >> 8)); + v.push_back(uint8_t(size)); + for (char c : std::string("sdtp")) v.push_back(uint8_t(c)); + v.push_back(0); v.push_back(0); v.push_back(0); v.push_back(0); // version + flags + v.insert(v.end(), num_samples, 0); + return v; +} + +Error parse_sdtp(uint32_t num_samples, uint32_t max_sequence_frames) +{ + std::vector data = sdtp_box(num_samples); + auto reader = std::make_shared(data.data(), data.size(), false); + BitstreamRange range(reader, data.size()); + + heif_security_limits limits = *heif_get_global_security_limits(); + limits.max_sequence_frames = max_sequence_frames; + + std::shared_ptr box; + return Box::read(range, &box, &limits); +} + +} // namespace + + +TEST_CASE("sdtp sample count is bounded by max_sequence_frames") +{ + SECTION("more samples than the limit allows is rejected") { + Error err = parse_sdtp(1000, 100); + REQUIRE(err.error_code == heif_error_Memory_allocation_error); + REQUIRE(err.sub_error_code == heif_suberror_Security_limit_exceeded); + } + + SECTION("a table within the limit is accepted") { + Error err = parse_sdtp(50, 100); + REQUIRE(err.error_code == heif_error_Ok); + } + + SECTION("the limit disabled (0) accepts any count") { + Error err = parse_sdtp(1000, 0); + REQUIRE(err.error_code == heif_error_Ok); + } +} diff -Nru libheif-1.23.4/tests/sequence_nonvisual_sample_entry.cc libheif-1.23.6/tests/sequence_nonvisual_sample_entry.cc --- libheif-1.23.4/tests/sequence_nonvisual_sample_entry.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/sequence_nonvisual_sample_entry.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,272 @@ +/* + libheif regression test for GHSA-8p2c-9wv4-rx4p: a visual sequence track whose + 'stsd' carries a non-visual sample entry (e.g. an 'mp4a' audio entry) that a + chunk is mapped to via 'stsc'. Such a chunk gets no decoder in Track::load(). + The decode loop guarded the decoder pointer only with assert(), so on an NDEBUG + build this was a NULL-pointer dereference through a virtual call. The file must + now be rejected with heif_error_Invalid_input at read time, not crash. + + 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 "sequence_file_builder.h" + +#include +#include + +using namespace seqfile; + +namespace { + +// A minimal but structurally valid 'hvc1' VisualSampleEntry. The 78-byte visual +// header is followed by a minimal 'hvcC' configuration record (no NAL arrays), +// which is enough for Decoder::alloc_for_sequence_sample_description_box() to +// construct an HEVC decoder object. No codec plugin is needed: the file is +// rejected during Track::load(), before any decoding is attempted. +std::vector make_hvc1_entry() +{ + std::vector vse; + for (int i = 0; i < 6; i++) vse.push_back(0); // SampleEntry reserved + put16(vse, 1); // data_reference_index + put16(vse, 0); // pre_defined + put16(vse, 0); // reserved + for (int i = 0; i < 3; i++) put32(vse, 0); // pre_defined + put16(vse, 4); // width + put16(vse, 4); // 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 (length-prefixed, empty) + put16(vse, 0x0018); // depth + put16(vse, 0xFFFF); // pre_defined (-1) + + // HEVCDecoderConfigurationRecord, 23 bytes, zero NAL arrays. + std::vector hvcc; + hvcc.push_back(1); // configuration_version + hvcc.push_back(0); // profile_space / tier / profile_idc + put32(hvcc, 0); // general_profile_compatibility_flags + for (int i = 0; i < 6; i++) hvcc.push_back(0); // general_constraint_indicator_flags + hvcc.push_back(0); // general_level_idc + put16(hvcc, 0); // min_spatial_segmentation_idc + hvcc.push_back(0); // parallelism_type + hvcc.push_back(1); // chroma_format (4:2:0) + hvcc.push_back(0); // bit_depth_luma_minus8 + hvcc.push_back(0); // bit_depth_chroma_minus8 + put16(hvcc, 0); // avg_frame_rate + hvcc.push_back(0); // constant_frame_rate / temporal layers / length_size + hvcc.push_back(0); // num_of_arrays + + append(vse, box("hvcC", hvcc)); + return box("hvc1", vse); +} + +// A non-visual sample entry of the given 4cc (a bare SampleEntry: 6 reserved +// bytes + data_reference_index). Any 4cc the box factory does not know becomes a +// generic Box, which does not cast to Box_VisualSampleEntry. +std::vector make_nonvisual_entry(const char* type) +{ + std::vector payload; + for (int i = 0; i < 6; i++) payload.push_back(0); + put16(payload, 1); // data_reference_index + return box(type, payload); +} + +// Build a 'pict' sequence with two samples in two chunks. The 'stsd' holds the +// hvc1 entry (index 1) plus 'second_entry' (index 2). Chunk 1 maps to sample +// description index 1; chunk 2 maps to 'chunk2_sdi'. +std::vector build_two_entry_visual_sequence(const std::vector& second_entry, + uint32_t chunk2_sdi) +{ + std::vector ftyp_payload; + put_fourcc(ftyp_payload, "msf1"); + put32(ftyp_payload, 0); + put_fourcc(ftyp_payload, "msf1"); + put_fourcc(ftyp_payload, "iso8"); + std::vector ftyp = box("ftyp", ftyp_payload); + + // mvhd (version 0) + std::vector mvhd_payload; + put32(mvhd_payload, 0); // version 0, flags 0 + put32(mvhd_payload, 0); // creation_time + put32(mvhd_payload, 0); // modification_time + put32(mvhd_payload, 1000); // timescale + put32(mvhd_payload, 2000); // duration + put32(mvhd_payload, 0x00010000); + put16(mvhd_payload, 0x0100); + put16(mvhd_payload, 0); + put32(mvhd_payload, 0); + put32(mvhd_payload, 0); + for (int i = 0; i < 9; i++) put32(mvhd_payload, 0); + for (int i = 0; i < 6; i++) put32(mvhd_payload, 0); + put32(mvhd_payload, 2); // next_track_ID + std::vector mvhd = box("mvhd", mvhd_payload); + + // tkhd (version 0) + std::vector tkhd_payload; + put32(tkhd_payload, 0x00000007); + put32(tkhd_payload, 0); + put32(tkhd_payload, 0); + put32(tkhd_payload, 1); // track_ID + put32(tkhd_payload, 0); + put32(tkhd_payload, 2000); // duration + put32(tkhd_payload, 0); + put32(tkhd_payload, 0); + put16(tkhd_payload, 0); + put16(tkhd_payload, 0); + put16(tkhd_payload, 0); + put16(tkhd_payload, 0); + for (int i = 0; i < 9; i++) put32(tkhd_payload, 0); + put32(tkhd_payload, 0x00040000); // width 4.0 + put32(tkhd_payload, 0x00040000); // height 4.0 + std::vector tkhd = box("tkhd", tkhd_payload); + + // mdhd (version 0) + std::vector mdhd_payload; + put32(mdhd_payload, 0); + put32(mdhd_payload, 0); + put32(mdhd_payload, 0); + put32(mdhd_payload, 1000); // timescale + put32(mdhd_payload, 2000); // duration + put16(mdhd_payload, 0x55c4); // language 'und' + put16(mdhd_payload, 0); + std::vector mdhd = box("mdhd", mdhd_payload); + + // hdlr 'pict' + std::vector hdlr_payload; + put32(hdlr_payload, 0); + put32(hdlr_payload, 0); + put_fourcc(hdlr_payload, "pict"); + put32(hdlr_payload, 0); + put32(hdlr_payload, 0); + put32(hdlr_payload, 0); + hdlr_payload.push_back(0); + std::vector hdlr = box("hdlr", hdlr_payload); + + // vmhd + std::vector vmhd_payload; + put32(vmhd_payload, 0x00000001); + put16(vmhd_payload, 0); + for (int i = 0; i < 3; i++) put16(vmhd_payload, 0); + std::vector vmhd = box("vmhd", vmhd_payload); + + // stsd with two entries + std::vector stsd_payload; + put32(stsd_payload, 0); + put32(stsd_payload, 2); // entry_count + append(stsd_payload, make_hvc1_entry()); + append(stsd_payload, second_entry); + std::vector stsd = box("stsd", stsd_payload); + + // stts: two samples + std::vector stts_payload; + put32(stts_payload, 0); + put32(stts_payload, 1); + put32(stts_payload, 2); // sample_count + put32(stts_payload, 1000); // sample_delta + std::vector stts = box("stts", stts_payload); + + // stsc: chunk 1 -> sdi 1, chunk 2 -> chunk2_sdi + std::vector stsc_payload; + put32(stsc_payload, 0); + put32(stsc_payload, 2); + put32(stsc_payload, 1); put32(stsc_payload, 1); put32(stsc_payload, 1); + put32(stsc_payload, 2); put32(stsc_payload, 1); put32(stsc_payload, chunk2_sdi); + std::vector stsc = box("stsc", stsc_payload); + + // stsz: fixed size, two samples + std::vector stsz_payload; + put32(stsz_payload, 0); + put32(stsz_payload, 16); // fixed sample size + put32(stsz_payload, 2); // sample_count + std::vector stsz = box("stsz", stsz_payload); + + // Assemble, then patch the two chunk offsets to point into the mdat payload. + auto make_stco = [](uint32_t off0, uint32_t off1) { + std::vector p; + put32(p, 0); + put32(p, 2); + put32(p, off0); + put32(p, off1); + return box("stco", p); + }; + auto assemble = [&](const std::vector& stco) { + 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(make_stco(0, 0)); + uint32_t mdat_payload_offset = static_cast(ftyp.size() + moov0.size() + 8); + std::vector moov = assemble(make_stco(mdat_payload_offset, mdat_payload_offset + 16)); + + std::vector mdat = box("mdat", std::vector(32, 0xA5)); + + return concat({ftyp, moov, mdat}); +} + +heif_error read_sequence(const std::vector& file) +{ + 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); + heif_context_free(ctx); + return err; +} + +} // namespace + +TEST_CASE("sequence with non-visual sample entry in chunk is rejected") +{ + // Chunk 2 maps to the second 'stsd' entry, which is not a VisualSampleEntry. + // Before the fix this reached a decoder-less chunk and crashed (NULL deref on + // an NDEBUG build, assert on a debug build). It must now be a clean error. + SECTION("mp4a audio entry") + { + heif_error err = read_sequence(build_two_entry_visual_sequence(make_nonvisual_entry("mp4a"), 2)); + REQUIRE(err.code == heif_error_Invalid_input); + } + + // The defect is codec-independent: any non-visual second entry triggers it. + SECTION("unknown four-cc entry") + { + heif_error err = read_sequence(build_two_entry_visual_sequence(make_nonvisual_entry("zzzz"), 2)); + REQUIRE(err.code == heif_error_Invalid_input); + } +} + +TEST_CASE("sequence that maps every chunk to the visual entry still loads") +{ + // Same file, but chunk 2 maps to the visual 'hvc1' entry (index 1) instead of + // the non-visual one. This is the matched control: it isolates the non-visual + // entry as the cause. Both chunks get a decoder, so the track loads without + // error (no frame is decoded here, so no codec plugin is required). + heif_error err = read_sequence(build_two_entry_visual_sequence(make_nonvisual_entry("mp4a"), 1)); + REQUIRE(err.code == heif_error_Ok); +} diff -Nru libheif-1.23.4/tests/sequence_vfr_durations.cc libheif-1.23.6/tests/sequence_vfr_durations.cc --- libheif-1.23.4/tests/sequence_vfr_durations.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/sequence_vfr_durations.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,175 @@ +/* + libheif regression test for the per-frame durations of visual sequences. + + MIT License + + Copyright (c) 2026 libheif contributors + + 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. +*/ + +// Track_Visual::decode_next_image_sample() looked up the duration of a decoded +// frame with the index of the sample that was pushed into the decoder last. That +// index runs ahead of the output by the decoder latency, so every frame reported +// the duration of the next sample and the last frame wrapped around to sample 0: +// a sequence with the durations {100, 250, 400} decoded as {250, 400, 100}. +// +// The test encodes a variable frame rate sequence and checks the durations after +// decoding in the native colorspace and after a conversion to RGB, which creates +// a new image that has to carry the duration over. + +#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_gray_image(uint32_t duration, uint8_t value) +{ + heif_image* img = nullptr; + REQUIRE(heif_image_create(WIDTH, HEIGHT, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &img).code == heif_error_Ok); + REQUIRE(img != nullptr); + + REQUIRE(heif_image_add_plane(img, heif_channel_interleaved, WIDTH, HEIGHT, 8).code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + + for (uint32_t y = 0; y < HEIGHT; ++y) { + std::fill(p + y * stride, p + y * stride + WIDTH * 3, value); + } + + heif_image_set_duration(img, duration); + return img; +} + +std::vector encode_vfr_sequence(const std::vector& durations, heif_compression_format format) +{ + 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; + heif_error err = heif_context_add_visual_sequence_track(ctx, (uint16_t) WIDTH, (uint16_t) HEIGHT, + heif_track_type_image_sequence, + nullptr, nullptr, &track); + INFO("add_visual_sequence_track: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(track != nullptr); + + for (size_t i = 0; i < durations.size(); ++i) { + heif_image* img = make_gray_image(durations[i], static_cast((i + 1) * 60)); + + err = heif_track_encode_sequence_image(track, img, encoder, nullptr); + INFO("encode_sequence_image: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + heif_image_release(img); + } + + 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); + + heif_track_release(track); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return file; +} + +std::vector decode_durations(const std::vector& file, + heif_colorspace colorspace, heif_chroma chroma) +{ + 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_track* track = heif_context_get_track(ctx, 0); + REQUIRE(track != nullptr); + + std::vector durations; + for (;;) { + heif_image* img = nullptr; + heif_error err = heif_track_decode_next_image(track, &img, colorspace, chroma, nullptr); + if (err.code == heif_error_End_of_sequence) { + break; + } + INFO("decode_next_image: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + durations.push_back(heif_image_get_duration(img)); + heif_image_release(img); + } + + heif_track_release(track); + heif_context_free(ctx); + + return durations; +} + +} // namespace + + +TEST_CASE("variable frame rate sequence reports the per-frame durations") +{ + heif_compression_format format = heif_compression_undefined; + for (heif_compression_format candidate : {heif_compression_AV1, heif_compression_HEVC}) { + if (heif_have_encoder_for_format(candidate) && heif_have_decoder_for_format(candidate)) { + format = candidate; + break; + } + } + if (format == heif_compression_undefined) { + SKIP("No AV1 or HEVC encoder/decoder pair available, skipping test"); + } + + const std::vector expected_durations = {100, 250, 400}; + std::vector file = encode_vfr_sequence(expected_durations, format); + REQUIRE(!file.empty()); + + CHECK(decode_durations(file, heif_colorspace_undefined, heif_chroma_undefined) == expected_durations); + CHECK(decode_durations(file, heif_colorspace_RGB, heif_chroma_interleaved_RGB) == expected_durations); +} diff -Nru libheif-1.23.4/tests/tai.cc libheif-1.23.6/tests/tai.cc --- libheif-1.23.4/tests/tai.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/tai.cc 2026-10-05 16:16:00.000000000 +0000 @@ -38,8 +38,10 @@ std::string filename = get_tests_output_file_path("tai-1.heic"); - heif_image* img = createImage_RGB_planar(); + // Query the encoder first: get_encoder_or_skip_test() may abort the test + // via SKIP(), and anything allocated before it would then be leaked. heif_encoder* enc = get_encoder_or_skip_test(heif_compression_HEVC); + heif_image* img = createImage_RGB_planar(); heif_context* ctx = heif_context_alloc(); heif_image_handle* handle; @@ -137,4 +139,153 @@ REQUIRE(timestamp->timestamp_generation_failure == 0); REQUIRE(timestamp->timestamp_is_modified == 0); heif_tai_timestamp_packet_release(timestamp); -} \ No newline at end of file +} + +// Pick a codec for which we have both an encoder and a decoder so that the +// test can encode, read back, decode and re-encode. +static heif_compression_format pick_roundtrip_format_or_skip() +{ + for (heif_compression_format format : {heif_compression_AV1, + heif_compression_HEVC, + heif_compression_uncompressed}) { + if (heif_have_encoder_for_format(format) && heif_have_decoder_for_format(format)) { + return format; + } + } + + SKIP("No codec with both encoder and decoder available, skipping test"); + return heif_compression_undefined; +} + + +static void require_tai_timestamp(heif_tai_timestamp_packet* tai, uint64_t expected) +{ + REQUIRE(tai != nullptr); + REQUIRE(tai->tai_timestamp == expected); + REQUIRE(tai->synchronization_state == 1); + REQUIRE(tai->timestamp_generation_failure == 0); + REQUIRE(tai->timestamp_is_modified == 1); + heif_tai_timestamp_packet_release(tai); +} + + +// Regression test for GHSA-qwpf-5wf7-r996. +// +// Encoding an image that carries a TAI timestamp copied the ImageDescription +// of the source image into the ImageItem through a sliced temporary. The +// temporary shared the raw heif_tai_timestamp_packet pointer with the source +// image and freed it when it went out of scope, leaving both the item and +// the source image with a dangling pointer (use-after-free while generating +// the 'itai' property, double free at teardown). +TEST_CASE( "image-tai-encode-keeps-timestamp" ) +{ + heif_error err{}; + + err = heif_init(nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_compression_format format = pick_roundtrip_format_or_skip(); + + const uint64_t expected_timestamp = 0x0123456789ABCDEFULL; + + heif_image* img = createImage_RGB_planar(); + + heif_tai_timestamp_packet* tai = heif_tai_timestamp_packet_alloc(); + tai->tai_timestamp = expected_timestamp; + tai->synchronization_state = 1; + tai->timestamp_generation_failure = 0; + tai->timestamp_is_modified = 1; + err = heif_image_set_tai_timestamp(img, tai); + REQUIRE(err.code == heif_error_Ok); + heif_tai_timestamp_packet_release(tai); + + heif_context* ctx = heif_context_alloc(); + heif_encoder* enc = nullptr; + err = heif_context_get_encoder_for_format(ctx, format, &enc); + REQUIRE(err.code == heif_error_Ok); + + // --- encode: the timestamp is copied from the image into the item + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + + // the encoded item carries the timestamp as an 'itai' property + + heif_item_id itemId = heif_image_handle_get_item_id(handle); + tai = nullptr; + err = heif_item_get_property_tai_timestamp(ctx, itemId, &tai); + REQUIRE(err.code == heif_error_Ok); + require_tai_timestamp(tai, expected_timestamp); + + // the source image must still own its own, intact timestamp + + tai = nullptr; + err = heif_image_get_tai_timestamp(img, &tai); + REQUIRE(err.code == heif_error_Ok); + require_tai_timestamp(tai, expected_timestamp); + + // encoding the same image a second time must work as well + + heif_image_handle* handle2 = nullptr; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle2); + REQUIRE(err.code == heif_error_Ok); + heif_image_handle_release(handle2); + + std::string filename = get_tests_output_file_path("tai-transcode.heif"); + err = heif_context_write_to_file(ctx, filename.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_context_free(ctx); + heif_image_release(img); + + + // --- transcode: decode the file and encode the decoded image again + + 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); + + img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle_release(handle); + heif_context_free(ctx); + + // the decoded image has the timestamp attached + + tai = nullptr; + err = heif_image_get_tai_timestamp(img, &tai); + REQUIRE(err.code == heif_error_Ok); + require_tai_timestamp(tai, expected_timestamp); + + ctx = heif_context_alloc(); + err = heif_context_get_encoder_for_format(ctx, format, &enc); + REQUIRE(err.code == heif_error_Ok); + + handle = nullptr; + err = heif_context_encode_image(ctx, img, enc, nullptr, &handle); + REQUIRE(err.code == heif_error_Ok); + + itemId = heif_image_handle_get_item_id(handle); + tai = nullptr; + err = heif_item_get_property_tai_timestamp(ctx, itemId, &tai); + REQUIRE(err.code == heif_error_Ok); + require_tai_timestamp(tai, expected_timestamp); + + tai = nullptr; + err = heif_image_get_tai_timestamp(img, &tai); + REQUIRE(err.code == heif_error_Ok); + require_tai_timestamp(tai, expected_timestamp); + + heif_image_handle_release(handle); + heif_encoder_release(enc); + heif_context_free(ctx); + heif_image_release(img); +} diff -Nru libheif-1.23.4/tests/tiling_combined_images.cc libheif-1.23.6/tests/tiling_combined_images.cc --- libheif-1.23.4/tests/tiling_combined_images.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/tiling_combined_images.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,1071 @@ +/* + 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 decoding the tiles of an image that is combined with another image. +// +// The alpha channel of an image is a separate image item with a tiling of its own. +// When a tile is decoded, the same tile of the alpha image is decoded and attached. +// This only works when both images have the same tiling: +// +// - When the tilings differ, a tile of the alpha image covers another area than the +// tile of the color image. The alpha tile was scaled to the size of the color +// tile, which gave a wrong alpha channel. Such an image is now exposed as a +// single tile, which is the whole image. +// +// - The tile position was converted from the transformed (e.g. rotated) image to +// the coded image twice: by the color image and again by the alpha image. With a +// rotation by 180 degrees, the alpha channel came from the opposite tile. +// +// An 'iden' image is exposed as a single tile, but it passed the tile request on +// to the image it is derived from. When that image has tiles, only its first tile +// was decoded, which then was rejected for its size. +// +// The tiles of a 'grid' image can have alpha images of their own (libheif writes +// grids with alpha like this). Decoding a single tile of the grid skipped the +// alpha image of the tile, so that the tile had no alpha channel. +// +// The tests build files with 'unci' image items without generic compression in +// memory. Each sample of the color image and of the alpha image has a different +// value. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +struct ItemSpec { + uint32_t width = 4; + uint32_t height = 4; + uint32_t tile_columns = 2; + uint32_t tile_rows = 2; +}; + +struct FileSpec { + ItemSpec color; + ItemSpec alpha; + + // Both images are rotated by 180 degrees ('irot'). + bool rotate_180 = false; + + // Both images are cropped to the 2x2 pixels in the center ('clap'). + bool clap = false; + + // The primary image is an 'iden' image derived from the color image. + // The color image has no alpha image in this case. + bool primary_is_iden = false; +}; + +constexpr uint32_t kNumColorComponents = 3; + +uint8_t color_value(const FileSpec& spec, uint32_t component, uint32_t x, uint32_t y) { + return static_cast((component + 1) * 50 + y * spec.color.width + x); +} + +uint8_t alpha_value(const FileSpec& spec, uint32_t x, uint32_t y) { + return static_cast(200 + y * spec.alpha.width + x); +} + +// The item data of an 'unci' image with pixel interleave: the tiles in raster order. +template +std::vector make_item_data(const ItemSpec& item, uint32_t num_components, ValueFunction value) { + uint32_t tile_width = item.width / item.tile_columns; + uint32_t tile_height = item.height / item.tile_rows; + + std::vector data; + for (uint32_t tile_y = 0; tile_y < item.tile_rows; tile_y++) { + for (uint32_t tile_x = 0; tile_x < item.tile_columns; tile_x++) { + for (uint32_t y = 0; y < tile_height; y++) { + for (uint32_t x = 0; x < tile_width; x++) { + for (uint32_t c = 0; c < num_components; c++) { + data.push_back(value(c, tile_x * tile_width + x, tile_y * tile_height + y)); + } + } + } + } + } + + return data; +} + +std::vector make_ispe(const ItemSpec& item) { + std::vector payload; + put_u32_be(payload, item.width); + put_u32_be(payload, item.height); + return make_box("ispe", payload, /*full=*/true); +} + +std::vector make_cmpd(const std::vector& component_types) { + std::vector payload; + put_u32_be(payload, static_cast(component_types.size())); + for (uint16_t type : component_types) { + put_u16_be(payload, type); + } + return make_box("cmpd", payload); +} + +// uncC (v0): 8-bit components, pixel interleave +std::vector make_uncC(const ItemSpec& item, uint16_t num_components) { + std::vector payload; + put_u32_be(payload, 0); // profile + put_u32_be(payload, num_components); + for (uint16_t c = 0; c < num_components; c++) { + put_u16_be(payload, c); // component_index + payload.push_back(7); // component_bit_depth_minus_one -> 8 bit + payload.push_back(0); // component_format (unsigned) + payload.push_back(0); // component_align_size + } + payload.push_back(0); // sampling_type (no subsampling) + payload.push_back(1); // interleave_type (pixel) + payload.push_back(0); // block_size + payload.push_back(0); // flags + put_u32_be(payload, 0); // pixel_size + put_u32_be(payload, 0); // row_align_size + put_u32_be(payload, 0); // tile_align_size + put_u32_be(payload, item.tile_columns - 1); + put_u32_be(payload, item.tile_rows - 1); + return make_box("uncC", payload, /*full=*/true); +} + +std::vector make_infe(uint16_t item_id, const char* type, bool hidden) { + std::vector payload; + put_u16_be(payload, item_id); + put_u16_be(payload, 0); // item_protection_index + append_fourcc(payload, type); + append_cstr(payload, ""); + return make_box("infe", payload, /*full=*/true, /*version=*/2, /*flags=*/hidden ? 1 : 0); +} + +std::vector make_reference(const char* type, uint16_t from_item, uint16_t to_item) { + std::vector payload; + put_u16_be(payload, from_item); + put_u16_be(payload, 1); // reference_count + put_u16_be(payload, to_item); + return make_box(type, payload); +} + +constexpr uint16_t kColorItem = 1; +constexpr uint16_t kAlphaItem = 2; +constexpr uint16_t kIdenItem = 3; + +// Build a file with an 'unci' color image and an 'unci' alpha image. The item data +// is stored in 'idat'. +std::vector build_heif(const FileSpec& spec) { + std::vector color_data = make_item_data(spec.color, kNumColorComponents, + [&](uint32_t c, uint32_t x, uint32_t y) { + return color_value(spec, c, x, y); + }); + std::vector alpha_data = make_item_data(spec.alpha, 1, + [&](uint32_t, uint32_t x, uint32_t y) { + return alpha_value(spec, x, y); + }); + + const bool with_alpha = !spec.primary_is_iden; + + 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, spec.primary_is_iden ? kIdenItem : kColorItem); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // --- iinf + + std::vector iinf_payload; + put_u16_be(iinf_payload, 2); // entry_count + append(iinf_payload, make_infe(kColorItem, "unci", /*hidden=*/false)); + if (with_alpha) { + append(iinf_payload, make_infe(kAlphaItem, "unci", /*hidden=*/true)); + } + else { + append(iinf_payload, make_infe(kIdenItem, "iden", /*hidden=*/false)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // --- iref + + std::vector iref_payload; + if (with_alpha) { + append(iref_payload, make_reference("auxl", kAlphaItem, kColorItem)); + } + else { + append(iref_payload, make_reference("dimg", kIdenItem, kColorItem)); + } + auto iref = make_box("iref", iref_payload, /*full=*/true); + + // --- ipco. The comments give the (1-based) property indices. + + std::vector auxC_payload; + append_cstr(auxC_payload, "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"); + + std::vector ipco_payload; + append(ipco_payload, make_ispe(spec.color)); // 1 + append(ipco_payload, make_cmpd({heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue})); // 2 + append(ipco_payload, make_uncC(spec.color, kNumColorComponents)); // 3 + append(ipco_payload, make_ispe(spec.alpha)); // 4 + append(ipco_payload, make_cmpd({heif_cmpd_component_type_monochrome})); // 5 + append(ipco_payload, make_uncC(spec.alpha, 1)); // 6 + append(ipco_payload, make_box("auxC", auxC_payload, /*full=*/true)); // 7 + + // The transformations are the same for the color image and the alpha image. + std::vector transformation_properties; + + uint8_t num_properties = 7; + + if (spec.rotate_180) { + append(ipco_payload, make_box("irot", {2})); + transformation_properties.push_back(++num_properties); + } + + if (spec.clap) { + std::vector clap_payload; + put_u32_be(clap_payload, 2); // cleanApertureWidthN + put_u32_be(clap_payload, 1); // cleanApertureWidthD + put_u32_be(clap_payload, 2); // cleanApertureHeightN + put_u32_be(clap_payload, 1); // cleanApertureHeightD + put_u32_be(clap_payload, 0); // horizOffN + put_u32_be(clap_payload, 1); // horizOffD + put_u32_be(clap_payload, 0); // vertOffN + put_u32_be(clap_payload, 1); // vertOffD + append(ipco_payload, make_box("clap", clap_payload)); + transformation_properties.push_back(++num_properties); + } + + auto ipco = make_box("ipco", ipco_payload); + + // --- ipma + + auto append_associations = [&](std::vector& out, uint16_t item_id, + std::vector properties, bool with_transformations) { + if (with_transformations) { + append(properties, transformation_properties); + } + + put_u16_be(out, item_id); + out.push_back(static_cast(properties.size())); + for (uint8_t property : properties) { + bool essential = (property != 7); // all but auxC + out.push_back(static_cast((essential ? 0x80 : 0) | property)); + } + }; + + std::vector ipma_payload; + put_u32_be(ipma_payload, 2); // entry_count + append_associations(ipma_payload, kColorItem, {1, 2, 3}, true); + if (with_alpha) { + append_associations(ipma_payload, kAlphaItem, {4, 5, 6, 7}, true); + } + else { + append_associations(ipma_payload, kIdenItem, {1}, false); + } + 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 and iloc (version 1): the items are stored in idat (construction_method=1). + + std::vector idat_payload = color_data; + if (with_alpha) { + append(idat_payload, alpha_data); + } + auto idat = make_box("idat", idat_payload); + + auto append_item_location = [](std::vector& out, uint16_t item_id, size_t offset, size_t length) { + put_u16_be(out, item_id); + put_u16_be(out, 0x0001); // construction_method=1 (idat) + put_u16_be(out, 0); // data_reference_index + put_u16_be(out, 1); // extent_count + put_u32_be(out, static_cast(offset)); + put_u32_be(out, static_cast(length)); + }; + + 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, with_alpha ? 2 : 1); // item_count + append_item_location(iloc_payload, kColorItem, 0, color_data.size()); + if (with_alpha) { + append_item_location(iloc_payload, kAlphaItem, color_data.size(), alpha_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, iref); + 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; +} + + +// Build a file with a 2x2 'grid' image. Each tile is an 'unci' color image of 2x2 pixels +// with an 'unci' alpha image of its own. The image content is the same as for the +// default FileSpec. +std::vector build_heif_grid_with_tile_alpha() { + const FileSpec spec; + + ItemSpec tile; + tile.width = 2; + tile.height = 2; + tile.tile_columns = 1; + tile.tile_rows = 1; + + constexpr uint16_t kGridItem = 1; + constexpr uint16_t kFirstColorTile = 2; + constexpr uint16_t kFirstAlphaTile = 6; + constexpr uint16_t kNumTiles = 4; + + // --- item data: the grid, the color tiles, the alpha tiles + + std::vector> item_data; + + std::vector grid_data; + grid_data.push_back(0); // version + grid_data.push_back(0); // flags + grid_data.push_back(1); // rows_minus_one + grid_data.push_back(1); // columns_minus_one + put_u16_be(grid_data, static_cast(spec.color.width)); // output_width + put_u16_be(grid_data, static_cast(spec.color.height)); // output_height + item_data.push_back(grid_data); + + for (uint32_t t = 0; t < kNumTiles; t++) { + item_data.push_back(make_item_data(tile, kNumColorComponents, + [&](uint32_t c, uint32_t x, uint32_t y) { + return color_value(spec, c, (t % 2) * 2 + x, (t / 2) * 2 + y); + })); + } + + for (uint32_t t = 0; t < kNumTiles; t++) { + item_data.push_back(make_item_data(tile, 1, + [&](uint32_t, uint32_t x, uint32_t y) { + return alpha_value(spec, (t % 2) * 2 + x, (t / 2) * 2 + y); + })); + } + + const auto num_items = static_cast(item_data.size()); + + 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, kGridItem); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + // --- iinf + + std::vector iinf_payload; + put_u16_be(iinf_payload, num_items); + append(iinf_payload, make_infe(kGridItem, "grid", /*hidden=*/false)); + for (uint16_t t = 0; t < 2 * kNumTiles; t++) { + append(iinf_payload, make_infe(static_cast(kFirstColorTile + t), "unci", /*hidden=*/true)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + // --- iref + + std::vector dimg_payload; + put_u16_be(dimg_payload, kGridItem); + put_u16_be(dimg_payload, kNumTiles); + for (uint16_t t = 0; t < kNumTiles; t++) { + put_u16_be(dimg_payload, static_cast(kFirstColorTile + t)); + } + + std::vector iref_payload; + append(iref_payload, make_box("dimg", dimg_payload)); + for (uint16_t t = 0; t < kNumTiles; t++) { + append(iref_payload, make_reference("auxl", + static_cast(kFirstAlphaTile + t), + static_cast(kFirstColorTile + t))); + } + auto iref = make_box("iref", iref_payload, /*full=*/true); + + // --- ipco. The comments give the (1-based) property indices. + + std::vector auxC_payload; + append_cstr(auxC_payload, "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"); + + std::vector ipco_payload; + append(ipco_payload, make_ispe(spec.color)); // 1 (grid) + append(ipco_payload, make_ispe(tile)); // 2 (tiles) + append(ipco_payload, make_cmpd({heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue})); // 3 + append(ipco_payload, make_uncC(tile, kNumColorComponents)); // 4 + append(ipco_payload, make_cmpd({heif_cmpd_component_type_monochrome})); // 5 + append(ipco_payload, make_uncC(tile, 1)); // 6 + append(ipco_payload, make_box("auxC", auxC_payload, /*full=*/true)); // 7 + auto ipco = make_box("ipco", ipco_payload); + + // --- ipma + + auto append_associations = [](std::vector& out, uint16_t item_id, const std::vector& properties) { + put_u16_be(out, item_id); + out.push_back(static_cast(properties.size())); + for (uint8_t property : properties) { + bool essential = (property != 7); // all but auxC + out.push_back(static_cast((essential ? 0x80 : 0) | property)); + } + }; + + std::vector ipma_payload; + put_u32_be(ipma_payload, num_items); + append_associations(ipma_payload, kGridItem, {1}); + for (uint16_t t = 0; t < kNumTiles; t++) { + append_associations(ipma_payload, static_cast(kFirstColorTile + t), {2, 3, 4}); + } + for (uint16_t t = 0; t < kNumTiles; t++) { + append_associations(ipma_payload, static_cast(kFirstAlphaTile + t), {2, 5, 6, 7}); + } + 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 and iloc (version 1): the items are stored in idat (construction_method=1). + + std::vector idat_payload; + + 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, num_items); + for (uint16_t i = 0; i < num_items; i++) { + put_u16_be(iloc_payload, static_cast(kGridItem + i)); // 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, static_cast(idat_payload.size())); + put_u32_be(iloc_payload, static_cast(item_data[i].size())); + + append(idat_payload, item_data[i]); + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + 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, iref); + 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; +} + + +// Build a file with an 'iovl' image of 6x4 pixels that overlays two 'unci' color images +// of 4x4 pixels with 2x2 tiles each. Both have the content of the default FileSpec. +// The second image is placed two pixels to the right of the first one. +std::vector build_heif_overlay(bool rotate_90) { + const FileSpec spec; + + ItemSpec canvas; + canvas.width = 6; + canvas.height = 4; + + constexpr uint16_t kOverlayItem = 1; + constexpr uint16_t kFirstLayer = 2; + constexpr uint16_t kNumLayers = 2; + + // --- item data: the overlay, the two layers + + std::vector> item_data; + + std::vector overlay_data; + overlay_data.push_back(0); // version + overlay_data.push_back(0); // flags: 16 bit fields + for (int i = 0; i < 4; i++) { + put_u16_be(overlay_data, 0); // canvas_fill_value + } + put_u16_be(overlay_data, static_cast(canvas.width)); // output_width + put_u16_be(overlay_data, static_cast(canvas.height)); // output_height + put_u16_be(overlay_data, 0); // first layer: horizontal_offset + put_u16_be(overlay_data, 0); // vertical_offset + put_u16_be(overlay_data, 2); // second layer: horizontal_offset + put_u16_be(overlay_data, 0); // vertical_offset + item_data.push_back(overlay_data); + + for (uint32_t i = 0; i < kNumLayers; i++) { + item_data.push_back(make_item_data(spec.color, kNumColorComponents, + [&](uint32_t c, uint32_t x, uint32_t y) { + return color_value(spec, c, x, y); + })); + } + + const auto num_items = static_cast(item_data.size()); + + 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, kOverlayItem); + auto pitm = make_box("pitm", pitm_payload, /*full=*/true); + + std::vector iinf_payload; + put_u16_be(iinf_payload, num_items); + append(iinf_payload, make_infe(kOverlayItem, "iovl", /*hidden=*/false)); + for (uint16_t i = 0; i < kNumLayers; i++) { + append(iinf_payload, make_infe(static_cast(kFirstLayer + i), "unci", /*hidden=*/true)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + std::vector dimg_payload; + put_u16_be(dimg_payload, kOverlayItem); + put_u16_be(dimg_payload, kNumLayers); + for (uint16_t i = 0; i < kNumLayers; i++) { + put_u16_be(dimg_payload, static_cast(kFirstLayer + i)); + } + + std::vector iref_payload; + append(iref_payload, make_box("dimg", dimg_payload)); + auto iref = make_box("iref", iref_payload, /*full=*/true); + + // --- ipco. The comments give the (1-based) property indices. + + std::vector ipco_payload; + append(ipco_payload, make_ispe(canvas)); // 1 (overlay) + append(ipco_payload, make_ispe(spec.color)); // 2 (layers) + append(ipco_payload, make_cmpd({heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue})); // 3 + append(ipco_payload, make_uncC(spec.color, kNumColorComponents)); // 4 + append(ipco_payload, make_box("irot", {1})); // 5 + auto ipco = make_box("ipco", ipco_payload); + + // --- ipma + + auto append_associations = [](std::vector& out, uint16_t item_id, const std::vector& properties) { + put_u16_be(out, item_id); + out.push_back(static_cast(properties.size())); + for (uint8_t property : properties) { + out.push_back(static_cast(0x80 | property)); // essential + } + }; + + std::vector ipma_payload; + put_u32_be(ipma_payload, num_items); + if (rotate_90) { + append_associations(ipma_payload, kOverlayItem, {1, 5}); + } + else { + append_associations(ipma_payload, kOverlayItem, {1}); + } + for (uint16_t i = 0; i < kNumLayers; i++) { + append_associations(ipma_payload, static_cast(kFirstLayer + i), {2, 3, 4}); + } + 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 and iloc (version 1): the items are stored in idat (construction_method=1). + + std::vector idat_payload; + + 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, num_items); + for (uint16_t i = 0; i < num_items; i++) { + put_u16_be(iloc_payload, static_cast(kOverlayItem + i)); // 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, static_cast(idat_payload.size())); + put_u32_be(iloc_payload, static_cast(item_data[i].size())); + + append(idat_payload, item_data[i]); + } + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + 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, iref); + 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; +} + + +// Opens the primary image of a file. +class OpenedImage { +public: + explicit OpenedImage(const FileSpec& spec) : OpenedImage(build_heif(spec)) {} + + explicit OpenedImage(std::vector file) : m_file(std::move(file)) { + m_ctx = heif_context_alloc(); + REQUIRE(m_ctx != nullptr); + + heif_error err = heif_context_read_from_memory_without_copy(m_ctx, m_file.data(), m_file.size(), nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_get_primary_image_handle(m_ctx, &m_handle); + INFO("primary image error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(m_handle != nullptr); + } + + ~OpenedImage() { + heif_image_handle_release(m_handle); + heif_context_free(m_ctx); + } + + OpenedImage(const OpenedImage&) = delete; + OpenedImage& operator=(const OpenedImage&) = delete; + + // The tiling of the coded image, without the image transformations. + heif_image_tiling tiling() const { + heif_image_tiling tiling{}; + heif_error err = heif_image_handle_get_image_tiling(m_handle, 0, &tiling); + REQUIRE(err.code == heif_error_Ok); + return tiling; + } + + // The error message is owned by the context, so it is only valid as long as this object lives. + heif_error decode_image(heif_image** out_img) { + return heif_decode_image(m_handle, out_img, heif_colorspace_RGB, heif_chroma_444, nullptr); + } + + heif_error decode_tile(heif_image** out_img, uint32_t tile_x, uint32_t tile_y, bool ignore_transformations = false) { + heif_decoding_options* options = heif_decoding_options_alloc(); + options->ignore_transformations = ignore_transformations; + + heif_error err = heif_image_handle_decode_image_tile(m_handle, out_img, heif_colorspace_RGB, heif_chroma_444, + options, tile_x, tile_y); + heif_decoding_options_free(options); + return err; + } + +private: + std::vector m_file; + heif_context* m_ctx = nullptr; + heif_image_handle* m_handle = nullptr; +}; + + +// Check that the image shows the area of the test image that starts at (x0;y0). +// The position refers to the image after the rotation, but before the cropping. +void check_pixels(const heif_image* img, const FileSpec& spec, bool rotated, bool with_alpha, + uint32_t x0, uint32_t y0, uint32_t width, uint32_t height) { + const heif_channel channels[kNumColorComponents + 1] = {heif_channel_R, heif_channel_G, heif_channel_B, + heif_channel_Alpha}; + + REQUIRE((heif_image_has_channel(img, heif_channel_Alpha) != 0) == with_alpha); + + for (uint32_t c = 0; c < kNumColorComponents + (with_alpha ? 1 : 0); c++) { + REQUIRE(heif_image_get_width(img, channels[c]) == static_cast(width)); + REQUIRE(heif_image_get_height(img, channels[c]) == static_cast(height)); + + int stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly(img, channels[c], &stride); + REQUIRE(plane != nullptr); + + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + // the position in the coded color image + uint32_t coded_x = x0 + x; + uint32_t coded_y = y0 + y; + if (rotated) { + coded_x = spec.color.width - 1 - coded_x; + coded_y = spec.color.height - 1 - coded_y; + } + + int expected; + if (c < kNumColorComponents) { + expected = color_value(spec, c, coded_x, coded_y); + } + else { + // The alpha image may have a lower resolution. + expected = alpha_value(spec, + coded_x * spec.alpha.width / spec.color.width, + coded_y * spec.alpha.height / spec.color.height); + } + + INFO("channel " << c << ", pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(plane[y * stride + x]) == expected); + } + } + } +} + +} // namespace + + +TEST_CASE("tiles of an image with an alpha image in the same tiling") { + for (bool rotate : {false, true}) { + for (bool half_resolution_alpha : {false, true}) { + INFO("rotate by 180 degrees: " << rotate); + INFO("alpha with half resolution: " << half_resolution_alpha); + + FileSpec spec; + spec.rotate_180 = rotate; + + if (half_resolution_alpha) { + // The same number of tiles, which cover the same area as the tiles of the color image. + spec.alpha.width = 2; + spec.alpha.height = 2; + } + + OpenedImage image(spec); + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 2); + REQUIRE(tiling.num_rows == 2); + REQUIRE(tiling.tile_width == 2); + REQUIRE(tiling.tile_height == 2); + + for (uint32_t ty = 0; ty < 2; ty++) { + for (uint32_t tx = 0; tx < 2; tx++) { + INFO("tile (" << tx << "," << ty << ")"); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, tx, ty); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, rotate, true, tx * 2, ty * 2, 2, 2); + heif_image_release(img); + } + } + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, rotate, true, 0, 0, 4, 4); + heif_image_release(img); + } + } +} + + +TEST_CASE("image with an alpha image in a different tiling is a single tile") { + for (bool tiled_color : {false, true}) { + for (bool rotate : {false, true}) { + INFO((tiled_color ? "color image with tiles, alpha image without" : "alpha image with tiles, color image without")); + INFO("rotate by 180 degrees: " << rotate); + + FileSpec spec; + spec.rotate_180 = rotate; + + ItemSpec& untiled = (tiled_color ? spec.alpha : spec.color); + untiled.tile_columns = 1; + untiled.tile_rows = 1; + + OpenedImage image(spec); + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == 4); + REQUIRE(tiling.tile_height == 4); + REQUIRE(tiling.image_width == 4); + REQUIRE(tiling.image_height == 4); + + // --- The single tile is the whole image. + + for (bool ignore_transformations : {false, true}) { + INFO("ignore_transformations: " << ignore_transformations); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, 0, 0, ignore_transformations); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, rotate && !ignore_transformations, true, 0, 0, 4, 4); + heif_image_release(img); + + // --- The tiles of the color image are not accessible. + + img = nullptr; + err = image.decode_tile(&img, 1, 0, ignore_transformations); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + + err = image.decode_tile(&img, 0, 1, ignore_transformations); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + } + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, rotate, true, 0, 0, 4, 4); + heif_image_release(img); + } + } +} + + +TEST_CASE("single tile of an image with an alpha image in a different tiling is not cropped") { + // A tile is not cropped by 'clap'. The crop is described by the offsets in heif_image_tiling. + // This also has to hold for the alpha image, which is decoded as a whole. + FileSpec spec; + spec.clap = true; + spec.alpha.tile_columns = 1; + spec.alpha.tile_rows = 1; + + OpenedImage image(spec); + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == 4); + REQUIRE(tiling.tile_height == 4); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, 0, 0); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, true, 0, 0, 4, 4); + heif_image_release(img); + + // The whole image is cropped to the 2x2 pixels in the center. + img = nullptr; + err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, true, 1, 1, 2, 2); + heif_image_release(img); +} + + +TEST_CASE("single tile of an 'iden' image derived from an image with tiles") { + FileSpec spec; + spec.primary_is_iden = true; + + OpenedImage image(spec); + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == 4); + REQUIRE(tiling.tile_height == 4); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, 0, 0); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, false, 0, 0, 4, 4); + heif_image_release(img); + + img = nullptr; + err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, false, 0, 0, 4, 4); + heif_image_release(img); +} + + +TEST_CASE("tiles of a grid image with an alpha image for each tile") { + const FileSpec spec; // only describes the image content + + OpenedImage image(build_heif_grid_with_tile_alpha()); + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 2); + REQUIRE(tiling.num_rows == 2); + REQUIRE(tiling.tile_width == 2); + REQUIRE(tiling.tile_height == 2); + + for (uint32_t ty = 0; ty < 2; ty++) { + for (uint32_t tx = 0; tx < 2; tx++) { + INFO("tile (" << tx << "," << ty << ")"); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, tx, ty); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, true, tx * 2, ty * 2, 2, 2); + heif_image_release(img); + } + } + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, spec, false, true, 0, 0, 4, 4); + heif_image_release(img); +} + + +TEST_CASE("overlay of images with tiles is a single tile") { + const FileSpec spec; // only describes the content of the overlaid images + + for (bool rotate : {false, true}) { + INFO("rotate by 90 degrees: " << rotate); + + OpenedImage image(build_heif_overlay(rotate)); + + // The overlaid images have 2x2 tiles each. They are placed at different positions, + // so their tiles do not form a tiling of the overlay image. + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == 6); + REQUIRE(tiling.tile_height == 4); + + heif_image* full = nullptr; + heif_error err = image.decode_image(&full); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(full != nullptr); + + const int width = heif_image_get_width(full, heif_channel_R); + const int height = heif_image_get_height(full, heif_channel_R); + REQUIRE(width == (rotate ? 4 : 6)); + REQUIRE(height == (rotate ? 6 : 4)); + + heif_image* tile = nullptr; + err = image.decode_tile(&tile, 0, 0); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(tile != nullptr); + + // --- The single tile is the whole image. + + const heif_channel channels[kNumColorComponents] = {heif_channel_R, heif_channel_G, heif_channel_B}; + + for (uint32_t c = 0; c < kNumColorComponents; c++) { + REQUIRE(heif_image_get_width(tile, channels[c]) == width); + REQUIRE(heif_image_get_height(tile, channels[c]) == height); + + int full_stride = 0; + int tile_stride = 0; + const uint8_t* full_plane = heif_image_get_plane_readonly(full, channels[c], &full_stride); + const uint8_t* tile_plane = heif_image_get_plane_readonly(tile, channels[c], &tile_stride); + REQUIRE(full_plane != nullptr); + REQUIRE(tile_plane != nullptr); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + INFO("channel " << c << ", pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(tile_plane[y * tile_stride + x]) == static_cast(full_plane[y * full_stride + x])); + + if (!rotate) { + // The second image is placed two pixels to the right and covers the first one. + int expected = (x >= 2) ? color_value(spec, c, x - 2, y) : color_value(spec, c, x, y); + REQUIRE(static_cast(full_plane[y * full_stride + x]) == expected); + } + } + } + } + + heif_image_release(tile); + heif_image_release(full); + + for (bool ignore_transformations : {false, true}) { + heif_image* img = nullptr; + err = image.decode_tile(&img, 1, 0, ignore_transformations); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + } + } +} diff -Nru libheif-1.23.4/tests/track_reference_duplicates.cc libheif-1.23.6/tests/track_reference_duplicates.cc --- libheif-1.23.4/tests/track_reference_duplicates.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/track_reference_duplicates.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,140 @@ +/* + 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. +*/ + +// A track reference type box may list the same track ID more than once. +// ISO/IEC 14496-12 does not forbid it, no consumer in libheif depends on the +// entries being unique, and a redundant entry is no reason to reject a file. +// Box_tref used to refuse such a box on both the read and the write path +// ("'tref' has double references"), so calling heif_track_add_reference_to_track() +// twice with the same target made the whole file unwritable, and a file written +// by another muxer with a repeated ID could not be opened. +// +// The test uses URI metadata tracks with raw samples, so it needs no codec. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" + +#include +#include + +namespace { + +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}; +} + +// Add a metadata track with one raw sample so that the track is complete. +heif_track* add_metadata_track(heif_context* ctx, const char* uri) +{ + heif_track* track = nullptr; + heif_error err = heif_context_add_uri_metadata_sequence_track(ctx, uri, nullptr, &track); + INFO("add_uri_metadata_sequence_track: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(track != nullptr); + + const uint8_t payload[] = {1, 2, 3, 4}; + heif_raw_sequence_sample* sample = heif_raw_sequence_sample_alloc(); + REQUIRE(sample != nullptr); + REQUIRE(heif_raw_sequence_sample_set_data(sample, payload, sizeof(payload)).code == heif_error_Ok); + heif_raw_sequence_sample_set_duration(sample, 1); + REQUIRE(heif_track_add_raw_sequence_sample(track, sample).code == heif_error_Ok); + heif_raw_sequence_sample_release(sample); + + return track; +} + +} // namespace + + +TEST_CASE("a track reference type box may list the same track twice") +{ + const uint32_t ref_type = heif_track_reference_type_description; + + // --- write a file in which track B references track A twice + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_track* track_a = add_metadata_track(ctx, "urn:test:a"); + heif_track* track_b = add_metadata_track(ctx, "urn:test:b"); + const uint32_t id_a = heif_track_get_id(track_a); + const uint32_t id_b = heif_track_get_id(track_b); + REQUIRE(id_a != id_b); + + heif_track_add_reference_to_track(track_b, ref_type, track_a); + heif_track_add_reference_to_track(track_b, ref_type, track_a); + + std::vector file; + heif_writer writer{}; + writer.writer_api_version = 1; + writer.write = mem_writer; + heif_error err = heif_context_write(ctx, &writer, &file); + INFO("write: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); // used to fail with "'tref' has double references" + + heif_track_release(track_a); + heif_track_release(track_b); + heif_context_free(ctx); + + // --- read it back + + ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr); + INFO("read: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); // used to fail while parsing the 'tref' box + REQUIRE(heif_context_has_sequence(ctx) == 1); + + track_a = heif_context_get_track(ctx, id_a); + track_b = heif_context_get_track(ctx, id_b); + REQUIRE(track_a != nullptr); + REQUIRE(track_b != nullptr); + + // Both entries are reported as written. + REQUIRE(heif_track_get_number_of_track_reference_types(track_b) == 1); + REQUIRE(heif_track_get_number_of_track_reference_of_type(track_b, ref_type) == 2); + + uint32_t targets[2] = {0, 0}; + REQUIRE(heif_track_get_references_from_track(track_b, ref_type, targets) == 2); + REQUIRE(targets[0] == id_a); + REQUIRE(targets[1] == id_a); + + // The reverse lookup lists the referring track once, not once per entry. + uint32_t referring[4] = {0, 0, 0, 0}; + REQUIRE(heif_track_find_referring_tracks(track_a, ref_type, referring, 4) == 1); + REQUIRE(referring[0] == id_b); + + REQUIRE(heif_track_get_number_of_track_reference_of_type(track_a, ref_type) == 0); + + heif_track_release(track_a); + heif_track_release(track_b); + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/tests/uncompressed_alpha_composite_depth_mismatch.cc libheif-1.23.6/tests/uncompressed_alpha_composite_depth_mismatch.cc --- libheif-1.23.4/tests/uncompressed_alpha_composite_depth_mismatch.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_alpha_composite_depth_mismatch.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,300 @@ +/* + 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-r7gr-2xm2-23wf, modeled on the reporter's PoC +// (a planar RGBA 'unci' image with R=16 bit, G=16 bit, B=8 bit, Alpha=16 bit). +// +// Op_flatten_alpha_plane composites the alpha plane onto the colour planes when +// the output cannot carry alpha. It reads every colour plane, and the alpha +// plane, through a single 'Pixel' type selected from one channel's bit depth, +// which fixes one byte stride for all of them. A file may declare per-component +// bit depths (e.g. 'unci'): with a 16-bit-wide Pixel the operator halves the +// 8-bit blue plane's real stride and reads two bytes per sample, walking off the +// end of the plane on every row -> a heap out-of-bounds read whose bytes are +// folded into the composited RGB output (information disclosure). +// +// The fix is made in the pipeline planner, not in the operator: ColorState +// carries one bit depth per plane, and Op_flatten_alpha_plane::state_after_conversion +// declines any input whose planes mix SDR (one byte per sample) and HDR (two +// bytes per sample). The RGB operators that could otherwise take over with a +// uniform declared output (and fail at runtime) decline a mismatch the same way, +// so no conversion pipeline exists and the decode is refused before any operator +// runs. This test builds such a file, confirms it still decodes natively to the +// mismatched-depth planar image (so the decoder path itself is unaffected), +// requires a decode with alpha compositing to a 16-bit interleaved RGB target to +// be refused cleanly as an unsupported conversion, and checks that the 8-bit +// interleaved target still works (Op_to_sdr_planes equalizes the planes first). +// Under the unfixed code the 16-bit decode reproduces the reporter's ASAN +// heap-buffer-overflow READ in Op_flatten_alpha_plane::convert_colorspace. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// Use a reasonably large image. The over-read is roughly one blue-plane row per +// row, so the image must be wide/tall enough that it exceeds the plane's +// allocated (stride-padded) size and reaches an AddressSanitizer redzone. Small +// images hide the bug because the stride padding absorbs the doubled read; +// HeifPixelImage also floors every plane allocation at 64 per dimension. +constexpr uint32_t WIDTH = 64; +constexpr uint32_t HEIGHT = 64; + +// Build a minimal HEIF file with a single 'unci' item: component (planar) +// interleave, 4:4:4, R=16 bit, G=16 bit, B=8 bit, Alpha=16 bit, uncompressed. +std::vector build_heif_unci_rgba_mismatched_blue_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: R, G, B, Alpha (component types 4, 5, 6, 7). + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 4); + put_u16_be(cmpd_payload, 4); // red + put_u16_be(cmpd_payload, 5); // green + put_u16_be(cmpd_payload, 6); // blue + put_u16_be(cmpd_payload, 7); // alpha + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): component (planar) interleave, 4:4:4, R=16, G=16, B=8, Alpha=16. + const uint8_t depths[4] = {16, 16, 8, 16}; // R, G, B, Alpha + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 4); // component_count + for (uint16_t idx = 0; idx < 4; 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(0); // sampling_type = 4:4:4 + uncC_payload.push_back(0); // interleave_type = component (planar) + 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 (planar): R plane (2 bytes/sample, big-endian), then G plane + // (2 bytes/sample), then B plane (1 byte/sample), then Alpha plane (2 + // bytes/sample). Alpha is near-maximum so the composite weights the colour + // sample heavily (this is what surfaces over-read bytes in the output). + std::vector tile_data; + tile_data.reserve(WIDTH * HEIGHT * (2 + 2 + 1 + 2)); + + for (uint32_t y = 0; y < HEIGHT; y++) // R (16 bit) + 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 < HEIGHT; y++) // G (16 bit) + for (uint32_t x = 0; x < WIDTH; x++) + put_u16_be(tile_data, static_cast(0x2000 + x + WIDTH * y)); + + for (uint32_t y = 0; y < HEIGHT; y++) // B (8 bit) + for (uint32_t x = 0; x < WIDTH; x++) + tile_data.push_back(static_cast(0x40 + x + WIDTH * y)); + + for (uint32_t y = 0; y < HEIGHT; y++) // Alpha (16 bit) + for (uint32_t x = 0; x < WIDTH; x++) + put_u16_be(tile_data, 0xFF00); + + 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 RGBA with mismatched colour bit depths refuses alpha compositing without heap overread") { + std::vector file = build_heif_unci_rgba_mismatched_blue_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); + + REQUIRE(heif_image_handle_get_width(handle) == static_cast(WIDTH)); + REQUIRE(heif_image_handle_get_height(handle) == static_cast(HEIGHT)); + + // The mismatched-depth planes decode natively without any color conversion, + // so the decoder path itself is unaffected. + { + 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); + if (img != nullptr) { + heif_image_release(img); + } + } + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + + heif_color_conversion_options_ext* ext = heif_color_conversion_options_ext_alloc(); + REQUIRE(ext != nullptr); + ext->alpha_composition_mode = heif_alpha_composition_mode_solid_color; + ext->background_red = 0xFFFF; + ext->background_green = 0xFFFF; + ext->background_blue = 0xFFFF; + options->color_conversion_options_ext = ext; + + // Request alpha compositing onto a solid background while decoding to a + // 16-bit interleaved RGB target. This used to select Op_flatten_alpha_plane + // with a 16-bit Pixel, which is where the over-read of the 8-bit blue plane + // occurred. With the fix no operator accepts the mixed SDR/HDR planes, so no + // conversion pipeline can be built and the decode must fail cleanly as an + // unsupported conversion rather than reading past the blue plane. Under the + // unfixed code this decode reproduces the reporter's ASAN heap-buffer-overflow + // READ in Op_flatten_alpha_plane::convert_colorspace. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE, options); + INFO("composite decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Unsupported_feature); + REQUIRE(err.subcode == heif_suberror_Unsupported_color_conversion); + REQUIRE(img == nullptr); + + if (img != nullptr) { + heif_image_release(img); + } + } + + // The same request for an 8-bit interleaved target must still work: the + // pipeline lowers every plane to 8 bits first (Op_to_sdr_planes handles each + // plane at its own depth), after which the planes are uniform and compositing + // is safe. The fix must not reject this. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, options); + INFO("8-bit composite decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(heif_image_get_chroma_format(img) == heif_chroma_interleaved_RGB); + REQUIRE(heif_image_has_channel(img, heif_channel_Alpha) == 0); + REQUIRE(heif_image_get_bits_per_pixel_range(img, heif_channel_interleaved) == 8); + heif_image_release(img); + } + + heif_color_conversion_options_ext_free(ext); + heif_decoding_options_free(options); + + heif_image_handle_release(handle); + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/tests/uncompressed_bayer_depth_conversion.cc libheif-1.23.6/tests/uncompressed_bayer_depth_conversion.cc --- libheif-1.23.4/tests/uncompressed_bayer_depth_conversion.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_bayer_depth_conversion.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,196 @@ +/* + 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: Op_to_hdr_planes and Op_to_sdr_planes accepted Bayer images at plan +// time (colorspace filter_array uses chroma planar, which has the same value as +// heif_chroma_monochrome) but their runtime only handles the Y/Cb/Cr/R/G/B/alpha planes, +// so they produced an image without any plane. The planner preferred that two-step route +// over demosaicing first: decoding an 8-bit Bayer 'unci' to 16-bit interleaved RGB and a +// 12-bit one to 8-bit interleaved RGB failed with "Invalid bit depth" from the Bayer +// operator, and a native decode of a 12-bit Bayer image with convert_hdr_to_8bit returned +// success with an image that had no planes at all. Both operators now decline the +// filter-array colorspace, so the Bayer operator runs first and the depth change is +// applied to its RGB output. +// +// The files are built through the public API and the uncompressed encoder. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_properties.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t W = 64; +constexpr uint32_t H = 64; + +std::string write_bayer_unci(int depth, const char* filename) +{ + 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, depth, &fa_id); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* fa = heif_image_get_component(image, fa_id, &stride); + REQUIRE(fa != nullptr); + for (uint32_t y = 0; y < H; y++) { + for (uint32_t x = 0; x < W; x++) { + uint32_t v = (x * 31 + y * 17) & ((1u << depth) - 1); + if (depth <= 8) { + fa[y * stride + x] = static_cast(v); + } + else { + reinterpret_cast(fa + y * stride)[x] = static_cast(v); + } + } + } + + 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); + + 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, 2, 2, pattern); + REQUIRE(err.code == heif_error_Ok); + + 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 path = get_tests_output_file_path(filename); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); + return path; +} + +// Decodes to an interleaved RGB target and checks that a plane of the expected depth exists. +void decode_to_interleaved(const std::string& path, heif_chroma chroma, int expected_range_bits) +{ + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_file(ctx, path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + heif_image_handle* handle = get_primary_image_handle(ctx); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, chroma, nullptr); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + CHECK(heif_image_get_chroma_format(img) == chroma); + CHECK(heif_image_get_bits_per_pixel_range(img, heif_channel_interleaved) == expected_range_bits); + CHECK(heif_image_get_width(img, heif_channel_interleaved) == static_cast(W)); + CHECK(heif_image_get_height(img, heif_channel_interleaved) == static_cast(H)); + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, heif_channel_interleaved, &stride); + CHECK(p != nullptr); + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} + +} // namespace + +TEST_CASE("Bayer unci depth conversion goes through the Bayer operator") +{ + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("uncompressed decoder not available"); + } + if (!heif_have_encoder_for_format(heif_compression_uncompressed)) { + SKIP("uncompressed encoder not available"); + } + + std::string bayer8 = write_bayer_unci(8, "bayer_8bit.heif"); + std::string bayer12 = write_bayer_unci(12, "bayer_12bit.heif"); + + SECTION("direct routes (unchanged)") { + decode_to_interleaved(bayer8, heif_chroma_interleaved_RGB, 8); + decode_to_interleaved(bayer12, heif_chroma_interleaved_RRGGBB_LE, 12); + } + + SECTION("8-bit Bayer to 16-bit interleaved RGB (was to_hdr on the filter array)") { + // An interleaved RRGGBB target without a requested depth is 10 bits. + decode_to_interleaved(bayer8, heif_chroma_interleaved_RRGGBB_LE, 10); + } + + SECTION("12-bit Bayer to 8-bit interleaved RGB (was to_sdr on the filter array)") { + decode_to_interleaved(bayer12, heif_chroma_interleaved_RGB, 8); + } + + SECTION("12-bit Bayer, native layout with convert_hdr_to_8bit, never yields an image without planes") { + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_file(ctx, bayer12.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + heif_image_handle* handle = get_primary_image_handle(ctx); + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + options->convert_hdr_to_8bit = true; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, options); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + + if (err.code == heif_error_Ok) { + // Lowering a Bayer image to 8 bits in place is not implemented today. If it is one + // day, the result has to carry the filter-array plane. + REQUIRE(img != nullptr); + CHECK(heif_image_has_channel(img, heif_channel_filter_array)); + heif_image_release(img); + } + else { + CHECK(err.code == heif_error_Unsupported_feature); + CHECK(err.subcode == heif_suberror_Unsupported_color_conversion); + CHECK(img == nullptr); + } + + heif_decoding_options_free(options); + heif_image_handle_release(handle); + heif_context_free(ctx); + } +} diff -Nru libheif-1.23.4/tests/uncompressed_box.cc libheif-1.23.6/tests/uncompressed_box.cc --- libheif-1.23.4/tests/uncompressed_box.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_box.cc 2026-10-05 16:16:00.000000000 +0000 @@ -54,7 +54,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = cmpd->dump(indent); + std::string dump_output = cmpd->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpd -----\nsize: 0 (header size: 0)\ncomponent_type: Y\n"); } @@ -93,7 +93,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = cmpd->dump(indent); + std::string dump_output = cmpd->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpd -----\nsize: 0 (header size: 0)\ncomponent_type: red\ncomponent_type: green\ncomponent_type: blue\n"); } @@ -127,7 +127,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = cmpd->dump(indent); + std::string dump_output = cmpd->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpd -----\nsize: 0 (header size: 0)\ncomponent_type: 0x8000\n| component_type_uri: http://example.com/custom_component_uri\ncomponent_type: 0x8002\n| component_type_uri: http://example.com/another_custom_component_uri\n"); } @@ -214,7 +214,7 @@ REQUIRE(bytes == expected); Indent indent; - std::string dump_output = uncC->dump(indent); + std::string dump_output = uncC->dump_to_string(indent); REQUIRE(dump_output == "Box: uncC -----\nsize: 0 (header size: 0)\nprofile: 1919378017 (rgba)\ncomponent_index: 0\n| component_bit_depth: 8\n| component_format: unsigned\n| component_align_size: 0\ncomponent_index: 1\n| component_bit_depth: 8\n| component_format: unsigned\n| component_align_size: 0\ncomponent_index: 2\n| component_bit_depth: 8\n| component_format: unsigned\n| component_align_size: 0\ncomponent_index: 3\n| component_bit_depth: 8\n| component_format: unsigned\n| component_align_size: 0\nsampling_type: no subsampling\ninterleave_type: pixel\nblock_size: 0\ncomponents_little_endian: 0\nblock_pad_lsb: 0\nblock_little_endian: 0\nblock_reversed: 0\npad_unknown: 0\npixel_size: 0\nrow_align_size: 0\ntile_align_size: 0\nnum_tile_cols: 1\nnum_tile_rows: 1\n"); } @@ -245,7 +245,7 @@ REQUIRE(uncC->get_number_of_tile_columns() == 2); REQUIRE(uncC->get_number_of_tile_rows() == 3); Indent indent; - std::string dumpResult = box->dump(indent); + std::string dumpResult = box->dump_to_string(indent); REQUIRE(dumpResult == "Box: uncC -----\n" "size: 64 (header size: 12)\n" "profile: 1919378017 (rgba)\n" @@ -382,7 +382,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = cmpC->dump(indent); + std::string dump_output = cmpC->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpC -----\nsize: 17 (header size: 12)\ncompression_type: defl\ncompressed_entity_type: 0\n"); } @@ -418,7 +418,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = cmpC->dump(indent); + std::string dump_output = cmpC->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpC -----\nsize: 17 (header size: 12)\ncompression_type: zlib\ncompressed_entity_type: 2\n"); } @@ -453,7 +453,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = cmpC->dump(indent); + std::string dump_output = cmpC->dump_to_string(indent); REQUIRE(dump_output == "Box: cmpC -----\nsize: 17 (header size: 12)\ncompression_type: brot\ncompressed_entity_type: 1\n"); } @@ -492,7 +492,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = icef->dump(indent); + std::string dump_output = icef->dump_to_string(indent); REQUIRE(dump_output == "Box: icef -----\nsize: 25 (header size: 12)\nnum_compressed_units: 2\nunit_offset: 2563, unit_size: 3\nunit_offset: 131850, unit_size: 7\n"); } @@ -534,7 +534,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = icef->dump(indent); + std::string dump_output = icef->dump_to_string(indent); REQUIRE(dump_output == "Box: icef -----\nsize: 21 (header size: 12)\nnum_compressed_units: 2\nunit_offset: 0, unit_size: 16387\nunit_offset: 16387, unit_size: 2567\n"); } @@ -576,7 +576,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = icef->dump(indent); + std::string dump_output = icef->dump_to_string(indent); REQUIRE(dump_output == "Box: icef -----\nsize: 33 (header size: 12)\nnum_compressed_units: 2\nunit_offset: 772, unit_size: 16843267\nunit_offset: 16909066, unit_size: 263431\n"); } @@ -620,7 +620,7 @@ REQUIRE(written == byteArray); Indent indent; - std::string dump_output = icef->dump(indent); + std::string dump_output = icef->dump_to_string(indent); REQUIRE(dump_output == "Box: icef -----\nsize: 49 (header size: 12)\nnum_compressed_units: 2\nunit_offset: 4294969859, unit_size: 8590000643\nunit_offset: 8590066442, unit_size: 12885165319\n"); } @@ -683,7 +683,7 @@ // Dump Indent indent; - std::string dump_output = parsed->dump(indent); + std::string dump_output = parsed->dump_to_string(indent); REQUIRE(dump_output == "Box: cloc -----\nsize: 13 (header size: 12)\nversion: 0\nflags: 0\nchroma_location: 2 (h=0, v=0)\n"); } @@ -778,7 +778,7 @@ // Dump Indent indent; - std::string dump_output = parsed->dump(indent); + std::string dump_output = parsed->dump_to_string(indent); REQUIRE(dump_output == "Box: splz -----\n" "size: 36 (header size: 12)\n" "version: 0\n" @@ -880,7 +880,7 @@ // Dump Indent indent; - std::string dump_output = parsed->dump(indent); + std::string dump_output = parsed->dump_to_string(indent); REQUIRE(dump_output == "Box: snuc -----\n" "size: 45 (header size: 12)\n" "version: 0\n" diff -Nru libheif-1.23.4/tests/uncompressed_encode.cc libheif-1.23.6/tests/uncompressed_encode.cc --- libheif-1.23.4/tests/uncompressed_encode.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_encode.cc 2026-10-05 16:16:00.000000000 +0000 @@ -1130,15 +1130,18 @@ }; 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); + // A tile whose Cb plane is far larger than the subsampled size can no longer be + // built: heif_image_add_plane() now rejects a Cb/Cr plane that is not the + // chroma-subsampled size of the image (GHSA-j2rv-58fh-w8pw), so such a tile can + // never reach the encoder's per-tile guard. Verify the rejection at construction. + heif_image *tile; + REQUIRE(heif_image_create(TW, TH, heif_colorspace_YCbCr, heif_chroma_420, &tile).code == heif_error_Ok); + REQUIRE(heif_image_add_plane(tile, heif_channel_Y, TW, TH, 8).code == heif_error_Ok); - REQUIRE(add_tile(prototype, tile).code != heif_error_Ok); + // Cb should be TW/2 x TH/2; 200 x 200 is far too large. + REQUIRE(heif_image_add_plane(tile, heif_channel_Cb, 200, 200, 8).code != heif_error_Ok); heif_image_release(tile); - heif_image_release(prototype); } SECTION("chroma format differs from the prototype") { diff -Nru libheif-1.23.4/tests/uncompressed_generic_compression_tiles.cc libheif-1.23.6/tests/uncompressed_generic_compression_tiles.cc --- libheif-1.23.4/tests/uncompressed_generic_compression_tiles.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_generic_compression_tiles.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,810 @@ +/* + 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-6fqc-p7r8-2g36. +// +// The uncC tiling of an 'unci' image describes how the uncompressed data is +// stored. Whether a tile can be decoded on its own depends on the compressed units +// of the generic compression (cmpC, icef): +// +// - Units that span several tiles (the full item, the full image of a component) +// force the decoder to decompress the item completely to get the data of any +// tile. The decoder did this once for each tile, which needs time proportional +// to num_tiles * item_size. A 4 kB file with 256x256 tiles kept a CPU busy for +// minutes. +// +// Such an item is now decompressed once when the image is decoded, and the image +// is exposed as a single tile in heif_image_tiling. Because the decompression +// cannot be observed through the security limits, the tests read the file +// through a heif_reader that counts the bytes read from the item data: they have +// to be read exactly once. +// +// - Units that are parts of a tile (rows, pixels) are listed in the order of the +// uncompressed data, so each tile is a run of consecutive units. These images +// keep their tiling, and decoding a tile only reads the units of this tile. +// +// The decompressed size is known in both cases. More or less data is rejected. +// +// The files use deflate generic compression, so the tests only run 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 +#include +#include +#include + +namespace { + +// A 4x4 RGB image with 2x2 tiles of 2x2 pixels. +constexpr uint32_t kWidth = 4; +constexpr uint32_t kHeight = 4; +constexpr uint32_t kTileColumns = 2; +constexpr uint32_t kTileRows = 2; +constexpr uint32_t kTileWidth = kWidth / kTileColumns; +constexpr uint32_t kTileHeight = kHeight / kTileRows; +constexpr uint32_t kNumComponents = 3; +constexpr uint32_t kNumTiles = kTileColumns * kTileRows; +constexpr uint32_t kTileDataSize = kTileWidth * kTileHeight * kNumComponents; + +constexpr uint8_t kInterleaveComponent = 0; +constexpr uint8_t kInterleavePixel = 1; +constexpr uint8_t kInterleaveTileComponent = 4; + +constexpr uint8_t kUnitFullItem = 0; +constexpr uint8_t kUnitImage = 1; +constexpr uint8_t kUnitRow = 3; +constexpr uint8_t kUnitPixel = 4; + +// Each sample of the image has a different value. +uint8_t pixel_value(uint32_t component, uint32_t x, uint32_t y) { + return static_cast((component + 1) * 50 + y * kWidth + x); +} + +void append_tile_component(std::vector& out, uint32_t tile, uint32_t component) { + uint32_t x0 = (tile % kTileColumns) * kTileWidth; + uint32_t y0 = (tile / kTileColumns) * kTileHeight; + + for (uint32_t y = 0; y < kTileHeight; y++) { + for (uint32_t x = 0; x < kTileWidth; x++) { + out.push_back(pixel_value(component, x0 + x, y0 + y)); + } + } +} + +// The uncompressed data of the image in the order in which the file stores it. +std::vector make_image_data(uint8_t interleave_type) { + std::vector data; + + switch (interleave_type) { + case kInterleaveComponent: + for (uint32_t tile = 0; tile < kNumTiles; tile++) { + for (uint32_t c = 0; c < kNumComponents; c++) { + append_tile_component(data, tile, c); + } + } + break; + + case kInterleavePixel: + for (uint32_t tile = 0; tile < kNumTiles; tile++) { + uint32_t x0 = (tile % kTileColumns) * kTileWidth; + uint32_t y0 = (tile / kTileColumns) * kTileHeight; + + for (uint32_t y = 0; y < kTileHeight; y++) { + for (uint32_t x = 0; x < kTileWidth; x++) { + for (uint32_t c = 0; c < kNumComponents; c++) { + data.push_back(pixel_value(c, x0 + x, y0 + y)); + } + } + } + } + break; + + case kInterleaveTileComponent: + for (uint32_t c = 0; c < kNumComponents; c++) { + for (uint32_t tile = 0; tile < kNumTiles; tile++) { + append_tile_component(data, tile, c); + } + } + break; + + default: + REQUIRE(false); + } + + return data; +} + +// Split the data into pieces of 'unit_size' bytes. +std::vector> split_into_units(const std::vector& data, size_t unit_size) { + std::vector> units; + + for (size_t pos = 0; pos < data.size(); pos += unit_size) { + size_t end = std::min(pos + unit_size, data.size()); + units.emplace_back(data.begin() + pos, data.begin() + end); + } + + return units; +} + +// Wrap the data into a raw deflate stream consisting of a single stored block. +std::vector deflate_stored(const std::vector& data) { + auto len = static_cast(data.size()); + auto nlen = static_cast(~len); + + std::vector out; + out.push_back(0x01); // BFINAL=1, BTYPE=00 (stored) + out.push_back(static_cast(len & 0xFF)); + out.push_back(static_cast(len >> 8)); + out.push_back(static_cast(nlen & 0xFF)); + out.push_back(static_cast(nlen >> 8)); + append(out, data); + return out; +} + +struct UnciFileSpec { + const char* description = ""; + + uint8_t interleave_type = kInterleaveComponent; + + // cmpC compressed_unit_type + uint8_t unit_type = kUnitFullItem; + + // Without an icef box, there has to be exactly one unit. + bool with_icef = false; + + // The uncompressed content of the compressed units. + std::vector> units; +}; + +struct UnciFile { + std::vector data; + + // The item data is at the end of the file. + size_t item_size = 0; + + // The sizes of the compressed units in the item data. + std::vector compressed_unit_sizes; +}; + +// Build an 'unci' image with deflate generic compression. The item data is stored +// in 'idat', which is the last box of the file. +UnciFile build_heif_unci(const UnciFileSpec& spec) { + UnciFile file; + + std::vector item_data; + for (const auto& unit : spec.units) { + std::vector compressed = deflate_stored(unit); + file.compressed_unit_sizes.push_back(compressed.size()); + append(item_data, compressed); + } + + file.item_size = item_data.size(); + + 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, kWidth); + put_u32_be(ispe_payload, kHeight); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd + std::vector cmpd_payload; + put_u32_be(cmpd_payload, kNumComponents); + put_u16_be(cmpd_payload, heif_cmpd_component_type_red); + put_u16_be(cmpd_payload, heif_cmpd_component_type_green); + put_u16_be(cmpd_payload, heif_cmpd_component_type_blue); + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): 8-bit components + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, kNumComponents); + for (uint16_t c = 0; c < kNumComponents; c++) { + put_u16_be(uncC_payload, c); // 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(spec.interleave_type); + 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, kTileColumns - 1); + put_u32_be(uncC_payload, kTileRows - 1); + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + // cmpC: deflate + std::vector cmpC_payload; + append_fourcc(cmpC_payload, "defl"); + cmpC_payload.push_back(spec.unit_type); + 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); + + uint8_t num_properties = 4; + + if (spec.with_icef) { + // icef: implied unit offsets, 32-bit unit sizes + std::vector icef_payload; + icef_payload.push_back(3 << 2); // unit_offset_code = 0, unit_size_code = 3 + put_u32_be(icef_payload, static_cast(file.compressed_unit_sizes.size())); + for (size_t size : file.compressed_unit_sizes) { + put_u32_be(icef_payload, static_cast(size)); + } + append(ipco_payload, make_box("icef", icef_payload, /*full=*/true)); + + num_properties = 5; + } + else { + REQUIRE(spec.units.size() == 1); + } + + 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(num_properties); // association_count + for (uint8_t i = 1; i <= num_properties; i++) { + ipma_payload.push_back(0x80 | i); // essential + } + 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); + + auto idat = make_box("idat", item_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(item_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // idat has to be the last box: the tests locate the item data at the end of the file. + 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); + + append(file.data, ftyp); + append(file.data, meta); + return file; +} + + +// A heif_reader on a memory buffer that counts the bytes read from the item data. +struct CountingReader { + const std::vector* data = nullptr; + int64_t position = 0; + + int64_t watched_start = 0; + int64_t watched_end = 0; + uint64_t watched_bytes_read = 0; +}; + +int64_t reader_get_position(void* userdata) { + return static_cast(userdata)->position; +} + +int reader_read(void* dst, size_t size, void* userdata) { + auto* reader = static_cast(userdata); + auto file_size = static_cast(reader->data->size()); + auto read_end = reader->position + static_cast(size); + + if (read_end > file_size) { + return 1; + } + + memcpy(dst, reader->data->data() + reader->position, size); + + int64_t overlap_start = std::max(reader->position, reader->watched_start); + int64_t overlap_end = std::min(read_end, reader->watched_end); + if (overlap_end > overlap_start) { + reader->watched_bytes_read += static_cast(overlap_end - overlap_start); + } + + reader->position = read_end; + return 0; +} + +int reader_seek(int64_t position, void* userdata) { + auto* reader = static_cast(userdata); + if (position < 0 || position > static_cast(reader->data->size())) { + return 1; + } + + reader->position = position; + return 0; +} + +heif_reader_grow_status reader_wait_for_file_size(int64_t target_size, void* userdata) { + auto* reader = static_cast(userdata); + return (target_size <= static_cast(reader->data->size())) + ? heif_reader_grow_status_size_reached + : heif_reader_grow_status_size_beyond_eof; +} + + +// Opens the primary image of a file through the counting reader. +class OpenedImage { +public: + explicit OpenedImage(const UnciFile& file) { + m_counting_reader.data = &file.data; + m_counting_reader.watched_start = static_cast(file.data.size() - file.item_size); + m_counting_reader.watched_end = static_cast(file.data.size()); + + m_reader.reader_api_version = 1; + m_reader.get_position = reader_get_position; + m_reader.read = reader_read; + m_reader.seek = reader_seek; + m_reader.wait_for_file_size = reader_wait_for_file_size; + + m_ctx = heif_context_alloc(); + REQUIRE(m_ctx != nullptr); + + heif_error err = heif_context_read_from_reader(m_ctx, &m_reader, &m_counting_reader, nullptr); + INFO("read error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_get_primary_image_handle(m_ctx, &m_handle); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(m_handle != nullptr); + } + + ~OpenedImage() { + heif_image_handle_release(m_handle); + heif_context_free(m_ctx); + } + + OpenedImage(const OpenedImage&) = delete; + OpenedImage& operator=(const OpenedImage&) = delete; + + heif_image_handle* handle() const { return m_handle; } + + heif_image_tiling tiling() const { + heif_image_tiling tiling{}; + heif_error err = heif_image_handle_get_image_tiling(m_handle, 1, &tiling); + REQUIRE(err.code == heif_error_Ok); + return tiling; + } + + // The error message is owned by the context, so it is only valid as long as this object lives. + heif_error decode_image(heif_image** out_img) { + m_counting_reader.watched_bytes_read = 0; + return heif_decode_image(m_handle, out_img, heif_colorspace_RGB, heif_chroma_444, nullptr); + } + + heif_error decode_tile(heif_image** out_img, uint32_t tile_x, uint32_t tile_y, bool ignore_transformations = false) { + heif_decoding_options* options = heif_decoding_options_alloc(); + options->ignore_transformations = ignore_transformations; + + m_counting_reader.watched_bytes_read = 0; + heif_error err = heif_image_handle_decode_image_tile(m_handle, out_img, heif_colorspace_RGB, heif_chroma_444, + options, tile_x, tile_y); + heif_decoding_options_free(options); + return err; + } + + // The number of bytes of the item data that the last decoding call has read. + uint64_t item_bytes_read() const { return m_counting_reader.watched_bytes_read; } + +private: + CountingReader m_counting_reader; + heif_reader m_reader{}; + heif_context* m_ctx = nullptr; + heif_image_handle* m_handle = nullptr; +}; + + +// Check that the image shows the area of the test image that starts at (x0;y0). +void check_pixels(const heif_image* img, uint32_t x0, uint32_t y0, uint32_t width, uint32_t height) { + const heif_channel channels[kNumComponents] = {heif_channel_R, heif_channel_G, heif_channel_B}; + + for (uint32_t c = 0; c < kNumComponents; c++) { + REQUIRE(heif_image_get_width(img, channels[c]) == static_cast(width)); + REQUIRE(heif_image_get_height(img, channels[c]) == static_cast(height)); + + int stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly(img, channels[c], &stride); + REQUIRE(plane != nullptr); + + for (uint32_t y = 0; y < height; y++) { + for (uint32_t x = 0; x < width; x++) { + INFO("component " << c << ", pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(plane[y * stride + x]) == static_cast(pixel_value(c, x0 + x, y0 + y))); + } + } + } +} + +} // namespace + + +TEST_CASE("unci image with compressed units spanning several tiles is decompressed once") { + std::vector specs; + + { + UnciFileSpec spec; + spec.description = "full item without icef"; + spec.units = {make_image_data(kInterleaveComponent)}; + specs.push_back(spec); + } + + { + UnciFileSpec spec; + spec.description = "full item with icef"; + spec.with_icef = true; + spec.units = {make_image_data(kInterleaveComponent)}; + specs.push_back(spec); + } + + { + UnciFileSpec spec; + spec.description = "one unit for the full image of each component"; + spec.interleave_type = kInterleaveTileComponent; + spec.unit_type = kUnitImage; + spec.with_icef = true; + spec.units = split_into_units(make_image_data(kInterleaveTileComponent), kWidth * kHeight); + specs.push_back(spec); + } + + { + // The rows of a tile are not contiguous in the uncompressed data. + UnciFileSpec spec; + spec.description = "row units with tile-component interleave"; + spec.interleave_type = kInterleaveTileComponent; + spec.unit_type = kUnitRow; + spec.with_icef = true; + spec.units = split_into_units(make_image_data(kInterleaveTileComponent), kTileWidth); + specs.push_back(spec); + } + + { + // Three units for four tiles + UnciFileSpec spec; + spec.description = "row units that cannot be assigned to the tiles"; + spec.interleave_type = kInterleavePixel; + spec.unit_type = kUnitRow; + spec.with_icef = true; + spec.units = split_into_units(make_image_data(kInterleavePixel), kWidth * kHeight); + REQUIRE(spec.units.size() == 3); + specs.push_back(spec); + } + + for (const UnciFileSpec& spec : specs) { + INFO("compressed units: " << spec.description); + + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + // --- The uncC tiles cannot be decoded independently, so the image is a single tile. + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == kWidth); + REQUIRE(tiling.tile_height == kHeight); + REQUIRE(tiling.image_width == kWidth); + REQUIRE(tiling.image_height == kHeight); + + // --- Decoding the image reads (and decompresses) the item once, not once per uncC tile. + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(image.item_bytes_read() == file.item_size); + check_pixels(img, 0, 0, kWidth, kHeight); + heif_image_release(img); + + // --- The single tile is the whole image. + + for (bool ignore_transformations : {false, true}) { + INFO("ignore_transformations: " << ignore_transformations); + + img = nullptr; + err = image.decode_tile(&img, 0, 0, ignore_transformations); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(image.item_bytes_read() == file.item_size); + check_pixels(img, 0, 0, kWidth, kHeight); + heif_image_release(img); + + // --- The uncC tiles are not accessible. + + img = nullptr; + err = image.decode_tile(&img, 1, 0, ignore_transformations); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + + err = image.decode_tile(&img, 0, 1, ignore_transformations); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + } + } +} + + +TEST_CASE("unci image with compressed units that are parts of a tile keeps its tiles") { + std::vector specs; + + { + // Two units per tile + UnciFileSpec spec; + spec.description = "row units with pixel interleave"; + spec.interleave_type = kInterleavePixel; + spec.unit_type = kUnitRow; + spec.units = split_into_units(make_image_data(kInterleavePixel), kTileWidth * kNumComponents); + specs.push_back(spec); + } + + { + // Six units per tile: two rows for each of the three components + UnciFileSpec spec; + spec.description = "row units with component interleave"; + spec.interleave_type = kInterleaveComponent; + spec.unit_type = kUnitRow; + spec.units = split_into_units(make_image_data(kInterleaveComponent), kTileWidth); + specs.push_back(spec); + } + + { + // Four units per tile + UnciFileSpec spec; + spec.description = "pixel units with pixel interleave"; + spec.interleave_type = kInterleavePixel; + spec.unit_type = kUnitPixel; + spec.units = split_into_units(make_image_data(kInterleavePixel), kNumComponents); + specs.push_back(spec); + } + + for (UnciFileSpec& spec : specs) { + INFO("compressed units: " << spec.description); + + spec.with_icef = true; + + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + REQUIRE(file.compressed_unit_sizes.size() % kNumTiles == 0); + size_t units_per_tile = file.compressed_unit_sizes.size() / kNumTiles; + + heif_image_tiling tiling = image.tiling(); + REQUIRE(tiling.num_columns == kTileColumns); + REQUIRE(tiling.num_rows == kTileRows); + REQUIRE(tiling.tile_width == kTileWidth); + REQUIRE(tiling.tile_height == kTileHeight); + + // --- Decoding a tile only reads the compressed units of this tile. + + for (uint32_t ty = 0; ty < kTileRows; ty++) { + for (uint32_t tx = 0; tx < kTileColumns; tx++) { + INFO("tile (" << tx << "," << ty << ")"); + + uint32_t tile_idx = ty * kTileColumns + tx; + + size_t tile_units_size = 0; + for (size_t i = 0; i < units_per_tile; i++) { + tile_units_size += file.compressed_unit_sizes[tile_idx * units_per_tile + i]; + } + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, tx, ty); + INFO("tile decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(image.item_bytes_read() == tile_units_size); + check_pixels(img, tx * kTileWidth, ty * kTileHeight, kTileWidth, kTileHeight); + heif_image_release(img); + } + } + + // --- A tile position outside of the tiling is rejected. + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, kTileColumns, 0, /*ignore_transformations=*/true); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + + // --- Decoding the whole image reads each unit once. + + err = image.decode_image(&img); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + REQUIRE(image.item_bytes_read() == file.item_size); + check_pixels(img, 0, 0, kWidth, kHeight); + heif_image_release(img); + } +} + + +TEST_CASE("unci item that is decompressed as a whole must have the size of the image data") { + for (bool with_icef : {false, true}) { + INFO("with icef: " << with_icef); + + std::vector image_data = make_image_data(kInterleaveComponent); + + UnciFileSpec spec; + spec.with_icef = with_icef; + + // --- one byte too much + + std::vector oversized_data = image_data; + oversized_data.push_back(0); + spec.units = {oversized_data}; + + { + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Decompression_invalid_data); + REQUIRE(img == nullptr); + } + + // --- one byte missing + + std::vector short_data = image_data; + short_data.pop_back(); + spec.units = {short_data}; + + { + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + heif_image* img = nullptr; + heif_error err = image.decode_image(&img); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_End_of_data); + REQUIRE(img == nullptr); + } + } +} + + +TEST_CASE("unci compressed units of a tile must have the size of the tile") { + // Row units with pixel interleave: two units per tile. + UnciFileSpec spec; + spec.interleave_type = kInterleavePixel; + spec.unit_type = kUnitRow; + spec.with_icef = true; + + const std::vector> units = split_into_units(make_image_data(kInterleavePixel), + kTileWidth * kNumComponents); + REQUIRE(units.size() == 2 * kNumTiles); + REQUIRE(units[0].size() + units[1].size() == kTileDataSize); + + // --- the second unit of the first tile has one byte too much + + spec.units = units; + spec.units[1].push_back(0); + + { + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, 0, 0); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Decompression_invalid_data); + REQUIRE(img == nullptr); + + // The other tiles do not depend on these units. + err = image.decode_tile(&img, 1, 0); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, kTileWidth, 0, kTileWidth, kTileHeight); + heif_image_release(img); + + img = nullptr; + err = image.decode_image(&img); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(img == nullptr); + } + + // --- the second unit of the first tile has one byte missing + + spec.units = units; + spec.units[1].pop_back(); + + { + UnciFile file = build_heif_unci(spec); + OpenedImage image(file); + + heif_image* img = nullptr; + heif_error err = image.decode_tile(&img, 0, 0); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_End_of_data); + REQUIRE(img == nullptr); + + err = image.decode_tile(&img, 1, 0); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + check_pixels(img, kTileWidth, 0, kTileWidth, kTileHeight); + heif_image_release(img); + } +} diff -Nru libheif-1.23.4/tests/uncompressed_grid_tile_mismatch.cc libheif-1.23.6/tests/uncompressed_grid_tile_mismatch.cc --- libheif-1.23.4/tests/uncompressed_grid_tile_mismatch.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_grid_tile_mismatch.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,589 @@ +/* + 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. +*/ + +// The canvas of a 'grid' image takes its planes and bit depths from the first decoded tile, +// and the other tiles are copied into it. Two things went wrong when the tiles of a grid do +// not have the same format (GHSA-vv35-6hxg-95x8): +// +// - The function that copies a tile refuses a tile with another bit depth, but its error was +// discarded. The tile was left out, its area stayed zero, and decoding reported success. +// - With parallel tile decoding, "the first decoded tile" is the tile of whichever thread +// finishes first. The format of the decoded image, and which of the tiles was left out, +// changed from run to run. +// +// The grid item now remembers the description of the first tile that is decoded, and every +// tile that is decoded afterwards has to have the same format: colorspace, chroma format, and +// the same components with the same datatypes and bit depths. A tile that differs is an +// error. Which tile is the first one still depends on timing, but that no longer matters: +// whichever it is, a tile of another format does not match it. +// +// This also covers the interface that decodes single tiles, which has no canvas to compare +// a tile with: the tiles that are decoded from one context are compared with each other. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include + +namespace { + +constexpr uint32_t TILE_W = 16; +constexpr uint32_t TILE_H = 16; + +// component types of 'cmpd' +constexpr uint16_t MONO = 0, Y = 1, CB = 2, CR = 3, R = 4, G = 5, B = 6, A = 7; + +struct Component +{ + uint16_t type; + uint8_t bit_depth; // 8 or 16 +}; + +struct Tile +{ + std::vector components; + uint8_t fill; // every byte of the tile data +}; + + +// A 'grid' of 'unci' tiles in one row. 'output_width' is the width of the grid image. +std::vector build_grid_file(const std::vector& tiles, uint32_t output_width) +{ + const uint16_t num_tiles = static_cast(tiles.size()); + + 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); + + // items: 1 = grid, 2... = unci tiles + std::vector iinf_payload; + put_u16_be(iinf_payload, static_cast(1 + num_tiles)); + for (uint16_t id = 1; id <= 1 + num_tiles; id++) { + std::vector infe_payload; + put_u16_be(infe_payload, id); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, id == 1 ? "grid" : "unci"); + append_cstr(infe_payload, ""); + append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + std::vector dimg_payload; + put_u16_be(dimg_payload, 1); // from the grid + put_u16_be(dimg_payload, num_tiles); + for (uint16_t i = 0; i < num_tiles; i++) { + put_u16_be(dimg_payload, static_cast(2 + i)); + } + auto iref = make_box("iref", make_box("dimg", dimg_payload), /*full=*/true); + + // properties: 1 = ispe of the grid, 2 = ispe of a tile, then cmpd and uncC of each tile + std::vector grid_ispe_payload; + put_u32_be(grid_ispe_payload, output_width); + put_u32_be(grid_ispe_payload, TILE_H); + + std::vector tile_ispe_payload; + put_u32_be(tile_ispe_payload, TILE_W); + put_u32_be(tile_ispe_payload, TILE_H); + + std::vector ipco_payload; + append(ipco_payload, make_box("ispe", grid_ispe_payload, /*full=*/true)); + append(ipco_payload, make_box("ispe", tile_ispe_payload, /*full=*/true)); + + for (const Tile& tile : tiles) { + const uint32_t num_components = static_cast(tile.components.size()); + + std::vector cmpd_payload; + put_u32_be(cmpd_payload, num_components); + for (const Component& c : tile.components) { + put_u16_be(cmpd_payload, c.type); + } + + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, num_components); + for (uint16_t idx = 0; idx < num_components; idx++) { + put_u16_be(uncC_payload, idx); // component_index + uncC_payload.push_back(static_cast(tile.components[idx].bit_depth - 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(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 (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 + + append(ipco_payload, make_box("cmpd", cmpd_payload)); + append(ipco_payload, make_box("uncC", uncC_payload, /*full=*/true)); + } + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 1u + num_tiles); // entry_count + put_u16_be(ipma_payload, 1); // the grid + ipma_payload.push_back(1); + ipma_payload.push_back(0x80 | 1); + for (uint16_t i = 0; i < num_tiles; i++) { + put_u16_be(ipma_payload, static_cast(2 + i)); + ipma_payload.push_back(3); + ipma_payload.push_back(0x80 | 2); + ipma_payload.push_back(static_cast(0x80 | (3 + 2 * i))); + ipma_payload.push_back(static_cast(0x80 | (4 + 2 * i))); + } + 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); + + // --- item data, all in 'idat' + + std::vector> item_data; + + std::vector grid_data; + grid_data.push_back(0); // version + grid_data.push_back(0); // flags + grid_data.push_back(0); // rows_minus_one + grid_data.push_back(static_cast(num_tiles - 1)); // columns_minus_one + put_u16_be(grid_data, static_cast(output_width)); + put_u16_be(grid_data, TILE_H); + item_data.push_back(grid_data); + + for (const Tile& tile : tiles) { + size_t size = 0; + for (const Component& c : tile.components) { + size += TILE_W * TILE_H * (c.bit_depth / 8); + } + item_data.emplace_back(size, tile.fill); + } + + std::vector idat_payload; + for (const auto& data : item_data) { + append(idat_payload, data); + } + auto idat = make_box("idat", idat_payload); + + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8)); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, static_cast(item_data.size())); + uint32_t offset = 0; + for (uint16_t i = 0; i < item_data.size(); i++) { + put_u16_be(iloc_payload, static_cast(1 + i)); + 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, offset); + put_u32_be(iloc_payload, static_cast(item_data[i].size())); + offset += static_cast(item_data[i].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, iref); + 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; +} + + +// What one decode of the file returned, in a form that can be compared between runs. +struct Outcome +{ + heif_error_code code = heif_error_Ok; + heif_suberror_code subcode = heif_suberror_Unspecified; + std::string message; + + bool has_alpha = false; + int bit_depth = 0; // of the first colour plane + std::vector first_row; // first byte of each sample of the first colour plane + std::vector alpha_row; // first byte of each alpha sample + + bool operator==(const Outcome&) const = default; +}; + + +Outcome decode(const std::vector& file) +{ + Outcome outcome; + + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + INFO("read: " << err.message); + 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); + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + outcome.code = err.code; + outcome.subcode = err.subcode; + outcome.message = err.message ? err.message : ""; + + if (img) { + heif_channel channel = heif_image_has_channel(img, heif_channel_Y) ? heif_channel_Y : heif_channel_R; + REQUIRE(heif_image_has_channel(img, channel)); + + auto first_bytes_of_row = [img](heif_channel ch) { + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, ch, &stride); + REQUIRE(p != nullptr); + int w = heif_image_get_width(img, ch); + int bytes_per_sample = (heif_image_get_bits_per_pixel_range(img, ch) + 7) / 8; + std::vector row; + for (int x = 0; x < w; x++) { + row.push_back(p[static_cast(x) * bytes_per_sample]); + } + return row; + }; + + outcome.bit_depth = heif_image_get_bits_per_pixel_range(img, channel); + outcome.first_row = first_bytes_of_row(channel); + + outcome.has_alpha = heif_image_has_channel(img, heif_channel_Alpha); + if (outcome.has_alpha) { + outcome.alpha_row = first_bytes_of_row(heif_channel_Alpha); + } + + heif_image_release(img); + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + return outcome; +} + + +// Decodes the file several times, because the tiles are decoded in parallel: the result +// must not depend on which tile is finished first and creates the canvas. +Outcome decode_repeatedly(const std::vector& file) +{ + Outcome first = decode(file); + for (int i = 0; i < 20; i++) { + Outcome again = decode(file); + INFO("run " << i << ": " << again.message); + REQUIRE(again == first); + } + return first; +} + + +// Decodes the given tiles of the grid, one after the other, through the tile interface of +// one context. +std::vector decode_tiles(const std::vector& file, std::initializer_list tiles_x) +{ + std::vector outcomes; + + heif_context* ctx = heif_context_alloc(); + 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); + + for (uint32_t tile_x : tiles_x) { + Outcome outcome; + + heif_image* img = nullptr; + err = heif_image_handle_decode_image_tile(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, + nullptr, tile_x, 0); + outcome.code = err.code; + outcome.subcode = err.subcode; + outcome.message = err.message ? err.message : ""; + + if (img) { + heif_channel channel = heif_image_has_channel(img, heif_channel_Y) ? heif_channel_Y : heif_channel_R; + REQUIRE(heif_image_has_channel(img, channel)); + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, channel, &stride); + REQUIRE(p != nullptr); + outcome.bit_depth = heif_image_get_bits_per_pixel_range(img, channel); + int bytes_per_sample = (outcome.bit_depth + 7) / 8; + for (int x = 0; x < heif_image_get_width(img, channel); x++) { + outcome.first_row.push_back(p[static_cast(x) * bytes_per_sample]); + } + outcome.has_alpha = heif_image_has_channel(img, heif_channel_Alpha); + + heif_image_release(img); + } + + outcomes.push_back(outcome); + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + return outcomes; +} + + +std::vector row_of(std::initializer_list tile_values) +{ + std::vector row; + for (uint8_t v : tile_values) { + row.insert(row.end(), TILE_W, v); + } + return row; +} + +} // namespace + + +TEST_CASE("grid with tiles of the same format") +{ + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + REQUIRE(outcome.code == heif_error_Ok); + CHECK(outcome.bit_depth == 8); + CHECK(outcome.first_row == row_of({0xAA, 0xBB})); +} + + +TEST_CASE("grid with tiles of different bit depths is refused") +{ + SECTION("8 bit, then 16 bit") { + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 16}}, 0xBB}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth); + } + + SECTION("16 bit, then 8 bit") { + auto file = build_grid_file({{{{MONO, 16}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth); + } + + SECTION("only one plane differs") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{R, 8}, {G, 8}, {B, 16}}, 0xBB}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_pixel_depth); + } +} + + +TEST_CASE("grid with tiles of different colorspaces is refused") +{ + // RGB and YCbCr 4:4:4 have the same chroma format but no plane in common. + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{Y, 8}, {CB, 8}, {CR, 8}}, 0xBB}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Wrong_tile_image_chroma_format); +} + + +TEST_CASE("grid with tiles that differ in their components is refused") +{ + // Only one of the tiles has an alpha plane. Whether the decoded image had an alpha plane + // used to depend on which tile was decoded first. + SECTION("first tile with alpha") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}}, 0x80}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Invalid_grid_data); + } + + SECTION("second tile with alpha") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x80}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + CHECK(outcome.code == heif_error_Invalid_input); + CHECK(outcome.subcode == heif_suberror_Invalid_grid_data); + } + + SECTION("all tiles with alpha") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x80}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + REQUIRE(outcome.code == heif_error_Ok); + CHECK(outcome.first_row == row_of({0x40, 0x80})); + REQUIRE(outcome.has_alpha); + CHECK(outcome.alpha_row == row_of({0x40, 0x80})); + } +} + + +TEST_CASE("grid with a tile completely outside of the image") +{ + // Three tiles in a row, but the image is only two tiles wide. The third tile has nothing + // to contribute. This is not an error. + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}, {{{MONO, 8}}, 0xCC}}, 2 * TILE_W); + + Outcome outcome = decode_repeatedly(file); + INFO("decode: " << outcome.message); + REQUIRE(outcome.code == heif_error_Ok); + CHECK(outcome.first_row == row_of({0xAA, 0xBB})); +} + + +TEST_CASE("decoding single grid tiles refuses a tile that does not have the format of the other tiles") +{ + SECTION("tiles of the same format") { + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W); + + auto tiles = decode_tiles(file, {0, 1}); + INFO("tile 0: " << tiles[0].message << ", tile 1: " << tiles[1].message); + REQUIRE(tiles[0].code == heif_error_Ok); + CHECK(tiles[0].first_row == row_of({0xAA})); + REQUIRE(tiles[1].code == heif_error_Ok); + CHECK(tiles[1].first_row == row_of({0xBB})); + } + + SECTION("another bit depth") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{R, 8}, {G, 8}, {B, 16}}, 0xBB}}, 2 * TILE_W); + + // The tile that is decoded first is the one the others are compared with. + for (bool reversed : {false, true}) { + auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1}); + INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message); + CHECK(tiles[0].code == heif_error_Ok); + CHECK(tiles[1].code == heif_error_Invalid_input); + CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_pixel_depth); + } + } + + SECTION("another colorspace") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}}, 0xAA}, {{{Y, 8}, {CB, 8}, {CR, 8}}, 0xBB}}, 2 * TILE_W); + + for (bool reversed : {false, true}) { + auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1}); + INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message); + CHECK(tiles[0].code == heif_error_Ok); + CHECK(tiles[1].code == heif_error_Invalid_input); + CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_chroma_format); + } + } + + SECTION("other components") { + auto file = build_grid_file({{{{R, 8}, {G, 8}, {B, 8}, {A, 8}}, 0x40}, {{{R, 8}, {G, 8}, {B, 8}}, 0x80}}, 2 * TILE_W); + + for (bool reversed : {false, true}) { + auto tiles = reversed ? decode_tiles(file, {1, 0}) : decode_tiles(file, {0, 1}); + INFO("reversed=" << reversed << ", first: " << tiles[0].message << ", second: " << tiles[1].message); + CHECK(tiles[0].code == heif_error_Ok); + CHECK(tiles[1].code == heif_error_Invalid_input); + CHECK(tiles[1].subcode == heif_suberror_Invalid_grid_data); + } + } + + SECTION("a tile whose bit depth contradicts the image handle") { + // The handle of the grid reports the bit depth of its first tile (8 bit). The second + // tile has 16 bit. It is refused even when it is the only tile that is decoded, because + // a decoded image must not have another bit depth than the handle reports. + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 16}}, 0xBB}}, 2 * TILE_W); + + auto tiles = decode_tiles(file, {1}); + INFO("tile 1: " << tiles[0].message); + CHECK(tiles[0].code == heif_error_Invalid_input); + + tiles = decode_tiles(file, {0, 1}); + INFO("tile 0: " << tiles[0].message << ", tile 1: " << tiles[1].message); + REQUIRE(tiles[0].code == heif_error_Ok); + CHECK(tiles[0].bit_depth == 8); + CHECK(tiles[1].code == heif_error_Invalid_input); + CHECK(tiles[1].subcode == heif_suberror_Wrong_tile_image_pixel_depth); + } + + SECTION("decoding the whole image after a single tile") { + auto file = build_grid_file({{{{MONO, 8}}, 0xAA}, {{{MONO, 8}}, 0xBB}}, 2 * TILE_W); + + // The stored description of a tile must not get in the way of later decoding. + heif_context* ctx = heif_context_alloc(); + 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; + REQUIRE(heif_context_get_primary_image_handle(ctx, &handle).code == heif_error_Ok); + + for (int round = 0; round < 3; round++) { + heif_image* img = nullptr; + err = heif_image_handle_decode_image_tile(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, + nullptr, 1, 0); + REQUIRE(err.code == heif_error_Ok); + heif_image_release(img); + + img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + INFO("decode: " << err.message); + REQUIRE(err.code == heif_error_Ok); + heif_image_release(img); + } + + heif_image_handle_release(handle); + heif_context_free(ctx); + } +} diff -Nru libheif-1.23.4/tests/uncompressed_grid_wide_alpha.cc libheif-1.23.6/tests/uncompressed_grid_wide_alpha.cc --- libheif-1.23.4/tests/uncompressed_grid_wide_alpha.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_grid_wide_alpha.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,406 @@ +/* + 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. +*/ + +// The canvas of a 'grid' image is cloned from the first decoded tile, including the tile's +// alpha plane, and that plane is then filled with "opaque" in case some tiles have no alpha. +// The fill value was computed as (1UL << alpha_bpp) - 1 behind assert(alpha_bpp <= 16). +// An 'unci' tile can have an alpha component with 32, 64 or 128 bits and with signed, float +// or complex samples, so a crafted grid aborted every process whose libheif is built with +// assertions, and shifted by 64 (undefined behaviour) otherwise (GHSA-3gwx-cjv4-jr74). +// +// The opaque fill now works for unsigned integer planes of up to 64 bits. For an alpha plane +// of another kind there is no opaque value: the plane stays zero where a tile has no alpha, +// and the decoded image carries a warning. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t TILE_W = 16; +constexpr uint32_t TILE_H = 16; + +struct AlphaFormat +{ + const char* name; + uint8_t bit_depth; + uint8_t component_format; // 0 = unsigned integer, 1 = float, 2 = complex, 3 = signed integer + std::vector tile0_sample; // one alpha sample of the left tile, big-endian + std::vector tile1_sample; // one alpha sample of the right tile, big-endian + + // whether the grid can fill in an opaque value for this kind of plane + bool has_opaque_value() const { return component_format == 0 && bit_depth <= 64; } + + // a complex sample consists of two numbers + size_t word_size() const { return (component_format == 2) ? bit_depth / 16 : bit_depth / 8; } +}; + + +std::vector tile_data(const AlphaFormat& alpha, uint8_t color_value, const std::vector& alpha_sample) +{ + // component interleave: the R, G and B planes (8 bit), then the alpha plane + std::vector data(3 * TILE_W * TILE_H, color_value); + for (uint32_t i = 0; i < TILE_W * TILE_H; i++) { + append(data, alpha_sample); + } + return data; +} + + +// A 'grid' of two 'unci' tiles (R, G, B with 8 bits and an alpha component) next to each other. +// With 'truncate_second_tile', the data of the right tile is too short to be decoded. +std::vector build_grid_file(const AlphaFormat& alpha, bool truncate_second_tile) +{ + 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); + + // items: 1 = grid, 2 and 3 = unci tiles + std::vector iinf_payload; + put_u16_be(iinf_payload, 3); + const char* item_types[3] = {"grid", "unci", "unci"}; + for (uint16_t id = 1; id <= 3; id++) { + std::vector infe_payload; + put_u16_be(infe_payload, id); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, item_types[id - 1]); + append_cstr(infe_payload, ""); + append(iinf_payload, make_box("infe", infe_payload, /*full=*/true, /*version=*/2)); + } + auto iinf = make_box("iinf", iinf_payload, /*full=*/true); + + std::vector dimg_payload; + put_u16_be(dimg_payload, 1); // from the grid + put_u16_be(dimg_payload, 2); // two references + put_u16_be(dimg_payload, 2); + put_u16_be(dimg_payload, 3); + auto iref = make_box("iref", make_box("dimg", dimg_payload), /*full=*/true); + + // properties: 1 = ispe of the grid, 2 = ispe of a tile, 3 = cmpd, 4 = uncC + std::vector grid_ispe_payload; + put_u32_be(grid_ispe_payload, 2 * TILE_W); + put_u32_be(grid_ispe_payload, TILE_H); + + std::vector tile_ispe_payload; + put_u32_be(tile_ispe_payload, TILE_W); + put_u32_be(tile_ispe_payload, TILE_H); + + std::vector cmpd_payload; + put_u32_be(cmpd_payload, 4); + put_u16_be(cmpd_payload, 4); // red + put_u16_be(cmpd_payload, 5); // green + put_u16_be(cmpd_payload, 6); // blue + put_u16_be(cmpd_payload, 7); // alpha + + const uint8_t depths[4] = {8, 8, 8, alpha.bit_depth}; + const uint8_t formats[4] = {0, 0, 0, alpha.component_format}; + + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, 4); // component_count + for (uint16_t idx = 0; idx < 4; 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(formats[idx]); // component_format + 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 (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 + + std::vector ipco_payload; + append(ipco_payload, make_box("ispe", grid_ispe_payload, /*full=*/true)); + append(ipco_payload, make_box("ispe", tile_ispe_payload, /*full=*/true)); + append(ipco_payload, make_box("cmpd", cmpd_payload)); + append(ipco_payload, make_box("uncC", uncC_payload, /*full=*/true)); + auto ipco = make_box("ipco", ipco_payload); + + std::vector ipma_payload; + put_u32_be(ipma_payload, 3); // entry_count + put_u16_be(ipma_payload, 1); // the grid + ipma_payload.push_back(1); + ipma_payload.push_back(0x80 | 1); + for (uint16_t id = 2; id <= 3; id++) { + put_u16_be(ipma_payload, id); // a tile + ipma_payload.push_back(3); + ipma_payload.push_back(0x80 | 2); + ipma_payload.push_back(0x80 | 3); + ipma_payload.push_back(0x80 | 4); + } + 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); + + // --- item data, all in 'idat' + + std::vector grid_data; + grid_data.push_back(0); // version + grid_data.push_back(0); // flags + grid_data.push_back(0); // rows_minus_one + grid_data.push_back(1); // columns_minus_one + put_u16_be(grid_data, 2 * TILE_W); + put_u16_be(grid_data, TILE_H); + + std::vector tile0 = tile_data(alpha, 0x40, alpha.tile0_sample); + std::vector tile1 = tile_data(alpha, 0x80, alpha.tile1_sample); + if (truncate_second_tile) { + tile1.resize(tile1.size() / 2); + } + + std::vector idat_payload; + append(idat_payload, grid_data); + append(idat_payload, tile0); + append(idat_payload, tile1); + auto idat = make_box("idat", idat_payload); + + const std::vector* item_data[3] = {&grid_data, &tile0, &tile1}; + + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8)); // offset_size=4, length_size=4 + put_u16_be(iloc_payload, 3); // item_count + uint32_t offset = 0; + for (uint16_t id = 1; id <= 3; id++) { + put_u16_be(iloc_payload, 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, offset); + put_u32_be(iloc_payload, static_cast(item_data[id - 1]->size())); + offset += static_cast(item_data[id - 1]->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, iref); + 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; +} + + +struct Decoded +{ + heif_context* ctx = nullptr; + heif_image_handle* handle = nullptr; + heif_image* img = nullptr; + heif_error_code code = heif_error_Ok; + std::string message; + + ~Decoded() + { + if (img) heif_image_release(img); + if (handle) heif_image_handle_release(handle); + if (ctx) heif_context_free(ctx); + } +}; + + +// The file data has to stay alive as long as the context. +void decode(const std::vector& file, heif_colorspace colorspace, heif_chroma chroma, Decoded& out) +{ + out.ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_memory_without_copy(out.ctx, file.data(), file.size(), nullptr); + INFO("read: " << err.message); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_get_primary_image_handle(out.ctx, &out.handle); + REQUIRE(err.code == heif_error_Ok); + + err = heif_decode_image(out.handle, &out.img, colorspace, chroma, nullptr); + out.code = err.code; + out.message = err.message ? err.message : ""; +} + + +// Whether the decoded image has the warning about an alpha plane without an opaque value. +bool has_alpha_warning(heif_image* img) +{ + int n = heif_image_get_decoding_warnings(img, 0, nullptr, 0); + for (int i = 0; i < n; i++) { + heif_error warning; + if (heif_image_get_decoding_warnings(img, i, &warning, 1) == 1 && + warning.code == heif_error_Unsupported_feature && + std::string(warning.message).find("alpha plane of the grid") != std::string::npos) { + return true; + } + } + return false; +} + + +// Compares the alpha samples of one tile area with the bytes of the expected sample +// (given in big-endian order, like in the file). 'word_size' is the size of the units that +// are stored in the byte order of the machine: the whole sample, or one of the two numbers +// of a complex sample. +void check_alpha_area(const heif_image* img, uint32_t x0, const std::vector& expected_be, size_t word_size) +{ + size_t stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly2(img, heif_channel_Alpha, &stride); + REQUIRE(plane != nullptr); + + const size_t bytes_per_sample = expected_be.size(); + + std::vector expected = expected_be; + const uint16_t probe = 1; + if (*reinterpret_cast(&probe) == 1) { + for (size_t i = 0; i < bytes_per_sample; i += word_size) { + std::reverse(expected.begin() + i, expected.begin() + i + word_size); + } + } + + for (uint32_t y = 0; y < TILE_H; y++) { + for (uint32_t x = x0; x < x0 + TILE_W; x++) { + INFO("alpha at (" << x << "," << y << ")"); + REQUIRE(memcmp(plane + y * stride + x * bytes_per_sample, expected.data(), bytes_per_sample) == 0); + } + } +} + +} // namespace + + +TEST_CASE("grid of unci tiles with alpha planes of all sample types") +{ + const std::vector formats = { + // unsigned integers: the opaque value is the largest value of the bit depth + {"8 bit unsigned", 8, 0, {0x12}, {0x56}}, + {"16 bit unsigned", 16, 0, {0x12, 0x34}, {0x56, 0x78}}, + {"32 bit unsigned", 32, 0, {0x01, 0x02, 0x03, 0x04}, {0xF1, 0xF2, 0xF3, 0xF4}}, + {"64 bit unsigned", 64, 0, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, {0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8}}, + + // no opaque value + {"16 bit signed", 16, 3, {0x12, 0x34}, {0xF6, 0x78}}, + {"32 bit float", 32, 1, {0x3F, 0x80, 0x00, 0x00}, {0x3F, 0x00, 0x00, 0x00}}, // 1.0 and 0.5 + {"64 bit float", 64, 1, {0x3F, 0xF0, 0, 0, 0, 0, 0, 0}, {0x3F, 0xE0, 0, 0, 0, 0, 0, 0}}, // 1.0 and 0.5 + {"128 bit complex", 128, 2, {0x3F, 0xF0, 0, 0, 0, 0, 0, 0, 0x3F, 0xE0, 0, 0, 0, 0, 0, 0}, + {0x3F, 0xE0, 0, 0, 0, 0, 0, 0, 0x3F, 0xF0, 0, 0, 0, 0, 0, 0}}, + }; + + for (const AlphaFormat& alpha : formats) { + // Sections in a loop need distinct names, otherwise only the first iteration runs them. + const std::string name = std::string(alpha.name) + " alpha: "; + + DYNAMIC_SECTION(name << "decoding without conversion returns the alpha plane of the tiles") { + std::vector file = build_grid_file(alpha, false); + + Decoded d; + decode(file, heif_colorspace_undefined, heif_chroma_undefined, d); + INFO("decode: " << d.message); + REQUIRE(d.code == heif_error_Ok); + REQUIRE(d.img != nullptr); + + CHECK(heif_image_get_width(d.img, heif_channel_Alpha) == static_cast(2 * TILE_W)); + CHECK(heif_image_get_height(d.img, heif_channel_Alpha) == static_cast(TILE_H)); + REQUIRE(heif_image_get_bits_per_pixel_range(d.img, heif_channel_Alpha) == alpha.bit_depth); + + check_alpha_area(d.img, 0, alpha.tile0_sample, alpha.word_size()); + check_alpha_area(d.img, TILE_W, alpha.tile1_sample, alpha.word_size()); + + size_t stride = 0; + const uint8_t* red = heif_image_get_plane_readonly2(d.img, heif_channel_R, &stride); + REQUIRE(red != nullptr); + CHECK(red[0] == 0x40); + CHECK(red[TILE_W] == 0x80); + + CHECK(has_alpha_warning(d.img) == !alpha.has_opaque_value()); + } + + DYNAMIC_SECTION(name << "the area of a tile that cannot be decoded is opaque, or zero with a warning") { + std::vector file = build_grid_file(alpha, true); + + Decoded d; + decode(file, heif_colorspace_undefined, heif_chroma_undefined, d); + INFO("decode: " << d.message); + REQUIRE(d.code == heif_error_Ok); + REQUIRE(d.img != nullptr); + REQUIRE(heif_image_get_bits_per_pixel_range(d.img, heif_channel_Alpha) == alpha.bit_depth); + + check_alpha_area(d.img, 0, alpha.tile0_sample, alpha.word_size()); + + // All bits set is the largest value of an unsigned integer whose bit depth fills + // its storage word, which is the case for all the unsigned formats above. + const uint8_t default_byte = alpha.has_opaque_value() ? 0xFF : 0x00; + check_alpha_area(d.img, TILE_W, std::vector(alpha.bit_depth / 8, default_byte), alpha.word_size()); + + CHECK(has_alpha_warning(d.img) == !alpha.has_opaque_value()); + } + + if (alpha.bit_depth > 16) { + DYNAMIC_SECTION(name << "a conversion is refused with an error") { + // The color conversion does not support planes of more than 16 bits. This has to be + // reported as an error. It used to be unreachable: the process aborted before. + std::vector file = build_grid_file(alpha, false); + + Decoded d; + decode(file, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, d); + INFO("decode: " << d.message); + CHECK(d.code != heif_error_Ok); + CHECK(d.img == nullptr); + } + } + } +} diff -Nru libheif-1.23.4/tests/uncompressed_interleaved_alpha_plane.cc libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane.cc --- libheif-1.23.4/tests/uncompressed_interleaved_alpha_plane.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,184 @@ +/* + 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-qfj5-c4pq-q998. +// +// heif_image_add_plane() accepted a separate heif_channel_Alpha plane on an image +// whose chroma format is interleaved. The interleaved RGB encoders of the +// uncompressed codec took the component list from the chroma format (three +// entries for interleaved RGB) but the decision whether to write alpha from +// has_alpha(), which also reports a separate alpha plane. With the two +// disagreeing, the encoder indexed the three-entry component list at [3]: a +// heap out-of-bounds read in release builds and an assertion failure in debug +// builds. +// +// An interleaved image carries its alpha inside the interleaved plane (the RGBA +// and RRGGBBAA formats), so a separate alpha plane on such an image is never a +// valid configuration. It is now rejected when the plane is added, and the +// uncompressed encoder additionally refuses such an image should it be +// assembled by another route. +// +// This file only uses the public API and builds in every configuration. The +// encoder-side check needs library-internal classes and is tested in +// uncompressed_interleaved_alpha_plane_internal.cc, which only builds with full +// symbol visibility. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include + +namespace { + +constexpr int WIDTH = 16; +constexpr int HEIGHT = 16; + +struct InterleavedFormat +{ + heif_chroma chroma; + int bit_depth; + const char* name; +}; + +// The bit depths are chosen so that every interleaved encoder of the uncompressed +// codec is covered: 8-bit RGB/RGBA use the pixel-interleave encoder, RRGGBB below +// 14 bits the block-pixel encoder, and the remaining formats the byte-aligned one. +const InterleavedFormat interleaved_formats[] = { + {heif_chroma_interleaved_RGB, 8, "RGB"}, + {heif_chroma_interleaved_RGBA, 8, "RGBA"}, + {heif_chroma_interleaved_RRGGBB_LE, 10, "RRGGBB_LE"}, + {heif_chroma_interleaved_RRGGBB_BE, 16, "RRGGBB_BE"}, + {heif_chroma_interleaved_RRGGBBAA_LE, 10, "RRGGBBAA_LE"}, + {heif_chroma_interleaved_RRGGBBAA_BE, 16, "RRGGBBAA_BE"}, +}; + + +// Fills every byte of the plane with 'value'. For the planes with 16-bit samples this gives +// the sample value 0x0101 * value, which has to stay within the bit depth of the plane (the +// encoder refuses an image with larger samples). +void fill_plane(heif_image* image, heif_channel channel, uint8_t value) +{ + size_t stride = 0; + uint8_t* plane = heif_image_get_plane2(image, channel, &stride); + REQUIRE(plane != nullptr); + + int height = heif_image_get_height(image, channel); + for (int y = 0; y < height; y++) { + memset(plane + y * stride, value, stride); + } +} + + +heif_error encode_uncompressed(heif_image* image) +{ + 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); + + heif_encoding_options* options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, image, encoder, options, &handle); + + if (handle) { + heif_image_handle_release(handle); + } + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return err; +} + +} // namespace + + +TEST_CASE("heif_image_add_plane rejects a separate alpha plane on an interleaved image") +{ + for (const auto& fmt : interleaved_formats) { + INFO(fmt.name); + + heif_image* image = nullptr; + heif_error err = heif_image_create(WIDTH, HEIGHT, heif_colorspace_RGB, fmt.chroma, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_interleaved, WIDTH, HEIGHT, fmt.bit_depth); + REQUIRE(err.code == heif_error_Ok); + fill_plane(image, heif_channel_interleaved, 0x02); + + err = heif_image_add_plane(image, heif_channel_Alpha, WIDTH, HEIGHT, fmt.bit_depth); + CHECK(err.code == heif_error_Usage_error); + CHECK(heif_image_has_channel(image, heif_channel_Alpha) == 0); + + // Without the separate alpha plane the image is valid and still encodes. Under the + // unfixed library this call read past the end of the interleaved component list for + // the formats without alpha. + CHECK(encode_uncompressed(image).code == heif_error_Ok); + + heif_image_release(image); + } +} + + +TEST_CASE("heif_image_add_plane rejects a separate alpha plane before the interleaved plane") +{ + // The chroma format is fixed at heif_image_create(), so the order in which the planes + // are added must not matter. + + heif_image* image = nullptr; + heif_error err = heif_image_create(WIDTH, HEIGHT, heif_colorspace_RGB, heif_chroma_interleaved_RGB, &image); + REQUIRE(err.code == heif_error_Ok); + + err = heif_image_add_plane(image, heif_channel_Alpha, WIDTH, HEIGHT, 8); + CHECK(err.code == heif_error_Usage_error); + CHECK(heif_image_has_channel(image, heif_channel_Alpha) == 0); + + heif_image_release(image); +} + + +TEST_CASE("Planar RGB images still accept a separate alpha plane") +{ + heif_image* image = nullptr; + heif_error err = heif_image_create(WIDTH, HEIGHT, heif_colorspace_RGB, heif_chroma_444, &image); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_R, heif_channel_G, heif_channel_B, heif_channel_Alpha}) { + err = heif_image_add_plane(image, channel, WIDTH, HEIGHT, 8); + REQUIRE(err.code == heif_error_Ok); + fill_plane(image, channel, 0x80); + } + + CHECK(heif_image_has_channel(image, heif_channel_Alpha) == 1); + CHECK(encode_uncompressed(image).code == heif_error_Ok); + + heif_image_release(image); +} + diff -Nru libheif-1.23.4/tests/uncompressed_interleaved_alpha_plane_internal.cc libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane_internal.cc --- libheif-1.23.4/tests/uncompressed_interleaved_alpha_plane_internal.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_interleaved_alpha_plane_internal.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,131 @@ +/* + 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-qfj5-c4pq-q998, internal-symbol part. +// +// The public-API part of this regression test lives in +// uncompressed_interleaved_alpha_plane.cc. This file covers the defense-in-depth +// check in the uncompressed encoder factory, which needs library-internal classes +// (HeifPixelImage, unc_encoder_factory) and therefore only builds with full symbol +// visibility (WITH_REDUCED_VISIBILITY=OFF). +// +// transfer_channel_from_image_as() moves a plane between images without going +// through add_channel(). This is how a decoded alpha auxiliary image is attached to +// the main image, so an interleaved image with a separate alpha plane can still be +// assembled inside the library even though heif_image_add_plane() rejects it. The +// uncompressed encoder must refuse such an image instead of reading past the end +// of the interleaved component list. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "api_structs.h" +#include "image/pixelimage.h" +#include "codecs/uncompressed/unc_encoder.h" + +#include + +namespace { + +constexpr int WIDTH = 16; +constexpr int HEIGHT = 16; + +struct InterleavedFormat +{ + heif_chroma chroma; + int bit_depth; + const char* name; +}; + +// The bit depths are chosen so that every interleaved encoder of the uncompressed +// codec is covered: 8-bit RGB/RGBA use the pixel-interleave encoder, RRGGBB below +// 14 bits the block-pixel encoder, and the remaining formats the byte-aligned one. +const InterleavedFormat interleaved_formats[] = { + {heif_chroma_interleaved_RGB, 8, "RGB"}, + {heif_chroma_interleaved_RGBA, 8, "RGBA"}, + {heif_chroma_interleaved_RRGGBB_LE, 10, "RRGGBB_LE"}, + {heif_chroma_interleaved_RRGGBB_BE, 16, "RRGGBB_BE"}, + {heif_chroma_interleaved_RRGGBBAA_LE, 10, "RRGGBBAA_LE"}, + {heif_chroma_interleaved_RRGGBBAA_BE, 16, "RRGGBBAA_BE"}, +}; + + +heif_error encode_uncompressed(heif_image* image) +{ + 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); + + heif_encoding_options* options = heif_encoding_options_alloc(); + options->macOS_compatibility_workaround_no_nclx_profile = true; + + heif_image_handle* handle = nullptr; + err = heif_context_encode_image(ctx, image, encoder, options, &handle); + + if (handle) { + heif_image_handle_release(handle); + } + heif_encoding_options_free(options); + heif_encoder_release(encoder); + heif_context_free(ctx); + + return err; +} + +} // namespace + + +TEST_CASE("Uncompressed encoder refuses an interleaved image carrying a separate alpha plane") +{ + const heif_security_limits* limits = heif_get_global_security_limits(); + + for (const auto& fmt : interleaved_formats) { + INFO(fmt.name); + + auto image = std::make_shared(); + image->create(WIDTH, HEIGHT, heif_colorspace_RGB, fmt.chroma); + REQUIRE(image->fill_new_channel(heif_channel_interleaved, 0x80, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok); + + auto alpha = std::make_shared(); + alpha->create(WIDTH, HEIGHT, heif_colorspace_monochrome, heif_chroma_monochrome); + REQUIRE(alpha->fill_new_channel(heif_channel_Y, 0xFF, WIDTH, HEIGHT, fmt.bit_depth, limits).error_code == heif_error_Ok); + + REQUIRE(image->transfer_channel_from_image_as(alpha, heif_channel_Y, heif_channel_Alpha).error_code == heif_error_Ok); + REQUIRE(image->has_channel(heif_channel_Alpha)); + + // The encoder factory is where the interleaved encoders are instantiated. + heif_encoding_options* options = heif_encoding_options_alloc(); + auto encoder = unc_encoder_factory::get_unc_encoder(image, *options); + heif_encoding_options_free(options); + CHECK(!encoder); + + // Same through the public encode entry point. + heif_image wrapper; + wrapper.image = image; + CHECK(encode_uncompressed(&wrapper).code != heif_error_Ok); + } +} diff -Nru libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_colorconv.cc libheif-1.23.6/tests/uncompressed_mixed_chroma_depth_colorconv.cc --- libheif-1.23.4/tests/uncompressed_mixed_chroma_depth_colorconv.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_mixed_chroma_depth_colorconv.cc 2026-10-05 16:16:00.000000000 +0000 @@ -34,14 +34,21 @@ // 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). +// The fix is made in the pipeline planner: ColorState carries one bit depth per +// plane, and every YCbCr operator declares in state_after_conversion() that it +// needs the colour planes to share one depth (color_channels_have_same_bpp()), so +// no pipeline is ever built through an operator that would read a chroma plane +// with the luma sample width. Op_to_sdr_planes lowers every plane on its own and +// can therefore still equalize such an image to 8 bits. +// +// This test builds such a file, confirms it still decodes natively to the +// mismatched-depth planar image (so the decoder path itself is unaffected), then +// requires the conversion to the reporter's 16-bit interleaved RGB target to be +// refused cleanly (no operator can widen the 8-bit chroma planes to 16 bits), and +// checks that the two 8-bit routes (interleaved RGB, and convert_hdr_to_8bit) +// succeed with the expected samples. Under the unfixed code the 16-bit 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" @@ -214,7 +221,7 @@ } // namespace -TEST_CASE("unci YCbCr with mismatched luma/chroma bit depths refuses RGB conversion without heap overread") { +TEST_CASE("unci YCbCr with mismatched luma/chroma bit depths converts without heap overread") { std::vector file = build_heif_unci_ycbcr_mismatched_luma_depth(); heif_context* ctx = heif_context_alloc(); @@ -233,18 +240,18 @@ 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. + // Converting to a high-bit-depth interleaved RGB target used to select + // Op_YCbCr420_to_RRGGBBaa, which is where the over-read occurred. That operator + // now declines colour planes of differing depth, and no other operator can widen + // the 8-bit chroma planes to the 16 bits the target needs, so the decode must fail + // cleanly rather than read 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(err.subcode == heif_suberror_Unsupported_color_conversion); REQUIRE(img == nullptr); if (img != nullptr) { @@ -252,6 +259,55 @@ } } + // An 8-bit target is reachable: Op_to_sdr_planes lowers the 16-bit luma plane + // and copies the 8-bit chroma planes, after which the planes agree. + { + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGB, nullptr); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + CHECK(heif_image_get_bits_per_pixel_range(img, heif_channel_interleaved) == 8); + CHECK(heif_image_get_width(img, heif_channel_interleaved) == static_cast(WIDTH)); + CHECK(heif_image_get_height(img, heif_channel_interleaved) == static_cast(HEIGHT)); + heif_image_release(img); + } + + // The same in the native layout with convert_hdr_to_8bit: the planes keep their + // values (luma 0x1000 >> 8 = 0x10 at the origin, chroma unchanged). The file's colr + // box is kept as the output profile; otherwise the default sRGB target would add a + // YCbCr -> RGB -> YCbCr round trip that clips the extreme test chroma. + { + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + options->convert_hdr_to_8bit = true; + options->output_image_nclx_profile_passthrough = 1; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, options); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + CHECK(heif_image_get_bits_per_pixel_range(img, channel) == 8); + } + + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, heif_channel_Y, &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == 0x10); + p = heif_image_get_plane_readonly2(img, heif_channel_Cb, &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == 0x40); + p = heif_image_get_plane_readonly2(img, heif_channel_Cr, &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == 0x80); + + heif_image_release(img); + heif_decoding_options_free(options); + } + heif_image_handle_release(handle); heif_context_free(ctx); } diff -Nru libheif-1.23.4/tests/uncompressed_mixed_interleave_bit_depth.cc libheif-1.23.6/tests/uncompressed_mixed_interleave_bit_depth.cc --- libheif-1.23.4/tests/uncompressed_mixed_interleave_bit_depth.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_mixed_interleave_bit_depth.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,440 @@ +/* + 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. +*/ + +// In mixed interleave mode of 'unci' (ISO/IEC 23001-17, 5.2.1.6.4), the Cb and Cr samples +// are stored like the components of a pixel in pixel interleave mode: each value is coded +// with exactly component_bit_depth bits, or, if component_align_size is set, as a +// byte-aligned word whose upper bits are padding. +// +// The decoder read each chroma sample with the width of the plane's storage word instead +// (16 bits for a 12-bit component). The bits of the neighbouring sample, or the padding bits, +// ended up in the upper bits of the value, so the decoded planes contained samples far above +// the bit depth they declare (GHSA-v5rj-g4wv-j5w3). Code that relies on the bit depth +// (libsharpyuv uses the samples as table indices) then read out of bounds. For well-formed +// files with a chroma bit depth other than 8 or 16, the chroma planes were simply decoded +// wrongly. The existing test files only use 8 and 16 bits, where the two widths coincide. +// +// These tests write the sample data with their own bit writer, following the specification, +// and require that every sample is decoded exactly as it was written. Padding bits are +// written as ones in one of the variants: the specification wants them to be zero, but a +// decoder must not let them leak into the sample values. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include + +namespace { + +struct Component +{ + uint16_t type; // 1 = Y, 2 = Cb, 3 = Cr + uint8_t bit_depth; + uint8_t align_size; // component_align_size +}; + +struct Layout +{ + std::string name; + uint32_t width; + uint32_t height; + uint8_t sampling_type; // 1 = 4:2:2, 2 = 4:2:0 + std::vector components; + uint32_t row_align_size; +}; + + +class TestBitWriter +{ +public: + explicit TestBitWriter(bool padding_bit) : m_padding_bit(padding_bit) {} + + void write_bits(uint32_t value, int n) + { + for (int i = n - 1; i >= 0; i--) { + write_bit((value >> i) & 1); + } + } + + void write_padding_bits(int n) + { + for (int i = 0; i < n; i++) { + write_bit(m_padding_bit); + } + } + + void pad_to_byte_boundary() + { + while (m_bits_in_last_byte != 0) { + write_bit(m_padding_bit); + } + } + + void pad_row(size_t row_start, uint32_t row_align_size) + { + pad_to_byte_boundary(); + if (row_align_size != 0) { + while ((m_data.size() - row_start) % row_align_size != 0) { + m_data.push_back(m_padding_bit ? 0xFF : 0x00); + } + } + } + + // Only valid on a byte boundary. + size_t size() const { return m_data.size(); } + + const std::vector& data() const { return m_data; } + +private: + void write_bit(uint32_t bit) + { + if (m_bits_in_last_byte == 0) { + m_data.push_back(0); + } + if (bit) { + m_data.back() = static_cast(m_data.back() | (0x80 >> m_bits_in_last_byte)); + } + m_bits_in_last_byte = (m_bits_in_last_byte + 1) % 8; + } + + std::vector m_data; + int m_bits_in_last_byte = 0; + bool m_padding_bit; +}; + + +// The value that is stored for a sample. It uses the whole range of the bit depth, including +// the largest value, so that a bit that is taken from the wrong place shows up. +uint32_t sample_value(const Component& c, uint32_t x, uint32_t y) +{ + uint32_t max_value = (1u << c.bit_depth) - 1; + uint32_t v = (x * 37 + y * 101 + c.type * 211) * 2654435761u; + if ((x + y) % 3 == 0) { + return max_value; + } + return (v >> 7) & max_value; +} + + +bool is_chroma(const Component& c) { return c.type == 2 || c.type == 3; } + + +void write_component_value(TestBitWriter& writer, const Component& c, uint32_t value) +{ + if (c.align_size != 0) { + writer.pad_to_byte_boundary(); + writer.write_padding_bits(c.align_size * 8 - c.bit_depth); + } + writer.write_bits(value, c.bit_depth); +} + + +std::vector build_sample_data(const Layout& layout, bool padding_bit) +{ + uint32_t chroma_width = layout.width / 2; + uint32_t chroma_height = (layout.sampling_type == 2) ? layout.height / 2 : layout.height; + + TestBitWriter writer(padding_bit); + bool chroma_written = false; + + for (size_t i = 0; i < layout.components.size(); i++) { + const Component& c = layout.components[i]; + + if (!is_chroma(c)) { + // as in component interleave mode + for (uint32_t y = 0; y < layout.height; y++) { + size_t row_start = writer.size(); + for (uint32_t x = 0; x < layout.width; x++) { + write_component_value(writer, c, sample_value(c, x, y)); + } + writer.pad_row(row_start, layout.row_align_size); + } + } + else if (!chroma_written) { + // The two chroma components are consecutive in the component list. They are stored + // as in pixel interleave mode, in the order in which they are declared. + const Component& c2 = layout.components[i + 1]; + REQUIRE(is_chroma(c2)); + + for (uint32_t y = 0; y < chroma_height; y++) { + size_t row_start = writer.size(); + for (uint32_t x = 0; x < chroma_width; x++) { + write_component_value(writer, c, sample_value(c, x, y)); + write_component_value(writer, c2, sample_value(c2, x, y)); + } + writer.pad_row(row_start, layout.row_align_size); + } + chroma_written = true; + } + } + + return writer.data(); +} + + +std::vector build_file(const Layout& layout, bool padding_bit) +{ + 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, layout.width); + put_u32_be(ispe_payload, layout.height); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + std::vector cmpd_payload; + put_u32_be(cmpd_payload, static_cast(layout.components.size())); + for (const Component& c : layout.components) { + put_u16_be(cmpd_payload, c.type); + } + auto cmpd = make_box("cmpd", cmpd_payload); + + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, static_cast(layout.components.size())); + for (uint16_t idx = 0; idx < layout.components.size(); idx++) { + put_u16_be(uncC_payload, idx); // component_index + uncC_payload.push_back(static_cast(layout.components[idx].bit_depth - 1)); // component_bit_depth_minus_one + uncC_payload.push_back(0); // component_format (unsigned) + uncC_payload.push_back(layout.components[idx].align_size); // component_align_size + } + uncC_payload.push_back(layout.sampling_type); + 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, layout.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); + + // The item contains exactly the bytes of the image. If the decoder computed a larger + // size for the sample data than the layout has, it would refuse the file. + std::vector sample_data = build_sample_data(layout, padding_bit); + auto idat = make_box("idat", sample_data); + + std::vector iloc_payload; + put_u16_be(iloc_payload, (4 << 12) | (4 << 8)); // 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(sample_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; +} + + +heif_channel channel_of(const Component& c) +{ + switch (c.type) { + case 2: return heif_channel_Cb; + case 3: return heif_channel_Cr; + default: return heif_channel_Y; + } +} + + +void check_decoding(const Layout& layout, bool padding_bit) +{ + INFO(layout.name << ", padding bits " << (padding_bit ? 1 : 0)); + + std::vector file = build_file(layout, padding_bit); + + 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); + INFO("read: " << err.message); + 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); + + // Decode without any conversion, so that the planes come back as the decoder filled them. + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + INFO("decode: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + uint32_t chroma_width = layout.width / 2; + uint32_t chroma_height = (layout.sampling_type == 2) ? layout.height / 2 : layout.height; + + for (const Component& c : layout.components) { + heif_channel channel = channel_of(c); + uint32_t w = is_chroma(c) ? chroma_width : layout.width; + uint32_t h = is_chroma(c) ? chroma_height : layout.height; + + REQUIRE(heif_image_get_bits_per_pixel_range(img, channel) == c.bit_depth); + REQUIRE(heif_image_get_width(img, channel) == static_cast(w)); + REQUIRE(heif_image_get_height(img, channel) == static_cast(h)); + + size_t stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly2(img, channel, &stride); + REQUIRE(plane != nullptr); + + const uint32_t max_value = (1u << c.bit_depth) - 1; + + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + uint32_t value; + if (c.bit_depth <= 8) { + value = plane[y * stride + x]; + } + else { + value = reinterpret_cast(plane + y * stride)[x]; + } + + INFO("component type " << c.type << " at (" << x << "," << y << ")"); + REQUIRE(value <= max_value); + REQUIRE(value == sample_value(c, x, y)); + } + } + } + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} + +} // namespace + + +TEST_CASE("unci mixed interleave reads chroma samples with their component bit depth") +{ + const uint16_t Y = 1, Cb = 2, Cr = 3; + + const std::vector layouts = { + // The configuration of the report: all components 12 bit, 4:2:2, no alignment. + {"12 bit 4:2:2", 16, 8, 1, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 0}, + {"12 bit 4:2:0", 16, 8, 2, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 0}, + {"10 bit 4:2:0", 16, 8, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 0}, + + // Rows that do not end on a byte boundary: 6 luma samples of 10 bits are 60 bits, + // 3 chroma pairs of 2 x 10 bits are 60 bits. + {"10 bit 4:2:0, rows with padding to the byte boundary", 6, 4, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 0}, + {"9 bit 4:2:2, rows with padding to the byte boundary", 6, 4, 1, {{Y, 9, 0}, {Cb, 9, 0}, {Cr, 9, 0}}, 0}, + + // Bit depths below 8 are stored in bytes and were read with 8 bits. + {"4 bit 4:2:2", 6, 4, 1, {{Y, 4, 0}, {Cb, 4, 0}, {Cr, 4, 0}}, 0}, + {"8 bit luma, 5 bit chroma", 8, 4, 2, {{Y, 8, 0}, {Cb, 5, 0}, {Cr, 5, 0}}, 0}, + + // Different depths of the two chroma components, and Cr declared before Cb. + {"Cb 10 bit, Cr 12 bit", 8, 4, 2, {{Y, 8, 0}, {Cb, 10, 0}, {Cr, 12, 0}}, 0}, + {"Cr 12 bit before Cb 10 bit", 8, 4, 2, {{Y, 8, 0}, {Cr, 12, 0}, {Cb, 10, 0}}, 0}, + {"chroma before luma", 8, 4, 1, {{Cb, 12, 0}, {Cr, 12, 0}, {Y, 12, 0}}, 0}, + + // Byte-aligned components: each value is a 16-bit word with padding in the upper bits. + {"12 bit in 16-bit words", 8, 4, 2, {{Y, 12, 2}, {Cb, 12, 2}, {Cr, 12, 2}}, 0}, + {"only Cb byte-aligned", 6, 4, 2, {{Y, 12, 0}, {Cb, 12, 2}, {Cr, 12, 0}}, 0}, + {"only Cr byte-aligned", 6, 4, 2, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 2}}, 0}, + {"5 bit in bytes", 6, 4, 1, {{Y, 5, 1}, {Cb, 5, 1}, {Cr, 5, 1}}, 0}, + + // Row alignment. + {"10 bit, rows aligned to 4 bytes", 6, 4, 2, {{Y, 10, 0}, {Cb, 10, 0}, {Cr, 10, 0}}, 4}, + {"12 bit, rows aligned to 8 bytes", 6, 4, 1, {{Y, 12, 0}, {Cb, 12, 0}, {Cr, 12, 0}}, 8}, + + // The depths at which the storage width and the bit depth coincide. + {"8 bit", 8, 4, 2, {{Y, 8, 0}, {Cb, 8, 0}, {Cr, 8, 0}}, 0}, + {"16 bit", 8, 4, 1, {{Y, 16, 0}, {Cb, 16, 0}, {Cr, 16, 0}}, 0}, + }; + + for (const Layout& layout : layouts) { + for (bool padding_bit : {false, true}) { + check_decoding(layout, padding_bit); + } + } +} diff -Nru libheif-1.23.4/tests/uncompressed_mixed_rgb_depth_to_sdr.cc libheif-1.23.6/tests/uncompressed_mixed_rgb_depth_to_sdr.cc --- libheif-1.23.4/tests/uncompressed_mixed_rgb_depth_to_sdr.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_mixed_rgb_depth_to_sdr.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,183 @@ +/* + 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: an 'unci' image whose colour planes have different bit depths, for +// example R=8, G=8, B=12, could not be decoded to 8-bit RGB (interleaved RGB, or planar +// with convert_hdr_to_8bit), while the very same planes in the order 12/8/8 could. +// Op_to_sdr_planes lowers every plane to 8 bits on its own, but it was only offered when +// the first colour plane was wider than 8 bits, because the planner asked for "the" colour +// depth of an image whose planes do not share one. ColorState now reports a colour depth +// only when all colour planes agree, and Op_to_sdr_planes declines only when all of them +// are already 8 bits. +// +// The files are built through the public API and the uncompressed encoder, so the test +// needs both halves of the uncompressed codec. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include + +namespace { + +constexpr uint32_t W = 64; +constexpr uint32_t H = 64; + +const heif_channel CHANNELS[3] = {heif_channel_R, heif_channel_G, heif_channel_B}; + +// Every plane holds one constant. At 8 bits these are the values below; a 12-bit plane +// holds the same value shifted up by four bits plus a few low bits that the reduction to +// 8 bits must drop again. +constexpr uint8_t VALUES[3] = {100, 50, 25}; + +std::string write_unci_rgb(const int bits[3], const char* filename) +{ + heif_image* image = nullptr; + heif_error err = heif_image_create(W, H, heif_colorspace_RGB, heif_chroma_444, &image); + REQUIRE(err.code == heif_error_Ok); + + for (int i = 0; i < 3; i++) { + err = heif_image_add_plane(image, CHANNELS[i], W, H, bits[i]); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(image, CHANNELS[i], &stride); + REQUIRE(p != nullptr); + + for (uint32_t y = 0; y < H; y++) { + for (uint32_t x = 0; x < W; x++) { + if (bits[i] == 8) { + p[y * stride + x] = VALUES[i]; + } + else { + reinterpret_cast(p + y * stride)[x] = static_cast((VALUES[i] << (bits[i] - 8)) | 0xB); + } + } + } + } + + 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 path = get_tests_output_file_path(filename); + err = heif_context_write_to_file(ctx, path.c_str()); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(image); + heif_context_free(ctx); + return path; +} + +heif_image* decode(const std::string& path, heif_colorspace colorspace, heif_chroma chroma, bool hdr_to_8bit) +{ + heif_context* ctx = heif_context_alloc(); + heif_error err = heif_context_read_from_file(ctx, path.c_str(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + heif_image_handle* handle = get_primary_image_handle(ctx); + + heif_decoding_options* options = heif_decoding_options_alloc(); + REQUIRE(options != nullptr); + options->convert_hdr_to_8bit = hdr_to_8bit; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, colorspace, chroma, options); + INFO("decode error (" << err.code << "/" << err.subcode << "): " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + heif_decoding_options_free(options); + heif_image_handle_release(handle); + heif_context_free(ctx); + return img; +} + +} // namespace + +TEST_CASE("unci RGB with mixed plane depths decodes to 8 bits in any plane order") +{ + if (!heif_have_decoder_for_format(heif_compression_uncompressed)) { + SKIP("uncompressed decoder not available"); + } + if (!heif_have_encoder_for_format(heif_compression_uncompressed)) { + SKIP("uncompressed encoder not available"); + } + + const int layouts[3][3] = {{8, 8, 12}, {12, 8, 8}, {8, 12, 8}}; + const char* filenames[3] = {"mixed_rgb_8_8_12.heif", "mixed_rgb_12_8_8.heif", "mixed_rgb_8_12_8.heif"}; + + for (int l = 0; l < 3; l++) { + const int* bits = layouts[l]; + INFO("R/G/B = " << bits[0] << "/" << bits[1] << "/" << bits[2]); + + std::string path = write_unci_rgb(bits, filenames[l]); + + // A native decode keeps the declared depths, so the file really is mixed. + { + heif_image* img = decode(path, heif_colorspace_undefined, heif_chroma_undefined, false); + for (int i = 0; i < 3; i++) { + CHECK(heif_image_get_bits_per_pixel_range(img, CHANNELS[i]) == bits[i]); + } + heif_image_release(img); + } + + // 8-bit interleaved RGB: the wide plane has to be lowered first. + { + heif_image* img = decode(path, heif_colorspace_RGB, heif_chroma_interleaved_RGB, false); + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, heif_channel_interleaved, &stride); + REQUIRE(p != nullptr); + const uint8_t* last = p + (H - 1) * stride + (W - 1) * 3; + for (int i = 0; i < 3; i++) { + CHECK(p[i] == VALUES[i]); + CHECK(last[i] == VALUES[i]); + } + heif_image_release(img); + } + + // Planar output with convert_hdr_to_8bit: every plane ends up at 8 bits. + { + heif_image* img = decode(path, heif_colorspace_undefined, heif_chroma_undefined, true); + for (int i = 0; i < 3; i++) { + CHECK(heif_image_get_bits_per_pixel_range(img, CHANNELS[i]) == 8); + size_t stride = 0; + const uint8_t* p = heif_image_get_plane_readonly2(img, CHANNELS[i], &stride); + REQUIRE(p != nullptr); + CHECK(p[0] == VALUES[i]); + CHECK(p[(H - 1) * stride + (W - 1)] == VALUES[i]); + } + heif_image_release(img); + } + } +} diff -Nru libheif-1.23.4/tests/uncompressed_sequence_tiles.cc libheif-1.23.6/tests/uncompressed_sequence_tiles.cc --- libheif-1.23.4/tests/uncompressed_sequence_tiles.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_sequence_tiles.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,397 @@ +/* + 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. +*/ + +// Test for uncompressed ('uncv') sequence frames with uncC tiles. +// +// The decoder fetches the data of each tile as a byte range of the sample. +// DataExtent::read_data(offset, size) ignored the requested range for samples +// and returned the whole sample. Each tile was therefore decoded from the start +// of the sample, so that all tiles showed the content of the first tile, and the +// whole sample was read from the file once for each tile. +// +// The test builds a 4x4 RGB frame with 2x2 tiles in which each sample value is +// different, decodes it and checks all pixels. It reads the file through a +// heif_reader that counts the bytes read from the sample data: they have to be +// read exactly once. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_sequences.h" +#include "test_utils.h" + +#include +#include +#include +#include + +namespace { + +constexpr uint16_t kWidth = 4; +constexpr uint16_t kHeight = 4; +constexpr uint32_t kTileColumns = 2; +constexpr uint32_t kTileRows = 2; +constexpr uint32_t kTileWidth = kWidth / kTileColumns; +constexpr uint32_t kTileHeight = kHeight / kTileRows; +constexpr uint32_t kNumComponents = 3; +constexpr uint32_t kSampleSize = kWidth * kHeight * kNumComponents; + +// Each sample value of the frame is different. +uint8_t pixel_value(uint32_t component, uint32_t x, uint32_t y) { + return static_cast((component + 1) * 50 + y * kWidth + x); +} + +void append_matrix(std::vector& out) { + for (uint32_t m : {0x00010000u, 0u, 0u, 0u, 0x00010000u, 0u, 0u, 0u, 0x40000000u}) { + put_u32_be(out, m); + } +} + +// Build an image sequence with a single 'pict' track. Its only sample is a 4x4 RGB +// frame with 2x2 tiles in pixel interleave. The sample data is in 'mdat', which is +// the last box of the file. +std::vector build_tiled_uncv_sequence() { + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "msf1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "msf1"); + append_fourcc(ftyp_payload, "iso8"); + auto ftyp = make_box("ftyp", ftyp_payload); + + // mvhd (v0) + std::vector mvhd_payload; + put_u32_be(mvhd_payload, 0); // creation_time + put_u32_be(mvhd_payload, 0); // modification_time + put_u32_be(mvhd_payload, 1000); // timescale + put_u32_be(mvhd_payload, 1); // duration + put_u32_be(mvhd_payload, 0x00010000); // rate + put_u16_be(mvhd_payload, 0x0100); // volume + put_u16_be(mvhd_payload, 0); + put_u32_be(mvhd_payload, 0); + put_u32_be(mvhd_payload, 0); + append_matrix(mvhd_payload); + for (int i = 0; i < 6; i++) { + put_u32_be(mvhd_payload, 0); + } + put_u32_be(mvhd_payload, 2); // next_track_ID + auto mvhd = make_box("mvhd", mvhd_payload, /*full=*/true); + + // tkhd (v0), flags: enabled | in_movie | in_preview + std::vector tkhd_payload; + put_u32_be(tkhd_payload, 0); + put_u32_be(tkhd_payload, 0); + put_u32_be(tkhd_payload, 1); // track_ID + put_u32_be(tkhd_payload, 0); + put_u32_be(tkhd_payload, 1); // duration + put_u32_be(tkhd_payload, 0); + put_u32_be(tkhd_payload, 0); + put_u16_be(tkhd_payload, 0); // layer + put_u16_be(tkhd_payload, 0); // alternate_group + put_u16_be(tkhd_payload, 0); // volume + put_u16_be(tkhd_payload, 0); + append_matrix(tkhd_payload); + put_u32_be(tkhd_payload, static_cast(kWidth) << 16); // width (16.16) + put_u32_be(tkhd_payload, static_cast(kHeight) << 16); // height (16.16) + auto tkhd = make_box("tkhd", tkhd_payload, /*full=*/true, /*version=*/0, /*flags=*/7); + + // mdhd (v0) + std::vector mdhd_payload; + put_u32_be(mdhd_payload, 0); + put_u32_be(mdhd_payload, 0); + put_u32_be(mdhd_payload, 1000); // timescale + put_u32_be(mdhd_payload, 1); // duration + put_u16_be(mdhd_payload, 0x55c4); // language 'und' + put_u16_be(mdhd_payload, 0); + auto mdhd = make_box("mdhd", mdhd_payload, /*full=*/true); + + 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 vmhd_payload; + put_u16_be(vmhd_payload, 0); + put_u16_be(vmhd_payload, 0); + put_u16_be(vmhd_payload, 0); + put_u16_be(vmhd_payload, 0); + auto vmhd = make_box("vmhd", vmhd_payload, /*full=*/true, /*version=*/0, /*flags=*/1); + + // cmpd + std::vector cmpd_payload; + put_u32_be(cmpd_payload, kNumComponents); + put_u16_be(cmpd_payload, heif_cmpd_component_type_red); + put_u16_be(cmpd_payload, heif_cmpd_component_type_green); + put_u16_be(cmpd_payload, heif_cmpd_component_type_blue); + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): 8-bit components, pixel interleave, 2x2 tiles + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, kNumComponents); + for (uint16_t c = 0; c < kNumComponents; c++) { + put_u16_be(uncC_payload, c); // 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(1); // interleave_type (pixel) + 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, kTileColumns - 1); + put_u32_be(uncC_payload, kTileRows - 1); + auto uncC = make_box("uncC", uncC_payload, /*full=*/true); + + // uncv VisualSampleEntry + std::vector uncv_payload; + for (int i = 0; i < 6; i++) { + uncv_payload.push_back(0); // reserved + } + put_u16_be(uncv_payload, 1); // data_reference_index + put_u16_be(uncv_payload, 0); // pre_defined + put_u16_be(uncv_payload, 0); // reserved + put_u32_be(uncv_payload, 0); // pre_defined + put_u32_be(uncv_payload, 0); + put_u32_be(uncv_payload, 0); + put_u16_be(uncv_payload, kWidth); + put_u16_be(uncv_payload, kHeight); + put_u32_be(uncv_payload, 0x00480000); // horizresolution 72 dpi + put_u32_be(uncv_payload, 0x00480000); // vertresolution 72 dpi + put_u32_be(uncv_payload, 0); // reserved + put_u16_be(uncv_payload, 1); // frame_count + for (int i = 0; i < 32; i++) { + uncv_payload.push_back(0); // compressorname + } + put_u16_be(uncv_payload, 0x0018); // depth + put_u16_be(uncv_payload, 0xFFFF); // pre_defined (-1) + append(uncv_payload, cmpd); + append(uncv_payload, uncC); + auto uncv = make_box("uncv", uncv_payload); + + std::vector stsd_payload; + put_u32_be(stsd_payload, 1); // entry_count + append(stsd_payload, uncv); + auto stsd = make_box("stsd", stsd_payload, /*full=*/true); + + std::vector stts_payload; + put_u32_be(stts_payload, 1); // entry_count + put_u32_be(stts_payload, 1); // sample_count + put_u32_be(stts_payload, 1); // sample_delta + auto stts = make_box("stts", stts_payload, /*full=*/true); + + std::vector stsc_payload; + put_u32_be(stsc_payload, 1); // entry_count + put_u32_be(stsc_payload, 1); // first_chunk + put_u32_be(stsc_payload, 1); // samples_per_chunk + put_u32_be(stsc_payload, 1); // sample_description_index + auto stsc = make_box("stsc", stsc_payload, /*full=*/true); + + std::vector stsz_payload; + put_u32_be(stsz_payload, kSampleSize); // fixed sample size + put_u32_be(stsz_payload, 1); // sample_count + auto stsz = make_box("stsz", stsz_payload, /*full=*/true); + + auto assemble_moov = [&](uint32_t chunk_offset) { + std::vector stco_payload; + put_u32_be(stco_payload, 1); // entry_count + put_u32_be(stco_payload, chunk_offset); + auto stco = make_box("stco", stco_payload, /*full=*/true); + + std::vector stbl_payload; + append(stbl_payload, stsd); + append(stbl_payload, stts); + append(stbl_payload, stsc); + append(stbl_payload, stsz); + append(stbl_payload, stco); + + std::vector minf_payload; + append(minf_payload, vmhd); + append(minf_payload, make_box("stbl", stbl_payload)); + + std::vector mdia_payload; + append(mdia_payload, mdhd); + append(mdia_payload, hdlr); + append(mdia_payload, make_box("minf", minf_payload)); + + std::vector trak_payload; + append(trak_payload, tkhd); + append(trak_payload, make_box("mdia", mdia_payload)); + + std::vector moov_payload; + append(moov_payload, mvhd); + append(moov_payload, make_box("trak", trak_payload)); + return make_box("moov", moov_payload); + }; + + // The chunk offset does not change the size of the moov box. + auto mdat_payload_offset = static_cast(ftyp.size() + assemble_moov(0).size() + 8 /* mdat header */); + auto moov = assemble_moov(mdat_payload_offset); + + // mdat: the tiles in raster order, each in pixel interleave + std::vector sample; + for (uint32_t tile_y = 0; tile_y < kTileRows; tile_y++) { + for (uint32_t tile_x = 0; tile_x < kTileColumns; tile_x++) { + for (uint32_t y = 0; y < kTileHeight; y++) { + for (uint32_t x = 0; x < kTileWidth; x++) { + for (uint32_t c = 0; c < kNumComponents; c++) { + sample.push_back(pixel_value(c, tile_x * kTileWidth + x, tile_y * kTileHeight + y)); + } + } + } + } + } + REQUIRE(sample.size() == kSampleSize); + + std::vector file; + append(file, ftyp); + append(file, moov); + append(file, make_box("mdat", sample)); + return file; +} + + +// A heif_reader on a memory buffer that counts the bytes read from the sample data. +struct CountingReader { + const std::vector* data = nullptr; + int64_t position = 0; + + int64_t watched_start = 0; + int64_t watched_end = 0; + uint64_t watched_bytes_read = 0; +}; + +int64_t reader_get_position(void* userdata) { + return static_cast(userdata)->position; +} + +int reader_read(void* dst, size_t size, void* userdata) { + auto* reader = static_cast(userdata); + auto file_size = static_cast(reader->data->size()); + auto read_end = reader->position + static_cast(size); + + if (read_end > file_size) { + return 1; + } + + memcpy(dst, reader->data->data() + reader->position, size); + + int64_t overlap_start = std::max(reader->position, reader->watched_start); + int64_t overlap_end = std::min(read_end, reader->watched_end); + if (overlap_end > overlap_start) { + reader->watched_bytes_read += static_cast(overlap_end - overlap_start); + } + + reader->position = read_end; + return 0; +} + +int reader_seek(int64_t position, void* userdata) { + auto* reader = static_cast(userdata); + if (position < 0 || position > static_cast(reader->data->size())) { + return 1; + } + + reader->position = position; + return 0; +} + +heif_reader_grow_status reader_wait_for_file_size(int64_t target_size, void* userdata) { + auto* reader = static_cast(userdata); + return (target_size <= static_cast(reader->data->size())) + ? heif_reader_grow_status_size_reached + : heif_reader_grow_status_size_beyond_eof; +} + +} // namespace + + +TEST_CASE("uncv sequence frame with tiles decodes each tile from its own data") { + std::vector file = build_tiled_uncv_sequence(); + + // The sample data is at the end of the file. + CountingReader counting_reader; + counting_reader.data = &file; + counting_reader.watched_start = static_cast(file.size() - kSampleSize); + counting_reader.watched_end = static_cast(file.size()); + + heif_reader reader{}; + reader.reader_api_version = 1; + reader.get_position = reader_get_position; + reader.read = reader_read; + reader.seek = reader_seek; + reader.wait_for_file_size = reader_wait_for_file_size; + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_reader(ctx, &reader, &counting_reader, nullptr); + INFO("read error: " << err.message); + 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); + + // Only count what the decoding reads. + counting_reader.watched_bytes_read = 0; + + heif_image* img = nullptr; + err = heif_track_decode_next_image(track, &img, heif_colorspace_RGB, heif_chroma_444, nullptr); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + const heif_channel channels[kNumComponents] = {heif_channel_R, heif_channel_G, heif_channel_B}; + + for (uint32_t c = 0; c < kNumComponents; c++) { + REQUIRE(heif_image_get_width(img, channels[c]) == kWidth); + REQUIRE(heif_image_get_height(img, channels[c]) == kHeight); + + int stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly(img, channels[c], &stride); + REQUIRE(plane != nullptr); + + for (uint32_t y = 0; y < kHeight; y++) { + for (uint32_t x = 0; x < kWidth; x++) { + INFO("component " << c << ", pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(plane[y * stride + x]) == static_cast(pixel_value(c, x, y))); + } + } + } + + // Before the fix, the whole sample was read once for each tile. + REQUIRE(counting_reader.watched_bytes_read == kSampleSize); + + heif_image_release(img); + heif_track_release(track); + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/tests/uncompressed_tile_data_fetch.cc libheif-1.23.6/tests/uncompressed_tile_data_fetch.cc --- libheif-1.23.4/tests/uncompressed_tile_data_fetch.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_tile_data_fetch.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,467 @@ +/* + 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-fcmw-5764-7rq8. +// +// An 'unci' image with generic compression and one compressed unit per image +// tile (cmpC compressed_unit_type == image_tile) combined with tile-component +// interleave (uncC interleave_type == 4) made unc_decoder::fetch_tile_data() +// fetch the tile data once per component. Since the image_tile branch of +// get_compressed_image_data_uncompressed() returns the whole unit irrespective +// of the requested range, the same unit was read and decompressed once for each +// component and all copies were concatenated: the tile buffer grew to +// num_components times the unit size, none of it charged against the memory +// limits. With 256 components and a unit that inflates to 20 MiB, a 22 kB file +// requested more than 5 GiB. +// +// The fix has four parts, each with a test below: +// +// 1. The unit is fetched once per tile. Because the excess memory was never +// accounted, it cannot be observed through the security limits. The test +// reads the file through a heif_reader that counts the bytes read from the +// compressed unit: they have to be read exactly once. It also checks the +// decoded pixels, as the unit holds the planes of all components. +// +// 2. The decompressor is told the size of the tile and stops with an error as +// soon as the unit yields more data than that. +// +// 3. A unit that yields less data than the tile is rejected, too. +// +// 4. The tile data is charged to the memory budget for as long as it is kept. +// This is independent of the generic compression, so the test uses an image +// without compression. A large row alignment pads each 16 byte row to 64 kiB. +// This makes the tile data much larger than the decoded image, so that it +// exceeds a memory budget that the decoded image itself fits into. +// +// The compressed files use deflate generic compression, so the tests only run +// 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 +#include +#include + +namespace { + +struct UnciFileSpec { + uint32_t width = 2; + uint32_t height = 2; + + // 'cmpd' component types. All components have 8 bits. + std::vector component_types = {heif_cmpd_component_type_red, + heif_cmpd_component_type_green, + heif_cmpd_component_type_blue}; + + uint8_t interleave_type = 4; // tile-component + uint32_t row_align_size = 0; + + // Whether 'item_data' is a single deflate compressed unit of type image_tile. + bool deflate_tile_unit = true; + + std::vector item_data; +}; + +// Tile data of the default 2x2 RGB image in tile-component interleave: +// the R plane, the G plane, the B plane. +const std::vector kTileData = { + 10, 11, 12, 13, // R + 20, 21, 22, 23, // G + 30, 31, 32, 33}; // B + +// Wrap the data into a raw deflate stream consisting of a single stored block. +std::vector deflate_stored(const std::vector& data) { + auto len = static_cast(data.size()); + auto nlen = static_cast(~len); + + std::vector out; + out.push_back(0x01); // BFINAL=1, BTYPE=00 (stored) + out.push_back(static_cast(len & 0xFF)); + out.push_back(static_cast(len >> 8)); + out.push_back(static_cast(nlen & 0xFF)); + out.push_back(static_cast(nlen >> 8)); + append(out, data); + return out; +} + +// Build an 'unci' image with a single tile. The item data is stored in 'idat', +// which is the last box of the file. +std::vector build_heif_unci(const UnciFileSpec& spec) { + 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); + + auto num_components = static_cast(spec.component_types.size()); + + // ispe + std::vector ispe_payload; + put_u32_be(ispe_payload, spec.width); + put_u32_be(ispe_payload, spec.height); + auto ispe = make_box("ispe", ispe_payload, /*full=*/true); + + // cmpd + std::vector cmpd_payload; + put_u32_be(cmpd_payload, num_components); + for (uint16_t type : spec.component_types) { + put_u16_be(cmpd_payload, type); + } + auto cmpd = make_box("cmpd", cmpd_payload); + + // uncC (v0): 8-bit components, a single tile. + std::vector uncC_payload; + put_u32_be(uncC_payload, 0); // profile + put_u32_be(uncC_payload, num_components); + for (uint16_t c = 0; c < num_components; c++) { + put_u16_be(uncC_payload, c); // 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(spec.interleave_type); + 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, spec.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); + + uint8_t num_properties = 3; + + if (spec.deflate_tile_unit) { + // cmpC: deflate, compressed_unit_type = image_tile (2). + std::vector cmpC_payload; + append_fourcc(cmpC_payload, "defl"); + cmpC_payload.push_back(2); + append(ipco_payload, make_box("cmpC", cmpC_payload, /*full=*/true)); + + // icef: one unit at the implied offset 0 with a 32-bit size field. + std::vector icef_payload; + icef_payload.push_back(3 << 2); // unit_offset_code = 0, unit_size_code = 3 + put_u32_be(icef_payload, 1); // num_compressed_units + put_u32_be(icef_payload, static_cast(spec.item_data.size())); + append(ipco_payload, make_box("icef", icef_payload, /*full=*/true)); + + num_properties = 5; + } + + 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(num_properties); // association_count + for (uint8_t i = 1; i <= num_properties; i++) { + ipma_payload.push_back(0x80 | i); // essential + } + 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); + + auto idat = make_box("idat", spec.item_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(spec.item_data.size())); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true, /*version=*/1); + + // idat has to be the last box: a test locates the item data at the end of the file. + 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; +} + + +// A heif_reader on a memory buffer that counts the bytes read from one byte range. +struct CountingReader { + const std::vector* data = nullptr; + int64_t position = 0; + + int64_t watched_start = 0; + int64_t watched_end = 0; + uint64_t watched_bytes_read = 0; +}; + +int64_t reader_get_position(void* userdata) { + return static_cast(userdata)->position; +} + +int reader_read(void* dst, size_t size, void* userdata) { + auto* reader = static_cast(userdata); + auto file_size = static_cast(reader->data->size()); + auto read_end = reader->position + static_cast(size); + + if (read_end > file_size) { + return 1; + } + + memcpy(dst, reader->data->data() + reader->position, size); + + int64_t overlap_start = std::max(reader->position, reader->watched_start); + int64_t overlap_end = std::min(read_end, reader->watched_end); + if (overlap_end > overlap_start) { + reader->watched_bytes_read += static_cast(overlap_end - overlap_start); + } + + reader->position = read_end; + return 0; +} + +int reader_seek(int64_t position, void* userdata) { + auto* reader = static_cast(userdata); + if (position < 0 || position > static_cast(reader->data->size())) { + return 1; + } + + reader->position = position; + return 0; +} + +heif_reader_grow_status reader_wait_for_file_size(int64_t target_size, void* userdata) { + auto* reader = static_cast(userdata); + return (target_size <= static_cast(reader->data->size())) + ? heif_reader_grow_status_size_reached + : heif_reader_grow_status_size_beyond_eof; +} + + +// Read the file from memory and decode its primary image without any color conversion. +// 'max_total_memory' replaces the default memory budget unless it is 0. +heif_error decode_primary_image(const std::vector& file, uint64_t max_total_memory = 0) { + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + if (max_total_memory != 0) { + heif_context_get_security_limits(ctx)->max_total_memory = max_total_memory; + } + + heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); + INFO("read error: " << err.message); + 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((img != nullptr) == (err.code == heif_error_Ok)); + + // The error message is owned by the context. The tests only compare the error codes. + err.message = ""; + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); + + return err; +} + +} // namespace + +TEST_CASE("unci image_tile compressed unit is fetched once for tile-component interleave") { + UnciFileSpec spec; + spec.item_data = deflate_stored(kTileData); + std::vector file = build_heif_unci(spec); + + CountingReader counting_reader; + counting_reader.data = &file; + counting_reader.watched_start = static_cast(file.size() - spec.item_data.size()); + counting_reader.watched_end = static_cast(file.size()); + + heif_reader reader{}; + reader.reader_api_version = 1; + reader.get_position = reader_get_position; + reader.read = reader_read; + reader.seek = reader_seek; + reader.wait_for_file_size = reader_wait_for_file_size; + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_reader(ctx, &reader, &counting_reader, nullptr); + INFO("read error: " << err.message); + 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); + + // Only count what the decoding reads. + counting_reader.watched_bytes_read = 0; + + heif_image* img = nullptr; + err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_444, nullptr); + INFO("decode error: " << err.message); + REQUIRE(err.code == heif_error_Ok); + REQUIRE(img != nullptr); + + // Before the fix, the unit was read (and decompressed) once per component. + REQUIRE(counting_reader.watched_bytes_read == spec.item_data.size()); + + const heif_channel channels[3] = {heif_channel_R, heif_channel_G, heif_channel_B}; + + for (uint32_t c = 0; c < 3; c++) { + REQUIRE(heif_image_get_width(img, channels[c]) == static_cast(spec.width)); + REQUIRE(heif_image_get_height(img, channels[c]) == static_cast(spec.height)); + + int stride = 0; + const uint8_t* plane = heif_image_get_plane_readonly(img, channels[c], &stride); + REQUIRE(plane != nullptr); + + for (uint32_t y = 0; y < spec.height; y++) { + for (uint32_t x = 0; x < spec.width; x++) { + INFO("component " << c << ", pixel (" << x << "," << y << ")"); + REQUIRE(static_cast(plane[y * stride + x]) == + static_cast(kTileData[c * spec.width * spec.height + y * spec.width + x])); + } + } + } + + heif_image_release(img); + heif_image_handle_release(handle); + heif_context_free(ctx); +} + + +TEST_CASE("unci image_tile compressed unit larger than the tile is rejected") { + std::vector oversized_tile_data = kTileData; + oversized_tile_data.push_back(0); + + UnciFileSpec spec; + spec.item_data = deflate_stored(oversized_tile_data); + + heif_error err = decode_primary_image(build_heif_unci(spec)); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_Decompression_invalid_data); +} + + +TEST_CASE("unci image_tile compressed unit smaller than the tile is rejected") { + std::vector short_tile_data = kTileData; + short_tile_data.pop_back(); + + UnciFileSpec spec; + spec.item_data = deflate_stored(short_tile_data); + + heif_error err = decode_primary_image(build_heif_unci(spec)); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(err.subcode == heif_suberror_End_of_data); +} + + +TEST_CASE("unci tile data is charged to the memory budget") { + // Each row of a component has 16 bytes of pixel data and is padded to 64 kiB. + // The decoded image has three planes of 256 bytes. The tile data has 1 MiB for + // each of the three components. + constexpr uint32_t size = 16; + constexpr uint32_t row_size = 65536; + constexpr uint32_t num_components = 3; + + UnciFileSpec spec; + spec.width = size; + spec.height = size; + spec.row_align_size = row_size; + spec.deflate_tile_unit = false; + spec.item_data.assign(size * row_size * num_components, 0); + + // component interleave (single read) and tile-component interleave (one read per component) + for (uint8_t interleave_type : {uint8_t{0}, uint8_t{4}}) { + INFO("interleave_type: " << static_cast(interleave_type)); + + spec.interleave_type = interleave_type; + std::vector file = build_heif_unci(spec); + + // The image decodes with the default limits. + heif_error err = decode_primary_image(file); + REQUIRE(err.code == heif_error_Ok); + + // A budget of 2 MiB is plenty for the decoded image, but not enough for the tile data. + err = decode_primary_image(file, 2 * 1024 * 1024); + REQUIRE(err.code == heif_error_Memory_allocation_error); + REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded); + } +} diff -Nru libheif-1.23.4/tests/uncompressed_tile_range_overflow.cc libheif-1.23.6/tests/uncompressed_tile_range_overflow.cc --- libheif-1.23.4/tests/uncompressed_tile_range_overflow.cc 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/tests/uncompressed_tile_range_overflow.cc 2026-10-05 16:16:00.000000000 +0000 @@ -27,26 +27,32 @@ // 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: +// A crafted 'unci' image advertises a 4096x4096 grid of 1x1 tiles. Large alignment +// values make the computed size of a tile 2^40 bytes, so for the final tile index +// (2^24 - 1) the range of the tile data wraps around: // // 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 +// With generic (zlib, full_item) compression, unc_decoder cut this range out of the +// decompressed item. Its 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 tests build the fixture in memory, open it (structurally valid, must succeed) +// and request the last tile: decoding must return a structured error instead of +// crashing. // -// The file uses real zlib generic compression, so the test only runs when the +// - Without generic compression, the tile range is read from the item data. +// +// - With full_item compression, the tiles cannot be decoded independently and the +// image is exposed as a single tile (GHSA-6fqc-p7r8-2g36), so the last uncC tile +// is not accessible anymore. The single tile is the whole image and has to be +// rejected as well. +// +// One file uses real zlib generic compression, so the tests only run when the // library was built with zlib (guarded in tests/CMakeLists.txt). #include "catch_amalgamated.hpp" @@ -68,9 +74,9 @@ 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() { +// Build a minimal HEIF file with a single 'unci' item and a 4096x4096 tile grid, +// optionally using generic zlib (full_item) compression. +std::vector build_heif_unci_overflow_tiling(bool generic_compression) { std::vector ftyp_payload; append_fourcc(ftyp_payload, "mif1"); put_u32_be(ftyp_payload, 0); @@ -149,17 +155,21 @@ append(ipco_payload, ispe); append(ipco_payload, cmpd); append(ipco_payload, uncC); - append(ipco_payload, cmpC); + if (generic_compression) { + 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(generic_compression ? 4 : 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 - ipma_payload.push_back(0x80 | 4); // essential, cmpC + if (generic_compression) { + ipma_payload.push_back(0x80 | 4); // essential, cmpC + } auto ipma = make_box("ipma", ipma_payload, /*full=*/true); std::vector iprp_payload; @@ -167,7 +177,8 @@ append(iprp_payload, ipma); auto iprp = make_box("iprp", iprp_payload); - // idat: the zlib payload (decompresses to 1 byte). + // idat: the zlib payload (decompresses to 1 byte). Without generic compression, + // these are 9 bytes of image data. auto idat = make_box("idat", kZlibOneByte); // iloc (version 1): item 1 stored in idat (construction_method=1). @@ -197,16 +208,13 @@ return file; } -} // namespace - -TEST_CASE("unci tile range overflow returns error instead of crashing") { - std::vector file = build_heif_unci_overflow_tiling(); - +// Open the file and get the handle of its primary image. +void open_primary_image(const std::vector& file, heif_context** out_ctx, heif_image_handle** out_handle) { 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. + // reachable by then decoding a tile. heif_error err = heif_context_read_from_memory_without_copy(ctx, file.data(), file.size(), nullptr); REQUIRE(err.code == heif_error_Ok); @@ -215,25 +223,88 @@ REQUIRE(err.code == heif_error_Ok); REQUIRE(handle != nullptr); + *out_ctx = ctx; + *out_handle = handle; +} + +} // namespace + +TEST_CASE("unci tile range overflow returns error instead of crashing") { + std::vector file = build_heif_unci_overflow_tiling(/*generic_compression=*/false); + + heif_context* ctx = nullptr; + heif_image_handle* handle = nullptr; + open_primary_image(file, &ctx, &handle); + 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. + // Request the last advertised tile. Its data range wraps around the 64 bit range. + // This must 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); + heif_error 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); +} + + +TEST_CASE("unci tile range overflow with full_item compression returns error instead of crashing") { + std::vector file = build_heif_unci_overflow_tiling(/*generic_compression=*/true); + + heif_context* ctx = nullptr; + heif_image_handle* handle = nullptr; + open_primary_image(file, &ctx, &handle); + + // The uncC tiles cannot be decoded independently, so the image is a single tile. + heif_image_tiling tiling; + heif_image_handle_get_image_tiling(handle, 1, &tiling); + REQUIRE(tiling.num_columns == 1); + REQUIRE(tiling.num_rows == 1); + REQUIRE(tiling.tile_width == WIDTH); + REQUIRE(tiling.tile_height == HEIGHT); + + heif_decoding_options* options = heif_decoding_options_alloc(); + + for (bool ignore_transformations : {false, true}) { + INFO("ignore_transformations: " << ignore_transformations); + options->ignore_transformations = ignore_transformations; + + // Before the image was exposed as a single tile, requesting the last uncC tile + // reached memcpy() with a wrapped source pointer and a 1 TiB length + // (out-of-bounds read / SIGSEGV). The tile does not exist anymore. + heif_image* img = nullptr; + heif_error err = heif_image_handle_decode_image_tile(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + options, + TILE_COLS - 1, TILE_ROWS - 1); + REQUIRE(err.code == heif_error_Usage_error); + REQUIRE(img == nullptr); + + // The single tile is the whole image, which has to be rejected because of its + // alignment values. + err = heif_image_handle_decode_image_tile(handle, &img, + heif_colorspace_undefined, heif_chroma_undefined, + options, + 0, 0); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(img == nullptr); } + + heif_decoding_options_free(options); + + heif_image* img = nullptr; + heif_error err = heif_decode_image(handle, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr); + REQUIRE(err.code == heif_error_Invalid_input); + REQUIRE(img == nullptr); + heif_image_handle_release(handle); heif_context_free(ctx); } diff -Nru libheif-1.23.4/tests/write_to_file.cc libheif-1.23.6/tests/write_to_file.cc --- libheif-1.23.4/tests/write_to_file.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/write_to_file.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,179 @@ +/* + 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. +*/ + +// heif_context_write_to_file() did not notice when the file could not be written and +// reported success. An application like heif-enc then ended without any message and without +// an output file. +// +// What is written does not matter here. The tests encode a small image with any encoder that +// is available, since a context without images cannot be written at all. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "test_utils.h" + +#include +#include +#include +#include + +namespace { + +struct WriteResult +{ + heif_error_code code; + heif_suberror_code subcode; + std::string message; +}; + +heif_compression_format pick_encoder_format() +{ + for (heif_compression_format format : {heif_compression_AV1, + heif_compression_HEVC, + heif_compression_JPEG, + heif_compression_uncompressed}) { + if (heif_have_encoder_for_format(format)) { + return format; + } + } + + return heif_compression_undefined; +} + +void encode_image(heif_context* ctx, heif_compression_format format) +{ + const uint32_t size = 64; + + heif_image* img = nullptr; + heif_error err = heif_image_create(size, size, heif_colorspace_YCbCr, heif_chroma_420, &img); + REQUIRE(err.code == heif_error_Ok); + + for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + const uint32_t plane_size = (channel == heif_channel_Y ? size : size / 2); + err = heif_image_add_plane(img, channel, plane_size, plane_size, 8); + REQUIRE(err.code == heif_error_Ok); + + size_t stride = 0; + uint8_t* p = heif_image_get_plane2(img, channel, &stride); + for (uint32_t y = 0; y < plane_size; y++) { + std::fill(p + y * stride, p + y * stride + plane_size, uint8_t{128}); + } + } + + heif_encoder* encoder = nullptr; + err = heif_context_get_encoder_for_format(ctx, format, &encoder); + REQUIRE(err.code == heif_error_Ok); + + err = heif_context_encode_image(ctx, img, encoder, nullptr, nullptr); + INFO("encoding error: " << (err.message ? err.message : "")); + REQUIRE(err.code == heif_error_Ok); + + heif_encoder_release(encoder); + heif_image_release(img); +} + +// heif_error::message may point into the context, so it is copied before the context is freed. +WriteResult write_to_file(const std::string& filename, heif_compression_format format) +{ + heif_context* ctx = heif_context_alloc(); + encode_image(ctx, format); + + heif_error err = heif_context_write_to_file(ctx, filename.c_str()); + WriteResult result{err.code, err.subcode, err.message ? err.message : ""}; + + heif_context_free(ctx); + return result; +} + +} // namespace + + +TEST_CASE("heif_context_write_to_file writes the file") +{ + const heif_compression_format format = pick_encoder_format(); + if (format == heif_compression_undefined) { + SKIP("no encoder available"); + } + + const std::string filename = get_tests_output_file_path("write_to_file.heif"); + + std::error_code ignored; + std::filesystem::remove(filename, ignored); + + WriteResult result = write_to_file(filename, format); + INFO("error (" << result.code << "/" << result.subcode << "): " << result.message); + REQUIRE(result.code == heif_error_Ok); + + REQUIRE(std::filesystem::exists(filename)); + CHECK(std::filesystem::file_size(filename) > 0); +} + + +TEST_CASE("heif_context_write_to_file reports a file that cannot be opened") +{ + const heif_compression_format format = pick_encoder_format(); + if (format == heif_compression_undefined) { + SKIP("no encoder available"); + } + + // a file in a directory that does not exist + const std::string filename = get_tests_output_file_path("no-such-directory/write_to_file.heif"); + REQUIRE_FALSE(std::filesystem::exists(std::filesystem::path(filename).parent_path())); + + WriteResult result = write_to_file(filename, format); + INFO("error (" << result.code << "/" << result.subcode << "): " << result.message); + + CHECK(result.code == heif_error_Encoding_error); + CHECK(result.subcode == heif_suberror_Cannot_write_output_data); + + // The message names the file. + CHECK(result.message.find(filename) != std::string::npos); + + CHECK_FALSE(std::filesystem::exists(filename)); +} + + +TEST_CASE("heif_context_write_to_file reports a failed write") +{ + const heif_compression_format format = pick_encoder_format(); + if (format == heif_compression_undefined) { + SKIP("no encoder available"); + } + + // The device /dev/full can be opened, and every write to it fails like a write to a full disk. + const std::string filename = "/dev/full"; + + std::error_code error; + if (!std::filesystem::is_character_file(filename, error)) { + SKIP("this system has no /dev/full"); + } + + WriteResult result = write_to_file(filename, format); + INFO("error (" << result.code << "/" << result.subcode << "): " << result.message); + + CHECK(result.code == heif_error_Encoding_error); + CHECK(result.subcode == heif_suberror_Cannot_write_output_data); +} diff -Nru libheif-1.23.4/tests/zero_length_memcpy.cc libheif-1.23.6/tests/zero_length_memcpy.cc --- libheif-1.23.4/tests/zero_length_memcpy.cc 1970-01-01 00:00:00.000000000 +0000 +++ libheif-1.23.6/tests/zero_length_memcpy.cc 2026-10-05 16:16:00.000000000 +0000 @@ -0,0 +1,144 @@ +/* + libheif regression tests for zero-length reads and copies into empty buffers + (GHSA-2764-mqj2-c458). + + 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. +*/ + +// A box with an empty payload is read into an empty std::vector, whose data() pointer is +// NULL. The same happens for an item whose iloc extent has length zero. Passing that NULL +// pointer to memcpy() is undefined behaviour (until C2y, WG14 N3322) and is reported by +// UBSan's nonnull-attribute check. These tests only detect a regression when they are run +// in a build with -fsanitize=undefined; in a plain build they merely exercise the paths. + +#include "catch_amalgamated.hpp" +#include "libheif/heif.h" +#include "libheif/heif_items.h" +#include "test_utils.h" + +#include +#include + +namespace { + +// ftyp + meta, where meta holds an unknown box with an empty payload (parsed as Box_other) +// and one item whose single iloc extent has length zero. +std::vector build_heif_with_empty_box_and_empty_item() +{ + std::vector ftyp_payload; + append_fourcc(ftyp_payload, "mif1"); + put_u32_be(ftyp_payload, 0); + append_fourcc(ftyp_payload, "mif1"); + auto ftyp = make_box("ftyp", ftyp_payload); + + // hdlr: handler type 'null' so that no image items are required. + std::vector hdlr_payload; + put_u32_be(hdlr_payload, 0); // pre_defined + append_fourcc(hdlr_payload, "null"); // handler_type + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + put_u32_be(hdlr_payload, 0); + hdlr_payload.push_back(0); // name + auto hdlr = make_box("hdlr", hdlr_payload, /*full=*/true); + + // infe (version 2): item 1, unprotected, item type 'test', empty name + std::vector infe_payload; + put_u16_be(infe_payload, 1); + put_u16_be(infe_payload, 0); + append_fourcc(infe_payload, "test"); + 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); + + // iloc (version 0): offset_size=4, length_size=4, base_offset_size=0, + // one item with a single extent of length 0 + std::vector iloc_payload; + iloc_payload.push_back(0x44); + iloc_payload.push_back(0x00); + put_u16_be(iloc_payload, 1); // item_count + put_u16_be(iloc_payload, 1); // item_ID + 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 + put_u32_be(iloc_payload, 0); // extent_length + auto iloc = make_box("iloc", iloc_payload, /*full=*/true); + + // Unknown box type with a header only. Box_other::parse() reads its zero-length payload. + auto empty_box = make_box("zzzz", {}); + + std::vector meta_payload; + append(meta_payload, hdlr); + append(meta_payload, iinf); + append(meta_payload, iloc); + append(meta_payload, empty_box); + auto meta = make_box("meta", meta_payload, /*full=*/true); + + std::vector file; + append(file, ftyp); + append(file, meta); + return file; +} + +} // namespace + + +TEST_CASE("zero-length box payload and item data do not pass NULL to memcpy") { + auto data = build_heif_with_empty_box_and_empty_item(); + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + // Read with a copy so that the memory-backed StreamReader is used for both the + // input copy and every box payload read. + heif_error err = heif_context_read_from_memory(ctx, data.data(), data.size(), nullptr); + REQUIRE(err.code == heif_error_Ok); + + REQUIRE(heif_context_get_number_of_items(ctx) == 1); + + uint8_t* item_data = nullptr; + size_t item_data_size = 123; + err = heif_item_get_item_data(ctx, 1, nullptr, &item_data, &item_data_size); + REQUIRE(err.code == heif_error_Ok); + CHECK(item_data_size == 0); + CHECK(item_data != nullptr); + heif_release_item_data(ctx, &item_data); + + heif_context_free(ctx); +} + + +TEST_CASE("reading an empty memory buffer does not pass NULL to memcpy") { + std::vector empty; + + heif_context* ctx = heif_context_alloc(); + REQUIRE(ctx != nullptr); + + heif_error err = heif_context_read_from_memory(ctx, empty.data(), 0, nullptr); + CHECK(err.code != heif_error_Ok); + + heif_context_free(ctx); +} diff -Nru libheif-1.23.4/third-party/aom.cmd libheif-1.23.6/third-party/aom.cmd --- libheif-1.23.4/third-party/aom.cmd 2026-09-06 14:45:17.000000000 +0000 +++ libheif-1.23.6/third-party/aom.cmd 2026-10-05 16:16:00.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 v3.15.0 --depth 1 https://aomedia.googlesource.com/aom +git clone -b v3.15.1 --depth 1 https://aomedia.googlesource.com/aom cd aom