Commit e379fd51 for libheif

commit e379fd5139cfde94913c3db5275ec49df9adbb19
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Sep 19 14:22:25 2026 +0200

    Track one bit depth per plane in ColorState instead of a single image-wide value

    ColorState carried a single bits_per_pixel for all colour planes, plus has_alpha
    and alpha_bits_per_pixel. That cannot describe images whose planes have different
    depths (an 'unci' item declares a depth per component), so operators that
    reinterpret several planes through one sample type had no way to decline such
    input in state_after_conversion() and had to check at runtime instead.

    Replace the three fields with one depth per plane (R, G, B, Y, Cb, Cr, alpha and
    the Bayer filter array), where 0 means the plane does not exist. has_alpha becomes
    a query on the alpha depth. Interleaved RGB formats are represented by their
    per-component depth in R/G/B (and alpha), so operators see the same fields as for
    planar RGB.

    New helpers: set_color_bits_per_pixel() fills the planes of the current
    colorspace/chroma, get_color_bits_per_pixel() returns the first colour plane (the
    old single value), and color_channels_have_same_bpp(), all_channels_have_same_bpp(),
    all_channels_sdr() and all_channels_hdr() let operators state their per-plane
    requirements. has_samples_wider_than_16bit() now checks every plane.

    convert_colorspace() keeps the input's per-plane depths untouched when the target
    has the same colorspace and chroma and no explicit depth, so an identity request
    on an image with differing plane depths (e.g. RGB 5/6/5) stays a no-op.

    Behaviour is unchanged for images whose planes share one depth. The helpers are
    not used by any operator yet.

diff --git a/libheif/color-conversion/alpha.cc b/libheif/color-conversion/alpha.cc
index 0d2478e0..e9483eda 100644
--- a/libheif/color-conversion/alpha.cc
+++ b/libheif/color-conversion/alpha.cc
@@ -71,8 +71,8 @@ Op_drop_alpha_plane::state_after_conversion(const ColorState& input_state,
        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 {};
   }

