Commit 12dc9cf7 for libheif

commit 12dc9cf71d06e1f62fb1ed11a9be44b662d5d4c3
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sun May 17 21:04:09 2026 +0200

    add unit-test for mini box orientation round-trip

diff --git a/libheif/file_layout.cc b/libheif/file_layout.cc
index 709aa8ae..bf27ab50 100644
--- a/libheif/file_layout.cc
+++ b/libheif/file_layout.cc
@@ -182,15 +182,11 @@ Error FileLayout::read(const std::shared_ptr<StreamReader>& stream, const heif_s
       std::shared_ptr<Box> mini_box;
       err = Box::read(mini_box_range, &mini_box, heif_get_global_security_limits());
       if (err) {
-        std::cout << "error reading mini box" << std::endl;
         return err;
       }

       m_boxes.push_back(mini_box);
       m_mini_box = std::dynamic_pointer_cast<Box_mini>(mini_box);
-      if (m_mini_box == nullptr) {
-        std::cout << "error casting mini box" << std::endl;
-      }

       mini_found = true;
     }
diff --git a/libheif/mini.cc b/libheif/mini.cc
index 43b61e4f..fd91057e 100644
--- a/libheif/mini.cc
+++ b/libheif/mini.cc
@@ -1592,12 +1592,28 @@ static heif_orientation transform_box_to_orientation(const std::shared_ptr<Box>&
         return heif_orientation_flip_horizontally;
       case heif_transform_mirror_direction_vertical:
         return heif_orientation_flip_vertically;
+      case heif_transform_mirror_direction_invalid:
+        return heif_orientation_normal;
     }
   }
   return heif_orientation_normal;
 }


+heif_orientation Box_mini::compute_orientation_from_properties(
+    const std::vector<std::shared_ptr<Box>>& properties)
+{
+  heif_orientation orientation = heif_orientation_normal;
+  for (auto& prop : properties) {
+    if (std::dynamic_pointer_cast<Box_irot>(prop) ||
+        std::dynamic_pointer_cast<Box_imir>(prop)) {
+      orientation = heif_orientation_concat(orientation, transform_box_to_orientation(prop));
+    }
+  }
+  return orientation;
+}
+
+
 // --- Extract codec config as raw bytes (without box header) ---

 static std::vector<uint8_t> extract_codec_config_bytes(const std::shared_ptr<Box>& codec_config_box)
@@ -1762,11 +1778,6 @@ std::shared_ptr<Box_mini> Box_mini::create_from_heif_file(HeifFile* file)
   std::shared_ptr<Box_colr> colr_icc;
   std::shared_ptr<Box> codec_config;

-  // Accumulate cumulative orientation by composing transforms in property
-  // order. Use heif_orientation_concat() so the result is correct regardless
-  // of whether irot or imir comes first in ipma.
-  heif_orientation orientation = heif_orientation_normal;
-
   for (auto& prop : properties) {
     if (auto p = std::dynamic_pointer_cast<Box_ispe>(prop)) {
       ispe = p;
@@ -1782,15 +1793,13 @@ std::shared_ptr<Box_mini> Box_mini::create_from_heif_file(HeifFile* file)
         colr_icc = p;
       }
     }
-    else if (std::dynamic_pointer_cast<Box_irot>(prop) ||
-             std::dynamic_pointer_cast<Box_imir>(prop)) {
-      orientation = heif_orientation_concat(orientation, transform_box_to_orientation(prop));
-    }
     else if (std::dynamic_pointer_cast<Box_av1C>(prop) || std::dynamic_pointer_cast<Box_hvcC>(prop)) {
       codec_config = prop;
     }
   }

+  heif_orientation orientation = compute_orientation_from_properties(properties);
+
   // Dimensions
   if (ispe) {
     mini->set_width(ispe->get_width());
diff --git a/libheif/mini.h b/libheif/mini.h
index fb764d87..bf2854cc 100644
--- a/libheif/mini.h
+++ b/libheif/mini.h
@@ -151,6 +151,13 @@ public:
   // Returns nullptr if conversion is not possible.
   static std::shared_ptr<Box_mini> create_from_heif_file(class HeifFile* file);

+  // Compose the cumulative EXIF-style orientation of a sequence of property
+  // boxes by composing irot/imir transforms in list order via
+  // heif_orientation_concat(). Non-transform properties are ignored.
+  // Returns heif_orientation_normal (1) for an empty list.
+  static heif_orientation compute_orientation_from_properties(
+      const std::vector<std::shared_ptr<Box>>& properties);
+
 protected:
   Error parse(BitstreamRange &range, const heif_security_limits *limits) override;

diff --git a/tests/mini_box.cc b/tests/mini_box.cc
index 7e739434..640f4986 100644
--- a/tests/mini_box.cc
+++ b/tests/mini_box.cc
@@ -531,3 +531,135 @@ TEST_CASE("check heif mini")
                         "main_item_data offset: 147, size: 19098\n");
 }

