avif_serialize/
boxes.rs

1use crate::constants::{ColorPrimaries, MatrixCoefficients, TransferCharacteristics};
2use crate::writer::{Writer, WriterBackend, IO};
3use arrayvec::ArrayVec;
4use std::io::Write;
5use std::num::NonZeroU32;
6use std::{fmt, io};
7
8pub trait MpegBox {
9    fn len(&self) -> usize;
10    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error>;
11}
12
13#[derive(Copy, Clone)]
14pub struct FourCC(pub [u8; 4]);
15
16impl fmt::Debug for FourCC {
17    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
18        match std::str::from_utf8(&self.0) {
19            Ok(s) => s.fmt(f),
20            Err(_) => self.0.fmt(f),
21        }
22    }
23}
24
25#[derive(Debug, Clone)]
26pub struct AvifFile<'data> {
27    pub ftyp: FtypBox,
28    pub meta: MetaBox<'data>,
29    pub mdat: MdatBox,
30}
31
32impl AvifFile<'_> {
33    /// Where the primary data starts inside the `mdat` box, for `iloc`'s offset
34    fn mdat_payload_start_offset(&self) -> u32 {
35        (self.ftyp.len() + self.meta.len()
36            + BASIC_BOX_SIZE) as u32 // mdat head
37    }
38
39    /// `iloc` is mostly unnecssary, high risk of out-of-buffer accesses in parsers that don't pay attention,
40    /// and also awkward to serialize, because its content depends on its own serialized byte size.
41    fn fix_iloc_positions(&mut self) {
42        let start_offset = self.mdat_payload_start_offset();
43        self.meta.iloc.absolute_offset_start = NonZeroU32::new(start_offset);
44    }
45
46    fn write_header(&mut self, out: &mut Vec<u8>) -> io::Result<()> {
47        if self.meta.iprp.ipco.ispe().is_none_or(|b| b.width == 0 || b.height == 0) {
48            return Err(io::Error::new(io::ErrorKind::InvalidInput, "missing width/height"));
49        }
50
51        self.fix_iloc_positions();
52
53        out.try_reserve_exact(self.ftyp.len() + self.meta.len())?;
54        let mut w = Writer::new(out);
55        self.ftyp.write(&mut w).map_err(|_| io::ErrorKind::OutOfMemory)?;
56        self.meta.write(&mut w).map_err(|_| io::ErrorKind::OutOfMemory)?;
57        Ok(())
58    }
59
60    pub fn file_size(&self) -> usize {
61        self.ftyp.len() + self.meta.len() + self.mdat.len(&self.meta.iloc)
62    }
63
64    pub fn write_to_vec(&mut self, out: &mut Vec<u8>) -> io::Result<()> {
65        let expected_file_size = self.file_size();
66        out.try_reserve_exact(expected_file_size)?;
67        let initial = out.len();
68        self.write_header(out)?;
69
70        let _ = self.mdat.write(&mut Writer::new(out), &self.meta.iloc);
71        let written = out.len() - initial;
72        debug_assert_eq!(expected_file_size, written);
73        Ok(())
74    }
75
76    pub fn write<W: Write>(&mut self, mut out: W) -> io::Result<()> {
77        let mut tmp = Vec::new();
78
79        self.write_header(&mut tmp)?;
80        out.write_all(&tmp)?;
81        drop(tmp);
82
83        self.mdat.write(&mut Writer::new(&mut IO(out)), &self.meta.iloc)
84    }
85}
86
87const BASIC_BOX_SIZE: usize = 8;
88const FULL_BOX_SIZE: usize = BASIC_BOX_SIZE + 4;
89
90#[derive(Debug, Clone)]
91pub struct FtypBox {
92    pub major_brand: FourCC,
93    pub minor_version: u32,
94    pub compatible_brands: ArrayVec<FourCC, 2>,
95}
96
97/// File Type box (chunk)
98impl MpegBox for FtypBox {
99    #[inline(always)]
100    fn len(&self) -> usize {
101        BASIC_BOX_SIZE
102        + 4 // brand
103        + 4 // ver
104        + 4 * self.compatible_brands.len()
105    }
106
107    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
108        let mut b = w.basic_box(self.len(), *b"ftyp")?;
109        b.push(&self.major_brand.0)?;
110        b.u32(self.minor_version)?;
111        for cb in &self.compatible_brands {
112            b.push(&cb.0)?;
113        }
114        Ok(())
115    }
116}
117
118/// Metadata box
119#[derive(Debug, Clone)]
120pub struct MetaBox<'data> {
121    pub hdlr: HdlrBox,
122    pub iloc: IlocBox<'data>,
123    pub iinf: IinfBox,
124    pub pitm: PitmBox,
125    pub iprp: IprpBox,
126    pub iref: IrefBox,
127}
128
129impl MpegBox for MetaBox<'_> {
130    #[inline]
131    fn len(&self) -> usize {
132        FULL_BOX_SIZE
133            + self.hdlr.len()
134            + self.pitm.len()
135            + self.iloc.len()
136            + self.iinf.len()
137            + self.iprp.len()
138            + if !self.iref.is_empty() { self.iref.len() } else { 0 }
139    }
140
141    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
142        let mut b = w.full_box(self.len(), *b"meta", 0)?;
143        self.hdlr.write(&mut b)?;
144        self.pitm.write(&mut b)?;
145        self.iloc.write(&mut b)?;
146        self.iinf.write(&mut b)?;
147        if !self.iref.is_empty() {
148            self.iref.write(&mut b)?;
149        }
150        self.iprp.write(&mut b)
151    }
152}
153
154/// Item Info box
155#[derive(Debug, Clone)]
156pub struct IinfBox {
157    pub items: ArrayVec<InfeBox, 3>,
158}
159
160impl MpegBox for IinfBox {
161    #[inline]
162    fn len(&self) -> usize {
163        FULL_BOX_SIZE
164        + 2 // num items u16
165        + self.items.iter().map(|item| item.len()).sum::<usize>()
166    }
167
168    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
169        let mut b = w.full_box(self.len(), *b"iinf", 0)?;
170        b.u16(self.items.len() as _)?;
171        for infe in &self.items {
172            infe.write(&mut b)?;
173        }
174        Ok(())
175    }
176}
177
178/// Item Info Entry box
179#[derive(Debug, Copy, Clone)]
180pub struct InfeBox {
181    pub id: u16,
182    pub typ: FourCC,
183    pub name: &'static str,
184}
185
186impl MpegBox for InfeBox {
187    #[inline(always)]
188    fn len(&self) -> usize {
189        FULL_BOX_SIZE
190        + 2 // id
191        + 2 // item_protection_index
192        + 4 // type
193        + self.name.len() + 1 // nul-terminated
194    }
195
196    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
197        let mut b = w.full_box(self.len(), *b"infe", 2)?;
198        b.u16(self.id)?;
199        b.u16(0)?;
200        b.push(&self.typ.0)?;
201        b.push(self.name.as_bytes())?;
202        b.u8(0)
203    }
204}
205
206#[derive(Debug, Clone)]
207pub struct HdlrBox {
208}
209
210impl MpegBox for HdlrBox {
211    #[inline(always)]
212    fn len(&self) -> usize {
213        FULL_BOX_SIZE + 4 + 4 + 13
214    }
215
216    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
217        // because an image format needs to be told it's an image format,
218        // and it does it the way classic MacOS used to, because Quicktime.
219        let mut b = w.full_box(self.len(), *b"hdlr", 0)?;
220        b.u32(0)?; // old MacOS file type handler
221        b.push(b"pict")?; // MacOS Quicktime subtype
222        b.u32(0)?; // Firefox 92 wants all 0 here
223        b.u32(0)?; // Reserved
224        b.u32(0)?; // Reserved
225        b.u8(0)?; // Pascal string for component name
226        Ok(())
227    }
228}
229
230/// Item properties + associations
231#[derive(Debug, Clone)]
232pub struct IprpBox {
233    pub ipco: IpcoBox,
234    pub ipma: IpmaBox,
235}
236
237impl MpegBox for IprpBox {
238    #[inline(always)]
239    fn len(&self) -> usize {
240        BASIC_BOX_SIZE
241            + self.ipco.len()
242            + self.ipma.len()
243    }
244
245    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
246        let mut b = w.basic_box(self.len(), *b"iprp")?;
247        self.ipco.write(&mut b)?;
248        self.ipma.write(&mut b)
249    }
250}
251
252#[derive(Debug, Clone)]
253#[non_exhaustive]
254pub enum IpcoProp {
255    Av1C(Av1CBox),
256    Pixi(PixiBox),
257    Ispe(IspeBox),
258    AuxC(AuxCBox),
259    Colr(ColrBox),
260    Clli(ClliBox),
261    Mdcv(MdcvBox),
262}
263
264impl IpcoProp {
265    pub fn len(&self) -> usize {
266        match self {
267            Self::Av1C(p) => p.len(),
268            Self::Pixi(p) => p.len(),
269            Self::Ispe(p) => p.len(),
270            Self::AuxC(p) => p.len(),
271            Self::Colr(p) => p.len(),
272            Self::Clli(p) => p.len(),
273            Self::Mdcv(p) => p.len(),
274        }
275    }
276
277    pub fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
278        match self {
279            Self::Av1C(p) => p.write(w),
280            Self::Pixi(p) => p.write(w),
281            Self::Ispe(p) => p.write(w),
282            Self::AuxC(p) => p.write(w),
283            Self::Colr(p) => p.write(w),
284            Self::Clli(p) => p.write(w),
285            Self::Mdcv(p) => p.write(w),
286        }
287    }
288}
289
290/// Item Property Container box
291#[derive(Debug, Clone)]
292pub struct IpcoBox {
293    props: ArrayVec<IpcoProp, 9>,
294}
295
296impl IpcoBox {
297    pub fn new() -> Self {
298        Self { props: ArrayVec::new() }
299    }
300
301    #[must_use]
302    pub fn push(&mut self, prop: IpcoProp) -> Option<u8> {
303        self.props.try_push(prop).ok()?;
304        Some(self.props.len() as u8) // the spec wants them off by one
305    }
306
307    pub(crate) fn ispe(&self) -> Option<&IspeBox> {
308        self.props.iter().find_map(|b| match b {
309            IpcoProp::Ispe(i) => Some(i),
310            _ => None,
311        })
312    }
313}
314
315impl MpegBox for IpcoBox {
316    #[inline]
317    fn len(&self) -> usize {
318        BASIC_BOX_SIZE
319            + self.props.iter().map(|a| a.len()).sum::<usize>()
320    }
321
322    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
323        let mut b = w.basic_box(self.len(), *b"ipco")?;
324        for p in &self.props {
325            p.write(&mut b)?;
326        }
327        Ok(())
328    }
329}
330
331#[derive(Debug, Copy, Clone)]
332pub struct AuxCBox {
333    pub urn: &'static str,
334}
335
336impl AuxCBox {
337    pub fn len(&self) -> usize {
338        FULL_BOX_SIZE + self.urn.len() + 1
339    }
340
341    pub fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
342        let mut b = w.full_box(self.len(), *b"auxC", 0)?;
343        b.push(self.urn.as_bytes())?;
344        b.u8(0)
345    }
346}
347
348/// Pixies, I guess.
349#[derive(Debug, Copy, Clone)]
350pub struct PixiBox {
351    pub depth: u8,
352    pub channels: u8,
353}
354
355impl PixiBox {
356    pub fn len(self) -> usize {
357        FULL_BOX_SIZE
358            + 1 + self.channels as usize
359    }
360
361    pub fn write<B: WriterBackend>(self, w: &mut Writer<B>) -> Result<(), B::Error> {
362        let mut b = w.full_box(self.len(), *b"pixi", 0)?;
363        b.u8(self.channels)?;
364        for _ in 0..self.channels {
365            b.u8(self.depth)?;
366        }
367        Ok(())
368    }
369}
370
371/// This is HEVC-specific and not for AVIF, but Chrome wants it :(
372#[derive(Debug, Copy, Clone)]
373pub struct IspeBox {
374    pub width: u32,
375    pub height: u32,
376}
377
378impl MpegBox for IspeBox {
379    #[inline(always)]
380    fn len(&self) -> usize {
381        FULL_BOX_SIZE + 4 + 4
382    }
383
384    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
385        let mut b = w.full_box(self.len(), *b"ispe", 0)?;
386        b.u32(self.width)?;
387        b.u32(self.height)
388    }
389}
390
391/// Property→image associations
392#[derive(Debug, Clone)]
393pub struct IpmaEntry {
394    pub item_id: u16,
395    pub prop_ids: ArrayVec<u8, 7>,
396}
397
398#[derive(Debug, Clone)]
399pub struct IpmaBox {
400    pub entries: ArrayVec<IpmaEntry, 2>,
401}
402
403impl MpegBox for IpmaBox {
404    #[inline]
405    fn len(&self) -> usize {
406        FULL_BOX_SIZE + 4 + self.entries.iter().map(|e| 2 + 1 + e.prop_ids.len()).sum::<usize>()
407    }
408
409    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
410        let mut b = w.full_box(self.len(), *b"ipma", 0)?;
411        b.u32(self.entries.len() as _)?; // entry count
412
413        for e in &self.entries {
414            b.u16(e.item_id)?;
415            b.u8(e.prop_ids.len() as u8)?; // assoc count
416            for &p in &e.prop_ids {
417                b.u8(p)?;
418            }
419        }
420        Ok(())
421    }
422}
423
424/// Item Reference box
425#[derive(Debug, Copy, Clone)]
426pub struct IrefEntryBox {
427    pub from_id: u16,
428    pub to_id: u16,
429    pub typ: FourCC,
430}
431
432impl MpegBox for IrefEntryBox {
433    #[inline(always)]
434    fn len(&self) -> usize {
435        BASIC_BOX_SIZE
436            + 2 // from
437            + 2 // refcount
438            + 2 // to
439    }
440
441    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
442        let mut b = w.basic_box(self.len(), self.typ.0)?;
443        b.u16(self.from_id)?;
444        b.u16(1)?;
445        b.u16(self.to_id)
446    }
447}
448
449#[derive(Debug, Clone)]
450pub struct IrefBox {
451    pub entries: ArrayVec<IrefEntryBox, 3>,
452}
453
454impl IrefBox {
455    pub fn is_empty(&self) -> bool {
456        self.entries.is_empty()
457    }
458}
459
460impl MpegBox for IrefBox {
461    #[inline(always)]
462    fn len(&self) -> usize {
463        FULL_BOX_SIZE + self.entries.iter().map(|e| e.len()).sum::<usize>()
464    }
465
466    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
467        let mut b = w.full_box(self.len(), *b"iref", 0)?;
468        for entry in &self.entries {
469            entry.write(&mut b)?;
470        }
471        Ok(())
472    }
473}
474
475/// Auxiliary item (alpha or depth map)
476#[derive(Debug, Copy, Clone)]
477#[allow(unused)]
478pub struct AuxlBox {}
479
480impl MpegBox for AuxlBox {
481    #[inline(always)]
482    fn len(&self) -> usize {
483        FULL_BOX_SIZE
484    }
485
486    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
487        w.full_box(self.len(), *b"auxl", 0)?;
488        Ok(())
489    }
490}
491
492/// ColourInformationBox
493#[derive(Debug, Copy, Clone, PartialEq)]
494pub struct ColrBox {
495    pub color_primaries: ColorPrimaries,
496    pub transfer_characteristics: TransferCharacteristics,
497    pub matrix_coefficients: MatrixCoefficients,
498    pub full_range_flag: bool, // u1 + u7
499}
500
501impl Default for ColrBox {
502    fn default() -> Self {
503        Self {
504            color_primaries: ColorPrimaries::Bt709,
505            transfer_characteristics: TransferCharacteristics::Srgb,
506            matrix_coefficients: MatrixCoefficients::Bt601,
507            full_range_flag: true,
508        }
509    }
510}
511
512impl MpegBox for ColrBox {
513    #[inline(always)]
514    fn len(&self) -> usize {
515        BASIC_BOX_SIZE + 4 + 2 + 2 + 2 + 1
516    }
517
518    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
519        let mut b = w.basic_box(self.len(), *b"colr")?;
520        b.u32(u32::from_be_bytes(*b"nclx"))?;
521        b.u16(self.color_primaries as u16)?;
522        b.u16(self.transfer_characteristics as u16)?;
523        b.u16(self.matrix_coefficients as u16)?;
524        b.u8(if self.full_range_flag { 1 << 7 } else { 0 })
525    }
526}
527
528/// Content Light Level Information box (`clli`), per ISOBMFF § 12.1.5 / CEA-861.3.
529///
530/// Signals the content light level of HDR content to the display.
531/// Both values are in cd/m² (nits).
532#[derive(Debug, Copy, Clone, PartialEq)]
533pub struct ClliBox {
534    /// Maximum light level of any single pixel in the content (MaxCLL).
535    pub max_content_light_level: u16,
536    /// Maximum average light level of any single frame in the content (MaxFALL).
537    pub max_pic_average_light_level: u16,
538}
539
540impl MpegBox for ClliBox {
541    #[inline(always)]
542    fn len(&self) -> usize {
543        BASIC_BOX_SIZE + 4
544    }
545
546    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
547        let mut b = w.basic_box(self.len(), *b"clli")?;
548        b.u16(self.max_content_light_level)?;
549        b.u16(self.max_pic_average_light_level)
550    }
551}
552
553/// Mastering Display Colour Volume box (`mdcv`), per ISOBMFF § 12.1.5 / SMPTE ST 2086.
554///
555/// Describes the color volume of the mastering display used to author the content.
556/// This does not describe the content itself — see [`ClliBox`] for that.
557#[derive(Debug, Copy, Clone, PartialEq)]
558pub struct MdcvBox {
559    /// Display primaries in CIE 1931 xy chromaticity, encoded as the value × 50000.
560    /// For example, D65 white (0.3127, 0.3290) encodes as (15635, 16450).
561    /// Order: \[green, blue, red\] per SMPTE ST 2086.
562    pub primaries: [(u16, u16); 3],
563    /// White point in CIE 1931 xy chromaticity, same encoding as `primaries`.
564    pub white_point: (u16, u16),
565    /// Maximum luminance of the mastering display in cd/m² × 10000.
566    /// For example, 1000 cd/m² = 10_000_000.
567    pub max_luminance: u32,
568    /// Minimum luminance of the mastering display in cd/m² × 10000.
569    /// For example, 0.005 cd/m² = 50.
570    pub min_luminance: u32,
571}
572
573impl MpegBox for MdcvBox {
574    #[inline(always)]
575    fn len(&self) -> usize {
576        BASIC_BOX_SIZE + 24
577    }
578
579    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
580        let mut b = w.basic_box(self.len(), *b"mdcv")?;
581        for &(x, y) in &self.primaries {
582            b.u16(x)?;
583            b.u16(y)?;
584        }
585        b.u16(self.white_point.0)?;
586        b.u16(self.white_point.1)?;
587        b.u32(self.max_luminance)?;
588        b.u32(self.min_luminance)
589    }
590}
591
592#[derive(Debug, Copy, Clone)]
593pub struct Av1CBox {
594    pub seq_profile: u8,
595    pub seq_level_idx_0: u8,
596    pub seq_tier_0: bool,
597    pub high_bitdepth: bool,
598    pub twelve_bit: bool,
599    pub monochrome: bool,
600    pub chroma_subsampling_x: bool,
601    pub chroma_subsampling_y: bool,
602    pub chroma_sample_position: u8,
603}
604
605impl MpegBox for Av1CBox {
606    #[inline(always)]
607    fn len(&self) -> usize {
608        BASIC_BOX_SIZE + 4
609    }
610
611    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
612        let mut b = w.basic_box(self.len(), *b"av1C")?;
613        let flags1 =
614            u8::from(self.seq_tier_0) << 7 |
615            u8::from(self.high_bitdepth) << 6 |
616            u8::from(self.twelve_bit) << 5 |
617            u8::from(self.monochrome) << 4 |
618            u8::from(self.chroma_subsampling_x) << 3 |
619            u8::from(self.chroma_subsampling_y) << 2 |
620            self.chroma_sample_position;
621
622        b.push(&[
623            0x81, // marker and version
624            (self.seq_profile << 5) | self.seq_level_idx_0, // x2d == 45
625            flags1,
626            0,
627        ])
628    }
629}
630
631#[derive(Debug, Copy, Clone)]
632pub struct PitmBox(pub u16);
633
634impl MpegBox for PitmBox {
635    #[inline(always)]
636    fn len(&self) -> usize {
637        FULL_BOX_SIZE + 2
638    }
639
640    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
641        let mut b = w.full_box(self.len(), *b"pitm", 0)?;
642        b.u16(self.0)
643    }
644}
645
646#[derive(Debug, Clone)]
647pub struct IlocBox<'data> {
648    /// update before writing
649    pub absolute_offset_start: Option<NonZeroU32>,
650    pub items: ArrayVec<IlocItem<'data>, 3>,
651}
652
653#[derive(Debug, Clone)]
654pub struct IlocItem<'data> {
655    pub id: u16,
656    pub extents: [IlocExtent<'data>; 1],
657}
658
659#[derive(Debug, Copy, Clone)]
660pub struct IlocExtent<'data> {
661    /// offset and len will be calculated when writing
662    pub data: &'data [u8],
663}
664
665impl MpegBox for IlocBox<'_> {
666    #[inline(always)]
667    #[allow(unused_parens)]
668    fn len(&self) -> usize {
669        FULL_BOX_SIZE
670        + 1 // offset_size, length_size
671        + 1 // base_offset_size, reserved
672        + 2 // num items
673        + self.items.iter().map(|i| ( // for each item
674            2 // id
675            + 2 // dat ref idx
676            + 0 // base_offset_size
677            + 2 // extent count
678            + i.extents.len() * ( // for each extent
679               4 // extent_offset
680               + 4 // extent_len
681            )
682        )).sum::<usize>()
683    }
684
685    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
686        let mut b = w.full_box(self.len(), *b"iloc", 0)?;
687        b.push(&[4 << 4 | 4, 0])?; // offset and length are 4 bytes
688
689        b.u16(self.items.len() as _)?; // num items
690        let mut next_start = if let Some(ok) = self.absolute_offset_start { ok.get() } else {
691            debug_assert!(false);
692            !0
693        };
694        for item in &self.items {
695            b.u16(item.id)?;
696            b.u16(0)?;
697            b.u16(item.extents.len() as _)?; // num extents
698            for ex in &item.extents {
699                let len = ex.data.len() as u32;
700                b.u32(next_start)?;
701                next_start += len;
702                b.u32(len)?;
703            }
704        }
705        Ok(())
706    }
707}
708
709#[derive(Debug, Clone)]
710pub struct MdatBox;
711
712impl MdatBox {
713    #[inline(always)]
714    fn len(&self, chunks: &IlocBox) -> usize {
715        BASIC_BOX_SIZE + chunks.items.iter().flat_map(|c| &c.extents).map(|d| d.data.len()).sum::<usize>()
716    }
717
718    fn write<B: WriterBackend>(&self, w: &mut Writer<B>, chunks: &IlocBox) -> Result<(), B::Error> {
719        let mut b = w.basic_box(self.len(chunks), *b"mdat")?;
720        for ch in chunks.items.iter().flat_map(|c| &c.extents) {
721            b.push(ch.data)?;
722        }
723        Ok(())
724    }
725}