Commit c0741a7a for libheif

commit c0741a7ae10745fd6d060fe49007a50200246673
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Sat Apr 11 21:05:37 2026 +0200

    add mini box write support

diff --git a/libheif/api/libheif/heif_context.cc b/libheif/api/libheif/heif_context.cc
index b45595c2..04c9c892 100644
--- a/libheif/api/libheif/heif_context.cc
+++ b/libheif/api/libheif/heif_context.cc
@@ -245,6 +245,19 @@ static heif_error heif_file_writer_write(heif_context* ctx,
 }


+heif_error heif_context_set_write_mini_format(heif_context* ctx, int enable)
+{
+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+  ctx->context->set_write_mini_format(enable != 0);
+  return Error::Ok.error_struct(ctx->context.get());
+#else
+  return Error(heif_error_Unsupported_feature,
+               heif_suberror_Unspecified,
+               "Mini format support not compiled in (ENABLE_EXPERIMENTAL_MINI_FORMAT=OFF)").error_struct(ctx->context.get());
+#endif
+}
+
+
 heif_error heif_context_write_to_file(heif_context* ctx,
                                       const char* filename)
 {
diff --git a/libheif/api/libheif/heif_context.h b/libheif/api/libheif/heif_context.h
index c978b351..e5fa71e3 100644
--- a/libheif/api/libheif/heif_context.h
+++ b/libheif/api/libheif/heif_context.h
@@ -295,6 +295,19 @@ heif_error heif_context_get_image_handle(heif_context* ctx,
 LIBHEIF_API
 void heif_context_debug_dump_boxes_to_file(heif_context* ctx, int fd);

+// ====================================================================================================
+//   Mini format (experimental)
+
+// Enable writing in the compact 'mini' box format (ISO/IEC 23008-12 DAmd2).
+// When enabled, the output file will use a single 'mini' box instead of the standard
+// meta+mdat box structure, if the file content is compatible with the mini format.
+// If the content cannot be represented as a mini box, the standard format is used as fallback.
+// Requires ENABLE_EXPERIMENTAL_MINI_FORMAT to be enabled at compile time.
+// Default: disabled.
+LIBHEIF_API
+heif_error heif_context_set_write_mini_format(heif_context*, int enable);
+
+
 // ====================================================================================================
 //   Write the heif_context to a HEIF file

diff --git a/libheif/bitstream.cc b/libheif/bitstream.cc
index aa2f1ad2..4393c059 100644
--- a/libheif/bitstream.cc
+++ b/libheif/bitstream.cc
@@ -761,6 +761,57 @@ void BitReader::refill()
 }


+// --- BitWriter ---
+
+void BitWriter::write_bits(uint32_t value, int n)
+{
+  assert(n >= 0 && n <= 32);
+
+  for (int i = n - 1; i >= 0; i--) {
+    uint8_t bit = (value >> i) & 1;
+    m_current_byte |= (bit << (7 - m_bits_in_current_byte));
+    m_bits_in_current_byte++;
+
+    if (m_bits_in_current_byte == 8) {
+      m_data.push_back(m_current_byte);
+      m_current_byte = 0;
+      m_bits_in_current_byte = 0;
+    }
+  }
+}
+
+void BitWriter::write_bytes(const std::vector<uint8_t>& data)
+{
+  write_bytes(data.data(), data.size());
+}
+
+void BitWriter::write_bytes(const uint8_t* data, size_t len)
+{
+  assert(m_bits_in_current_byte == 0);
+  m_data.insert(m_data.end(), data, data + len);
+}
+
+void BitWriter::skip_to_byte_boundary()
+{
+  if (m_bits_in_current_byte > 0) {
+    m_data.push_back(m_current_byte);
+    m_current_byte = 0;
+    m_bits_in_current_byte = 0;
+  }
+}
+
+std::vector<uint8_t> BitWriter::get_data() const
+{
+  std::vector<uint8_t> result = m_data;
+  if (m_bits_in_current_byte > 0) {
+    result.push_back(m_current_byte);
+  }
+  return result;
+}
+
+
+// --- StreamWriter ---
+
 void StreamWriter::write8(uint8_t v)
 {
   if (m_position == m_data.size()) {
diff --git a/libheif/bitstream.h b/libheif/bitstream.h
index 649285dc..a532ad74 100644
--- a/libheif/bitstream.h
+++ b/libheif/bitstream.h
@@ -470,6 +470,44 @@ private:
 };


+class BitWriter
+{
+public:
+  // Write n bits from the LSBs of value (n must be in [0,32])
+  void write_bits(uint32_t value, int n);
+
+  void write_bits8(uint8_t value, int n) { assert(n >= 0 && n <= 8); write_bits(value, n); }
+
+  void write_bits16(uint16_t value, int n) { assert(n >= 0 && n <= 16); write_bits(value, n); }
+
+  void write_bits32(uint32_t value, int n) { assert(n >= 0 && n <= 32); write_bits(value, n); }
+
+  void write_bits32s(int32_t value) { write_bits(static_cast<uint32_t>(value), 32); }
+
+  void write_flag(bool flag) { write_bits(flag ? 1 : 0, 1); }
+
+  // Write raw bytes. Must be at a byte boundary.
+  void write_bytes(const std::vector<uint8_t>& data);
+
+  void write_bytes(const uint8_t* data, size_t len);
+
+  // Pad with zero bits to the next byte boundary. No-op if already aligned.
+  void skip_to_byte_boundary();
+
+  // Return all written data. Flushes any partial byte (zero-padded).
+  std::vector<uint8_t> get_data() const;
+
+  int get_current_byte_index() const { return static_cast<int>(m_data.size()); }
+
+  int64_t get_bits_written() const { return static_cast<int64_t>(m_data.size()) * 8 + m_bits_in_current_byte; }
+
+private:
+  std::vector<uint8_t> m_data;
+  uint8_t m_current_byte = 0;
+  int m_bits_in_current_byte = 0; // bits already written into m_current_byte (0-7), packed from MSB
+};
+
+
 class StreamWriter
 {
 public:
diff --git a/libheif/context.cc b/libheif/context.cc
index 05719cb7..f0ac28b8 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -467,6 +467,14 @@ std::string HeifContext::debug_dump_boxes() const
 }


+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+void HeifContext::set_write_mini_format(bool enable)
+{
+  m_heif_file->set_write_mini_format(enable);
+}
+#endif
+
+
 static bool item_type_is_image(uint32_t item_type, const std::string& content_type)
 {
   return (item_type == fourcc("hvc1") ||
diff --git a/libheif/context.h b/libheif/context.h
index 0cc58034..fbfd408f 100644
--- a/libheif/context.h
+++ b/libheif/context.h
@@ -138,6 +138,10 @@ public:

   [[nodiscard]] Error write(StreamWriter& writer);

+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+  void set_write_mini_format(bool enable);
+#endif
+
   // Create all boxes necessary for an empty HEIF file.
   // Note that this is no valid HEIF file, since some boxes (e.g. pitm) are generated, but
   // contain no valid data yet.
diff --git a/libheif/file.cc b/libheif/file.cc
index f4c70dd9..b7372ef2 100644
--- a/libheif/file.cc
+++ b/libheif/file.cc
@@ -29,6 +29,9 @@
 #include "codecs/avif_boxes.h"
 #include "codecs/hevc_boxes.h"
 #include "sequences/seq_boxes.h"
+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+#include "mini.h"
+#endif

 #include <cstdint>
 #include <fstream>
@@ -253,6 +256,39 @@ void HeifFile::derive_box_versions()

 void HeifFile::write(StreamWriter& writer)
 {
+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+  if (m_write_mini_format) {
+    std::string reason;
+    if (Box_mini::can_convert_to_mini(this, reason)) {
+      auto mini = Box_mini::create_from_heif_file(this);
+      if (mini) {
+        // Adjust ftyp for mini format
+        auto ftyp = get_ftyp_box();
+
+        // Determine codec brand from primary item type
+        uint32_t item_type = get_item_type_4cc(get_primary_image_ID());
+        heif_brand2 codec_brand = 0;
+        if (item_type == fourcc("av01")) {
+          codec_brand = heif_brand2_avif;
+        }
+        else if (item_type == fourcc("hvc1")) {
+          codec_brand = heif_brand2_heic;
+        }
+
+        ftyp->set_major_brand(fourcc("mif3"));
+        ftyp->set_minor_version(codec_brand);
+        ftyp->clear_compatible_brands();
+
+        // Write ftyp + mini (no mdat needed)
+        ftyp->write(writer);
+        mini->write(writer);
+        return;
+      }
+    }
+    // Fall through to normal write if conversion fails
+  }
+#endif
+
   for (auto& box : m_top_level_boxes) {
 #if ENABLE_EXPERIMENTAL_MINI_FORMAT
     if (box == nullptr) {
diff --git a/libheif/file.h b/libheif/file.h
index 4790a364..304c6a6b 100644
--- a/libheif/file.h
+++ b/libheif/file.h
@@ -96,6 +96,11 @@ public:

   void write(StreamWriter& writer);

+#if ENABLE_EXPERIMENTAL_MINI_FORMAT
+  void set_write_mini_format(bool enable) { m_write_mini_format = enable; }
+  bool get_write_mini_format() const { return m_write_mini_format; }
+#endif
+
   int get_num_images() const { return static_cast<int>(m_infe_boxes.size()); }

   heif_item_id get_primary_image_ID() const { return m_pitm_box ? m_pitm_box->get_item_ID() : 0; }
@@ -268,6 +273,7 @@ private:
   std::shared_ptr<Box_meta> m_meta_box;
 #if ENABLE_EXPERIMENTAL_MINI_FORMAT
   std::shared_ptr<Box_mini> m_mini_box; // meta alternative
+  bool m_write_mini_format = false;
 #endif

   std::shared_ptr<Box_iloc> m_iloc_box;
diff --git a/libheif/mini.cc b/libheif/mini.cc
index 6752c484..51a78278 100644
--- a/libheif/mini.cc
+++ b/libheif/mini.cc
@@ -21,12 +21,15 @@

 #include "mini.h"
 #include "file.h"
+#include "nclx.h"
 #include "codecs/avif_boxes.h"
 #include "codecs/hevc_boxes.h"

+#include <algorithm>
 #include <cmath>
 #include <cstddef>
 #include <memory>
+#include <sstream>
 #include <string>
 #include <vector>
 #include <utility>
@@ -771,6 +774,427 @@ std::string Box_mini::dump(Indent &indent) const
 }


+static void write_cclv_to_bits(BitWriter& bits, const Box_cclv& cclv)
+{
+  bool primaries_present = cclv.ccv_primaries_are_valid();
+  bool min_lum_present = cclv.min_luminance_is_valid();
+  bool max_lum_present = cclv.max_luminance_is_valid();
+  bool avg_lum_present = cclv.avg_luminance_is_valid();
+
+  bits.write_bits(0, 2); // ccv_cancel_flag, ccv_persistence_flag
+  bits.write_flag(primaries_present);
+  bits.write_flag(min_lum_present);
+  bits.write_flag(max_lum_present);
+  bits.write_flag(avg_lum_present);
+  bits.write_bits(0, 2); // reserved
+
+  if (primaries_present) {
+    bits.write_bits32s(cclv.get_ccv_primary_x0());
+    bits.write_bits32s(cclv.get_ccv_primary_y0());
+    bits.write_bits32s(cclv.get_ccv_primary_x1());
+    bits.write_bits32s(cclv.get_ccv_primary_y1());
+    bits.write_bits32s(cclv.get_ccv_primary_x2());
+    bits.write_bits32s(cclv.get_ccv_primary_y2());
+  }
+  if (min_lum_present) {
+    bits.write_bits32(cclv.get_min_luminance(), 32);
+  }
+  if (max_lum_present) {
+    bits.write_bits32(cclv.get_max_luminance(), 32);
+  }
+  if (avg_lum_present) {
+    bits.write_bits32(cclv.get_avg_luminance(), 32);
+  }
+}
+
+
+Error Box_mini::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+
+  BitWriter bits;
+
+  // --- Bit-packed header ---
+
+  // First byte: version(2) + 6 flags
+  bits.write_bits8(m_version, 2);
+  bits.write_flag(m_explicit_codec_types_flag);
+  bits.write_flag(m_float_flag);
+  bits.write_flag(m_full_range_flag);
+  bits.write_flag(m_alpha_flag);
+  bits.write_flag(m_explicit_cicp_flag);
+  bits.write_flag(m_hdr_flag);
+
+  // Second byte area: icc(1), exif(1), xmp(1), chroma_subsampling(2), orientation(3)
+  bits.write_flag(m_icc_flag);
+  bits.write_flag(m_exif_flag);
+  bits.write_flag(m_xmp_flag);
+  bits.write_bits8(m_chroma_subsampling, 2);
+  bits.write_bits8(m_orientation - 1, 3);
+
+  // Dimensions
+  bool large_dimensions_flag = (m_width > 128) || (m_height > 128);
+  bits.write_flag(large_dimensions_flag);
+  bits.write_bits32(m_width - 1, large_dimensions_flag ? 15 : 7);
+  bits.write_bits32(m_height - 1, large_dimensions_flag ? 15 : 7);
+
+  // Chroma centering
+  if (m_chroma_subsampling == 1 || m_chroma_subsampling == 2) {
+    bits.write_flag(m_chroma_is_horizontally_centered);
+  }
+  if (m_chroma_subsampling == 1) {
+    bits.write_flag(m_chroma_is_vertically_centered);
+  }
+
+  // Bit depth
+  if (m_float_flag) {
+    // bit_depth_log2 = log2(m_bit_depth), stored as (log2 - 4)
+    uint8_t bit_depth_log2;
+    switch (m_bit_depth) {
+      case 16:  bit_depth_log2 = 4; break;
+      case 32:  bit_depth_log2 = 5; break;
+      case 64:  bit_depth_log2 = 6; break;
+      case 128: bit_depth_log2 = 7; break;
+      default:  bit_depth_log2 = 4; break; // fallback
+    }
+    bits.write_bits8(bit_depth_log2 - 4, 2);
+  }
+  else {
+    bool high_bit_depth_flag = (m_bit_depth > 8);
+    bits.write_flag(high_bit_depth_flag);
+    if (high_bit_depth_flag) {
+      bits.write_bits8(m_bit_depth - 9, 3);
+    }
+  }
+
+  // Alpha premultiplied
+  if (m_alpha_flag) {
+    bits.write_flag(m_alpha_is_premultiplied);
+  }
+
+  // CICP
+  if (m_explicit_cicp_flag) {
+    bits.write_bits8(static_cast<uint8_t>(m_colour_primaries), 8);
+    bits.write_bits8(static_cast<uint8_t>(m_transfer_characteristics), 8);
+    if (m_chroma_subsampling != 0) {
+      bits.write_bits8(static_cast<uint8_t>(m_matrix_coefficients), 8);
+    }
+  }
+
+  // Explicit codec types
+  if (m_explicit_codec_types_flag) {
+    bits.write_bits32(m_infe_type, 32);
+    bits.write_bits32(m_codec_config_type, 32);
+  }
+
+  // --- HDR block ---
+  if (m_hdr_flag) {
+    bits.write_flag(m_gainmap_flag);
+
+    if (m_gainmap_flag) {
+      bits.write_bits32(m_gainmap_width - 1, large_dimensions_flag ? 15 : 7);
+      bits.write_bits32(m_gainmap_height - 1, large_dimensions_flag ? 15 : 7);
+      bits.write_bits8(m_gainmap_matrix_coefficients, 8);
+      bits.write_flag(m_gainmap_full_range_flag);
+      bits.write_bits8(m_gainmap_chroma_subsampling, 2);
+
+      if (m_gainmap_chroma_subsampling == 1 || m_gainmap_chroma_subsampling == 2) {
+        bits.write_flag(m_gainmap_chroma_is_horizontally_centred);
+      }
+      if (m_gainmap_chroma_subsampling == 1) {
+        bits.write_flag(m_gainmap_chroma_is_vertically_centred);
+      }
+
+      bits.write_flag(m_gainmap_float_flag);
+
+      if (m_gainmap_float_flag) {
+        uint8_t gm_bit_depth_log2;
+        switch (m_gainmap_bit_depth) {
+          case 16:  gm_bit_depth_log2 = 4; break;
+          case 32:  gm_bit_depth_log2 = 5; break;
+          case 64:  gm_bit_depth_log2 = 6; break;
+          case 128: gm_bit_depth_log2 = 7; break;
+          default:  gm_bit_depth_log2 = 4; break;
+        }
+        bits.write_bits8(gm_bit_depth_log2 - 4, 2);
+      }
+      else {
+        bool gainmap_high_bit_depth_flag = (m_gainmap_bit_depth > 8);
+        bits.write_flag(gainmap_high_bit_depth_flag);
+        if (gainmap_high_bit_depth_flag) {
+          bits.write_bits8(m_gainmap_bit_depth - 9, 3);
+        }
+      }
+
+      bits.write_flag(m_tmap_icc_flag);
+      bits.write_flag(m_tmap_explicit_cicp_flag);
+      if (m_tmap_explicit_cicp_flag) {
+        bits.write_bits8(static_cast<uint8_t>(m_tmap_colour_primaries), 8);
+        bits.write_bits8(static_cast<uint8_t>(m_tmap_transfer_characteristics), 8);
+        bits.write_bits8(static_cast<uint8_t>(m_tmap_matrix_coefficients), 8);
+        bits.write_flag(m_tmap_full_range_flag);
+      }
+    }
+
+    // HDR metadata flags
+    bits.write_flag(m_clli != nullptr);
+    bits.write_flag(m_mdcv != nullptr);
+    bits.write_flag(m_cclv != nullptr);
+    bits.write_flag(m_amve != nullptr);
+    bits.write_flag(m_reve_flag);
+    bits.write_flag(m_ndwt_flag);
+
+    if (m_clli) {
+      bits.write_bits16(m_clli->clli.max_content_light_level, 16);
+      bits.write_bits16(m_clli->clli.max_pic_average_light_level, 16);
+    }
+
+    if (m_mdcv) {
+      for (int c = 0; c < 3; c++) {
+        bits.write_bits16(m_mdcv->mdcv.display_primaries_x[c], 16);
+        bits.write_bits16(m_mdcv->mdcv.display_primaries_y[c], 16);
+      }
+      bits.write_bits16(m_mdcv->mdcv.white_point_x, 16);
+      bits.write_bits16(m_mdcv->mdcv.white_point_y, 16);
+      bits.write_bits32(m_mdcv->mdcv.max_display_mastering_luminance, 32);
+      bits.write_bits32(m_mdcv->mdcv.min_display_mastering_luminance, 32);
+    }
+
+    if (m_cclv) {
+      write_cclv_to_bits(bits, *m_cclv);
+    }
+
+    if (m_amve) {
+      bits.write_bits32(m_amve->amve.ambient_illumination, 32);
+      bits.write_bits16(m_amve->amve.ambient_light_x, 16);
+      bits.write_bits16(m_amve->amve.ambient_light_y, 16);
+    }
+
+    if (m_reve_flag) {
+      // TODO: ReferenceViewingEnvironment isn't published yet — write zeros
+      bits.write_bits32(0, 32);
+      bits.write_bits16(0, 16);
+      bits.write_bits16(0, 16);
+      bits.write_bits32(0, 32);
+      bits.write_bits16(0, 16);
+      bits.write_bits16(0, 16);
+    }
+
+    if (m_ndwt_flag) {
+      // TODO: NominalDiffuseWhite isn't published yet — write zero
+      bits.write_bits32(0, 32);
+    }
+
+    // Tmap HDR metadata (if gainmap)
+    if (m_gainmap_flag) {
+      bits.write_flag(m_tmap_clli != nullptr);
+      bits.write_flag(m_tmap_mdcv != nullptr);
+      bits.write_flag(m_tmap_cclv != nullptr);
+      bits.write_flag(m_tmap_amve != nullptr);
+      bits.write_flag(m_tmap_reve_flag);
+      bits.write_flag(m_tmap_ndwt_flag);
+
+      if (m_tmap_clli) {
+        bits.write_bits16(m_tmap_clli->clli.max_content_light_level, 16);
+        bits.write_bits16(m_tmap_clli->clli.max_pic_average_light_level, 16);
+      }
+
+      if (m_tmap_mdcv) {
+        for (int c = 0; c < 3; c++) {
+          bits.write_bits16(m_tmap_mdcv->mdcv.display_primaries_x[c], 16);
+          bits.write_bits16(m_tmap_mdcv->mdcv.display_primaries_y[c], 16);
+        }
+        bits.write_bits16(m_tmap_mdcv->mdcv.white_point_x, 16);
+        bits.write_bits16(m_tmap_mdcv->mdcv.white_point_y, 16);
+        bits.write_bits32(m_tmap_mdcv->mdcv.max_display_mastering_luminance, 32);
+        bits.write_bits32(m_tmap_mdcv->mdcv.min_display_mastering_luminance, 32);
+      }
+
+      if (m_tmap_cclv) {
+        write_cclv_to_bits(bits, *m_tmap_cclv);
+      }
+
+      if (m_tmap_amve) {
+        bits.write_bits32(m_tmap_amve->amve.ambient_illumination, 32);
+        bits.write_bits16(m_tmap_amve->amve.ambient_light_x, 16);
+        bits.write_bits16(m_tmap_amve->amve.ambient_light_y, 16);
+      }
+
+      if (m_tmap_reve_flag) {
+        bits.write_bits32(0, 32);
+        bits.write_bits16(0, 16);
+        bits.write_bits16(0, 16);
+        bits.write_bits32(0, 32);
+        bits.write_bits16(0, 16);
+        bits.write_bits16(0, 16);
+      }
+
+      if (m_tmap_ndwt_flag) {
+        bits.write_bits32(0, 32);
+      }
+    }
+  }
+
+  // --- Size fields ---
+
+  // Determine actual data sizes for write path
+  uint32_t icc_data_size = static_cast<uint32_t>(m_icc_data.size());
+  uint32_t tmap_icc_data_size = static_cast<uint32_t>(m_tmap_icc_data.size());
+  uint32_t gainmap_metadata_size = static_cast<uint32_t>(m_gainmap_metadata.size());
+  uint32_t main_item_data_size = static_cast<uint32_t>(m_main_item_data.size());
+  uint32_t alpha_item_data_size = static_cast<uint32_t>(m_alpha_item_data.size());
+  uint32_t gainmap_item_data_size = static_cast<uint32_t>(m_gainmap_item_data.size());
+  uint32_t exif_data_size = static_cast<uint32_t>(m_exif_data_bytes.size());
+  uint32_t xmp_data_size = static_cast<uint32_t>(m_xmp_data_bytes.size());
+
+  uint32_t main_item_codec_config_size = static_cast<uint32_t>(m_main_item_codec_config.size());
+  uint32_t alpha_item_codec_config_size = 0;
+  if (m_alpha_flag && alpha_item_data_size > 0) {
+    // If alpha codec config differs from main, we need to write it separately
+    if (m_alpha_item_codec_config != m_main_item_codec_config) {
+      alpha_item_codec_config_size = static_cast<uint32_t>(m_alpha_item_codec_config.size());
+    }
+    // else size stays 0, meaning "reuse main config"
+  }
+  uint32_t gainmap_item_codec_config_size = 0;
+  if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) {
+    if (m_gainmap_item_codec_config != m_main_item_codec_config) {
+      gainmap_item_codec_config_size = static_cast<uint32_t>(m_gainmap_item_codec_config.size());
+    }
+  }
+
+  // Compute "large" flags based on actual sizes
+  bool large_metadata_flag = false;
+  if (m_icc_flag || m_exif_flag || m_xmp_flag || (m_hdr_flag && m_gainmap_flag)) {
+    // Check if any metadata size exceeds 10-bit capacity
+    // ICC/exif/xmp store (size-1), max representable size with 10 bits = 1024
+    // gainmap_metadata/tmap_icc store raw or (size-1), same limit
+    uint32_t max_meta = 0;
+    if (m_icc_flag) max_meta = std::max(max_meta, icc_data_size);
+    if (m_exif_flag) max_meta = std::max(max_meta, exif_data_size);
+    if (m_xmp_flag) max_meta = std::max(max_meta, xmp_data_size);
+    if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) max_meta = std::max(max_meta, tmap_icc_data_size);
+    if (m_hdr_flag && m_gainmap_flag) max_meta = std::max(max_meta, gainmap_metadata_size + 1); // gainmap_metadata is raw, others are size-1
+    large_metadata_flag = (max_meta > 1024);
+
+    bits.write_flag(large_metadata_flag);
+  }
+
+  bool large_codec_config_flag = (main_item_codec_config_size > 7 ||
+                                  alpha_item_codec_config_size > 7 ||
+                                  gainmap_item_codec_config_size > 7);
+  bits.write_flag(large_codec_config_flag);
+
+  bool large_item_data_flag = (main_item_data_size > 32768 ||  // main stores size-1, max representable = 32768
+                               alpha_item_data_size > 32767 ||
+                               gainmap_item_data_size > 32767);
+  bits.write_flag(large_item_data_flag);
+
+  // Write size fields in parse order
+  if (m_icc_flag) {
+    bits.write_bits32(icc_data_size - 1, large_metadata_flag ? 20 : 10);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) {
+    bits.write_bits32(tmap_icc_data_size - 1, large_metadata_flag ? 20 : 10);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag) {
+    bits.write_bits32(gainmap_metadata_size, large_metadata_flag ? 20 : 10);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag) {
+    bits.write_bits32(gainmap_item_data_size, large_item_data_flag ? 28 : 15);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) {
+    bits.write_bits32(gainmap_item_codec_config_size, large_codec_config_flag ? 12 : 3);
+  }
+
+  bits.write_bits32(main_item_codec_config_size, large_codec_config_flag ? 12 : 3);
+  bits.write_bits32(main_item_data_size - 1, large_item_data_flag ? 28 : 15);
+
+  if (m_alpha_flag) {
+    bits.write_bits32(alpha_item_data_size, large_item_data_flag ? 28 : 15);
+  }
+
+  if (m_alpha_flag && alpha_item_data_size > 0) {
+    bits.write_bits32(alpha_item_codec_config_size, large_codec_config_flag ? 12 : 3);
+  }
+
+  if (m_exif_flag || m_xmp_flag) {
+    bits.write_flag(m_exif_xmp_compressed_flag);
+  }
+
+  if (m_exif_flag) {
+    bits.write_bits32(exif_data_size - 1, large_metadata_flag ? 20 : 10);
+  }
+  if (m_xmp_flag) {
+    bits.write_bits32(xmp_data_size - 1, large_metadata_flag ? 20 : 10);
+  }
+
+  // --- Byte alignment ---
+  bits.skip_to_byte_boundary();
+
+  // --- Byte-aligned data blocks ---
+
+  // Codec configs
+  if (main_item_codec_config_size > 0) {
+    bits.write_bytes(m_main_item_codec_config);
+  }
+
+  if (m_alpha_flag && alpha_item_data_size > 0) {
+    if (alpha_item_codec_config_size > 0) {
+      bits.write_bytes(m_alpha_item_codec_config);
+    }
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) {
+    if (gainmap_item_codec_config_size > 0) {
+      bits.write_bytes(m_gainmap_item_codec_config);
+    }
+  }
+
+  // ICC and metadata
+  if (m_icc_flag) {
+    bits.write_bytes(m_icc_data);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && m_tmap_icc_flag) {
+    bits.write_bytes(m_tmap_icc_data);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && gainmap_metadata_size > 0) {
+    bits.write_bytes(m_gainmap_metadata);
+  }
+
+  // Image data (order: alpha, gainmap, main, exif, xmp)
+  if (m_alpha_flag && alpha_item_data_size > 0) {
+    bits.write_bytes(m_alpha_item_data);
+  }
+
+  if (m_hdr_flag && m_gainmap_flag && gainmap_item_data_size > 0) {
+    bits.write_bytes(m_gainmap_item_data);
+  }
+
+  bits.write_bytes(m_main_item_data);
+
+  if (m_exif_flag) {
+    bits.write_bytes(m_exif_data_bytes);
+  }
+
+  if (m_xmp_flag) {
+    bits.write_bytes(m_xmp_data_bytes);
+  }
+
+  // Flush to StreamWriter
+  writer.write(bits.get_data());
+
+  prepend_header(writer, box_start);
+  return Error::Ok;
+}
+
+
 static uint32_t get_item_type_for_brand(const heif_brand2 brand)
 {
   switch(brand) {
@@ -1065,3 +1489,483 @@ Error Box_mini::create_expanded_boxes(class HeifFile* file)

   return Error::Ok;
 }
+
+
+// --- Reverse mapping: irot/imir to EXIF orientation ---
+
+static uint8_t compute_orientation_from_transforms(const Box_irot* irot, const Box_imir* imir)
+{
+  int rotation = irot ? irot->get_rotation_ccw() : 0;
+  bool has_mirror = (imir != nullptr);
+  heif_transform_mirror_direction mirror_dir = has_mirror ? imir->get_mirror_direction() : heif_transform_mirror_direction_horizontal;
+
+  // Reverse of create_expanded_boxes (mini.cc lines 955-981):
+  // orientation 1: no transform
+  // orientation 2: irot(180) only
+  // orientation 3: imir(horizontal) only
+  // orientation 4: imir(vertical) only
+  // orientation 5: irot(270) only
+  // orientation 6: irot(90) + imir(horizontal)
+  // orientation 7: irot(90) only
+  // orientation 8: irot(90) + imir(vertical)
+
+  if (!has_mirror) {
+    switch (rotation) {
+      case 0:   return 1;
+      case 180: return 2;
+      case 270: return 5;
+      case 90:  return 7;
+      default:  return 1;
+    }
+  }
+  else {
+    if (mirror_dir == heif_transform_mirror_direction_horizontal) {
+      switch (rotation) {
+        case 0:  return 3;
+        case 90: return 6;
+        default: return 1;
+      }
+    }
+    else { // vertical
+      switch (rotation) {
+        case 0:  return 4;
+        case 90: return 8;
+        default: return 1;
+      }
+    }
+  }
+}
+
+
+// --- 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)
+{
+  if (!codec_config_box) {
+    return {};
+  }
+
+  StreamWriter temp_writer;
+  codec_config_box->write(temp_writer);
+  auto full_data = temp_writer.get_data();
+
+  // Strip the 8-byte box header (size + fourcc)
+  if (full_data.size() <= 8) {
+    return {};
+  }
+  return std::vector<uint8_t>(full_data.begin() + 8, full_data.end());
+}
+
+
+// --- Eligibility check ---
+
+bool Box_mini::can_convert_to_mini(const HeifFile* file, std::string& out_reason)
+{
+  // Must have a primary item
+  heif_item_id primary_id = file->get_primary_image_ID();
+  if (primary_id == 0) {
+    out_reason = "no primary item";
+    return false;
+  }
+
+  // Check primary item type
+  uint32_t item_type = file->get_item_type_4cc(primary_id);
+  if (item_type != fourcc("av01") && item_type != fourcc("hvc1")) {
+    out_reason = "primary item type not supported for mini (need av01 or hvc1)";
+    return false;
+  }
+
+  // Check dimensions
+  std::vector<std::shared_ptr<Box>> properties;
+  file->get_properties(primary_id, properties);
+
+  for (auto& prop : properties) {
+    if (auto ispe = std::dynamic_pointer_cast<Box_ispe>(prop)) {
+      if (ispe->get_width() > 32768 || ispe->get_height() > 32768) {
+        out_reason = "dimensions exceed mini box limits";
+        return false;
+      }
+    }
+  }
+
+  // Check that we don't have unsupported derived image types
+  auto item_ids = file->get_item_IDs();
+  heif_item_id alpha_id = 0;
+  heif_item_id exif_id = 0;
+  heif_item_id xmp_id = 0;
+
+  for (auto id : item_ids) {
+    if (id == primary_id) continue;
+
+    uint32_t type = file->get_item_type_4cc(id);
+    if (type == fourcc("grid") || type == fourcc("iovl") || type == fourcc("iden")) {
+      out_reason = "derived image items (grid/overlay/identity) not supported in mini";
+      return false;
+    }
+
+    // Check for alpha auxiliary
+    auto iref = file->get_iref_box();
+    if (iref) {
+      auto refs = iref->get_references(id, fourcc("auxl"));
+      if (!refs.empty() && refs[0] == primary_id) {
+        if (alpha_id != 0) {
+          out_reason = "multiple alpha items not supported in mini";
+          return false;
+        }
+        alpha_id = id;
+        continue;
+      }
+      auto cdsc_refs = iref->get_references(id, fourcc("cdsc"));
+      if (!cdsc_refs.empty() && cdsc_refs[0] == primary_id) {
+        if (type == fourcc("Exif")) {
+          if (exif_id != 0) {
+            out_reason = "multiple EXIF items not supported in mini";
+            return false;
+          }
+          exif_id = id;
+          continue;
+        }
+        if (type == fourcc("mime")) {
+          auto infe = file->get_infe_box(id);
+          if (infe && infe->get_content_type() == "application/rdf+xml") {
+            if (xmp_id != 0) {
+              out_reason = "multiple XMP items not supported in mini";
+              return false;
+            }
+            xmp_id = id;
+            continue;
+          }
+          else {
+            out_reason = "unsupported mime item type for mini: " + (infe ? infe->get_content_type() : "unknown");
+            return false;
+          }
+        }
+      }
+    }
+
+    // If it's a hidden item or an item type we know about, skip it.
+    // Otherwise, it's unsupported for mini.
+    auto infe = file->get_infe_box(id);
+    if (infe && !infe->is_hidden_item() && type != item_type) {
+      out_reason = "unsupported additional item type for mini: " + fourcc_to_string(type);
+      return false;
+    }
+  }
+
+  // The mini box has a single compressed flag for both EXIF and XMP.
+  // If both are present, they must use the same compression method.
+  if (exif_id != 0 && xmp_id != 0) {
+    auto exif_infe = file->get_infe_box(exif_id);
+    auto xmp_infe = file->get_infe_box(xmp_id);
+    bool exif_compressed = (exif_infe && exif_infe->get_content_encoding() == "deflate");
+    bool xmp_compressed = (xmp_infe && xmp_infe->get_content_encoding() == "deflate");
+    if (exif_compressed != xmp_compressed) {
+      out_reason = "EXIF and XMP have different compression methods";
+      return false;
+    }
+  }
+
+  return true;
+}
+
+
+// --- Meta-to-Mini conversion ---
+
+std::shared_ptr<Box_mini> Box_mini::create_from_heif_file(HeifFile* file)
+{
+  std::string reason;
+  if (!can_convert_to_mini(file, reason)) {
+    return nullptr;
+  }
+
+  auto mini = std::make_shared<Box_mini>();
+  mini->set_version(0);
+
+  heif_item_id primary_id = file->get_primary_image_ID();
+  uint32_t item_type = file->get_item_type_4cc(primary_id);
+
+  bool is_avif = (item_type == fourcc("av01"));
+
+  // For av01/hvc1 the codec is identified via the ftyp minor version brand,
+  // so we don't need explicit codec type fields in the mini bitstream.
+  mini->set_explicit_codec_types_flag(false);
+
+  // Get properties for primary item
+  std::vector<std::shared_ptr<Box>> properties;
+  file->get_properties(primary_id, properties);
+
+  // Extract properties
+  std::shared_ptr<Box_ispe> ispe;
+  std::shared_ptr<Box_pixi> pixi;
+  std::shared_ptr<Box_colr> colr_nclx;
+  std::shared_ptr<Box_colr> colr_icc;
+  std::shared_ptr<Box_irot> irot;
+  std::shared_ptr<Box_imir> imir;
+  std::shared_ptr<Box> codec_config;
+
+  for (auto& prop : properties) {
+    if (auto p = std::dynamic_pointer_cast<Box_ispe>(prop)) {
+      ispe = p;
+    }
+    else if (auto p = std::dynamic_pointer_cast<Box_pixi>(prop)) {
+      pixi = p;
+    }
+    else if (auto p = std::dynamic_pointer_cast<Box_colr>(prop)) {
+      if (p->get_color_profile_type() == fourcc("nclx")) {
+        colr_nclx = p;
+      }
+      else {
+        colr_icc = p;
+      }
+    }
+    else if (auto p = std::dynamic_pointer_cast<Box_irot>(prop)) {
+      irot = p;
+    }
+    else if (auto p = std::dynamic_pointer_cast<Box_imir>(prop)) {
+      imir = p;
+    }
+    else if (std::dynamic_pointer_cast<Box_av1C>(prop) || std::dynamic_pointer_cast<Box_hvcC>(prop)) {
+      codec_config = prop;
+    }
+  }
+
+  // Dimensions
+  if (ispe) {
+    mini->set_width(ispe->get_width());
+    mini->set_height(ispe->get_height());
+  }
+
+  // Bit depth
+  if (pixi && pixi->get_num_channels() > 0) {
+    mini->set_bit_depth(static_cast<uint8_t>(pixi->get_bits_per_channel(0)));
+  }
+  mini->set_float_flag(false); // TODO: detect float from codec config
+
+  // CICP / color
+  bool has_icc = (colr_icc != nullptr);
+  mini->set_icc_flag(has_icc);
+
+  if (has_icc) {
+    auto raw_profile = std::dynamic_pointer_cast<const color_profile_raw>(colr_icc->get_color_profile());
+    if (raw_profile) {
+      mini->set_icc_data(raw_profile->get_data());
+    }
+  }
+
+  if (colr_nclx) {
+    auto nclx = std::dynamic_pointer_cast<const color_profile_nclx>(colr_nclx->get_color_profile());
+    if (nclx) {
+      auto profile = nclx->get_nclx_color_profile();
+      mini->set_colour_primaries(profile.m_colour_primaries);
+      mini->set_transfer_characteristics(profile.m_transfer_characteristics);
+      mini->set_matrix_coefficients(profile.m_matrix_coefficients);
+      mini->set_full_range_flag(profile.m_full_range_flag);
+    }
+  }
+
+  // Determine chroma subsampling from codec config
+  // mini chroma_subsampling values: 0=monochrome, 1=4:2:0, 2=4:2:2, 3=4:4:4
+  uint8_t chroma_sub = 0;
+  if (is_avif) {
+    auto av1c = std::dynamic_pointer_cast<Box_av1C>(codec_config);
+    if (av1c) {
+      auto& config = av1c->get_configuration();
+      if (config.chroma_subsampling_x == 1 && config.chroma_subsampling_y == 1) {
+        chroma_sub = 1; // 4:2:0
+      }
+      else if (config.chroma_subsampling_x == 1 && config.chroma_subsampling_y == 0) {
+        chroma_sub = 2; // 4:2:2
+      }
+      else if (config.chroma_subsampling_x == 0 && config.chroma_subsampling_y == 0) {
+        if (config.monochrome) {
+          chroma_sub = 0;
+        }
+        else {
+          chroma_sub = 3; // 4:4:4
+        }
+      }
+    }
+  }
+  else if (item_type == fourcc("hvc1")) {
+    auto hvcc = std::dynamic_pointer_cast<Box_hvcC>(codec_config);
+    if (hvcc) {
+      // HEVC chroma_format uses the same values as mini chroma_subsampling
+      chroma_sub = hvcc->get_configuration().chroma_format;
+    }
+  }
+  mini->set_chroma_subsampling(chroma_sub);
+
+  // Determine if explicit CICP is needed (vs implicit defaults)
+  bool need_explicit_cicp = true;
+  uint16_t default_primaries = has_icc ? 2 : 1;
+  uint16_t default_transfer = has_icc ? 2 : 13;
+  uint16_t default_matrix = (chroma_sub == 0) ? 2 : 6;
+
+  if (colr_nclx) {
+    auto nclx = std::dynamic_pointer_cast<const color_profile_nclx>(colr_nclx->get_color_profile());
+    if (nclx) {
+      auto profile = nclx->get_nclx_color_profile();
+      if (profile.m_colour_primaries == default_primaries &&
+          profile.m_transfer_characteristics == default_transfer &&
+          profile.m_matrix_coefficients == default_matrix) {
+        need_explicit_cicp = false;
+      }
+    }
+  }
+  else {
+    // No NCLX profile, use defaults
+    mini->set_colour_primaries(default_primaries);
+    mini->set_transfer_characteristics(default_transfer);
+    mini->set_matrix_coefficients(default_matrix);
+    need_explicit_cicp = false;
+  }
+  mini->set_explicit_cicp_flag(need_explicit_cicp);
+
+  // Orientation
+  uint8_t orientation = compute_orientation_from_transforms(irot.get(), imir.get());
+  mini->set_orientation(orientation);
+
+  // Codec config
+  auto config_bytes = extract_codec_config_bytes(codec_config);
+  mini->set_main_item_codec_config(config_bytes);
+
+  // Find alpha, exif, xmp items
+  heif_item_id alpha_id = 0;
+  heif_item_id exif_id = 0;
+  heif_item_id xmp_id = 0;
+
+  auto item_ids = file->get_item_IDs();
+  auto iref = file->get_iref_box();
+
+  for (auto id : item_ids) {
+    if (id == primary_id) continue;
+    if (!iref) continue;
+
+    auto auxl_refs = iref->get_references(id, fourcc("auxl"));
+    if (!auxl_refs.empty() && auxl_refs[0] == primary_id) {
+      alpha_id = id;
+      continue;
+    }
+
+    auto cdsc_refs = iref->get_references(id, fourcc("cdsc"));
+    if (!cdsc_refs.empty() && cdsc_refs[0] == primary_id) {
+      uint32_t type = file->get_item_type_4cc(id);
+      if (type == fourcc("Exif")) {
+        exif_id = id;
+      }
+      else if (type == fourcc("mime")) {
+        auto infe = file->get_infe_box(id);
+        if (infe && infe->get_content_type() == "application/rdf+xml") {
+          xmp_id = id;
+        }
+      }
+    }
+  }
+
+  // Alpha
+  mini->set_alpha_flag(alpha_id != 0);
+  if (alpha_id != 0) {
+    mini->set_alpha_is_premultiplied(false); // TODO: detect from auxC
+
+    // Alpha codec config
+    std::vector<std::shared_ptr<Box>> alpha_props;
+    file->get_properties(alpha_id, alpha_props);
+    for (auto& prop : alpha_props) {
+      if (std::dynamic_pointer_cast<Box_av1C>(prop) || std::dynamic_pointer_cast<Box_hvcC>(prop)) {
+        auto alpha_config_bytes = extract_codec_config_bytes(prop);
+        mini->set_alpha_item_codec_config(alpha_config_bytes);
+        break;
+      }
+    }
+
+    // Alpha item data from iloc
+    auto iloc = file->get_iloc_box();
+    if (iloc) {
+      for (auto& item : iloc->get_items()) {
+        if (item.item_ID == alpha_id) {
+          std::vector<uint8_t> data;
+          for (auto& extent : item.extents) {
+            data.insert(data.end(), extent.data.begin(), extent.data.end());
+          }
+          mini->set_alpha_item_data(std::move(data));
+          break;
+        }
+      }
+    }
+  }
+
+  // EXIF and XMP share a single compressed flag in the mini box.
+  // Determine it from whichever item is present (can_convert_to_mini already
+  // verified they agree when both exist).
+  mini->set_exif_flag(exif_id != 0);
+  mini->set_xmp_flag(xmp_id != 0);
+
+  bool metadata_compressed = false;
+  if (exif_id != 0) {
+    auto infe = file->get_infe_box(exif_id);
+    if (infe && infe->get_content_encoding() == "deflate") {
+      metadata_compressed = true;
+    }
+  }
+  if (xmp_id != 0) {
+    auto infe = file->get_infe_box(xmp_id);
+    if (infe && infe->get_content_encoding() == "deflate") {
+      metadata_compressed = true;
+    }
+  }
+  mini->set_exif_xmp_compressed_flag(metadata_compressed);
+
+  if (exif_id != 0) {
+    auto iloc = file->get_iloc_box();
+    if (iloc) {
+      for (auto& item : iloc->get_items()) {
+        if (item.item_ID == exif_id) {
+          std::vector<uint8_t> data;
+          for (auto& extent : item.extents) {
+            data.insert(data.end(), extent.data.begin(), extent.data.end());
+          }
+          mini->set_exif_data(std::move(data));
+          break;
+        }
+      }
+    }
+  }
+
+  if (xmp_id != 0) {
+    auto iloc = file->get_iloc_box();
+    if (iloc) {
+      for (auto& item : iloc->get_items()) {
+        if (item.item_ID == xmp_id) {
+          std::vector<uint8_t> data;
+          for (auto& extent : item.extents) {
+            data.insert(data.end(), extent.data.begin(), extent.data.end());
+          }
+          mini->set_xmp_data(std::move(data));
+          break;
+        }
+      }
+    }
+  }
+
+  // Main item data from iloc
+  {
+    auto iloc = file->get_iloc_box();
+    if (iloc) {
+      for (auto& item : iloc->get_items()) {
+        if (item.item_ID == primary_id) {
+          std::vector<uint8_t> data;
+          for (auto& extent : item.extents) {
+            data.insert(data.end(), extent.data.begin(), extent.data.end());
+          }
+          mini->set_main_item_data(std::move(data));
+          break;
+        }
+      }
+    }
+  }
+
+  // HDR / gainmap: not yet implemented for conversion
+  mini->set_hdr_flag(false);
+
+  return mini;
+}
diff --git a/libheif/mini.h b/libheif/mini.h
index 0bbad99b..b1b584cb 100644
--- a/libheif/mini.h
+++ b/libheif/mini.h
@@ -38,6 +38,8 @@ public:

   Error create_expanded_boxes(class HeifFile* file);

+  // --- Getters ---
+
   bool get_icc_flag() const { return m_icc_flag; }
   bool get_exif_flag() const { return m_exif_flag; }
   bool get_xmp_flag() const { return m_xmp_flag; }
@@ -69,8 +71,85 @@ public:
   uint16_t get_matrix_coefficients() const { return m_matrix_coefficients; }
   bool get_full_range_flag() const { return m_full_range_flag; }

+  // --- Setters (for write path) ---
+
+  void set_version(uint8_t v) { m_version = v; }
+  void set_explicit_codec_types_flag(bool f) { m_explicit_codec_types_flag = f; }
+  void set_float_flag(bool f) { m_float_flag = f; }
+  void set_full_range_flag(bool f) { m_full_range_flag = f; }
+  void set_alpha_flag(bool f) { m_alpha_flag = f; }
+  void set_explicit_cicp_flag(bool f) { m_explicit_cicp_flag = f; }
+  void set_hdr_flag(bool f) { m_hdr_flag = f; }
+  void set_icc_flag(bool f) { m_icc_flag = f; }
+  void set_exif_flag(bool f) { m_exif_flag = f; }
+  void set_xmp_flag(bool f) { m_xmp_flag = f; }
+  void set_chroma_subsampling(uint8_t cs) { m_chroma_subsampling = cs; }
+  void set_orientation(uint8_t o) { m_orientation = o; }
+  void set_width(uint32_t w) { m_width = w; }
+  void set_height(uint32_t h) { m_height = h; }
+  void set_bit_depth(uint8_t bd) { m_bit_depth = bd; }
+  void set_chroma_is_horizontally_centered(bool f) { m_chroma_is_horizontally_centered = f; }
+  void set_chroma_is_vertically_centered(bool f) { m_chroma_is_vertically_centered = f; }
+  void set_alpha_is_premultiplied(bool f) { m_alpha_is_premultiplied = f; }
+  void set_colour_primaries(uint16_t cp) { m_colour_primaries = cp; }
+  void set_transfer_characteristics(uint16_t tc) { m_transfer_characteristics = tc; }
+  void set_matrix_coefficients(uint16_t mc) { m_matrix_coefficients = mc; }
+  void set_infe_type(uint32_t t) { m_infe_type = t; }
+  void set_codec_config_type(uint32_t t) { m_codec_config_type = t; }
+  void set_exif_xmp_compressed_flag(bool f) { m_exif_xmp_compressed_flag = f; }
+
+  void set_main_item_codec_config(std::vector<uint8_t> data) { m_main_item_codec_config = std::move(data); }
+  void set_alpha_item_codec_config(std::vector<uint8_t> data) { m_alpha_item_codec_config = std::move(data); }
+  void set_gainmap_item_codec_config(std::vector<uint8_t> data) { m_gainmap_item_codec_config = std::move(data); }
+  void set_icc_data(std::vector<uint8_t> data) { m_icc_data = std::move(data); }
+
+  void set_main_item_data(std::vector<uint8_t> data) { m_main_item_data = std::move(data); }
+  void set_alpha_item_data(std::vector<uint8_t> data) { m_alpha_item_data = std::move(data); }
+  void set_gainmap_item_data(std::vector<uint8_t> data) { m_gainmap_item_data = std::move(data); }
+  void set_exif_data(std::vector<uint8_t> data) { m_exif_data_bytes = std::move(data); }
+  void set_xmp_data(std::vector<uint8_t> data) { m_xmp_data_bytes = std::move(data); }
+
+  // Gainmap setters
+  void set_gainmap_flag(bool f) { m_gainmap_flag = f; }
+  void set_gainmap_width(uint32_t w) { m_gainmap_width = w; }
+  void set_gainmap_height(uint32_t h) { m_gainmap_height = h; }
+  void set_gainmap_matrix_coefficients(uint8_t mc) { m_gainmap_matrix_coefficients = mc; }
+  void set_gainmap_full_range_flag(bool f) { m_gainmap_full_range_flag = f; }
+  void set_gainmap_chroma_subsampling(uint8_t cs) { m_gainmap_chroma_subsampling = cs; }
+  void set_gainmap_float_flag(bool f) { m_gainmap_float_flag = f; }
+  void set_gainmap_bit_depth(uint8_t bd) { m_gainmap_bit_depth = bd; }
+  void set_tmap_icc_flag(bool f) { m_tmap_icc_flag = f; }
+  void set_tmap_explicit_cicp_flag(bool f) { m_tmap_explicit_cicp_flag = f; }
+  void set_tmap_colour_primaries(uint16_t cp) { m_tmap_colour_primaries = cp; }
+  void set_tmap_transfer_characteristics(uint16_t tc) { m_tmap_transfer_characteristics = tc; }
+  void set_tmap_matrix_coefficients(uint16_t mc) { m_tmap_matrix_coefficients = mc; }
+  void set_tmap_full_range_flag(bool f) { m_tmap_full_range_flag = f; }
+  void set_tmap_icc_data(std::vector<uint8_t> data) { m_tmap_icc_data = std::move(data); }
+  void set_gainmap_metadata(std::vector<uint8_t> data) { m_gainmap_metadata = std::move(data); }
+
+  // HDR metadata setters
+  void set_clli(std::shared_ptr<Box_clli> box) { m_clli = std::move(box); }
+  void set_mdcv(std::shared_ptr<Box_mdcv> box) { m_mdcv = std::move(box); }
+  void set_cclv(std::shared_ptr<Box_cclv> box) { m_cclv = std::move(box); }
+  void set_amve(std::shared_ptr<Box_amve> box) { m_amve = std::move(box); }
+  void set_tmap_clli(std::shared_ptr<Box_clli> box) { m_tmap_clli = std::move(box); }
+  void set_tmap_mdcv(std::shared_ptr<Box_mdcv> box) { m_tmap_mdcv = std::move(box); }
+  void set_tmap_cclv(std::shared_ptr<Box_cclv> box) { m_tmap_cclv = std::move(box); }
+  void set_tmap_amve(std::shared_ptr<Box_amve> box) { m_tmap_amve = std::move(box); }
+
   std::string dump(Indent &) const override;

+  Error write(StreamWriter& writer) const override;
+
+  // Check if a HeifFile can be represented as a mini box.
+  // Returns true if conversion is possible. If false, out_reason explains why not.
+  static bool can_convert_to_mini(const class HeifFile* file, std::string& out_reason);
+
+  // Create a Box_mini from a HeifFile's meta box structure.
+  // This is the inverse of create_expanded_boxes().
+  // Returns nullptr if conversion is not possible.
+  static std::shared_ptr<Box_mini> create_from_heif_file(class HeifFile* file);
+
 protected:
   Error parse(BitstreamRange &range, const heif_security_limits *limits) override;

@@ -155,6 +234,13 @@ private:
   uint32_t m_exif_data_size = 0;
   uint64_t m_xmp_item_data_offset = 0;
   uint32_t m_xmp_data_size = 0;
+
+  // Image data for write path (not populated during parse)
+  std::vector<uint8_t> m_main_item_data;
+  std::vector<uint8_t> m_alpha_item_data;
+  std::vector<uint8_t> m_gainmap_item_data;
+  std::vector<uint8_t> m_exif_data_bytes;
+  std::vector<uint8_t> m_xmp_data_bytes;
 };

 #endif
diff --git a/tests/bitstream_tests.cc b/tests/bitstream_tests.cc
index aa0309e4..ee6e21a4 100644
--- a/tests/bitstream_tests.cc
+++ b/tests/bitstream_tests.cc
@@ -77,6 +77,161 @@ TEST_CASE("read uint32") {
   REQUIRE(overlap == 0b111000101000001100001111000111);
 }

+// --- BitWriter tests ---
+
+TEST_CASE("bitwriter single flags") {
+  BitWriter writer;
+  writer.write_flag(true);
+  writer.write_flag(false);
+  writer.write_flag(true);
+  writer.write_flag(true);
+  writer.write_flag(false);
+  writer.write_flag(false);
+  writer.write_flag(true);
+  writer.write_flag(false);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 1);
+  REQUIRE(data[0] == 0b10110010);
+}
+
+TEST_CASE("bitwriter multi-bit values") {
+  BitWriter writer;
+  writer.write_bits(0b01, 2);   // 01
+  writer.write_bits(0b110, 3);  // 110
+  writer.write_bits(0b101, 3);  // 101
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 1);
+  REQUIRE(data[0] == 0b01110101);
+}
+
+TEST_CASE("bitwriter cross-byte boundary") {
+  BitWriter writer;
+  writer.write_bits(0b11111, 5);  // 5 bits
+  writer.write_bits(0b000111, 6); // 6 bits - crosses byte boundary
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 2);
+  REQUIRE(data[0] == 0b11111000);
+  REQUIRE(data[1] == 0b11100000); // remaining 3 bits + 5 zero-padded
+}
+
+TEST_CASE("bitwriter 16-bit value") {
+  BitWriter writer;
+  writer.write_bits16(0x1234, 16);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 2);
+  REQUIRE(data[0] == 0x12);
+  REQUIRE(data[1] == 0x34);
+}
+
+TEST_CASE("bitwriter 32-bit value") {
+  BitWriter writer;
+  writer.write_bits32(0xDEADBEEF, 32);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 4);
+  REQUIRE(data[0] == 0xDE);
+  REQUIRE(data[1] == 0xAD);
+  REQUIRE(data[2] == 0xBE);
+  REQUIRE(data[3] == 0xEF);
+}
+
+TEST_CASE("bitwriter skip_to_byte_boundary") {
+  BitWriter writer;
+  writer.write_bits(0b101, 3);
+  REQUIRE(writer.get_bits_written() == 3);
+  writer.skip_to_byte_boundary();
+  REQUIRE(writer.get_bits_written() == 8);
+  writer.write_bits8(0xFF, 8);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 2);
+  REQUIRE(data[0] == 0b10100000);
+  REQUIRE(data[1] == 0xFF);
+}
+
+TEST_CASE("bitwriter skip_to_byte_boundary already aligned") {
+  BitWriter writer;
+  writer.write_bits8(0xAB, 8);
+  writer.skip_to_byte_boundary(); // should be no-op
+  writer.write_bits8(0xCD, 8);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 2);
+  REQUIRE(data[0] == 0xAB);
+  REQUIRE(data[1] == 0xCD);
+}
+
+TEST_CASE("bitwriter write_bytes") {
+  BitWriter writer;
+  writer.write_bits8(0xAA, 8);
+  std::vector<uint8_t> bytes = {0x01, 0x02, 0x03};
+  writer.write_bytes(bytes);
+  auto data = writer.get_data();
+  REQUIRE(data.size() == 4);
+  REQUIRE(data[0] == 0xAA);
+  REQUIRE(data[1] == 0x01);
+  REQUIRE(data[2] == 0x02);
+  REQUIRE(data[3] == 0x03);
+}
+
+TEST_CASE("bitwriter round-trip with BitReader") {
+  // Write a sequence of mixed-width values
+  BitWriter writer;
+  writer.write_bits(2, 2);        // version = 2
+  writer.write_flag(false);       // flag1
+  writer.write_flag(true);        // flag2
+  writer.write_flag(true);        // flag3
+  writer.write_flag(false);       // flag4
+  writer.write_flag(true);        // flag5
+  writer.write_flag(false);       // flag6
+  writer.write_flag(true);        // flag7
+  writer.write_flag(false);       // flag8
+  writer.write_flag(true);        // flag9
+  writer.write_bits(1, 2);        // chroma = 1
+  writer.write_bits(4, 3);        // orientation = 4
+  writer.write_flag(true);        // large_dim
+  writer.write_bits(255, 15);     // width-1
+  writer.write_bits(127, 15);     // height-1
+  writer.write_bits8(0xAB, 8);    // some byte
+
+  auto data = writer.get_data();
+
+  // Read back with BitReader
+  BitReader reader(data.data(), (int)data.size());
+  REQUIRE(reader.get_bits(2) == 2);      // version
+  REQUIRE(reader.get_flag() == false);    // flag1
+  REQUIRE(reader.get_flag() == true);     // flag2
+  REQUIRE(reader.get_flag() == true);     // flag3
+  REQUIRE(reader.get_flag() == false);    // flag4
+  REQUIRE(reader.get_flag() == true);     // flag5
+  REQUIRE(reader.get_flag() == false);    // flag6
+  REQUIRE(reader.get_flag() == true);     // flag7
+  REQUIRE(reader.get_flag() == false);    // flag8
+  REQUIRE(reader.get_flag() == true);     // flag9
+  REQUIRE(reader.get_bits(2) == 1);       // chroma
+  REQUIRE(reader.get_bits(3) == 4);       // orientation
+  REQUIRE(reader.get_flag() == true);     // large_dim
+  REQUIRE(reader.get_bits(15) == 255);    // width-1
+  REQUIRE(reader.get_bits(15) == 127);    // height-1
+  REQUIRE(reader.get_bits8(8) == 0xAB);   // byte
+}
+
+TEST_CASE("bitwriter get_current_byte_index") {
+  BitWriter writer;
+  REQUIRE(writer.get_current_byte_index() == 0);
+  writer.write_bits8(0xFF, 8);
+  REQUIRE(writer.get_current_byte_index() == 1);
+  writer.write_bits(0, 3);  // partial byte not yet flushed
+  REQUIRE(writer.get_current_byte_index() == 1);
+  writer.skip_to_byte_boundary();
+  REQUIRE(writer.get_current_byte_index() == 2);
+}
+
+TEST_CASE("bitwriter zero bits") {
+  BitWriter writer;
+  writer.write_bits(0, 0);  // writing 0 bits should be a no-op
+  REQUIRE(writer.get_bits_written() == 0);
+  auto data = writer.get_data();
+  REQUIRE(data.empty());
+}
+
 TEST_CASE("read float") {
   std::vector<uint8_t> byteArray{0x40, 0x00, 0x00, 0x00};
   std::shared_ptr<StreamReader_memory> stream = std::make_shared<StreamReader_memory>(byteArray.data(), (int)byteArray.size(), false);
diff --git a/tests/mini_box.cc b/tests/mini_box.cc
index f8499b2d..b871645b 100644
--- a/tests/mini_box.cc
+++ b/tests/mini_box.cc
@@ -122,6 +122,265 @@ TEST_CASE("mini")
                         "main_item_data offset: 37, size: 53\n");
 }

