Commit 5f6169ae for libheif
commit 5f6169aec0e6a7d39f9e1078653a3203093c98ae
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Sep 19 14:54:02 2026 +0200
Let color-conversion operators declare the sample width they read
The operators picked uint8_t or uint16_t samples from an 8-bit SDR/HDR split
(bits_per_pixel > 8 meant "two bytes per sample"). That dates from when 16 bits
was the maximum and is wrong for the 32-, 64- and 128-bit components that
'unci' can carry: HeifPixelImage stores those with 4, 8 or 16 bytes per sample.
Nothing wider than 16 bits could reach an operator, because each of them
guarded against it separately through has_samples_wider_than_16bit() and
convert_colorspace() has a backstop, but the sample type never followed the
allocation, so those guards were load-bearing.
Make the sample width explicit instead. bytes_per_sample_for_bit_depth() in
pixelimage.h is now the single mapping from bit depth to storage width, used
by plane allocation and by the operators, so the two cannot drift. ColorState
gained get_bytes_per_sample(), get_max_bytes_per_sample(),
color_channels_have_bytes_per_sample() and all_channels_have_bytes_per_sample(),
and each operator declares in state_after_conversion() which width it can read
(sizeof(Pixel) for the templated ones, 1 or 2 for the others). Strides are
divided by the sample width instead of by a conditional 2, alpha rows are
copied with their own width, and the operators that switch on the width at
runtime refuse anything else. has_samples_wider_than_16bit() and the
all_channels_sdr()/all_channels_hdr() helpers have no callers left and are
removed; the check added for GHSA-r7gr-2xm2-23wf in Op_flatten_alpha_plane
becomes all_channels_have_bytes_per_sample(sizeof(Pixel)).
The 16-bit backstop and the YCbCr luma/chroma check in convert_colorspace()
now also run when no pipeline could be built, so callers keep getting the
specific error instead of the generic "unsupported color conversion".
A new test builds 32-, 64- and 128-bit YCbCr, RGB and monochrome states and
checks that the operators decline them and that no pipeline is constructible.
diff --git a/libheif/color-conversion/alpha.cc b/libheif/color-conversion/alpha.cc
index 21ded126..9055b70f 100644
--- a/libheif/color-conversion/alpha.cc
+++ b/libheif/color-conversion/alpha.cc
@@ -134,18 +134,12 @@ Op_flatten_alpha_plane<Pixel>::state_after_conversion(const ColorState& input_st
const heif_color_conversion_options& options,
const heif_color_conversion_options_ext& options_ext) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
// The colour planes and the alpha plane are all read through the single 'Pixel' type
- // below, so every plane must be SDR (one byte per sample) for the uint8_t instance and
- // HDR (two bytes per sample) for the uint16_t instance. A file may declare a different
- // depth per plane ('unci'); reading a one-byte plane as two-byte samples would run past
- // its end (GHSA-r7gr-2xm2-23wf). Decline a mixture instead.
- if (hdr ? !input_state.all_channels_hdr() : !input_state.all_channels_sdr()) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // below, so every plane must be stored with sizeof(Pixel) bytes per sample. A file may
+ // declare a different depth per plane ('unci'); reading a one-byte plane as two-byte
+ // samples would run past its end (GHSA-r7gr-2xm2-23wf), and a plane wider than two
+ // bytes cannot be read through either instance. Decline anything else.
+ if (!input_state.all_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -255,11 +249,9 @@ Op_flatten_alpha_plane<Pixel>::convert_colorspace(const std::shared_ptr<const He
size_t stride_out;
p_out = (Pixel*)outimg->get_channel_memory(channel, &stride_out);
- if (sizeof(Pixel) == 2) {
- stride_alpha /= 2;
- stride_in /= 2;
- stride_out /= 2;
- }
+ stride_alpha /= sizeof(Pixel);
+ stride_in /= sizeof(Pixel);
+ stride_out /= sizeof(Pixel);
if (options_ext.alpha_composition_mode == heif_alpha_composition_mode_solid_color ||
(options_ext.alpha_composition_mode == heif_alpha_composition_mode_checkerboard && options_ext.checkerboard_square_size == 0)) {
@@ -374,7 +366,8 @@ Op_adjust_alpha_bit_depth::state_after_conversion(const ColorState& input_state,
// Rewrites the alpha plane from its own bit depth to the colour bit depth, so both
// ends have to be accessible as 8- or 16-bit samples.
- if (has_samples_wider_than_16bit(input_state)) {
+ if (input_state.get_bytes_per_sample(heif_channel_Alpha) > 2 ||
+ bytes_per_sample_for_bit_depth(input_state.get_color_bits_per_pixel()) > 2) {
return {};
}
@@ -425,7 +418,10 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
return err;
}
- if (input_alpha_bpp <= 8 && target_bpp > 8) {
+ int input_bytes = bytes_per_sample_for_bit_depth(input_alpha_bpp);
+ int target_bytes = bytes_per_sample_for_bit_depth(target_bpp);
+
+ if (input_bytes == 1 && target_bytes == 2) {
// Upscale: 8-bit alpha -> HDR using bit replication
const uint8_t* p_in;
size_t stride_in;
@@ -442,7 +438,7 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
p_out[y * stride_out + x] = (uint16_t) replicate_sample_bits(in, input_alpha_bpp, target_bpp);
}
}
- else if (input_alpha_bpp > 8 && target_bpp <= 8) {
+ else if (input_bytes == 2 && target_bytes == 1) {
// Downscale: HDR alpha -> 8-bit
const uint16_t* p_in;
size_t stride_in;
@@ -460,7 +456,7 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
p_out[y * stride_out + x] = (uint8_t) (p_in[y * stride_in + x] >> shift);
}
}
- else if (input_alpha_bpp > 8 && target_bpp > 8) {
+ else if (input_bytes == 2 && target_bytes == 2) {
// HDR alpha -> different HDR: rescale within uint16_t
const uint16_t* p_in;
size_t stride_in;
@@ -487,7 +483,7 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
}
}
}
- else {
+ else if (input_bytes == 1 && target_bytes == 1) {
// SDR alpha -> different SDR (both <= 8)
const uint8_t* p_in;
size_t stride_in;
@@ -512,6 +508,11 @@ Op_adjust_alpha_bit_depth::convert_colorspace(const std::shared_ptr<const HeifPi
}
}
}
+ else {
+ return Error{heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_bit_depth,
+ "Alpha bit depth adjustment only supports 8- and 16-bit sample storage."};
+ }
return outimg;
}
diff --git a/libheif/color-conversion/bayer_bilinear.cc b/libheif/color-conversion/bayer_bilinear.cc
index 941d81f8..5d7128e1 100644
--- a/libheif/color-conversion/bayer_bilinear.cc
+++ b/libheif/color-conversion/bayer_bilinear.cc
@@ -42,12 +42,13 @@ Op_bayer_bilinear_to_RGB24_32::state_after_conversion(const ColorState& input_st
output_state.colorspace = heif_colorspace_RGB;
int bpp = input_state.bits_per_pixel_filter_array;
+ int bytes_per_sample = input_state.get_bytes_per_sample(heif_channel_filter_array);
if (bpp == 8) {
output_state.chroma = heif_chroma_interleaved_RGB;
output_state.set_color_bits_per_pixel(8);
}
- else if (bpp > 8 && bpp <= 16) {
+ else if (bytes_per_sample == 2) {
output_state.chroma = heif_chroma_interleaved_RRGGBB_LE;
output_state.set_color_bits_per_pixel(bpp);
}
@@ -102,9 +103,13 @@ Op_bayer_bilinear_to_RGB24_32::convert_colorspace(const std::shared_ptr<const He
}
int bpp = input->get_bits_per_pixel(heif_channel_filter_array);
- bool hdr = bpp > 8;
+ int bytes_per_sample = bytes_per_sample_for_bit_depth(bpp);
- heif_chroma out_chroma = hdr ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB;
+ if (bytes_per_sample != 1 && bytes_per_sample != 2) {
+ return Error::InternalError;
+ }
+
+ heif_chroma out_chroma = (bytes_per_sample == 2) ? heif_chroma_interleaved_RRGGBB_LE : heif_chroma_interleaved_RGB;
auto outimg = std::make_shared<HeifPixelImage>();
@@ -201,7 +206,7 @@ Op_bayer_bilinear_to_RGB24_32::convert_colorspace(const std::shared_ptr<const He
}
};
- if (hdr) {
+ if (bytes_per_sample == 2) {
demosaic(reinterpret_cast<const uint16_t*>(in_p),
reinterpret_cast<uint16_t*>(out_p),
in_stride / 2, out_stride / 2);
diff --git a/libheif/color-conversion/chroma_sampling.cc b/libheif/color-conversion/chroma_sampling.cc
index 511a05d5..2a2e82f8 100644
--- a/libheif/color-conversion/chroma_sampling.cc
+++ b/libheif/color-conversion/chroma_sampling.cc
@@ -43,13 +43,9 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::state_after_conversion(const ColorState&
return {};
}
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
- if ((input_state.bits_per_pixel_Y > 8) != hdr) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // The three colour planes are read through the same 'Pixel' type, so they must be stored
+ // with sizeof(Pixel) bytes per sample. The alpha plane is copied through at its own width.
+ if (!input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -88,8 +84,6 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::convert_colorspace(const std::shared_ptr
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
int bpp_y = input->get_bits_per_pixel(heif_channel_Y);
int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb);
int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr);
@@ -101,19 +95,10 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::convert_colorspace(const std::shared_ptr
bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
}
- if (!hdr) {
- if (bpp_y > 8 ||
- bpp_cb > 8 ||
- bpp_cr > 8) {
- return Error::InternalError;
- }
- }
- else {
- if (bpp_y <= 8 ||
- bpp_cb <= 8 ||
- bpp_cr <= 8) {
- return Error::InternalError;
- }
+ if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cb) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cr) != static_cast<int>(sizeof(Pixel))) {
+ return Error::InternalError;
}
@@ -159,14 +144,12 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::convert_colorspace(const std::shared_ptr
out_cb = (Pixel*) outimg->get_channel_memory(heif_channel_Cb, &out_cb_stride);
out_cr = (Pixel*) outimg->get_channel_memory(heif_channel_Cr, &out_cr_stride);
- if (hdr) {
- in_y_stride /= 2;
- in_cb_stride /= 2;
- in_cr_stride /= 2;
- out_y_stride /= 2;
- out_cb_stride /= 2;
- out_cr_stride /= 2;
- }
+ in_y_stride /= sizeof(Pixel);
+ in_cb_stride /= sizeof(Pixel);
+ in_cr_stride /= sizeof(Pixel);
+ out_y_stride /= sizeof(Pixel);
+ out_cb_stride /= sizeof(Pixel);
+ out_cr_stride /= sizeof(Pixel);
// We only copy the alpha, do not access it as 16 bit
@@ -231,12 +214,12 @@ Op_YCbCr444_to_YCbCr420_average<Pixel>::convert_colorspace(const std::shared_ptr
// TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha
for (y = 0; y < height; y++) {
- uint32_t copyWidth = (hdr ? width * 2 : width);
+ size_t copyWidth = static_cast<size_t>(width) * sizeof(Pixel);
memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth);
if (has_alpha) {
- uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
}
}
@@ -270,13 +253,9 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::state_after_conversion(const ColorState&
return {};
}
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
- if ((input_state.bits_per_pixel_Y > 8) != hdr) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // The three colour planes are read through the same 'Pixel' type, so they must be stored
+ // with sizeof(Pixel) bytes per sample. The alpha plane is copied through at its own width.
+ if (!input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -315,8 +294,6 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::convert_colorspace(const std::shared_ptr
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
int bpp_y = input->get_bits_per_pixel(heif_channel_Y);
int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb);
int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr);
@@ -328,19 +305,10 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::convert_colorspace(const std::shared_ptr
bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
}
- if (!hdr) {
- if (bpp_y > 8 ||
- bpp_cb > 8 ||
- bpp_cr > 8) {
- return Error::InternalError;
- }
- }
- else {
- if (bpp_y <= 8 ||
- bpp_cb <= 8 ||
- bpp_cr <= 8) {
- return Error::InternalError;
- }
+ if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cb) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cr) != static_cast<int>(sizeof(Pixel))) {
+ return Error::InternalError;
}
@@ -399,14 +367,12 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::convert_colorspace(const std::shared_ptr
}
- if (hdr) {
- in_y_stride /= 2;
- in_cb_stride /= 2;
- in_cr_stride /= 2;
- out_y_stride /= 2;
- out_cb_stride /= 2;
- out_cr_stride /= 2;
- }
+ in_y_stride /= sizeof(Pixel);
+ in_cb_stride /= sizeof(Pixel);
+ in_cr_stride /= sizeof(Pixel);
+ out_y_stride /= sizeof(Pixel);
+ out_cb_stride /= sizeof(Pixel);
+ out_cr_stride /= sizeof(Pixel);
// --- fill right border if the image size is odd
@@ -436,12 +402,12 @@ Op_YCbCr444_to_YCbCr422_average<Pixel>::convert_colorspace(const std::shared_ptr
// TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha
for (y = 0; y < height; y++) {
- uint32_t copyWidth = (hdr ? width * 2 : width);
+ size_t copyWidth = static_cast<size_t>(width) * sizeof(Pixel);
memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth);
if (has_alpha) {
- uint32_t alphaCopyWidth = (bpp_a>8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
}
}
@@ -475,13 +441,9 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState
return {};
}
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
- if ((input_state.bits_per_pixel_Y > 8) != hdr) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // The three colour planes are read through the same 'Pixel' type, so they must be stored
+ // with sizeof(Pixel) bytes per sample. The alpha plane is copied through at its own width.
+ if (!input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -516,8 +478,6 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
int bpp_y = input->get_bits_per_pixel(heif_channel_Y);
int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb);
int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr);
@@ -529,19 +489,10 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
}
- if (!hdr) {
- if (bpp_y > 8 ||
- bpp_cb > 8 ||
- bpp_cr > 8) {
- return Error::InternalError;
- }
- }
- else {
- if (bpp_y <= 8 ||
- bpp_cb <= 8 ||
- bpp_cr <= 8) {
- return Error::InternalError;
- }
+ if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cb) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cr) != static_cast<int>(sizeof(Pixel))) {
+ return Error::InternalError;
}
@@ -597,14 +548,12 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
}
- if (hdr) {
- in_y_stride /= 2;
- in_cb_stride /= 2;
- in_cr_stride /= 2;
- out_y_stride /= 2;
- out_cb_stride /= 2;
- out_cr_stride /= 2;
- }
+ in_y_stride /= sizeof(Pixel);
+ in_cb_stride /= sizeof(Pixel);
+ in_cr_stride /= sizeof(Pixel);
+ out_y_stride /= sizeof(Pixel);
+ out_cb_stride /= sizeof(Pixel);
+ out_cr_stride /= sizeof(Pixel);
/*
* We assume that chroma pixels are located in the center of 2x2 luma pixels.
@@ -719,12 +668,12 @@ Op_YCbCr420_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
// TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha
for (y = 0; y < height; y++) {
- uint32_t copyWidth = (hdr ? width * 2 : width);
+ size_t copyWidth = static_cast<size_t>(width) * sizeof(Pixel);
memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth);
if (has_alpha) {
- uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
}
}
@@ -759,13 +708,9 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::state_after_conversion(const ColorState
return {};
}
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
- if ((input_state.bits_per_pixel_Y > 8) != hdr) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // The three colour planes are read through the same 'Pixel' type, so they must be stored
+ // with sizeof(Pixel) bytes per sample. The alpha plane is copied through at its own width.
+ if (!input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -800,8 +745,6 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
int bpp_y = input->get_bits_per_pixel(heif_channel_Y);
int bpp_cb = input->get_bits_per_pixel(heif_channel_Cb);
int bpp_cr = input->get_bits_per_pixel(heif_channel_Cr);
@@ -813,19 +756,10 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
}
- if (!hdr) {
- if (bpp_y > 8 ||
- bpp_cb > 8 ||
- bpp_cr > 8) {
- return Error::InternalError;
- }
- }
- else {
- if (bpp_y <= 8 ||
- bpp_cb <= 8 ||
- bpp_cr <= 8) {
- return Error::InternalError;
- }
+ if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cb) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cr) != static_cast<int>(sizeof(Pixel))) {
+ return Error::InternalError;
}
@@ -881,14 +815,12 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
}
- if (hdr) {
- in_y_stride /= 2;
- in_cb_stride /= 2;
- in_cr_stride /= 2;
- out_y_stride /= 2;
- out_cb_stride /= 2;
- out_cr_stride /= 2;
- }
+ in_y_stride /= sizeof(Pixel);
+ in_cb_stride /= sizeof(Pixel);
+ in_cr_stride /= sizeof(Pixel);
+ out_y_stride /= sizeof(Pixel);
+ out_cb_stride /= sizeof(Pixel);
+ out_cr_stride /= sizeof(Pixel);
/*
* We assume that chroma pixels are located in the center of 2x1 luma pixels.
@@ -949,12 +881,12 @@ Op_YCbCr422_bilinear_to_YCbCr444<Pixel>::convert_colorspace(const std::shared_pt
// TODO: check whether we can use HeifPixelImage::transfer_channel_from_image_as() instead of copying Y and Alpha
for (y = 0; y < height; y++) {
- uint32_t copyWidth = (hdr ? width * 2 : width);
+ size_t copyWidth = static_cast<size_t>(width) * sizeof(Pixel);
memcpy(&out_y[y * out_y_stride], &in_y[y * in_y_stride], copyWidth);
if (has_alpha) {
- uint32_t alphaCopyWidth = (bpp_a > 8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
}
}
diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index ae1e1b36..6d2a06be 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -326,15 +326,34 @@ bool ColorState::all_channels_have_same_bpp() const
}
-bool ColorState::all_channels_sdr() const
+int ColorState::get_bytes_per_sample(heif_channel channel) const
{
- return all_existing_planes_satisfy(*this, true, [](int bpp) { return bpp <= 8; });
+ int bpp = get_bits_per_pixel(channel);
+ return bpp != 0 ? bytes_per_sample_for_bit_depth(bpp) : 0;
}
-bool ColorState::all_channels_hdr() const
+int ColorState::get_max_bytes_per_sample() const
{
- return all_existing_planes_satisfy(*this, true, [](int bpp) { return bpp > 8; });
+ // The bit depth to sample width mapping is monotonic, so the widest plane is the deepest.
+ int max_bpp = get_max_bits_per_pixel();
+ return max_bpp != 0 ? bytes_per_sample_for_bit_depth(max_bpp) : 0;
+}
+
+
+bool ColorState::color_channels_have_bytes_per_sample(int bytes) const
+{
+ return all_existing_planes_satisfy(*this, false, [bytes](int bpp) {
+ return bytes_per_sample_for_bit_depth(bpp) == bytes;
+ });
+}
+
+
+bool ColorState::all_channels_have_bytes_per_sample(int bytes) const
+{
+ return all_existing_planes_satisfy(*this, true, [bytes](int bpp) {
+ return bytes_per_sample_for_bit_depth(bpp) == bytes;
+ });
}
@@ -848,15 +867,16 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
ColorConversionPipeline pipeline;
bool success = pipeline.construct_pipeline(input_state, output_state, options, *options_ext);
- if (!success) {
- return Error{heif_error_Unsupported_feature,
- heif_suberror_Unsupported_color_conversion};
- }
- if (pipeline.is_nop()) {
+ if (success && pipeline.is_nop()) {
return input;
}
- else {
+
+ {
+ // The two checks below also run when no pipeline could be built, so that the caller
+ // gets the specific reason (a plane wider than 16 bits, or YCbCr planes of differing
+ // depth) instead of the generic "unsupported color conversion" error.
+ //
// Every color-conversion operator is written for 8-bit or 16-bit integer samples.
// They access the planes through uint8_t* / uint16_t* and derive shift amounts and
// midpoint values from the bit depth (e.g. '128 << (bpp - 8)' in Op_mono_to_YCbCr420).
@@ -868,11 +888,12 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
// the image through untouched, so wide components stay accessible to the caller.
//
// This is a backstop, not the primary defence. The constraint belongs in each
- // operator's state_after_conversion(), and every operator now declares it there
- // (most through has_samples_wider_than_16bit()), so construct_pipeline() above
- // already fails for a wider input and a real conversion never reaches this loop.
- // Keep it until an operator actually supports more than 16 bits per component,
- // then remove it together with that operator's call to the helper.
+ // operator's state_after_conversion(), and every operator declares there which
+ // sample width it can read (ColorState::color_channels_have_bytes_per_sample() and
+ // friends, derived from the same bit depth to storage width mapping that
+ // HeifPixelImage uses), so construct_pipeline() above already fails for a wider
+ // input and a real conversion never reaches this loop. Keep it until an operator
+ // actually supports more than 16 bits per component, then remove it.
for (heif_channel channel : channels) {
if (input->get_bits_per_pixel(channel) > 16) {
@@ -904,9 +925,14 @@ Result<std::shared_ptr<HeifPixelImage>> convert_colorspace(const std::shared_ptr
"Color conversion of YCbCr images with differing luma and chroma bit depths is not supported."};
}
}
+ }
- return pipeline.convert_image(input, limits);
+ if (!success) {
+ return Error{heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_color_conversion};
}
+
+ return pipeline.convert_image(input, limits);
}
diff --git a/libheif/color-conversion/colorconversion.h b/libheif/color-conversion/colorconversion.h
index 9621e38d..169bdf8a 100644
--- a/libheif/color-conversion/colorconversion.h
+++ b/libheif/color-conversion/colorconversion.h
@@ -84,11 +84,19 @@ struct ColorState
// 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;
+ // Number of bytes HeifPixelImage stores per sample of the given plane (1, 2, 4, 8 or 16),
+ // 0 if the plane does not exist. Operators access samples through uint8_t or uint16_t
+ // pointers, so they declare the sample width they can handle, not just a bit depth range.
+ int get_bytes_per_sample(heif_channel channel) const;
- // True if all existing planes, including alpha, have more than 8 bits.
- bool all_channels_hdr() const;
+ // Largest sample width over all existing planes, including alpha.
+ int get_max_bytes_per_sample() const;
+
+ // True if all existing colour planes (R/G/B or Y/Cb/Cr) are stored with 'bytes' per sample.
+ bool color_channels_have_bytes_per_sample(int bytes) const;
+
+ // True if all existing planes, including alpha, are stored with 'bytes' per sample.
+ bool all_channels_have_bytes_per_sample(int bytes) const;
bool operator==(const ColorState&) const;
};
@@ -96,24 +104,13 @@ struct ColorState
std::ostream& operator<<(std::ostream& ostr, const ColorState& state);
-// True if 'state' has a colour or alpha component wider than 16 bits.
-//
-// Every conversion operator reads and writes sample data through uint8_t* or uint16_t*
-// and derives shift amounts from the bit depth, so none of them can handle a wider
-// component. Images with wider components do exist: 'unci' components may be up to 256
-// bits and we store up to 128 of them (64-bit integers, 32/64-bit floats, complex
-// numbers) so that they can be read through the component API.
-//
-// An operator that cannot handle a bit depth must not offer itself to the pipeline for
-// it, so each operator states its own supported range in state_after_conversion(). This
-// helper spells out the upper bound they currently all share; operators with a tighter
-// or different range (an exact 8 bits, or an explicit list) say so themselves instead.
-// When an operator gains support for wider samples it simply stops calling this, and the
-// catch-all in convert_colorspace() can go away.
-inline bool has_samples_wider_than_16bit(const ColorState& state)
-{
- return state.get_max_bits_per_pixel() > 16;
-}
+// Note on sample widths: HeifPixelImage stores a plane with 1, 2, 4, 8 or 16 bytes per
+// sample depending on its bit depth (bytes_per_sample_for_bit_depth() in pixelimage.h);
+// 'unci' components may be up to 128 bits wide. Every conversion operator, however, reads
+// samples through uint8_t* or uint16_t*. An operator therefore declares in
+// state_after_conversion() which sample width it accepts (ColorState::
+// color_channels_have_bytes_per_sample() and friends) instead of relying on an 8-bit
+// SDR/HDR split, which would misread any plane wider than 16 bits.
// These are some integer constants for typical color conversion Op speed costs.
// The integer value is the speed cost. Any other integer can be assigned to the speed cost.
diff --git a/libheif/color-conversion/hdr_sdr.cc b/libheif/color-conversion/hdr_sdr.cc
index ef16fb3b..ffffd5c5 100644
--- a/libheif/color-conversion/hdr_sdr.cc
+++ b/libheif/color-conversion/hdr_sdr.cc
@@ -155,9 +155,8 @@ Op_to_sdr_planes::state_after_conversion(const ColorState& input_state,
return {};
}
- // Every channel, alpha included, is read through a uint16_t* and shifted down by
- // (bits_per_pixel - 8).
- if (has_samples_wider_than_16bit(input_state)) {
+ // Every channel, alpha included, is read as uint8_t or uint16_t samples.
+ if (input_state.get_max_bytes_per_sample() > 2) {
return {};
}
@@ -204,8 +203,9 @@ Op_to_sdr_planes::convert_colorspace(const std::shared_ptr<const HeifPixelImage>
heif_channel_Alpha}) {
if (input->has_channel(channel)) {
int input_bits = input->get_bits_per_pixel(channel);
+ int input_bytes = bytes_per_sample_for_bit_depth(input_bits);
- if (input_bits > 8) {
+ if (input_bytes == 2) {
uint32_t width = input->get_width(channel);
uint32_t height = input->get_height(channel);
if (auto err = outimg->add_channel(channel, width, height, 8, limits)) {
@@ -270,8 +270,12 @@ Op_to_sdr_planes::convert_colorspace(const std::shared_ptr<const HeifPixelImage>
int in = p_in[y * stride_in + x];
p_out[y * stride_out + x] = (uint8_t) ((in * mulFactor) >> 8);
}
- } else {
+ } else if (input_bits == 8) {
outimg->copy_new_channel_from(input, channel, channel, limits);
+ } else {
+ return Error{heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_bit_depth,
+ "Op_to_sdr_planes: only 8- and 16-bit sample storage is supported"};
}
}
}
diff --git a/libheif/color-conversion/monochrome.cc b/libheif/color-conversion/monochrome.cc
index acce06ba..7a572fdc 100644
--- a/libheif/color-conversion/monochrome.cc
+++ b/libheif/color-conversion/monochrome.cc
@@ -33,7 +33,8 @@ Op_mono_to_YCbCr420::state_after_conversion(const ColorState& input_state,
return {};
}
- if (has_samples_wider_than_16bit(input_state)) {
+ // The luma plane is read as uint8_t or uint16_t samples; alpha is copied at its own width.
+ if (input_state.get_bytes_per_sample(heif_channel_Y) > 2) {
return {};
}
@@ -90,7 +91,9 @@ Op_mono_to_YCbCr420::convert_colorspace(const std::shared_ptr<const HeifPixelIma
}
- if (input_bpp <= 8) {
+ int bytes_per_sample = bytes_per_sample_for_bit_depth(input_bpp);
+
+ if (bytes_per_sample == 1) {
uint8_t* out_cb, * out_cr, * out_y;
size_t out_cb_stride = 0, out_cr_stride = 0, out_y_stride = 0;
@@ -114,7 +117,7 @@ Op_mono_to_YCbCr420::convert_colorspace(const std::shared_ptr<const HeifPixelIma
width);
}
}
- else {
+ else if (bytes_per_sample == 2) {
uint16_t* out_cb, * out_cr, * out_y;
size_t out_cb_stride = 0, out_cr_stride = 0, out_y_stride = 0;
@@ -144,6 +147,11 @@ Op_mono_to_YCbCr420::convert_colorspace(const std::shared_ptr<const HeifPixelIma
width * 2);
}
}
+ else {
+ return Error{heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_bit_depth,
+ "Monochrome to YCbCr conversion only supports 8- and 16-bit sample storage."};
+ }
if (has_alpha) {
const uint8_t* in_a;
@@ -154,7 +162,7 @@ Op_mono_to_YCbCr420::convert_colorspace(const std::shared_ptr<const HeifPixelIma
in_a = input->get_channel_memory(heif_channel_Alpha, &in_a_stride);
out_a = outimg->get_channel_memory(heif_channel_Alpha, &out_a_stride);
- uint32_t memory_width = (alpha_bpp > 8 ? width * 2 : width);
+ size_t memory_width = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(alpha_bpp));
for (uint32_t y = 0; y < height; y++) {
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], memory_width);
diff --git a/libheif/color-conversion/rgb2rgb.cc b/libheif/color-conversion/rgb2rgb.cc
index 21a7f19d..3442dc97 100644
--- a/libheif/color-conversion/rgb2rgb.cc
+++ b/libheif/color-conversion/rgb2rgb.cc
@@ -169,7 +169,8 @@ Op_RGB_HDR_to_RRGGBBaa_BE::state_after_conversion(const ColorState& input_state,
return {};
}
- if (has_samples_wider_than_16bit(input_state)) {
+ // All planes, alpha included, are read as uint16_t samples.
+ if (!input_state.all_channels_have_bytes_per_sample(2)) {
return {};
}
@@ -220,9 +221,9 @@ Op_RGB_HDR_to_RRGGBBaa_BE::convert_colorspace(const std::shared_ptr<const HeifPi
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- if (input->get_bits_per_pixel(heif_channel_R) <= 8 ||
- input->get_bits_per_pixel(heif_channel_G) <= 8 ||
- input->get_bits_per_pixel(heif_channel_B) <= 8) {
+ if (bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_R)) != 2 ||
+ bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_G)) != 2 ||
+ bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_B)) != 2) {
return Error::InternalError;
}
@@ -230,7 +231,7 @@ Op_RGB_HDR_to_RRGGBBaa_BE::convert_colorspace(const std::shared_ptr<const HeifPi
bool output_has_alpha = input_has_alpha || target_state.has_alpha();
if (input_has_alpha) {
- if (input->get_bits_per_pixel(heif_channel_Alpha) <= 8) {
+ if (bytes_per_sample_for_bit_depth(input->get_bits_per_pixel(heif_channel_Alpha)) != 2) {
return Error::InternalError;
}
@@ -468,7 +469,8 @@ Op_RRGGBBaa_BE_to_RGB_HDR::state_after_conversion(const ColorState& input_state,
return {};
}
- if (has_samples_wider_than_16bit(input_state)) {
+ // Interleaved RRGGBB samples are two bytes each.
+ if (!input_state.color_channels_have_bytes_per_sample(2)) {
return {};
}
@@ -689,7 +691,7 @@ Op_RRGGBBaa_swap_endianness::state_after_conversion(const ColorState& input_stat
// Swaps the two bytes of each component, which is only meaningful for components
// that are stored in 16 bits.
- if (has_samples_wider_than_16bit(input_state)) {
+ if (!input_state.color_channels_have_bytes_per_sample(2)) {
return {};
}
diff --git a/libheif/color-conversion/rgb2yuv.cc b/libheif/color-conversion/rgb2yuv.cc
index b392fa70..4dee7697 100644
--- a/libheif/color-conversion/rgb2yuv.cc
+++ b/libheif/color-conversion/rgb2yuv.cc
@@ -34,24 +34,16 @@ Op_RGB_to_YCbCr<Pixel>::state_after_conversion(const ColorState& input_state,
const heif_color_conversion_options& options,
const heif_color_conversion_options_ext& options_ext) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
if (input_state.colorspace != heif_colorspace_RGB ||
input_state.chroma != heif_chroma_444) {
return {};
}
- if ((input_state.bits_per_pixel_R > 8) != hdr) {
- return {};
- }
-
// All three colour planes are read through the same 'Pixel' type and converted with one
- // set of range constants, so they must share one bit depth.
- if (!input_state.color_channels_have_same_bpp()) {
- return {};
- }
-
- if (has_samples_wider_than_16bit(input_state)) {
+ // set of range constants, so they must share one bit depth and be stored with
+ // sizeof(Pixel) bytes per sample.
+ if (!input_state.color_channels_have_same_bpp() ||
+ !input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -116,8 +108,6 @@ Op_RGB_to_YCbCr<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
uint32_t width = input->get_width();
uint32_t height = input->get_height();
@@ -126,7 +116,7 @@ Op_RGB_to_YCbCr<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
int subV = chroma_v_subsampling(chroma);
int bpp = input->get_bits_per_pixel(heif_channel_R);
- if (bpp < 8 || (bpp > 8) != hdr) {
+ if (bpp < 8 || bytes_per_sample_for_bit_depth(bpp) != static_cast<int>(sizeof(Pixel))) {
return Error::InternalError;
}
@@ -188,14 +178,12 @@ Op_RGB_to_YCbCr<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
out_a = nullptr;
}
- if (hdr) {
- in_r_stride /= 2;
- in_g_stride /= 2;
- in_b_stride /= 2;
- out_y_stride /= 2;
- out_cb_stride /= 2;
- out_cr_stride /= 2;
- }
+ in_r_stride /= sizeof(Pixel);
+ in_g_stride /= sizeof(Pixel);
+ in_b_stride /= sizeof(Pixel);
+ out_y_stride /= sizeof(Pixel);
+ out_cb_stride /= sizeof(Pixel);
+ out_cr_stride /= sizeof(Pixel);
uint16_t halfRange = (uint16_t) (1 << (bpp - 1));
int32_t fullRange = (1 << bpp) - 1;
@@ -312,7 +300,7 @@ Op_RGB_to_YCbCr<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
if (has_alpha) {
int bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
- int alphaCopyWidth = (bpp_a > 8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
for (y = 0; y < height; y++) {
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
@@ -350,7 +338,8 @@ Op_RRGGBBxx_HDR_to_YCbCr420::state_after_conversion(const ColorState& input_stat
return {};
}
- if (has_samples_wider_than_16bit(input_state)) {
+ // The interleaved samples are assembled from two bytes each.
+ if (!input_state.color_channels_have_bytes_per_sample(2)) {
return {};
}
diff --git a/libheif/color-conversion/rgb2yuv_sharp.cc b/libheif/color-conversion/rgb2yuv_sharp.cc
index a7b3cf56..b761a8f8 100644
--- a/libheif/color-conversion/rgb2yuv_sharp.cc
+++ b/libheif/color-conversion/rgb2yuv_sharp.cc
@@ -187,13 +187,9 @@ Op_Any_RGB_to_YCbCr_420_Sharp::convert_colorspace(
}
}
- int input_bytes_per_sample =
- (input_chroma == heif_chroma_interleaved_RGB ||
- input_chroma == heif_chroma_interleaved_RGBA ||
- (input_chroma == heif_chroma_444 &&
- input->get_bits_per_pixel(heif_channel_R) <= 8))
- ? 1
- : 2;
+ int input_bytes_per_sample = bytes_per_sample_for_bit_depth(
+ input->get_bits_per_pixel(input_chroma == heif_chroma_444 ? heif_channel_R
+ : heif_channel_interleaved));
const uint8_t* in_r, * in_g, * in_b, * in_a = nullptr;
size_t in_stride = 0;
diff --git a/libheif/color-conversion/yuv2rgb.cc b/libheif/color-conversion/yuv2rgb.cc
index d5e1cf34..3772ac3f 100644
--- a/libheif/color-conversion/yuv2rgb.cc
+++ b/libheif/color-conversion/yuv2rgb.cc
@@ -58,9 +58,9 @@ Op_YCbCr_to_RGB<Pixel>::state_after_conversion(const ColorState& input_state,
// BT.2020 NCL. A correct CL path needs EOTF inversion on Cb/Cr, not the linear matrix below.
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
- if ((input_state.bits_per_pixel_Y > 8) != hdr) {
+ // The three colour planes are read through the same 'Pixel' type, so they must be stored
+ // with sizeof(Pixel) bytes per sample. The alpha plane is copied through at its own width.
+ if (!input_state.color_channels_have_bytes_per_sample(static_cast<int>(sizeof(Pixel)))) {
return {};
}
@@ -75,11 +75,6 @@ Op_YCbCr_to_RGB<Pixel>::state_after_conversion(const ColorState& input_state,
return {};
}
- // Also covers the alpha plane, which is copied through at its own bit depth below.
- if (has_samples_wider_than_16bit(input_state)) {
- return {};
- }
-
std::vector<ColorStateWithCost> states;
ColorState output_state;
@@ -106,8 +101,6 @@ Op_YCbCr_to_RGB<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
const heif_color_conversion_options_ext& options_ext,
const heif_security_limits* limits) const
{
- bool hdr = !std::is_same<Pixel, uint8_t>::value;
-
heif_chroma chroma = input->get_chroma_format();
int bpp_y = input->get_bits_per_pixel(heif_channel_Y);
@@ -121,19 +114,10 @@ Op_YCbCr_to_RGB<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
bpp_a = input->get_bits_per_pixel(heif_channel_Alpha);
}
- if (!hdr) {
- if (bpp_y != 8 ||
- bpp_cb != 8 ||
- bpp_cr != 8) {
- return Error::InternalError;
- }
- }
- else {
- if (bpp_y == 8 ||
- bpp_cb == 8 ||
- bpp_cr == 8) {
- return Error::InternalError;
- }
+ if (bytes_per_sample_for_bit_depth(bpp_y) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cb) != static_cast<int>(sizeof(Pixel)) ||
+ bytes_per_sample_for_bit_depth(bpp_cr) != static_cast<int>(sizeof(Pixel))) {
+ return Error::InternalError;
}
@@ -201,14 +185,12 @@ Op_YCbCr_to_RGB<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
int shiftH = chroma_h_subsampling(chroma) - 1;
int shiftV = chroma_v_subsampling(chroma) - 1;
- if (hdr) {
- in_y_stride /= 2;
- in_cb_stride /= 2;
- in_cr_stride /= 2;
- out_r_stride /= 2;
- out_g_stride /= 2;
- out_b_stride /= 2;
- }
+ in_y_stride /= sizeof(Pixel);
+ in_cb_stride /= sizeof(Pixel);
+ in_cr_stride /= sizeof(Pixel);
+ out_r_stride /= sizeof(Pixel);
+ out_g_stride /= sizeof(Pixel);
+ out_b_stride /= sizeof(Pixel);
int matrix_coeffs = 2;
bool full_range_flag = true;
@@ -297,7 +279,7 @@ Op_YCbCr_to_RGB<Pixel>::convert_colorspace(const std::shared_ptr<const HeifPixel
}
if (has_alpha) {
- int alphaCopyWidth = (bpp_a>8 ? width * 2 : width);
+ size_t alphaCopyWidth = static_cast<size_t>(width) * static_cast<size_t>(bytes_per_sample_for_bit_depth(bpp_a));
memcpy(&out_a[y * out_a_stride], &in_a[y * in_a_stride], alphaCopyWidth);
}
}
@@ -597,7 +579,8 @@ Op_YCbCr420_to_RRGGBBaa::state_after_conversion(const ColorState& input_state,
return {};
}
- if (has_samples_wider_than_16bit(input_state)) {
+ // All planes, alpha included, are read as uint16_t samples.
+ if (!input_state.all_channels_have_bytes_per_sample(2)) {
return {};
}
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index c197c290..a7c72c27 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -432,12 +432,7 @@ Error HeifPixelImage::ComponentStorage::alloc(uint32_t width, uint32_t height, h
m_datatype = datatype;
// Cache bytes-per-pixel for the inner-loop get_bytes_per_pixel().
- int bytes_per_component;
- if (bit_depth <= 8) bytes_per_component = 1;
- else if (bit_depth <= 16) bytes_per_component = 2;
- else if (bit_depth <= 32) bytes_per_component = 4;
- else if (bit_depth <= 64) bytes_per_component = 8;
- else bytes_per_component = 16;
+ int bytes_per_component = bytes_per_sample_for_bit_depth(bit_depth);
// m_bytes_per_pixel is a uint8_t. bytes_per_component * num_interleaved_components can
// exceed 255 even though num_interleaved_components itself is already bounded to <= 255
diff --git a/libheif/image/pixelimage.h b/libheif/image/pixelimage.h
index 3a5dc13b..cf7c8e69 100644
--- a/libheif/image/pixelimage.h
+++ b/libheif/image/pixelimage.h
@@ -57,6 +57,28 @@ std::vector<heif_chroma> get_valid_chroma_values_for_colorspace(heif_colorspace
+// Number of bytes HeifPixelImage uses to store one sample of the given bit depth
+// (1, 2, 4, 8 or 16). This is the single source of truth for the sample width: plane
+// allocation uses it, and so does every code path that reinterprets plane memory as
+// uint8_t/uint16_t samples (color conversion in particular), so that they cannot drift.
+inline int bytes_per_sample_for_bit_depth(int bit_depth)
+{
+ if (bit_depth <= 8) {
+ return 1;
+ }
+ if (bit_depth <= 16) {
+ return 2;
+ }
+ if (bit_depth <= 32) {
+ return 4;
+ }
+ if (bit_depth <= 64) {
+ return 8;
+ }
+ return 16;
+}
+
+
class HeifPixelImage : public std::enable_shared_from_this<HeifPixelImage>,
public ImageDescription,
public ErrorBuffer
diff --git a/tests/conversion.cc b/tests/conversion.cc
index cdf096b8..7151f399 100644
--- a/tests/conversion.cc
+++ b/tests/conversion.cc
@@ -28,6 +28,9 @@
#include "catch_amalgamated.hpp"
#include "color-conversion/colorconversion.h"
#include "color-conversion/hdr_sdr.h"
+#include "color-conversion/monochrome.h"
+#include "color-conversion/rgb2yuv.h"
+#include "color-conversion/yuv2rgb.h"
#include "image/pixelimage.h"
#include <cmath>
@@ -958,6 +961,48 @@ TEST_CASE("Mismatched alpha bit depth - conversion correctness") {
}
+// An 'unci' component may be 32, 64 or 128 bits wide. HeifPixelImage stores such planes with
+// 4, 8 or 16 bytes per sample, but every conversion operator reads samples through uint8_t
+// or uint16_t pointers. An operator must therefore decline such input in
+// state_after_conversion() based on the sample width (not on an 8-bit SDR/HDR split, which
+// would treat a 64-bit plane like a 16-bit one), so that no pipeline is ever built for it.
+TEST_CASE("Conversion operators decline planes wider than 16 bits", "[heif_image]")
+{
+ heif_color_conversion_options options{};
+ std::unique_ptr<heif_color_conversion_options_ext, void(*)(heif_color_conversion_options_ext*)>
+ options_ext(heif_color_conversion_options_ext_alloc(), heif_color_conversion_options_ext_free);
+
+ ColorState rgb8(heif_colorspace_RGB, heif_chroma_444, false, 8);
+ ColorState rrggbb16(heif_colorspace_RGB, heif_chroma_interleaved_RRGGBB_LE, false, 16);
+ ColorState ycbcr8(heif_colorspace_YCbCr, heif_chroma_444, false, 8);
+ nclx_default_if_undefined(ycbcr8);
+
+ for (int bits : {32, 64, 128}) {
+ INFO("bits=" << bits);
+
+ ColorState ycbcr(heif_colorspace_YCbCr, heif_chroma_444, false, bits);
+ nclx_default_if_undefined(ycbcr);
+ ColorState rgb(heif_colorspace_RGB, heif_chroma_444, false, bits);
+ ColorState mono(heif_colorspace_monochrome, heif_chroma_monochrome, false, bits);
+
+ CHECK(ycbcr.get_bytes_per_sample(heif_channel_Y) > 2);
+ CHECK_FALSE(ycbcr.color_channels_have_bytes_per_sample(2));
+
+ // The individual operators must not offer themselves ...
+ CHECK(Op_YCbCr_to_RGB<uint16_t>().state_after_conversion(ycbcr, rgb, options, *options_ext).empty());
+ CHECK(Op_RGB_to_YCbCr<uint16_t>().state_after_conversion(rgb, ycbcr, options, *options_ext).empty());
+ CHECK(Op_mono_to_YCbCr420().state_after_conversion(mono, ycbcr, options, *options_ext).empty());
+ CHECK(Op_to_sdr_planes().state_after_conversion(rgb, rgb8, options, *options_ext).empty());
+
+ // ... and consequently no pipeline can be built from such an input.
+ ColorConversionPipeline pipeline;
+ CHECK_FALSE(pipeline.construct_pipeline(ycbcr, rgb8, options, *options_ext));
+ CHECK_FALSE(pipeline.construct_pipeline(rgb, rrggbb16, options, *options_ext));
+ CHECK_FALSE(pipeline.construct_pipeline(mono, ycbcr8, options, *options_ext));
+ }
+}
+
+
// Regression test for GHSA-8857-r8x5-7499. Op_to_hdr_planes widens an 8-bit input to a higher
// bit depth with out = (in << (m-8)) | (in >> (16-m)). For m > 16 the right shift exponent
// (16-m) becomes negative, which is undefined behavior (UBSan: "shift exponent is negative"),