Commit fc33dc8e for libheif

commit fc33dc8e0525c03efa7b3b362a34f16ce1054a9b
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Wed May 13 22:15:15 2026 +0200

    iterate components instead of heif_channels when rotating or cropping image

diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index 10e10dfb..d75ddd9a 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -296,7 +296,6 @@ static uint32_t rounded_size(uint32_t s)
 }

 void HeifPixelImage::register_plane_descriptions(ImageComponent& plane,
-                                                 heif_channel channel,
                                                  const std::vector<uint16_t>& component_types)
 {
   for (uint16_t type : component_types) {
@@ -305,7 +304,7 @@ void HeifPixelImage::register_plane_descriptions(ImageComponent& plane,

     ComponentDescription desc;
     desc.component_id = id;
-    desc.channel = channel;
+    desc.channel = plane.m_channel;
     desc.component_type = type;
     desc.datatype = plane.m_datatype;
     desc.bit_depth = plane.m_bit_depth;
@@ -317,6 +316,32 @@ void HeifPixelImage::register_plane_descriptions(ImageComponent& plane,
 }


+void HeifPixelImage::register_plane_descriptions(ImageComponent& plane,
+                                                 const std::vector<const ComponentDescription*>& source_descriptions)
+{
+  for (const ComponentDescription* src : source_descriptions) {
+    uint32_t id = mint_component_id();
+    plane.m_component_ids.push_back(id);
+
+    // Start from the source description so per-component metadata
+    // (component_type, gimi_content_id, has_data_plane, ...) is preserved.
+    // If the lookup failed (shouldn't happen on a well-formed source),
+    // fall back to a default-initialized description.
+    ComponentDescription desc;
+    if (src) {
+      desc = *src;
+    }
+    desc.component_id = id;
+    desc.channel = plane.m_channel;
+    desc.datatype = plane.m_datatype;
+    desc.bit_depth = plane.m_bit_depth;
+    desc.width = plane.m_width;
+    desc.height = plane.m_height;
+    add_component_description(std::move(desc));
+  }
+}
+
+
 Error HeifPixelImage::add_plane(heif_channel channel, uint32_t width, uint32_t height, int bit_depth,
                                 const heif_security_limits* limits)
 {
@@ -339,7 +364,7 @@ Error HeifPixelImage::add_plane(heif_channel channel, uint32_t width, uint32_t h
     return err;
   }

-  register_plane_descriptions(plane, channel, map_channel_to_component_type(channel, m_chroma));
+  register_plane_descriptions(plane, map_channel_to_component_type(channel, m_chroma));
   m_planes.push_back(std::move(plane));
   return Error::Ok;
 }
@@ -355,7 +380,7 @@ Error HeifPixelImage::add_channel(heif_channel channel, uint32_t width, uint32_t
     return err;
   }

-  register_plane_descriptions(plane, channel, map_channel_to_component_type(channel, m_chroma));
+  register_plane_descriptions(plane, map_channel_to_component_type(channel, m_chroma));
   m_planes.push_back(std::move(plane));
   return Error::Ok;
 }
@@ -1213,8 +1238,6 @@ Result<std::shared_ptr<HeifPixelImage>> HeifPixelImage::rotate_ccw(int angle_deg
   // --- rotate all channels

   for (const auto &component: m_planes) {
-    heif_channel channel = component.m_channel;
-
     uint32_t out_plane_width = component.m_width;
     uint32_t out_plane_height = component.m_height;

@@ -1222,29 +1245,44 @@ Result<std::shared_ptr<HeifPixelImage>> HeifPixelImage::rotate_ccw(int angle_deg
       std::swap(out_plane_width, out_plane_height);
     }

-    Error err = out_img->add_channel(channel, out_plane_width, out_plane_height, component.m_datatype, component.m_bit_depth, limits);
-    if (err) {
+    ImageComponent out_component;
+    out_component.m_channel = component.m_channel;
+
+    if (Error err = out_component.alloc(out_plane_width, out_plane_height,
+                                        component.m_datatype, component.m_bit_depth,
+                                        component.m_num_interleaved_components,
+                                        limits, out_img->m_memory_handle)) {
       return err;
     }

-    // The output plane is the last one added
-    ImageComponent& out_plane = out_img->m_planes.back();
+    // Clone per-component metadata (component_type, gimi_content_id, ...)
+    // from the source descriptions rather than re-deriving from chroma, so
+    // images built via add_component() preserve their original component
+    // types and content ids.
+    std::vector<const ComponentDescription*> src_descs;
+    src_descs.reserve(component.m_component_ids.size());
+    for (uint32_t cid : component.m_component_ids) {
+      src_descs.push_back(find_component_description(cid));
+    }
+    out_img->register_plane_descriptions(out_component, src_descs);

     if (component.m_bit_depth <= 8) {
-      component.rotate_ccw<uint8_t>(angle_degrees, out_plane);
+      component.rotate_ccw<uint8_t>(angle_degrees, out_component);
     }
     else if (component.m_bit_depth <= 16) {
-      component.rotate_ccw<uint16_t>(angle_degrees, out_plane);
+      component.rotate_ccw<uint16_t>(angle_degrees, out_component);
     }
     else if (component.m_bit_depth <= 32) {
-      component.rotate_ccw<uint32_t>(angle_degrees, out_plane);
+      component.rotate_ccw<uint32_t>(angle_degrees, out_component);
     }
     else if (component.m_bit_depth <= 64) {
-      component.rotate_ccw<uint64_t>(angle_degrees, out_plane);
+      component.rotate_ccw<uint64_t>(angle_degrees, out_component);
     }
     else if (component.m_bit_depth <= 128) {
-      component.rotate_ccw<heif_complex64>(angle_degrees, out_plane);
+      component.rotate_ccw<heif_complex64>(angle_degrees, out_component);
     }
+
+    out_img->m_planes.push_back(std::move(out_component));
   }
   // --- pass the color profiles to the new image

@@ -1424,21 +1462,32 @@ Result<std::shared_ptr<HeifPixelImage>> HeifPixelImage::crop(uint32_t left, uint
     uint32_t plane_top = get_subsampled_size_v(top, channel, m_chroma, scaling_mode::is_divisible);
     uint32_t plane_bottom = get_subsampled_size_v(bottom, channel, m_chroma, scaling_mode::round_down);

-    auto err = out_img->add_channel(channel,
-                                    plane_right - plane_left + 1,
+    ImageComponent out_plane;
+    out_plane.m_channel = channel;
+
+    if (Error err = out_plane.alloc(plane_right - plane_left + 1,
                                     plane_bottom - plane_top + 1,
-                                    component.m_datatype,
-                                    component.m_bit_depth,
-                                    limits);
-    if (err) {
+                                    component.m_datatype, component.m_bit_depth,
+                                    component.m_num_interleaved_components,
+                                    limits, out_img->m_memory_handle)) {
       return err;
     }

-    // The output plane is the last one added
-    ImageComponent& out_plane = out_img->m_planes.back();
+    // Clone per-component metadata (component_type, gimi_content_id, ...)
+    // from the source descriptions rather than re-deriving from chroma, so
+    // images built via add_component() preserve their original component
+    // types and content ids.
+    std::vector<const ComponentDescription*> src_descs;
+    src_descs.reserve(component.m_component_ids.size());
+    for (uint32_t cid : component.m_component_ids) {
+      src_descs.push_back(find_component_description(cid));
+    }
+    out_img->register_plane_descriptions(out_plane, src_descs);

     int bytes_per_pixel = component.get_bytes_per_pixel();
     component.crop(plane_left, plane_right, plane_top, plane_bottom, bytes_per_pixel, out_plane);
+
+    out_img->m_planes.push_back(std::move(out_plane));
   }

   // --- pass the color profiles to the new image
@@ -2135,7 +2184,7 @@ Result<uint32_t> HeifPixelImage::add_component(uint32_t width, uint32_t height,
     return {err};
   }

-  register_plane_descriptions(plane, channel, {component_type});
+  register_plane_descriptions(plane, std::vector<uint16_t>{component_type});
   uint32_t component_id = plane.m_component_ids.front();
   m_planes.push_back(std::move(plane));
   return component_id;
diff --git a/libheif/image/pixelimage.h b/libheif/image/pixelimage.h
index 51ebb485..630419be 100644
--- a/libheif/image/pixelimage.h
+++ b/libheif/image/pixelimage.h
@@ -371,13 +371,22 @@ private:

   // After plane.alloc() has succeeded, mints fresh component ids, appends
   // them to plane.m_component_ids, and pushes fully-populated
-  // ComponentDescription entries for each component_type. Must only be
-  // called once allocation has succeeded so that no descriptions are
-  // registered for a plane that failed to materialize.
+  // ComponentDescription entries for each component_type. Channel, datatype,
+  // bit_depth, width and height are read from `plane`. Must only be called
+  // once allocation has succeeded so that no descriptions are registered for
+  // a plane that failed to materialize.
   void register_plane_descriptions(ImageComponent& plane,
-                                   heif_channel channel,
                                    const std::vector<uint16_t>& component_types);

+  // Overload that clones existing ComponentDescriptions (preserving
+  // component_type, gimi_content_id, has_data_plane, and any other per-id
+  // metadata). Geometry/datatype/bit_depth/channel fields are overwritten
+  // from `plane`; component_ids are freshly minted on this image. Use this
+  // when allocating a new plane that mirrors an existing one (e.g.
+  // geometry-preserving transforms like rotation and crop).
+  void register_plane_descriptions(ImageComponent& plane,
+                                   const std::vector<const ComponentDescription*>& source_descriptions);
+
   uint32_t m_width = 0;
   uint32_t m_height = 0;
   heif_colorspace m_colorspace = heif_colorspace_undefined;