Commit 00c618cd for libheif
commit 00c618cd11f8062d6ed45f592705061a12b3cb7c
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Apr 13 14:47:38 2026 +0200
track alpha bpp through color conversion pipeline and adjust if needed (#1673)
diff --git a/libheif/color-conversion/alpha.cc b/libheif/color-conversion/alpha.cc
index 08651b0b..7a316a81 100644
--- a/libheif/color-conversion/alpha.cc
+++ b/libheif/color-conversion/alpha.cc
@@ -106,6 +106,10 @@ 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) {
+ return {};
+ }
+
// only drop alpha plane if it is not needed in output
if ((input_state.chroma != heif_chroma_monochrome &&
@@ -285,3 +289,169 @@ Op_flatten_alpha_plane<Pixel>::convert_colorspace(const std::shared_ptr<const He
template class Op_flatten_alpha_plane<uint8_t>;
template class Op_flatten_alpha_plane<uint16_t>;
+
+
+std::vector<ColorStateWithCost>
+Op_adjust_alpha_bit_depth::state_after_conversion(const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext) const
+{
+ // Only applicable when alpha BPP differs from color BPP
+ if (!input_state.has_alpha ||
+ input_state.get_alpha_bits_per_pixel() == input_state.bits_per_pixel) {
+ return {};
+ }
+
+ // Only for planar formats with alpha
+ if (input_state.chroma != heif_chroma_monochrome &&
+ input_state.chroma != heif_chroma_420 &&
+ input_state.chroma != heif_chroma_422 &&
+ input_state.chroma != heif_chroma_444) {
+ return {};
+ }
+
+ std::vector<ColorStateWithCost> states;
+
+ ColorState output_state = input_state;
+ output_state.alpha_bits_per_pixel = input_state.bits_per_pixel;
+
+ states.emplace_back(output_state, SpeedCosts_Unoptimized);
+
+ return states;
+}
+
+
+Result<std::shared_ptr<HeifPixelImage>>
+Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+ const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext,
+ const heif_security_limits* limits) const
+{
+ uint32_t width = input->get_width();
+ uint32_t height = input->get_height();
+
+ auto outimg = std::make_shared<HeifPixelImage>();
+ outimg->create(width, height, input->get_colorspace(), input->get_chroma_format());
+
+ // Copy all non-alpha channels unchanged
+ for (heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr,
+ heif_channel_R, heif_channel_G, heif_channel_B}) {
+ if (input->has_channel(channel)) {
+ outimg->copy_new_plane_from(input, channel, channel, limits);
+ }
+ }
+
+ if (!input->has_channel(heif_channel_Alpha)) {
+ return outimg;
+ }
+
+ int input_alpha_bpp = input->get_bits_per_pixel(heif_channel_Alpha);
+ int target_bpp = input_state.bits_per_pixel;
+
+ uint32_t alpha_width = input->get_width(heif_channel_Alpha);
+ uint32_t alpha_height = input->get_height(heif_channel_Alpha);
+
+ if (auto err = outimg->add_plane(heif_channel_Alpha, alpha_width, alpha_height, target_bpp, limits)) {
+ return err;
+ }
+
+ if (input_alpha_bpp <= 8 && target_bpp > 8) {
+ // Upscale: 8-bit alpha -> HDR using pattern replication
+ const uint8_t* p_in;
+ size_t stride_in;
+ p_in = input->get_plane(heif_channel_Alpha, &stride_in);
+
+ uint16_t* p_out;
+ size_t stride_out;
+ p_out = (uint16_t*) outimg->get_plane(heif_channel_Alpha, &stride_out);
+ stride_out /= 2;
+
+ int shift1 = target_bpp - input_alpha_bpp;
+ int shift2 = 2 * input_alpha_bpp - target_bpp;
+
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ int in = p_in[y * stride_in + x];
+ p_out[y * stride_out + x] = (uint16_t) ((in << shift1) | (in >> shift2));
+ }
+ }
+ else if (input_alpha_bpp > 8 && target_bpp <= 8) {
+ // Downscale: HDR alpha -> 8-bit
+ const uint16_t* p_in;
+ size_t stride_in;
+ p_in = (const uint16_t*) input->get_plane(heif_channel_Alpha, &stride_in);
+ stride_in /= 2;
+
+ uint8_t* p_out;
+ size_t stride_out;
+ p_out = outimg->get_plane(heif_channel_Alpha, &stride_out);
+
+ int shift = input_alpha_bpp - 8;
+
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift);
+ }
+ }
+ else if (input_alpha_bpp > 8 && target_bpp > 8) {
+ // HDR alpha -> different HDR: rescale within uint16_t
+ const uint16_t* p_in;
+ size_t stride_in;
+ p_in = (const uint16_t*) input->get_plane(heif_channel_Alpha, &stride_in);
+ stride_in /= 2;
+
+ uint16_t* p_out;
+ size_t stride_out;
+ p_out = (uint16_t*) outimg->get_plane(heif_channel_Alpha, &stride_out);
+ stride_out /= 2;
+
+ if (target_bpp > input_alpha_bpp) {
+ int shift1 = target_bpp - input_alpha_bpp;
+ int shift2 = 2 * input_alpha_bpp - target_bpp;
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ int in = p_in[y * stride_in + x];
+ p_out[y * stride_out + x] = (uint16_t) ((in << shift1) | (in >> shift2));
+ }
+ }
+ else {
+ int shift = input_alpha_bpp - target_bpp;
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ p_out[y * stride_out + x] = (uint16_t) (p_in[y * stride_in + x] >> shift);
+ }
+ }
+ }
+ else {
+ // SDR alpha -> different SDR (both <= 8)
+ const uint8_t* p_in;
+ size_t stride_in;
+ p_in = input->get_plane(heif_channel_Alpha, &stride_in);
+
+ uint8_t* p_out;
+ size_t stride_out;
+ p_out = outimg->get_plane(heif_channel_Alpha, &stride_out);
+
+ if (target_bpp > input_alpha_bpp) {
+ int shift1 = target_bpp - input_alpha_bpp;
+ int shift2 = 2 * input_alpha_bpp - target_bpp;
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ int in = p_in[y * stride_in + x];
+ p_out[y * stride_out + x] = (uint8_t) ((in << shift1) | (in >> shift2));
+ }
+ }
+ else {
+ int shift = input_alpha_bpp - target_bpp;
+ for (uint32_t y = 0; y < alpha_height; y++)
+ for (uint32_t x = 0; x < alpha_width; x++) {
+ p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift);
+ }
+ }
+ }
+
+ return outimg;
+}
diff --git a/libheif/color-conversion/alpha.h b/libheif/color-conversion/alpha.h
index 60ad84e6..cd596630 100644
--- a/libheif/color-conversion/alpha.h
+++ b/libheif/color-conversion/alpha.h
@@ -64,4 +64,22 @@ public:
const heif_security_limits* limits) const override;
};
+class Op_adjust_alpha_bit_depth : public ColorConversionOperation
+{
+public:
+ std::vector<ColorStateWithCost>
+ state_after_conversion(const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext) const override;
+
+ Result<std::shared_ptr<HeifPixelImage>>
+ convert_colorspace(const std::shared_ptr<const HeifPixelImage>& input,
+ const ColorState& input_state,
+ const ColorState& target_state,
+ const heif_color_conversion_options& options,
+ const heif_color_conversion_options_ext& options_ext,
+ const heif_security_limits* limits) const override;
+};
+
#endif //LIBHEIF_COLORCONVERSION_ALPHA_H
diff --git a/libheif/color-conversion/chroma_sampling.cc b/libheif/color-conversion/chroma_sampling.cc
index 9bafdff8..2de71686 100644
--- a/libheif/color-conversion/chroma_sampling.cc
+++ b/libheif/color-conversion/chroma_sampling.cc
@@ -67,6 +67,7 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::state_after_conversion(const ColorState&
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.nclx = input_state.nclx;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -290,6 +291,7 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::state_after_conversion(const ColorState&
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.nclx = input_state.nclx;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -487,6 +489,7 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState
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.nclx = input_state.nclx;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -767,6 +770,7 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState
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.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 1c0be8a5..819fa6e4 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -158,6 +158,12 @@ bool ColorState::operator==(const ColorState& b) const
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);
@@ -200,6 +206,10 @@ std::ostream& operator<<(std::ostream& ostr, const ColorState& state)
<< " bpp(R)=" << state.bits_per_pixel
<< " alpha=" << (state.has_alpha ? "yes" : "no");
+ if (state.has_alpha && state.get_alpha_bits_per_pixel() != state.bits_per_pixel) {
+ ostr << " alpha_bpp=" << state.get_alpha_bits_per_pixel();
+ }
+
if (state.colorspace == heif_colorspace_YCbCr) {
ostr << " matrix-coefficients=" << state.nclx.get_matrix_coefficients()
<< " colour-primaries=" << state.nclx.get_colour_primaries()
@@ -245,6 +255,7 @@ void ColorConversionPipeline::init_ops()
ops.emplace_back(std::make_shared<Op_drop_alpha_plane>());
ops.emplace_back(std::make_shared<Op_flatten_alpha_plane<uint8_t>>());
ops.emplace_back(std::make_shared<Op_flatten_alpha_plane<uint16_t>>());
+ ops.emplace_back(std::make_shared<Op_adjust_alpha_bit_depth>());
ops.emplace_back(std::make_shared<Op_to_hdr_planes>());
ops.emplace_back(std::make_shared<Op_to_sdr_planes>());
ops.emplace_back(std::make_shared<Op_YCbCr420_bilinear_to_YCbCr444<uint8_t>>());
@@ -531,6 +542,10 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
assert(!channels.empty());
input_state.bits_per_pixel = input->get_bits_per_pixel(*(channels.begin()));
+ if (input_state.has_alpha && input->has_channel(heif_channel_Alpha)) {
+ input_state.alpha_bits_per_pixel = input->get_bits_per_pixel(heif_channel_Alpha);
+ }
+
ColorState output_state = input_state;
output_state.colorspace = target_colorspace;
output_state.chroma = target_chroma;
diff --git a/libheif/color-conversion/colorconversion.h b/libheif/color-conversion/colorconversion.h
index bc000eba..f1553fcb 100644
--- a/libheif/color-conversion/colorconversion.h
+++ b/libheif/color-conversion/colorconversion.h
@@ -34,6 +34,7 @@ struct ColorState
heif_chroma chroma = heif_chroma_undefined;
bool has_alpha = false;
int bits_per_pixel = 8;
+ int alpha_bits_per_pixel = 0; // 0 = not set, treated as bits_per_pixel
// ColorConversionOperations can assume that the input and target nclx has no 'unspecified' values
// if the colorspace is heif_colorspace_YCbCr. Otherwise, the values should preferably be 'unspecified'.
@@ -44,6 +45,9 @@ struct ColorState
ColorState(heif_colorspace colorspace, heif_chroma chroma, bool has_alpha, int bits_per_pixel)
: colorspace(colorspace), chroma(chroma), has_alpha(has_alpha), bits_per_pixel(bits_per_pixel) {}
+ // Returns effective alpha BPP (treats 0 as bits_per_pixel for backward compatibility)
+ int get_alpha_bits_per_pixel() const { return alpha_bits_per_pixel ? alpha_bits_per_pixel : bits_per_pixel; }
+
bool operator==(const ColorState&) const;
};
diff --git a/libheif/color-conversion/hdr_sdr.cc b/libheif/color-conversion/hdr_sdr.cc
index fa8de4f6..fab0cd09 100644
--- a/libheif/color-conversion/hdr_sdr.cc
+++ b/libheif/color-conversion/hdr_sdr.cc
@@ -44,6 +44,7 @@ Op_to_hdr_planes::state_after_conversion(const ColorState& input_state,
output_state = input_state;
output_state.bits_per_pixel = target_state.bits_per_pixel;
+ output_state.alpha_bits_per_pixel = target_state.bits_per_pixel;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -134,6 +135,7 @@ Op_to_sdr_planes::state_after_conversion(const ColorState& input_state,
output_state = input_state;
output_state.bits_per_pixel = 8;
+ output_state.alpha_bits_per_pixel = 8;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
diff --git a/libheif/color-conversion/monochrome.cc b/libheif/color-conversion/monochrome.cc
index 064e2716..1d22dafe 100644
--- a/libheif/color-conversion/monochrome.cc
+++ b/libheif/color-conversion/monochrome.cc
@@ -43,6 +43,7 @@ Op_mono_to_YCbCr420::state_after_conversion(const ColorState& input_state,
output_state.chroma = heif_chroma_420;
output_state.has_alpha = input_state.has_alpha;
output_state.bits_per_pixel = input_state.bits_per_pixel;
+ output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel;
states.emplace_back(output_state, SpeedCosts_OptimizedSoftware);
@@ -175,6 +176,10 @@ Op_mono_to_RGB24_32::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) {
+ return {};
+ }
+
std::vector<ColorStateWithCost> states;
ColorState output_state;
diff --git a/libheif/color-conversion/rgb2rgb.cc b/libheif/color-conversion/rgb2rgb.cc
index 4e779268..87ebfa8d 100644
--- a/libheif/color-conversion/rgb2rgb.cc
+++ b/libheif/color-conversion/rgb2rgb.cc
@@ -37,6 +37,10 @@ Op_RGB_to_RGB24_32::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) {
+ return {};
+ }
+
std::vector<ColorStateWithCost> states;
ColorState output_state;
@@ -160,6 +164,10 @@ 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) {
+ return {};
+ }
+
std::vector<ColorStateWithCost> states;
ColorState output_state;
@@ -298,6 +306,10 @@ Op_RGB_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) {
+ return {};
+ }
+
std::vector<ColorStateWithCost> states;
ColorState output_state;
@@ -440,6 +452,7 @@ Op_RRGGBBaa_BE_to_RGB_HDR::state_after_conversion(const ColorState& input_state,
output_state.chroma = heif_chroma_444;
output_state.has_alpha = target_state.has_alpha;
output_state.bits_per_pixel = input_state.bits_per_pixel;
+ output_state.alpha_bits_per_pixel = input_state.bits_per_pixel;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -556,6 +569,7 @@ Op_RGB24_32_to_RGB::state_after_conversion(const ColorState& input_state,
output_state.chroma = heif_chroma_444;
output_state.has_alpha = target_state.has_alpha;
output_state.bits_per_pixel = input_state.bits_per_pixel;
+ output_state.alpha_bits_per_pixel = input_state.bits_per_pixel;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
diff --git a/libheif/color-conversion/rgb2yuv.cc b/libheif/color-conversion/rgb2yuv.cc
index b570fe6b..d8132b2f 100644
--- a/libheif/color-conversion/rgb2yuv.cc
+++ b/libheif/color-conversion/rgb2yuv.cc
@@ -50,6 +50,10 @@ Op_RGB_to_YCbCr<Pixel>::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) {
+ return {};
+ }
+
int matrix = target_state.nclx.get_matrix_coefficients();
if (matrix == 11 || matrix == 14) {
return {};
diff --git a/libheif/color-conversion/yuv2rgb.cc b/libheif/color-conversion/yuv2rgb.cc
index c98893be..b9d33190 100644
--- a/libheif/color-conversion/yuv2rgb.cc
+++ b/libheif/color-conversion/yuv2rgb.cc
@@ -79,6 +79,7 @@ Op_YCbCr_to_RGB<Pixel>::state_after_conversion(const ColorState& input_state,
output_state.chroma = heif_chroma_444;
output_state.has_alpha = input_state.has_alpha; // we simply keep the old alpha plane
output_state.bits_per_pixel = input_state.bits_per_pixel;
+ output_state.alpha_bits_per_pixel = input_state.alpha_bits_per_pixel;
states.emplace_back(output_state, SpeedCosts_Unoptimized);
@@ -446,6 +447,10 @@ Op_YCbCr420_to_RGB32::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) {
+ return {};
+ }
+
int matrix = input_state.nclx.get_matrix_coefficients();
if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) {
return {};
@@ -576,6 +581,10 @@ 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) {
+ return {};
+ }
+
int matrix = input_state.nclx.get_matrix_coefficients();
if (matrix == 0 || matrix == 8 || matrix == 11 || matrix == 14) {
return {};