@@ -87,7 +87,7 @@ Op_drop_alpha_plane::state_after_conversion(const ColorState& input_state,
   // --- 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);

@@ -136,10 +136,10 @@ Op_flatten_alpha_plane<Pixel>::state_after_conversion(const ColorState& input_st
 {
   bool hdr = !std::is_same<Pixel, uint8_t>::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.
+  // TODO: this Op only works when all channels are either HDR or all are SDR
+  //       (see ColorState::all_channels_sdr() / all_channels_hdr()).

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.get_color_bits_per_pixel() > 8) != hdr) {
     return {};
   }

@@ -147,7 +147,7 @@ Op_flatten_alpha_plane<Pixel>::state_after_conversion(const ColorState& input_st
     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.get_color_bits_per_pixel()) {
     return {};
   }

@@ -157,8 +157,8 @@ Op_flatten_alpha_plane<Pixel>::state_after_conversion(const ColorState& input_st
        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 +173,7 @@ Op_flatten_alpha_plane<Pixel>::state_after_conversion(const ColorState& input_st
   // --- 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);

@@ -200,7 +200,7 @@ Op_flatten_alpha_plane<Pixel>::convert_colorspace(const std::shared_ptr<const He
                                                                                    heif_colorspace_RGB,
                                                                                    heif_chroma_444,
                                                                                    input_state.nclx,
-                                                                                   input_state.bits_per_pixel,
+                                                                                   input_state.get_color_bits_per_pixel(),
                                                                                    options, &options_ext_skip_alpha,
                                                                                    limits);
     if (!convInput) {
@@ -223,7 +223,7 @@ Op_flatten_alpha_plane<Pixel>::convert_colorspace(const std::shared_ptr<const He
   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);
+    outimg->add_channel(channel, width, height, target_state.get_bits_per_pixel(channel), limits);

     const Pixel* p_alpha;
     size_t stride_alpha;
@@ -331,7 +331,7 @@ Op_flatten_alpha_plane<Pixel>::convert_colorspace(const std::shared_ptr<const He
                                                                               input_raw->get_colorspace(),
                                                                               input_raw->get_chroma_format(),
                                                                               input_state.nclx,
-                                                                              input_state.bits_per_pixel,
+                                                                              input_state.get_color_bits_per_pixel(),
                                                                               options, &options_ext_skip_alpha,
                                                                               limits);
     if (!convOutput) {
@@ -357,8 +357,8 @@ Op_adjust_alpha_bit_depth::state_after_conversion(const ColorState& input_state,
                                                   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) {
+  if (!input_state.has_alpha() ||
+      input_state.bits_per_pixel_alpha == input_state.get_color_bits_per_pixel()) {
     return {};
   }

@@ -379,7 +379,7 @@ Op_adjust_alpha_bit_depth::state_after_conversion(const ColorState& input_state,
   std::vector<ColorStateWithCost> states;

   ColorState output_state = input_state;
-  output_state.alpha_bits_per_pixel = input_state.bits_per_pixel;
+  output_state.bits_per_pixel_alpha = input_state.get_color_bits_per_pixel();

   states.emplace_back(output_state, SpeedCosts_Unoptimized);

@@ -414,7 +414,7 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
   }

   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_color_bits_per_pixel();

   uint32_t alpha_width = input->get_width(heif_channel_Alpha);
   uint32_t alpha_height = input->get_height(heif_channel_Alpha);
diff --git a/libheif/color-conversion/bayer_bilinear.cc b/libheif/color-conversion/bayer_bilinear.cc
index 4ce307c1..941d81f8 100644
--- a/libheif/color-conversion/bayer_bilinear.cc
+++ b/libheif/color-conversion/bayer_bilinear.cc
@@ -40,15 +40,16 @@ Op_bayer_bilinear_to_RGB24_32::state_after_conversion(const ColorState& input_st

   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;
+
+  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 (bpp > 8 && bpp <= 16) {
     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 {};
diff --git a/libheif/color-conversion/chroma_sampling.cc b/libheif/color-conversion/chroma_sampling.cc
index 3d0a9cd1..511a05d5 100644
--- a/libheif/color-conversion/chroma_sampling.cc
+++ b/libheif/color-conversion/chroma_sampling.cc
@@ -45,7 +45,7 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::state_after_conversion(const ColorState&

   bool hdr = !std::is_same<Pixel, uint8_t>::value;

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.bits_per_pixel_Y > 8) != hdr) {
     return {};
   }

@@ -69,9 +69,8 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::state_after_conversion(const ColorState&

   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);
@@ -273,7 +272,7 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::state_after_conversion(const ColorState&

   bool hdr = !std::is_same<Pixel, uint8_t>::value;

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.bits_per_pixel_Y > 8) != hdr) {
     return {};
   }

@@ -297,9 +296,8 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::state_after_conversion(const ColorState&

   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);
@@ -479,7 +477,7 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState

   bool hdr = !std::is_same<Pixel, uint8_t>::value;

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.bits_per_pixel_Y > 8) != hdr) {
     return {};
   }

@@ -499,9 +497,8 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState

   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);
@@ -764,7 +761,7 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState

   bool hdr = !std::is_same<Pixel, uint8_t>::value;

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.bits_per_pixel_Y > 8) != hdr) {
     return {};
   }

@@ -784,9 +781,8 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState

   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);
diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index eee9333b..ae1e1b36 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -147,23 +147,214 @@ static void __attribute__ ((unused)) print_spec(std::ostream& ostr, const std::s
 #endif


+ColorState::ColorState(heif_colorspace cs, heif_chroma chr, bool with_alpha, int bpp)
+    : colorspace(cs), chroma(chr)
+{
+  set_color_bits_per_pixel(bpp);
+  bits_per_pixel_alpha = with_alpha ? bpp : 0;
+}
+
+
+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;
+  }
+
+  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_color_bits_per_pixel() const
+{
+  if (bits_per_pixel_Y != 0) {
+    return bits_per_pixel_Y;
+  }
+  if (bits_per_pixel_R != 0) {
+    return bits_per_pixel_R;
+  }
+  if (bits_per_pixel_filter_array != 0) {
+    return bits_per_pixel_filter_array;
+  }
+  return 0;
+}
+
+
+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<typename Pred>
+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;
+}
+
+
+bool ColorState::color_channels_have_same_bpp() const
+{
+  int ref = get_color_bits_per_pixel();
+  return all_existing_planes_satisfy(*this, false, [ref](int bpp) { return bpp == ref; });
+}
+
+
+bool ColorState::all_channels_have_same_bpp() const
+{
+  int ref = get_color_bits_per_pixel();
+  return all_existing_planes_satisfy(*this, true, [ref](int bpp) { return bpp == ref; });
+}
+
+
+bool ColorState::all_channels_sdr() const
+{
+  return all_existing_planes_satisfy(*this, true, [](int bpp) { return bpp <= 8; });
+}
+
+
+bool ColorState::all_channels_hdr() const
+{
+  return all_existing_planes_satisfy(*this, true, [](int bpp) { return bpp > 8; });
+}
+
+
 bool ColorState::operator==(const ColorState& b) const
 {
   bool mainParamsMatch = (colorspace == b.colorspace &&
                           chroma == b.chroma &&
-                          has_alpha == b.has_alpha &&
-                          bits_per_pixel == b.bits_per_pixel);
+                          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);

   if (!mainParamsMatch) {
     return false;
   }

