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