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