Commit 3dc45d73 for libheif

commit 3dc45d73f3935f9cbd36abd1baf50ba3640904d6
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Mon Apr 13 14:52:13 2026 +0200

    add tests for alpha bpp conversion (#1673)

diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index 819fa6e4..184f0038 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -604,6 +604,9 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
     output_state.bits_per_pixel = 10;
   }

+  // Output alpha should always match the output color BPP
+  output_state.alpha_bits_per_pixel = output_state.bits_per_pixel;
+
   ColorConversionPipeline pipeline;
   bool success = pipeline.construct_pipeline(input_state, output_state, options, *options_ext);
   if (!success) {
diff --git a/tests/conversion.cc b/tests/conversion.cc
index 6d016fb3..5ebd6640 100644
--- a/tests/conversion.cc
+++ b/tests/conversion.cc
@@ -756,3 +756,133 @@ TEST_CASE("RGB 5-6-5 to RGB")
   assert_plane(out, heif_channel_G, {28, 32, 36, 40, 44, 48});
   assert_plane(out, heif_channel_B, {107, 115, 123, 132, 140, 148});
 }
+
+
+TEST_CASE("Mismatched alpha bit depth - pipeline construction") {
+  heif_color_conversion_options options{};
+  options.preferred_chroma_downsampling_algorithm = heif_chroma_downsampling_average;
+  options.preferred_chroma_upsampling_algorithm = heif_chroma_upsampling_bilinear;
+
+  std::unique_ptr<heif_color_conversion_options_ext, void(*)(heif_color_conversion_options_ext*)>
+      options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free);
+
+  SECTION("10-bit color, 8-bit alpha -> interleaved RGBA 8-bit") {
+    ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 10);
+    input_state.alpha_bits_per_pixel = 8;
+    nclx_default_if_undefined(input_state);
+
+    ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8);
+
+    ColorConversionPipeline pipeline;
+    bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext);
+    INFO("pipeline: " << pipeline.debug_dump_pipeline());
+    REQUIRE(supported);
+  }
+
+  SECTION("8-bit color, 10-bit alpha -> interleaved RGBA 8-bit") {
+    ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 8);
+    input_state.alpha_bits_per_pixel = 10;
+    nclx_default_if_undefined(input_state);
+
+    ColorState target_state(heif_colorspace_RGB, heif_chroma_interleaved_RGBA, true, 8);
+
+    ColorConversionPipeline pipeline;
+    bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext);
+    INFO("pipeline: " << pipeline.debug_dump_pipeline());
+    REQUIRE(supported);
+  }
+
+  SECTION("10-bit color, 8-bit alpha -> planar RGB 10-bit") {
+    ColorState input_state(heif_colorspace_YCbCr, heif_chroma_420, true, 10);
+    input_state.alpha_bits_per_pixel = 8;
+    nclx_default_if_undefined(input_state);
+
+    ColorState target_state(heif_colorspace_RGB, heif_chroma_444, true, 10);
+    target_state.alpha_bits_per_pixel = 10;
+
+    ColorConversionPipeline pipeline;
+    bool supported = pipeline.construct_pipeline(input_state, target_state, options, *options_ext);
+    INFO("pipeline: " << pipeline.debug_dump_pipeline());
+    REQUIRE(supported);
+  }
+}
+
+
+TEST_CASE("Mismatched alpha bit depth - conversion correctness") {
+  heif_color_conversion_options options{};
+
+  SECTION("10-bit RGB color with 8-bit alpha -> interleaved RGBA 8-bit") {
+    const uint32_t width = 4;
+    const uint32_t height = 2;
+
+    auto img = std::make_shared<HeifPixelImage>();
+    img->create(width, height, heif_colorspace_RGB, heif_chroma_444);
+
+    // Create 10-bit color planes filled with a known value (512 = mid-range for 10-bit)
+    img->fill_new_plane(heif_channel_R, 512, width, height, 10, nullptr);
+    img->fill_new_plane(heif_channel_G, 256, width, height, 10, nullptr);
+    img->fill_new_plane(heif_channel_B, 768, width, height, 10, nullptr);
+    // Create 8-bit alpha plane filled with 200
+    img->fill_new_plane(heif_channel_Alpha, 200, width, height, 8, nullptr);
+
+    // Verify the mismatch
+    REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 10);
+    REQUIRE(img->get_bits_per_pixel(heif_channel_Alpha) == 8);
+
+    auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA,
+                                     nclx_profile::defaults(), 8, options, nullptr,
+                                     heif_get_disabled_security_limits());
+    REQUIRE(result);
+    auto out = *result;
+
+    CHECK(out->get_colorspace() == heif_colorspace_RGB);
+    CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGBA);
+
+    // Verify the output has correct interleaved pixel values
+    size_t stride;
+    const uint8_t* p = out->get_plane(heif_channel_interleaved, &stride);
+    REQUIRE(p != nullptr);
+
+    // Check first pixel: R=512>>2=128, G=256>>2=64, B=768>>2=192, A=200
+    CHECK(p[0] == 128);  // R
+    CHECK(p[1] == 64);   // G
+    CHECK(p[2] == 192);  // B
+    CHECK(p[3] == 200);  // A (preserved from 8-bit alpha)
+  }
+
+  SECTION("8-bit RGB color with 10-bit alpha -> interleaved RGBA 8-bit") {
+    const uint32_t width = 4;
+    const uint32_t height = 2;
+
+    auto img = std::make_shared<HeifPixelImage>();
+    img->create(width, height, heif_colorspace_RGB, heif_chroma_444);
+
+    // Create 8-bit color planes
+    img->fill_new_plane(heif_channel_R, 128, width, height, 8, nullptr);
+    img->fill_new_plane(heif_channel_G, 64, width, height, 8, nullptr);
+    img->fill_new_plane(heif_channel_B, 192, width, height, 8, nullptr);
+    // Create 10-bit alpha plane (value 800 -> 800>>2 = 200 when converted to 8-bit)
+    img->fill_new_plane(heif_channel_Alpha, 800, width, height, 10, nullptr);
+
+    REQUIRE(img->get_bits_per_pixel(heif_channel_R) == 8);
+    REQUIRE(img->get_bits_per_pixel(heif_channel_Alpha) == 10);
+
+    auto result = convert_colorspace(img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA,
+                                     nclx_profile::defaults(), 8, options, nullptr,
+                                     heif_get_disabled_security_limits());
+    REQUIRE(result);
+    auto out = *result;
+
+    CHECK(out->get_colorspace() == heif_colorspace_RGB);
+    CHECK(out->get_chroma_format() == heif_chroma_interleaved_RGBA);
+
+    size_t stride;
+    const uint8_t* p = out->get_plane(heif_channel_interleaved, &stride);
+    REQUIRE(p != nullptr);
+
+    CHECK(p[0] == 128);  // R (unchanged)
+    CHECK(p[1] == 64);   // G (unchanged)
+    CHECK(p[2] == 192);  // B (unchanged)
+    CHECK(p[3] == 200);  // A (10-bit 800 >> 2 = 200)
+  }
+}