Commit d13e8518 for libheif
commit d13e85186307d37940240ce13c476e32d17ea546
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Fri May 15 00:06:31 2026 +0200
improved check of decoded image size
diff --git a/libheif/api/libheif/heif_image.cc b/libheif/api/libheif/heif_image.cc
index 1bd870a5..7fdf0044 100644
--- a/libheif/api/libheif/heif_image.cc
+++ b/libheif/api/libheif/heif_image.cc
@@ -68,15 +68,13 @@ int heif_image_get_height(const heif_image* img, heif_channel channel)
int heif_image_get_primary_width(const heif_image* img)
{
- uint32_t primary_component = img->image->get_primary_component_id();
- return uint32_to_int(img->image->get_width(primary_component));
+ return uint32_to_int(img->image->get_width());
}
int heif_image_get_primary_height(const heif_image* img)
{
- uint32_t primary_component = img->image->get_primary_component_id();
- return uint32_to_int(img->image->get_height(primary_component));
+ return uint32_to_int(img->image->get_height());
}
diff --git a/libheif/api/libheif/heif_image.h b/libheif/api/libheif/heif_image.h
index aee9c85a..6b903d27 100644
--- a/libheif/api/libheif/heif_image.h
+++ b/libheif/api/libheif/heif_image.h
@@ -209,9 +209,10 @@ LIBHEIF_API
int heif_image_get_height(const heif_image* img, heif_channel channel);
/**
- * Get the width of the main channel.
+ * Get the logical width of the image.
*
- * This is the Y channel in YCbCr or mono, or any in RGB.
+ * For well-formed images this equals the size of the main channel (the Y channel
+ * in YCbCr or mono, or the RGB channels). Subsampled chroma channels may be smaller.
*
* @param img the image to get the primary width for
* @return the width in pixels
@@ -220,9 +221,10 @@ LIBHEIF_API
int heif_image_get_primary_width(const heif_image* img);
/**
- * Get the height of the main channel.
+ * Get the logical height of the image.
*
- * This is the Y channel in YCbCr or mono, or any in RGB.
+ * For well-formed images this equals the size of the main channel (the Y channel
+ * in YCbCr or mono, or the RGB channels). Subsampled chroma channels may be smaller.
*
* @param img the image to get the primary height for
* @return the height in pixels
diff --git a/libheif/context.cc b/libheif/context.cc
index 7ccca778..b91468f2 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1423,21 +1423,8 @@ Result<std::shared_ptr<HeifPixelImage>> HeifContext::decode_image(heif_item_id I
// without a populated description list (grid/overlay/iden).
img->apply_descriptions_from(*imgitem);
-
- // --- check that the decoded image has the claimed size (only check if transformations are applied)
-
- if (!options.ignore_transformations && !decode_only_tile) {
- uint32_t primary_component = img->get_primary_component_id();
-
- if (imgitem->get_width() != img->get_width(primary_component) ||
- imgitem->get_height() != img->get_height(primary_component)) {
- return Error{
- heif_error_Invalid_input,
- heif_suberror_Invalid_image_size,
- "Decoded image does not have the claimed size."
- };
- }
- }
+ // Note: the decoded image is validated against the signaled size inside
+ // ImageItem::decode_image() (via the per-item check_decoded_image_size()).
// --- convert to output chroma format
diff --git a/libheif/image-items/grid.cc b/libheif/image-items/grid.cc
index cf22d877..55658808 100644
--- a/libheif/image-items/grid.cc
+++ b/libheif/image-items/grid.cc
@@ -228,6 +228,12 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem_Grid::decode_compressed_image(
}
}
+// Note: ImageItem_Grid does not override check_decoded_image_size(). The composed
+// grid image is built to the grid-header size by construction (decode_and_paste_tile_image
+// creates the canvas at get_grid_spec() size), so checking it against that same size
+// would be tautological. The base default checks the composed image against 'ispe',
+// which is the meaningful cross-check (grid-header size vs signaled size).
+
#if ENABLE_PARALLEL_TILE_DECODING
static void wait_for_jobs(std::deque<std::future<Error> >* jobs) {
if (jobs->empty()) {
diff --git a/libheif/image-items/iden.h b/libheif/image-items/iden.h
index c3f58dbc..30a3127e 100644
--- a/libheif/image-items/iden.h
+++ b/libheif/image-items/iden.h
@@ -68,6 +68,13 @@ public:
bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
std::set<heif_item_id> processed_ids) const override;
+ // iden forwards decoding to the referenced item, which validates its own decoded
+ // size. Re-checking here against this iden's (possibly absent) 'ispe' would be wrong.
+ Error check_decoded_image_size(const HeifPixelImage&, bool, uint32_t, uint32_t) const override
+ {
+ return Error::Ok;
+ }
+
heif_brand2 get_compatible_brand() const override;
private:
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index f8b66498..be374c40 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -893,6 +893,12 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
return Error(heif_error_Decoder_plugin_error, heif_suberror_Unspecified);
}
+ // --- validate the decoded image against the signaled size (pre-transform)
+
+ if (Error err = check_decoded_image_size(*img, decode_tile_only, tile_x0, tile_y0)) {
+ return err;
+ }
+
std::shared_ptr<HeifFile> file = m_heif_context->get_heif_file();
@@ -1141,6 +1147,38 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_compressed_image(const
}
+Error ImageItem::check_decoded_image_size(const HeifPixelImage& img,
+ bool decode_tile_only,
+ uint32_t tile_x0, uint32_t tile_y0) const
+{
+ uint32_t expected_w, expected_h;
+
+ if (decode_tile_only) {
+ // The decoded buffer is a single tile, sized to the signaled tile size.
+ get_tile_size(expected_w, expected_h);
+ }
+ else {
+ // Pre-transform coded size from the 'ispe' property.
+ expected_w = get_ispe_width();
+ expected_h = get_ispe_height();
+ }
+
+ // No 'ispe' / no tile size known -> cannot validate (a missing-'ispe' warning is
+ // already emitted upstream). Skip rather than reject.
+ if (expected_w == 0 || expected_h == 0) {
+ return Error::Ok;
+ }
+
+ if (!img.primary_planes_have_size(expected_w, expected_h)) {
+ return Error{heif_error_Invalid_input,
+ heif_suberror_Invalid_image_size,
+ "Decoded image does not have the size signaled in the file."};
+ }
+
+ return Error::Ok;
+}
+
+
heif_image_tiling ImageItem::get_heif_image_tiling() const
{
// --- Return a dummy tiling consisting of only a single tile for the whole image
diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index de4401cb..0a390f06 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -360,6 +360,16 @@ public:
uint32_t tile_y0,
std::set<heif_item_id> processed_ids) const;
+ // Validate the just-decoded pixel image against the size signaled for this item.
+ // Called by decode_image() right after decode_compressed_image(), BEFORE transforms,
+ // so the reference is the pre-transform coded size (ispe), or the signaled tile size
+ // for a tile decode -- NOT get_width()/get_height() (which are post-transform).
+ // Default impl handles plain coded codecs (HEVC/AVC/VVC/AVIF/JPEG). Subclasses
+ // override where the size source or component layout differs.
+ virtual Error check_decoded_image_size(const HeifPixelImage& img,
+ bool decode_tile_only,
+ uint32_t tile_x0, uint32_t tile_y0) const;
+
Result<std::vector<std::shared_ptr<Box>>> get_properties() const;
bool has_essential_property_other_than(const std::set<uint32_t>&) const;
diff --git a/libheif/image-items/jpeg2000.cc b/libheif/image-items/jpeg2000.cc
index 406b4a1a..8e2ef90a 100644
--- a/libheif/image-items/jpeg2000.cc
+++ b/libheif/image-items/jpeg2000.cc
@@ -96,6 +96,34 @@ void ImageItem_JPEG2000::set_decoder_input_data()
m_decoder->set_data_extent(std::move(extent));
}
+Error ImageItem_JPEG2000::check_decoded_image_size(const HeifPixelImage& img,
+ bool decode_tile_only,
+ uint32_t tile_x0, uint32_t tile_y0) const
+{
+ uint32_t expected_w, expected_h;
+
+ if (decode_tile_only) {
+ get_tile_size(expected_w, expected_h);
+ }
+ else {
+ expected_w = get_ispe_width();
+ expected_h = get_ispe_height();
+ }
+
+ if (expected_w == 0 || expected_h == 0) {
+ return Error::Ok;
+ }
+
+ // Only the logical image size is checked; the component layout may be arbitrary.
+ if (img.get_width() != expected_w || img.get_height() != expected_h) {
+ return Error{heif_error_Invalid_input,
+ heif_suberror_Invalid_image_size,
+ "Decoded image does not have the size signaled in the file."};
+ }
+
+ return Error::Ok;
+}
+
heif_brand2 ImageItem_JPEG2000::get_compatible_brand() const
{
return heif_brand2_j2ki;
diff --git a/libheif/image-items/jpeg2000.h b/libheif/image-items/jpeg2000.h
index 991e8c6a..def84d08 100644
--- a/libheif/image-items/jpeg2000.h
+++ b/libheif/image-items/jpeg2000.h
@@ -55,6 +55,13 @@ public:
void set_decoder_input_data() override;
+ // JPEG2000 codestreams (SIZ marker) can carry arbitrary component grids and
+ // subsampling, so the generic per-plane check is not appropriate. Validate only
+ // the logical image size (set by the plugin from SIZ Xsiz/Ysiz) against 'ispe'.
+ Error check_decoded_image_size(const HeifPixelImage& img,
+ bool decode_tile_only,
+ uint32_t tile_x0, uint32_t tile_y0) const override;
+
private:
std::shared_ptr<class Decoder_JPEG2000> m_decoder;
std::shared_ptr<class Encoder_JPEG2000> m_encoder;
diff --git a/libheif/image-items/overlay.cc b/libheif/image-items/overlay.cc
index 5073c92f..acf0381f 100644
--- a/libheif/image-items/overlay.cc
+++ b/libheif/image-items/overlay.cc
@@ -279,6 +279,12 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem_Overlay::decode_compressed_ima
return decode_overlay_image(options, processed_ids);
}
+// Note: ImageItem_Overlay does not override check_decoded_image_size(). The overlay
+// canvas is built to the overlay-header size by construction (decode_overlay_image
+// creates it at m_overlay_spec canvas size), so checking it against that same size
+// would be tautological. The base default checks the canvas against 'ispe', which is
+// the meaningful cross-check (overlay-header size vs signaled size).
+
Result<std::shared_ptr<HeifPixelImage>> ImageItem_Overlay::decode_overlay_image(const heif_decoding_options& options,
std::set<heif_item_id> processed_ids) const
diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index bcd9bb43..ae5fa650 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -697,88 +697,42 @@ uint32_t HeifPixelImage::get_height(uint32_t component_id) const
}
-uint32_t HeifPixelImage::get_primary_component_id() const
+bool HeifPixelImage::primary_planes_have_size(uint32_t width, uint32_t height) const
{
- // first pass: search for a visual channel
+ auto channel_has_size = [&](heif_channel channel) {
+ // get_width()/get_height() return 0 for an absent channel -> mismatch.
+ return get_width(channel) == width && get_height(channel) == height;
+ };
- for (uint32_t idx=0; idx<m_storage.size(); idx++) {
- switch (m_storage[idx].m_channel) {
- case heif_channel_interleaved:
- case heif_channel_Y:
- case heif_channel_R:
- case heif_channel_G:
- case heif_channel_B:
- case heif_channel_filter_array:
- return m_storage[idx].m_component_ids[0];
- default:
- ; // NOP
- }
- }
-
- // second pass: if we have a cmpd table, use component types
-
- for (const auto& comp : get_component_descriptions()) {
- switch (comp.component_type) {
- case heif_unci_component_type_Y:
- case heif_unci_component_type_monochrome:
- case heif_unci_component_type_red:
- case heif_unci_component_type_green:
- case heif_unci_component_type_blue:
- case heif_unci_component_type_cyan:
- case heif_unci_component_type_magenta:
- case heif_unci_component_type_yellow:
- case heif_unci_component_type_key_black:
- case heif_unci_component_type_filter_array:
- case heif_unci_component_type_palette:
- return comp.component_id;
-
- default:
- ; // NOP
- }
- }
-
- // third pass: allow anything
+ switch (m_colorspace) {
+ case heif_colorspace_monochrome:
+ case heif_colorspace_YCbCr:
+ // Cb/Cr may be legitimately subsampled, so only the Y plane is checked.
+ return channel_has_size(heif_channel_Y);
- if (!m_storage.empty()) {
- return m_storage[0].m_component_ids[0];
- }
+ case heif_colorspace_RGB:
+ if (m_chroma == heif_chroma_444) {
+ // planar RGB: all three planes must be present and have the full size
+ return channel_has_size(heif_channel_R) &&
+ channel_has_size(heif_channel_G) &&
+ channel_has_size(heif_channel_B);
+ }
+ else {
+ return channel_has_size(heif_channel_interleaved);
+ }
- return 0; // invalid component ID
-}
+ case heif_colorspace_filter_array:
+ return channel_has_size(heif_channel_filter_array);
-#if 0
-uint32_t HeifPixelImage::get_primary_width() const
-{
- if (m_colorspace == heif_colorspace_RGB) {
- if (m_chroma == heif_chroma_444) {
- return get_width(heif_channel_G);
- }
- else {
- return get_width(heif_channel_interleaved);
- }
- }
- else {
- return get_width(heif_channel_Y);
+ case heif_colorspace_undefined:
+ default:
+ // Multi-component / custom-colorspace images (CMYK, bayer configs, ...)
+ // cannot be checked generically; codec-specific overrides handle these.
+ return true;
}
}
-uint32_t HeifPixelImage::get_primary_height() const
-{
- if (m_colorspace == heif_colorspace_RGB) {
- if (m_chroma == heif_chroma_444) {
- return get_height(heif_channel_G);
- }
- else {
- return get_height(heif_channel_interleaved);
- }
- }
- else {
- return get_height(heif_channel_Y);
- }
-}
-#endif
-
std::set<heif_channel> HeifPixelImage::get_channel_set() const
{
std::set<heif_channel> channels;
diff --git a/libheif/image/pixelimage.h b/libheif/image/pixelimage.h
index 6bb649a2..ca59de85 100644
--- a/libheif/image/pixelimage.h
+++ b/libheif/image/pixelimage.h
@@ -98,13 +98,14 @@ public:
bool has_odd_height() const { return !!(m_height & 1); }
- // TODO: currently only defined for colorspace RGB, YCbCr, Monochrome
- //uint32_t get_primary_width() const;
-
- // TODO: currently only defined for colorspace RGB, YCbCr, Monochrome
- //uint32_t get_primary_height() const;
-
- uint32_t get_primary_component_id() const;
+ // Returns true if the "primary" pixel plane(s) have exactly the given size.
+ // Which plane is "primary" depends on the colorspace:
+ // YCbCr / monochrome -> the Y plane (Cb/Cr may be legitimately subsampled)
+ // RGB planar -> R, G and B planes must all be present and all equal
+ // RGB interleaved -> the interleaved plane
+ // filter_array -> the filter_array plane
+ // undefined / custom -> not checked here; returns true
+ bool primary_planes_have_size(uint32_t width, uint32_t height) const;
heif_chroma get_chroma_format() const { return m_chroma; }