Commit 215cbdad for libheif

commit 215cbdadef517b43b30889eb277ca1ba14b09bfa
Author: 21Z121Z1 <89170834+21Z121Z1@users.noreply.github.com>
Date:   Sat Oct 3 20:04:52 2026 +0800

    Fix 4:2:0 bilinear chroma border indexing

    The border loops use chroma-sample coordinates. An extra division by two selects the wrong Cb and Cr samples after the first chroma interval.

    Use cx and cy directly. Extend the bilinear upsampling test to cover all four borders.

diff --git a/libheif/color-conversion/chroma_sampling.cc b/libheif/color-conversion/chroma_sampling.cc
index b93a8095..a38139e2 100644
--- a/libheif/color-conversion/chroma_sampling.cc
+++ b/libheif/color-conversion/chroma_sampling.cc
@@ -592,10 +592,10 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt

   // 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);
+    out_cb[0 * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[cx] + 1 * in_cb[cx + 1] + 2) / 4);
+    out_cb[0 * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[cx] + 3 * in_cb[cx + 1] + 2) / 4);
+    out_cr[0 * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[cx] + 1 * in_cr[cx + 1] + 2) / 4);
+    out_cr[0 * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[cx] + 3 * in_cr[cx + 1] + 2) / 4);
   }

   // top right corner
@@ -606,10 +606,10 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt

   // 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);
+    out_cb[(2 * cy + 1) * out_cb_stride + 0] = (Pixel) ((3 * in_cb[cy * in_cb_stride] + 1 * in_cb[(cy + 1) * in_cb_stride] + 2) / 4);
+    out_cb[(2 * cy + 2) * out_cb_stride + 0] = (Pixel) ((1 * in_cb[cy * in_cb_stride] + 3 * in_cb[(cy + 1) * in_cb_stride] + 2) / 4);
+    out_cr[(2 * cy + 1) * out_cr_stride + 0] = (Pixel) ((3 * in_cr[cy * in_cr_stride] + 1 * in_cr[(cy + 1) * in_cr_stride] + 2) / 4);
+    out_cr[(2 * cy + 2) * out_cr_stride + 0] = (Pixel) ((1 * in_cr[cy * in_cr_stride] + 3 * in_cr[(cy + 1) * in_cr_stride] + 2) / 4);
   }

   // bottom left corner
@@ -621,20 +621,20 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
   // 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);
+      out_cb[(2 * cy + 1) * out_cb_stride + width - 1] = (Pixel) ((3 * in_cb[cy * in_cb_stride + width / 2 - 1] + 1 * in_cb[(cy + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
+      out_cb[(2 * cy + 2) * out_cb_stride + width - 1] = (Pixel) ((1 * in_cb[cy * in_cb_stride + width / 2 - 1] + 3 * in_cb[(cy + 1) * in_cb_stride + width / 2 - 1] + 2) / 4);
+      out_cr[(2 * cy + 1) * out_cr_stride + width - 1] = (Pixel) ((3 * in_cr[cy * in_cr_stride + width / 2 - 1] + 1 * in_cr[(cy + 1) * in_cr_stride + width / 2 - 1] + 2) / 4);
+      out_cr[(2 * cy + 2) * out_cr_stride + width - 1] = (Pixel) ((1 * in_cr[cy * in_cr_stride + width / 2 - 1] + 3 * in_cr[(cy + 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);
+      out_cb[(height - 1) * out_cb_stride + 2 * cx + 1] = (Pixel) ((3 * in_cb[(height / 2 - 1) * in_cb_stride + cx] + 1 * in_cb[(height / 2 - 1) * in_cb_stride + cx + 1] + 2) / 4);
+      out_cb[(height - 1) * out_cb_stride + 2 * cx + 2] = (Pixel) ((1 * in_cb[(height / 2 - 1) * in_cb_stride + cx] + 3 * in_cb[(height / 2 - 1) * in_cb_stride + cx + 1] + 2) / 4);
+      out_cr[(height - 1) * out_cr_stride + 2 * cx + 1] = (Pixel) ((3 * in_cr[(height / 2 - 1) * in_cr_stride + cx] + 1 * in_cr[(height / 2 - 1) * in_cr_stride + cx + 1] + 2) / 4);
+      out_cr[(height - 1) * out_cr_stride + 2 * cx + 2] = (Pixel) ((1 * in_cr[(height / 2 - 1) * in_cr_stride + cx] + 3 * in_cr[(height / 2 - 1) * in_cr_stride + cx + 1] + 2) / 4);
     }
   }

diff --git a/tests/conversion.cc b/tests/conversion.cc
index 8d6a95f1..7456151f 100644
--- a/tests/conversion.cc
+++ b/tests/conversion.cc
@@ -693,17 +693,19 @@ TEST_CASE("Bilinear upsampling", "[heif_image]")
       .only_use_preferred_chroma_algorithm = true};

   std::shared_ptr<HeifPixelImage> img = std::make_shared<HeifPixelImage>();
-  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());
@@ -712,19 +714,22 @@ TEST_CASE("Bilinear upsampling", "[heif_image]")

   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, 200,
-                   204, 190, 163, 150,
-                   101, 88, 63, 50,
-                   50, 38, 13, 0
+                   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
                });
 }