-  if (has_alpha && b.has_alpha) {
-    if (get_alpha_bits_per_pixel() != b.get_alpha_bits_per_pixel()) {
-      return false;
-    }
-  }
-
   if (colorspace == heif_colorspace_YCbCr) {
     bool ycbcr_parameters_match = nclx.equal_except_transfer_curve(b.nclx);

@@ -202,12 +393,27 @@ struct Node

 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;

-  if (state.has_alpha && state.get_alpha_bits_per_pixel() != state.bits_per_pixel) {
-    ostr << " alpha_bpp=" << state.get_alpha_bits_per_pixel();
+  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()) {
+    ostr << " alpha_bpp=" << state.bits_per_pixel_alpha;
+  }
+  else {
+    ostr << " alpha=no";
   }

   if (state.colorspace == heif_colorspace_YCbCr) {
@@ -531,7 +737,6 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
   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());
   if (input->has_nclx_color_profile()) {
     input_state.nclx = input->get_color_profile_nclx();
   }
@@ -540,10 +745,27 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr

   std::set<enum heif_channel> 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);
+  // 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 (input->has_channel(channel)) {
+      input_state.set_bits_per_pixel(channel, input->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 (input->has_channel(heif_channel_interleaved)) {
+    int bpp = input->get_bits_per_pixel(heif_channel_interleaved);
+    input_state.set_bits_per_pixel(heif_channel_interleaved, bpp);
+    if (is_interleaved_with_alpha(input->get_chroma_format())) {
+      input_state.bits_per_pixel_alpha = bpp;
+    }
   }

   ColorState output_state = input_state;
@@ -569,28 +791,29 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
   // 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;
   }

   // interleaved RRGGBB formats have to be >8-bit.
@@ -600,12 +823,28 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
        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_state.get_color_bits_per_pixel()) <= 8) {
+    output_color_bpp = 10;
+  }
+
+  bool same_plane_layout = (target_colorspace == input_state.colorspace &&
+                            target_chroma == input_state.chroma);
+
+  if (output_color_bpp == 0 && same_plane_layout) {
+    // No depth change requested and the plane layout stays the same: keep the input
+    // planes exactly as they are, even when their depths differ from each other.
+    output_color_bpp = input_state.get_color_bits_per_pixel();
+  }
+  else {
+    if (output_color_bpp == 0) {
+      output_color_bpp = input_state.get_color_bits_per_pixel();
+    }
+
+    output_state.set_color_bits_per_pixel(output_color_bpp);
   }

   // Output alpha should always match the output color BPP
-  output_state.alpha_bits_per_pixel = output_state.bits_per_pixel;
+  output_state.bits_per_pixel_alpha = output_has_alpha ? output_color_bpp : 0;

   ColorConversionPipeline pipeline;
   bool success = pipeline.construct_pipeline(input_state, output_state, options, *options_ext);
diff --git a/libheif/color-conversion/colorconversion.h b/libheif/color-conversion/colorconversion.h
index e1a6d8a6..9621e38d 100644
--- a/libheif/color-conversion/colorconversion.h
+++ b/libheif/color-conversion/colorconversion.h
@@ -32,9 +32,21 @@ struct ColorState
 {
   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,41 @@ struct ColorState

   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);
+
+  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);
+
+  // Bit depth of the first colour plane (Y for YCbCr/monochrome, R for RGB, or the filter array).
+  // This only characterizes the whole image when all colour planes have the same depth,
+  // see color_channels_have_same_bpp().
+  int get_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;
+
+  // True if all existing planes, including alpha, have the same bit depth.
+  bool all_channels_have_same_bpp() const;
+
+  // True if all existing planes, including alpha, have at most 8 bits.
+  bool all_channels_sdr() 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, have more than 8 bits.
+  bool all_channels_hdr() const;

   bool operator==(const ColorState&) const;
 };
@@ -70,8 +112,7 @@ std::ostream& operator<<(std::ostream& ostr, const ColorState& state);
 // 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);
