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