+TEST_CASE("mini write round-trip from scratch")
+{
+  // Construct a Box_mini from scratch using setters
+  auto mini = std::make_shared<Box_mini>();
+  mini->set_version(0);
+  mini->set_explicit_codec_types_flag(false);
+  mini->set_float_flag(false);
+  mini->set_full_range_flag(true);
+  mini->set_alpha_flag(false);
+  mini->set_explicit_cicp_flag(false);
+  mini->set_hdr_flag(false);
+  mini->set_icc_flag(false);
+  mini->set_exif_flag(false);
+  mini->set_xmp_flag(false);
+  mini->set_chroma_subsampling(3);  // 4:4:4
+  mini->set_orientation(1);
+  mini->set_width(256);
+  mini->set_height(256);
+  mini->set_bit_depth(8);
+  mini->set_colour_primaries(1);
+  mini->set_transfer_characteristics(13);
+  mini->set_matrix_coefficients(6);
+
+  // Codec config (4 bytes)
+  mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00});
+
+  // Fake main item data (10 bytes)
+  std::vector<uint8_t> fake_data(10, 0xAB);
+  mini->set_main_item_data(fake_data);
+
+  // Write
+  StreamWriter writer;
+  Error error = mini->write(writer);
+  REQUIRE(error == Error::Ok);
+
+  // Parse back
+  auto written_data = writer.get_data();
+  auto reader = std::make_shared<StreamReader_memory>(written_data.data(), written_data.size(), false);
+  BitstreamRange range(reader, written_data.size());
+
+  std::shared_ptr<Box> box;
+  error = Box::read(range, &box, heif_get_global_security_limits());
+  REQUIRE(error == Error::Ok);
+  auto mini2 = std::dynamic_pointer_cast<Box_mini>(box);
+  REQUIRE(mini2 != nullptr);
+
+  // Compare
+  REQUIRE(mini2->get_width() == 256);
+  REQUIRE(mini2->get_height() == 256);
+  REQUIRE(mini2->get_bit_depth() == 8);
+  REQUIRE(mini2->get_icc_flag() == false);
+  REQUIRE(mini2->get_exif_flag() == false);
+  REQUIRE(mini2->get_xmp_flag() == false);
+  REQUIRE(mini2->get_full_range_flag() == true);
+  REQUIRE(mini2->get_colour_primaries() == 1);
+  REQUIRE(mini2->get_transfer_characteristics() == 13);
+  REQUIRE(mini2->get_matrix_coefficients() == 6);
+  REQUIRE(mini2->get_orientation() == 1);
+  REQUIRE(mini2->get_main_item_codec_config().size() == 4);
+  REQUIRE(mini2->get_main_item_codec_config() == std::vector<uint8_t>({0x81, 0x20, 0x00, 0x00}));
+  REQUIRE(mini2->get_main_item_data_size() == 10);
+}
+
+
+TEST_CASE("mini write round-trip with alpha and ICC from scratch")
+{
+  auto mini = std::make_shared<Box_mini>();
+  mini->set_version(0);
+  mini->set_explicit_codec_types_flag(false);
+  mini->set_float_flag(false);
+  mini->set_full_range_flag(true);
+  mini->set_alpha_flag(true);
+  mini->set_explicit_cicp_flag(false);
+  mini->set_hdr_flag(false);
+  mini->set_icc_flag(true);
+  mini->set_exif_flag(false);
+  mini->set_xmp_flag(false);
+  mini->set_chroma_subsampling(3);
+  mini->set_orientation(1);
+  mini->set_width(256);
+  mini->set_height(256);
+  mini->set_bit_depth(8);
+  mini->set_alpha_is_premultiplied(false);
+
+  // CICP defaults for ICC: primaries=2, transfer=2, matrix=6
+  mini->set_colour_primaries(2);
+  mini->set_transfer_characteristics(2);
+  mini->set_matrix_coefficients(6);
+
+  mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00});
+  // Alpha uses same codec config (will be zero-size in bitstream = reuse main)
+  mini->set_alpha_item_codec_config({0x81, 0x20, 0x00, 0x00});
+
+  // Fake ICC data
+  std::vector<uint8_t> icc_data(100, 0xCC);
+  mini->set_icc_data(icc_data);
+
+  // Fake image data
+  std::vector<uint8_t> main_data(50, 0xAA);
+  mini->set_main_item_data(main_data);
+  std::vector<uint8_t> alpha_data(30, 0xBB);
+  mini->set_alpha_item_data(alpha_data);
+
+  // Write
+  StreamWriter writer;
+  Error error = mini->write(writer);
+  REQUIRE(error == Error::Ok);
+
+  // Parse back
+  auto written_data = writer.get_data();
+  auto reader = std::make_shared<StreamReader_memory>(written_data.data(), written_data.size(), false);
+  BitstreamRange range(reader, written_data.size());
+
+  std::shared_ptr<Box> box;
+  error = Box::read(range, &box, heif_get_global_security_limits());
+  REQUIRE(error == Error::Ok);
+  auto mini2 = std::dynamic_pointer_cast<Box_mini>(box);
+  REQUIRE(mini2 != nullptr);
+
+  REQUIRE(mini2->get_width() == 256);
+  REQUIRE(mini2->get_height() == 256);
+  REQUIRE(mini2->get_bit_depth() == 8);
+  REQUIRE(mini2->get_icc_flag() == true);
+  REQUIRE(mini2->get_full_range_flag() == true);
+  REQUIRE(mini2->get_colour_primaries() == 2);
+  REQUIRE(mini2->get_transfer_characteristics() == 2);
+  REQUIRE(mini2->get_matrix_coefficients() == 6);
+  REQUIRE(mini2->get_icc_data().size() == 100);
+  REQUIRE(mini2->get_icc_data() == icc_data);
+  REQUIRE(mini2->get_main_item_codec_config() == std::vector<uint8_t>({0x81, 0x20, 0x00, 0x00}));
+  REQUIRE(mini2->get_alpha_item_codec_config() == std::vector<uint8_t>({0x81, 0x20, 0x00, 0x00}));
+  REQUIRE(mini2->get_main_item_data_size() == 50);
+  REQUIRE(mini2->get_alpha_item_data_size() == 30);
+}
+
+
+TEST_CASE("mini write round-trip with exif and xmp from scratch")
+{
+  auto mini = std::make_shared<Box_mini>();
+  mini->set_version(0);
+  mini->set_explicit_codec_types_flag(false);
+  mini->set_float_flag(false);
+  mini->set_full_range_flag(true);
+  mini->set_alpha_flag(false);
+  mini->set_explicit_cicp_flag(true);
+  mini->set_hdr_flag(false);
+  mini->set_icc_flag(true);
+  mini->set_exif_flag(true);
+  mini->set_xmp_flag(true);
+  mini->set_chroma_subsampling(1);  // 4:2:0
+  mini->set_orientation(1);
+  mini->set_width(320);
+  mini->set_height(240);
+  mini->set_bit_depth(10);
+  mini->set_chroma_is_horizontally_centered(true);
+  mini->set_chroma_is_vertically_centered(false);
+  mini->set_colour_primaries(9);
+  mini->set_transfer_characteristics(16);
+  mini->set_matrix_coefficients(9);
+  mini->set_exif_xmp_compressed_flag(false);
+
+  mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00});
+
+  std::vector<uint8_t> icc_data(200, 0xDD);
+  mini->set_icc_data(icc_data);
+
+  std::vector<uint8_t> main_data(100, 0xAA);
+  mini->set_main_item_data(main_data);
+
+  std::vector<uint8_t> exif_data(80, 0xEE);
+  mini->set_exif_data(exif_data);
+
+  std::vector<uint8_t> xmp_data(150, 0xFF);
+  mini->set_xmp_data(xmp_data);
+
+  // Write
+  StreamWriter writer;
+  Error error = mini->write(writer);
+  REQUIRE(error == Error::Ok);
+
+  // Parse back
+  auto written_data = writer.get_data();
+  auto reader = std::make_shared<StreamReader_memory>(written_data.data(), written_data.size(), false);
+  BitstreamRange range(reader, written_data.size());
+
+  std::shared_ptr<Box> box;
+  error = Box::read(range, &box, heif_get_global_security_limits());
+  REQUIRE(error == Error::Ok);
+  auto mini2 = std::dynamic_pointer_cast<Box_mini>(box);
+  REQUIRE(mini2 != nullptr);
+
+  REQUIRE(mini2->get_width() == 320);
+  REQUIRE(mini2->get_height() == 240);
+  REQUIRE(mini2->get_bit_depth() == 10);
+  REQUIRE(mini2->get_icc_flag() == true);
+  REQUIRE(mini2->get_exif_flag() == true);
+  REQUIRE(mini2->get_xmp_flag() == true);
+  REQUIRE(mini2->get_colour_primaries() == 9);
+  REQUIRE(mini2->get_transfer_characteristics() == 16);
+  REQUIRE(mini2->get_matrix_coefficients() == 9);
+  REQUIRE(mini2->get_orientation() == 1);
+  REQUIRE(mini2->get_icc_data().size() == 200);
+  REQUIRE(mini2->get_icc_data() == icc_data);
+  REQUIRE(mini2->get_main_item_data_size() == 100);
+  REQUIRE(mini2->get_exif_item_data_size() == 80);
+  REQUIRE(mini2->get_xmp_item_data_size() == 150);
+}
+
+
+TEST_CASE("mini write round-trip small dimensions")
+{
+  // Test with small dimensions (7-bit, no large_dimensions_flag)
+  auto mini = std::make_shared<Box_mini>();
+  mini->set_version(0);
+  mini->set_explicit_codec_types_flag(false);
+  mini->set_float_flag(false);
+  mini->set_full_range_flag(true);
+  mini->set_alpha_flag(false);
+  mini->set_explicit_cicp_flag(false);
+  mini->set_hdr_flag(false);
+  mini->set_icc_flag(false);
+  mini->set_exif_flag(false);
+  mini->set_xmp_flag(false);
+  mini->set_chroma_subsampling(1);
+  mini->set_orientation(3);
+  mini->set_width(64);
+  mini->set_height(48);
+  mini->set_bit_depth(8);
+  mini->set_chroma_is_horizontally_centered(true);
+  mini->set_chroma_is_vertically_centered(true);
+  mini->set_colour_primaries(1);
+  mini->set_transfer_characteristics(13);
+  mini->set_matrix_coefficients(6);
+
+  mini->set_main_item_codec_config({0x81, 0x20, 0x00, 0x00});
+  mini->set_main_item_data(std::vector<uint8_t>(20, 0x42));
+
+  StreamWriter writer;
+  Error error = mini->write(writer);
+  REQUIRE(error == Error::Ok);
+
+  auto written_data = writer.get_data();
+  auto reader = std::make_shared<StreamReader_memory>(written_data.data(), written_data.size(), false);
+  BitstreamRange range(reader, written_data.size());
+
+  std::shared_ptr<Box> box;
+  error = Box::read(range, &box, heif_get_global_security_limits());
+  REQUIRE(error == Error::Ok);
+  auto mini2 = std::dynamic_pointer_cast<Box_mini>(box);
+  REQUIRE(mini2 != nullptr);
+
+  REQUIRE(mini2->get_width() == 64);
+  REQUIRE(mini2->get_height() == 48);
+  REQUIRE(mini2->get_orientation() == 3);
+  REQUIRE(mini2->get_bit_depth() == 8);
+  REQUIRE(mini2->get_main_item_data_size() == 20);
+}
+
+
 TEST_CASE("check mini+alpha version")
 {
   auto istr = std::unique_ptr<std::istream>(new std::ifstream(tests_data_directory + "/simple_osm_tile_alpha.avif", std::ios::binary));