+
+// --- Orientation round-trip tests ---
+//
+// Exercise Box_mini::compute_orientation_from_properties() — the static helper
+// that walks a list of irot/imir property boxes and composes them via
+// heif_orientation_concat() to recover the cumulative EXIF orientation.
+// The order in ipma is not fixed across files, so the recovery must work
+// regardless of whether irot or imir appears first.
+
+namespace {
+
+std::shared_ptr<Box_irot> make_irot(int rotation_ccw)
+{
+  auto b = std::make_shared<Box_irot>();
+  b->set_rotation_ccw(rotation_ccw);
+  return b;
+}
+
+std::shared_ptr<Box_imir> make_imir(heif_transform_mirror_direction dir)
+{
+  auto b = std::make_shared<Box_imir>();
+  b->set_mirror_direction(dir);
+  return b;
+}
+
+// A non-transform property to verify it's ignored.
+std::shared_ptr<Box> make_ispe()
+{
+  auto b = std::make_shared<Box_ispe>();
+  b->set_size(64, 64);
+  return b;
+}
+
+} // namespace
+
+TEST_CASE("Box_mini::compute_orientation_from_properties — single boxes")
+{
+  // Empty -> normal
+  REQUIRE(Box_mini::compute_orientation_from_properties({}) == heif_orientation_normal);
+
+  // Non-transform properties are ignored.
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_ispe()}) == heif_orientation_normal);
+
+  // Single irot at every supported angle (Box_irot stores rotation in
+  // counter-clockwise degrees; EXIF labels rotations clockwise).
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(0)})   == heif_orientation_normal);
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(180)}) == heif_orientation_rotate_180);
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(270)}) == heif_orientation_rotate_90_cw);  // 270 ccw == 90 cw
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_irot(90)})  == heif_orientation_rotate_270_cw); // 90 ccw == 270 cw
+
+  // Single imir in each direction.
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_imir(heif_transform_mirror_direction_horizontal)})
+          == heif_orientation_flip_horizontally);
+  REQUIRE(Box_mini::compute_orientation_from_properties({make_imir(heif_transform_mirror_direction_vertical)})
+          == heif_orientation_flip_vertically);
+}
+
+TEST_CASE("Box_mini::compute_orientation_from_properties — irot followed by imir")
+{
+  // This is the canonical order emitted by HeifFile::add_orientation_properties:
+  // rotation first, then mirror.
+
+  using H = std::pair<std::vector<std::shared_ptr<Box>>, heif_orientation>;
+  std::vector<H> cases = {
+      // The two combinations add_orientation_properties produces (orientations
+      // 5 and 7 in the EXIF mapping).
+      { {make_irot(270), make_imir(heif_transform_mirror_direction_horizontal)},
+        heif_orientation_rotate_90_cw_then_flip_horizontally },                 // 5
+      { {make_irot(270), make_imir(heif_transform_mirror_direction_vertical)},
+        heif_orientation_rotate_90_cw_then_flip_vertically },                   // 7
+
+      // A few more rotation+mirror pairs that exercise different table cells.
+      { {make_irot(180), make_imir(heif_transform_mirror_direction_horizontal)},
+        heif_orientation_flip_vertically },                                     // 4
+      { {make_irot(180), make_imir(heif_transform_mirror_direction_vertical)},
+        heif_orientation_flip_horizontally },                                   // 2
+      { {make_irot(90),  make_imir(heif_transform_mirror_direction_horizontal)},
+        heif_orientation_rotate_90_cw_then_flip_vertically },                   // 7
+  };
+
+  for (auto& [transforms, expected] : cases) {
+    INFO("expected orientation = " << expected);
+    REQUIRE(Box_mini::compute_orientation_from_properties(transforms) == expected);
+  }
+}
+
+TEST_CASE("Box_mini::compute_orientation_from_properties — imir followed by irot")
+{
+  // add_orientation_properties always emits irot-then-imir, but a file
+  // produced by other tools could place them in the reverse order. The
+  // heif_orientation_concat() composition must give the right answer for
+  // that order too. These expected values come from the concat table in
+  // heif_encoding.cc: concat(imir, irot).
+
+  using H = std::pair<std::vector<std::shared_ptr<Box>>, heif_orientation>;
+  std::vector<H> cases = {
+      // imir(H) then irot(270 ccw / 90 cw) = concat(2, 6) = 7
+      { {make_imir(heif_transform_mirror_direction_horizontal), make_irot(270)},
+        heif_orientation_rotate_90_cw_then_flip_vertically },                   // 7
+
+      // imir(H) then irot(180) = concat(2, 3) = 4
+      { {make_imir(heif_transform_mirror_direction_horizontal), make_irot(180)},
+        heif_orientation_flip_vertically },                                     // 4
+
+      // imir(V) then irot(270 ccw / 90 cw) = concat(4, 6) = 5
+      { {make_imir(heif_transform_mirror_direction_vertical),   make_irot(270)},
+        heif_orientation_rotate_90_cw_then_flip_horizontally },                 // 5
+
+      // imir(V) then irot(90 ccw / 270 cw) = concat(4, 8) = 7 again (different path)
+      { {make_imir(heif_transform_mirror_direction_vertical),   make_irot(90)},
+        heif_orientation_rotate_90_cw_then_flip_vertically },                   // 7
+  };
+
+  for (auto& [transforms, expected] : cases) {
+    INFO("expected orientation = " << expected);
+    REQUIRE(Box_mini::compute_orientation_from_properties(transforms) == expected);
+  }
+}
+
+TEST_CASE("Box_mini::compute_orientation_from_properties — mixed with non-transform properties")
+{
+  // Non-transform properties between transforms must not affect the result.
+  std::vector<std::shared_ptr<Box>> props = {
+      make_ispe(),
+      make_irot(270),
+      make_ispe(),
+      make_imir(heif_transform_mirror_direction_horizontal),
+      make_ispe(),
+  };
+  REQUIRE(Box_mini::compute_orientation_from_properties(props)
+          == heif_orientation_rotate_90_cw_then_flip_horizontally); // 5
+}