+  return state.get_max_bits_per_pixel() > 16;
 }

 // These are some integer constants for typical color conversion Op speed costs.
diff --git a/libheif/color-conversion/hdr_sdr.cc b/libheif/color-conversion/hdr_sdr.cc
index 34031724..e76264cd 100644
--- a/libheif/color-conversion/hdr_sdr.cc
+++ b/libheif/color-conversion/hdr_sdr.cc
@@ -32,7 +32,7 @@ Op_to_hdr_planes::state_after_conversion(const ColorState& input_state,
        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
+      input_state.get_color_bits_per_pixel() != 8) { // TODO: support for <8 bpp
     return {};
   }

@@ -41,8 +41,8 @@ Op_to_hdr_planes::state_after_conversion(const ColorState& input_state,
   // 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) {
+  if (target_state.get_color_bits_per_pixel() <= 8 ||
+      target_state.get_color_bits_per_pixel() > 16) {
     return {};
   }

@@ -53,8 +53,10 @@ Op_to_hdr_planes::state_after_conversion(const ColorState& input_state,
   // --- 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_state.get_color_bits_per_pixel());
+  if (output_state.has_alpha()) {
+    output_state.bits_per_pixel_alpha = target_state.get_color_bits_per_pixel();
+  }

   states.emplace_back(output_state, SpeedCosts_Unoptimized);

@@ -87,12 +89,12 @@ Op_to_hdr_planes::convert_colorspace(const std::shared_ptr<const HeifPixelImage>
     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_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_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
@@ -140,11 +142,11 @@ Op_to_sdr_planes::state_after_conversion(const ColorState& input_state,
        input_state.chroma != heif_chroma_420 &&
        input_state.chroma != heif_chroma_422 &&
        input_state.chroma != heif_chroma_444) ||
-      input_state.bits_per_pixel == 8) {
+      input_state.get_color_bits_per_pixel() == 8) {
     return {};
   }

-  if (target_state.bits_per_pixel != 8) {
+  if (target_state.get_color_bits_per_pixel() != 8) {
     return {};
   }

@@ -161,8 +163,10 @@ Op_to_sdr_planes::state_after_conversion(const ColorState& input_state,
   // --- 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);

diff --git a/libheif/color-conversion/monochrome.cc b/libheif/color-conversion/monochrome.cc
index afdebdb0..acce06ba 100644
--- a/libheif/color-conversion/monochrome.cc
+++ b/libheif/color-conversion/monochrome.cc
@@ -45,9 +45,8 @@ Op_mono_to_YCbCr420::state_after_conversion(const ColorState& input_state,

   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);

@@ -176,11 +175,11 @@ Op_mono_to_RGB24_32::state_after_conversion(const ColorState& input_state,

   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 +189,11 @@ Op_mono_to_RGB24_32::state_after_conversion(const ColorState& input_state,

   // --- 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 +203,8 @@ Op_mono_to_RGB24_32::state_after_conversion(const ColorState& input_state,

   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 +231,7 @@ Op_mono_to_RGB24_32::convert_colorspace(const std::shared_ptr<const HeifPixelIma

   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 +257,7 @@ Op_mono_to_RGB24_32::convert_colorspace(const std::shared_ptr<const HeifPixelIma

   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 --git a/libheif/color-conversion/rgb2rgb.cc b/libheif/color-conversion/rgb2rgb.cc
index 777021d7..4bb78495 100644
--- a/libheif/color-conversion/rgb2rgb.cc
+++ b/libheif/color-conversion/rgb2rgb.cc
@@ -33,11 +33,11 @@ Op_RGB_to_RGB24_32::state_after_conversion(const ColorState& input_state,
 {
   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 {};
   }

@@ -50,8 +50,8 @@ Op_RGB_to_RGB24_32::state_after_conversion(const ColorState& input_state,

   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 +59,8 @@ Op_RGB_to_RGB24_32::state_after_conversion(const ColorState& input_state,

   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 +77,7 @@ Op_RGB_to_RGB24_32::convert_colorspace(const std::shared_ptr<const HeifPixelImag
                                        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,7 +160,7 @@ Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   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 {};
   }

@@ -168,7 +168,7 @@ Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,
     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 {};
   }

@@ -178,11 +178,11 @@ Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   // --- 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 +192,8 @@ Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   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);

@@ -217,7 +217,7 @@ Op_RGB_HDR_to_RRGGBBaa_BE::convert_colorspace(const std::shared_ptr<const HeifPi
   }

   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) {
@@ -318,11 +318,11 @@ Op_RGB_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   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 +332,11 @@ Op_RGB_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   // --- 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 +346,8 @@ Op_RGB_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,

   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 +372,7 @@ Op_RGB_to_RRGGBBaa_BE::convert_colorspace(const std::shared_ptr<const HeifPixelI
   //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,7 +454,7 @@ Op_RRGGBBaa_BE_to_RGB_HDR::state_after_conversion(const ColorState& input_state,
   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 {};
   }

@@ -470,9 +470,8 @@ Op_RRGGBBaa_BE_to_RGB_HDR::state_after_conversion(const ColorState& input_state,

   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 +489,7 @@ Op_RRGGBBaa_BE_to_RGB_HDR::convert_colorspace(const std::shared_ptr<const HeifPi
 {
   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<HeifPixelImage>();

@@ -575,7 +574,7 @@ Op_RGB24_32_to_RGB::state_after_conversion(const ColorState& input_state,
   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 +586,8 @@ Op_RGB24_32_to_RGB::state_after_conversion(const ColorState& input_state,

   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 +604,7 @@ Op_RGB24_32_to_RGB::convert_colorspace(const std::shared_ptr<const HeifPixelImag
                                        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<HeifPixelImage>();

@@ -702,8 +700,8 @@ Op_RRGGBBaa_swap_endianness::state_after_conversion(const ColorState& input_stat
       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 +720,8 @@ Op_RRGGBBaa_swap_endianness::state_after_conversion(const ColorState& input_stat
       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 --git a/libheif/color-conversion/rgb2yuv.cc b/libheif/color-conversion/rgb2yuv.cc
index 12678128..355775fd 100644
--- a/libheif/color-conversion/rgb2yuv.cc
+++ b/libheif/color-conversion/rgb2yuv.cc
@@ -36,25 +36,25 @@ Op_RGB_to_YCbCr<Pixel>::state_after_conversion(const ColorState& input_state,
 {
   bool hdr = !std::is_same<Pixel, uint8_t>::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)) {
+  if ((input_state.bits_per_pixel_R > 8) != hdr) {
     return {};
   }

-  // TODO: add support for <8 bpp
-  if (input_state.bits_per_pixel < 8) {
+  if (has_samples_wider_than_16bit(input_state)) {
     return {};
   }

-  if (input_state.colorspace != heif_colorspace_RGB ||
-      input_state.chroma != heif_chroma_444) {
+  // TODO: add support for <8 bpp
+  if (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 {};
   }

@@ -80,8 +80,8 @@ Op_RGB_to_YCbCr<Pixel>::state_after_conversion(const ColorState& input_state,

     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 +91,8 @@ Op_RGB_to_YCbCr<Pixel>::state_after_conversion(const ColorState& input_state,

     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);
@@ -340,7 +340,7 @@ Op_RRGGBBxx_HDR_to_YCbCr420::state_after_conversion(const ColorState& input_stat
         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 {};
   }

@@ -369,8 +369,8 @@ Op_RRGGBBxx_HDR_to_YCbCr420::state_after_conversion(const ColorState& input_stat

   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);
@@ -544,7 +544,7 @@ Op_RGB24_32_to_YCbCr::state_after_conversion(const ColorState& input_state,

   // 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 +565,8 @@ Op_RGB24_32_to_YCbCr::state_after_conversion(const ColorState& input_state,

   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 +617,7 @@ Op_RGB24_32_to_YCbCr::convert_colorspace(const std::shared_ptr<const HeifPixelIm
   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 +847,7 @@ Op_RGB24_32_to_YCbCr444_GBR::state_after_conversion(const ColorState& input_stat

   // 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 +865,8 @@ Op_RGB24_32_to_YCbCr444_GBR::state_after_conversion(const ColorState& input_stat

   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 +891,7 @@ Op_RGB24_32_to_YCbCr444_GBR::convert_colorspace(const std::shared_ptr<const Heif
   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 --git a/libheif/color-conversion/rgb2yuv_sharp.cc b/libheif/color-conversion/rgb2yuv_sharp.cc
index 8b9c3f4a..35bc4c11 100644
--- a/libheif/color-conversion/rgb2yuv_sharp.cc
+++ b/libheif/color-conversion/rgb2yuv_sharp.cc
@@ -86,13 +86,17 @@ Op_Any_RGB_to_YCbCr_420_Sharp::state_after_conversion(
     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) {
+  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 +109,7 @@ Op_Any_RGB_to_YCbCr_420_Sharp::state_after_conversion(
   // 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 +124,8 @@ Op_Any_RGB_to_YCbCr_420_Sharp::state_after_conversion(

   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 +167,9 @@ Op_Any_RGB_to_YCbCr_420_Sharp::convert_colorspace(
       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)) {
diff --git a/libheif/color-conversion/yuv2rgb.cc b/libheif/color-conversion/yuv2rgb.cc
index 625e01e0..d5e1cf34 100644
--- a/libheif/color-conversion/yuv2rgb.cc
+++ b/libheif/color-conversion/yuv2rgb.cc
@@ -60,18 +60,18 @@ Op_YCbCr_to_RGB<Pixel>::state_after_conversion(const ColorState& input_state,

   bool hdr = !std::is_same<Pixel, uint8_t>::value;

-  if ((input_state.bits_per_pixel > 8) != hdr) {
+  if ((input_state.bits_per_pixel_Y > 8) != hdr) {
     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) {
+  if (matrix == 16 && input_state.bits_per_pixel_Y > 14) {
     return {};
   }

@@ -88,9 +88,8 @@ Op_YCbCr_to_RGB<Pixel>::state_after_conversion(const ColorState& input_state,

   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);

@@ -327,8 +326,8 @@ Op_YCbCr420_to_RGB24::state_after_conversion(const ColorState& input_state,

   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.bits_per_pixel_Y != 8 ||
+      input_state.has_alpha()) {
     return {};
   }

@@ -348,8 +347,8 @@ Op_YCbCr420_to_RGB24::state_after_conversion(const ColorState& input_state,

   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);

@@ -460,11 +459,11 @@ Op_YCbCr420_to_RGB32::state_after_conversion(const ColorState& input_state,

   if (input_state.colorspace != heif_colorspace_YCbCr ||
       input_state.chroma != heif_chroma_420 ||
-      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 {};
   }

@@ -484,8 +483,8 @@ Op_YCbCr420_to_RGB32::state_after_conversion(const ColorState& input_state,

   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);

@@ -594,7 +593,7 @@ Op_YCbCr420_to_RRGGBBaa::state_after_conversion(const ColorState& input_state,

   if (input_state.colorspace != heif_colorspace_YCbCr ||
       input_state.chroma != heif_chroma_420 ||
-      input_state.bits_per_pixel <= 8) {
+      input_state.bits_per_pixel_Y <= 8) {
     return {};
   }

@@ -602,7 +601,7 @@ Op_YCbCr420_to_RRGGBBaa::state_after_conversion(const ColorState& input_state,
     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 +617,19 @@ Op_YCbCr420_to_RRGGBBaa::state_after_conversion(const ColorState& input_state,
   // --- 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);

diff --git a/tests/conversion.cc b/tests/conversion.cc
index 61a8d782..cdf096b8 100644
--- a/tests/conversion.cc
+++ b/tests/conversion.cc
@@ -172,17 +172,17 @@ std::vector<Plane> GetPlanes(const ColorState& state, int width, int height) {
   std::vector<Plane> 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 +190,13 @@ std::vector<Plane> GetPlanes(const ColorState& state, int width, int height) {
               ? 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 +212,17 @@ std::vector<Plane> GetPlanes(const ColorState& state, int width, int height) {
       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 +322,15 @@ void TestConversion(const std::string& test_name, ColorState input_state,
   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 +347,10 @@ void TestConversion(const std::string& test_name, ColorState input_state,
     std::shared_ptr<HeifPixelImage> 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<<bpp)-1
-    bool expect_alpha_max = !target_state.has_alpha;
+    bool expect_alpha_max = !target_state.has_alpha();
     bool expect_lossless =
         input_state.colorspace == target_state.colorspace &&
-        input_state.bits_per_pixel == target_state.bits_per_pixel &&
+        input_state.get_color_bits_per_pixel() == target_state.get_color_bits_per_pixel() &&
         (input_state.chroma == target_state.chroma ||
          (input_state.chroma != heif_chroma_420 &&
           input_state.chroma != heif_chroma_422 &&
@@ -777,7 +777,7 @@ TEST_CASE("Mismatched alpha bit depth - pipeline construction") {

   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 +790,7 @@ TEST_CASE("Mismatched alpha bit depth - pipeline construction") {

   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 +803,11 @@ TEST_CASE("Mismatched alpha bit depth - pipeline construction") {

   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);