Import avif-serialize 0.8.1 upstream upstream/0.8.1
authorDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 07:21:12 +0000 (16:21 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Thu, 6 Apr 2023 07:21:12 +0000 (16:21 +0900)
.cargo_vcs_info.json [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
Cargo.toml.orig [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
src/boxes.rs [new file with mode: 0644]
src/constants.rs [new file with mode: 0644]
src/lib.rs [new file with mode: 0644]
src/writer.rs [new file with mode: 0644]

diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644 (file)
index 0000000..ca33e0f
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "707051d30afbfe34321b85da48b542cf16e5022e"
+  },
+  "path_in_vcs": ""
+}
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..e1ed2a8
--- /dev/null
@@ -0,0 +1,130 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
+name = "avif-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87fb3f9afbbb9f71ae53da13771284895f51c6acc77cfb3571ab966780250dee"
+dependencies = [
+ "bitreader",
+ "byteorder",
+ "fallible_collections",
+ "log",
+ "static_assertions",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.1"
+dependencies = [
+ "arrayvec",
+ "avif-parse",
+]
+
+[[package]]
+name = "bitreader"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84ea71c85d1fe98fe67a9b9988b1695bc24c0b0d3bfb18d4c510f44b4b09941"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "fallible_collections"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f57ccc32870366ae684be48b32a1a2e196f98a42a9b4361fe77e13fd4a34755"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..d7aed66
--- /dev/null
@@ -0,0 +1,47 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "avif-serialize"
+version = "0.8.1"
+authors = ["Kornel Lesiński <kornel@geekhood.net>"]
+include = [
+    "src/*.rs",
+    "Cargo.toml",
+    "README.md",
+    "LICENSE",
+]
+description = "Minimal writer for AVIF header structure (MPEG/HEIF/MIAF/ISO-BMFF)"
+homepage = "https://lib.rs/avif-serialize"
+readme = "README.md"
+keywords = [
+    "avif",
+    "heif",
+    "bmff",
+    "av1",
+    "mux",
+]
+categories = [
+    "multimedia::images",
+    "encoding",
+]
+license = "BSD-3-Clause"
+repository = "https://github.com/kornelski/avif-serialize"
+
+[dependencies.arrayvec]
+version = "0.7.2"
+
+[dev-dependencies.avif-parse]
+version = "1.0.0"
+
+[badges.maintenance]
+status = "passively-maintained"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644 (file)
index 0000000..35c2df0
--- /dev/null
@@ -0,0 +1,23 @@
+[package]
+name = "avif-serialize"
+version = "0.8.1"
+authors = ["Kornel Lesiński <kornel@geekhood.net>"]
+edition = "2021"
+license = "BSD-3-Clause"
+description = "Minimal writer for AVIF header structure (MPEG/HEIF/MIAF/ISO-BMFF)"
+readme = "README.md"
+categories = ["multimedia::images", "encoding"]
+keywords = ["avif", "heif", "bmff", "av1", "mux"]
+repository = "https://github.com/kornelski/avif-serialize"
+homepage = "https://lib.rs/avif-serialize"
+include = ["src/*.rs", "Cargo.toml", "README.md", "LICENSE"]
+
+[dependencies]
+arrayvec = "0.7.2"
+
+[dev-dependencies]
+mp4parse = { git = "https://github.com/mozilla/mp4parse-rust", rev = "c6ba5afd856c158d9cfc1a447165fcfaaf2b797c" }
+avif-parse = "1.0.0"
+
+[badges]
+maintenance = { status = "passively-maintained" }
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..6cffeaa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2020, Cloudflare, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..d6df2dd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+# AVIF image serializer (muxer)
+
+Minimal writer for AVIF header structure. This is lean, safe-Rust alternative to [libavif](//lib.rs/libavif).
+It creates the jungle of MPEG/HEIF/MIAF/ISO-BMFF "boxes" as appropriate for AVIF files. Supports alpha channel embedding.
+
+Compatible with decoders in Chrome 85+, libavif v0.8.1, and Firefox 92.
+
+Together with [rav1e](//lib.rs/rav1e) it allows pure-Rust AVIF image encoding.
+
+## Requirements
+
+* [Latest stable](https://rustup.rs) version of Rust.
+
+## Usage
+
+1. Compress pixels using an AV1 encoder, such as [rav1e](//lib.rs/rav1e). [libaom](//lib.rs/libaom-sys) works too.
+
+2. Call `avif_serialize::serialize_to_vec(av1_data, None, width, height, 8)`
+
+See [cavif](https://github.com/kornelski/cavif-rs) for example usage.
+
diff --git a/src/boxes.rs b/src/boxes.rs
new file mode 100644 (file)
index 0000000..9fdb328
--- /dev/null
@@ -0,0 +1,645 @@
+use crate::constants::ColorPrimaries;
+use crate::constants::MatrixCoefficients;
+use crate::constants::TransferCharacteristics;
+use crate::writer::Writer;
+use crate::writer::WriterBackend;
+use crate::writer::IO;
+use arrayvec::ArrayVec;
+use std::fmt;
+use std::io;
+use std::io::Write;
+
+pub trait MpegBox {
+    fn len(&self) -> usize;
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error>;
+}
+
+#[derive(Copy, Clone)]
+pub struct FourCC(pub [u8; 4]);
+
+impl fmt::Debug for FourCC {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match std::str::from_utf8(&self.0) {
+            Ok(s) => s.fmt(f),
+            Err(_) => self.0.fmt(f),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct AvifFile<'data> {
+    pub ftyp: FtypBox,
+    pub meta: MetaBox,
+    pub mdat: MdatBox<'data>,
+}
+
+impl AvifFile<'_> {
+    /// Where the primary data starts inside the `mdat` box, for `iloc`'s offset
+    fn mdat_payload_start_offset(&self) -> u32 {
+        (self.ftyp.len() + self.meta.len()
+            + BASIC_BOX_SIZE) as u32 // mdat head
+    }
+
+    /// `iloc` is mostly unnecssary, high risk of out-of-buffer accesses in parsers that don't pay attention,
+    /// and also awkward to serialize, because its content depends on its own serialized byte size.
+    fn fix_iloc_positions(&mut self) {
+        let start_offset = self.mdat_payload_start_offset();
+        for iloc_item in self.meta.iloc.items.iter_mut() {
+            for ex in iloc_item.extents.iter_mut() {
+                let abs = match ex.offset {
+                    IlocOffset::Relative(n) => n as u32 + start_offset,
+                    IlocOffset::Absolute(_) => continue,
+                };
+                ex.offset = IlocOffset::Absolute(abs);
+            }
+        }
+    }
+
+    pub fn write<W: Write>(&mut self, mut out: W) -> io::Result<()> {
+        self.fix_iloc_positions();
+
+        let mut tmp = Vec::with_capacity(self.ftyp.len() + self.meta.len());
+        let mut w = Writer::new(&mut tmp);
+        let _ = self.ftyp.write(&mut w);
+        let _ = self.meta.write(&mut w);
+        drop(w);
+        out.write_all(&tmp)?;
+        drop(tmp);
+
+        let mut out = IO(out);
+        let mut w = Writer::new(&mut out);
+        self.mdat.write(&mut w)?;
+        Ok(())
+    }
+}
+
+const BASIC_BOX_SIZE: usize = 8;
+const FULL_BOX_SIZE: usize = BASIC_BOX_SIZE + 4;
+
+#[derive(Debug, Clone)]
+pub struct FtypBox {
+    pub major_brand: FourCC,
+    pub minor_version: u32,
+    pub compatible_brands: ArrayVec<FourCC, 2>,
+}
+
+/// File Type box (chunk)
+impl MpegBox for FtypBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE
+        + 4 // brand
+        + 4 // ver
+        + 4 * self.compatible_brands.len()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"ftyp")?;
+        b.push(&self.major_brand.0)?;
+        b.u32(self.minor_version)?;
+        for cb in &self.compatible_brands {
+            b.push(&cb.0)?;
+        }
+        Ok(())
+    }
+}
+
+/// Metadata box
+#[derive(Debug, Clone)]
+pub struct MetaBox {
+    pub hdlr: HdlrBox,
+    pub iloc: IlocBox,
+    pub iinf: IinfBox,
+    pub pitm: PitmBox,
+    pub iprp: IprpBox,
+    pub iref: ArrayVec<IrefBox, 2>,
+}
+
+impl MpegBox for MetaBox {
+    #[inline]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE
+            + self.hdlr.len()
+            + self.pitm.len()
+            + self.iloc.len()
+            + self.iinf.len()
+            + self.iprp.len()
+            + self.iref.iter().map(|b| b.len()).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"meta", 0)?;
+        self.hdlr.write(&mut b)?;
+        self.pitm.write(&mut b)?;
+        self.iloc.write(&mut b)?;
+        self.iinf.write(&mut b)?;
+        for iref in &self.iref {
+            iref.write(&mut b)?;
+        }
+        self.iprp.write(&mut b)
+    }
+}
+
+/// Item Info box
+#[derive(Debug, Clone)]
+pub struct IinfBox {
+    pub items: ArrayVec<InfeBox, 2>,
+}
+
+impl MpegBox for IinfBox {
+    #[inline]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE
+        + 2 // num items u16
+        + self.items.iter().map(|item| item.len()).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"iinf", 0)?;
+        b.u16(self.items.len() as _)?;
+        for infe in self.items.iter() {
+            infe.write(&mut b)?;
+        }
+        Ok(())
+    }
+}
+
+/// Item Info Entry box
+#[derive(Debug, Copy, Clone)]
+pub struct InfeBox {
+    pub id: u16,
+    pub typ: FourCC,
+    pub name: &'static str,
+}
+
+impl MpegBox for InfeBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE
+        + 2 // id
+        + 2 // item_protection_index
+        + 4 // type
+        + self.name.as_bytes().len() + 1 // nul-terminated
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"infe", 2)?;
+        b.u16(self.id)?;
+        b.u16(0)?;
+        b.push(&self.typ.0)?;
+        b.push(self.name.as_bytes())?;
+        b.u8(0)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct HdlrBox {
+}
+
+impl MpegBox for HdlrBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE + 4 + 4 + 13
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        // because an image format needs to be told it's an image format,
+        // and it does it the way classic MacOS used to, because Quicktime.
+        b.full_box(*b"hdlr", 0)?;
+        b.u32(0)?; // old MacOS file type handler
+        b.push(b"pict")?; // MacOS Quicktime subtype
+        b.u32(0)?; // Firefox 92 wants all 0 here
+        b.u32(0)?; // Reserved
+        b.u32(0)?; // Reserved
+        b.u8(0)?; // Pascal string for component name
+        Ok(())
+    }
+}
+
+/// Item properties + associations
+#[derive(Debug, Clone)]
+pub struct IprpBox {
+    pub ipco: IpcoBox,
+    pub ipma: IpmaBox,
+}
+
+impl MpegBox for IprpBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE
+            + self.ipco.len()
+            + self.ipma.len()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"iprp")?;
+        self.ipco.write(&mut b)?;
+        self.ipma.write(&mut b)
+    }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum IpcoProp {
+    Av1C(Av1CBox),
+    Pixi(PixiBox),
+    Ispe(IspeBox),
+    AuxC(AuxCBox),
+    Colr(ColrBox),
+}
+
+impl IpcoProp {
+    pub fn len(&self) -> usize {
+        match self {
+            Self::Av1C(p) => p.len(),
+            Self::Pixi(p) => p.len(),
+            Self::Ispe(p) => p.len(),
+            Self::AuxC(p) => p.len(),
+            Self::Colr(p) => p.len(),
+        }
+    }
+
+    pub fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        match self {
+            Self::Av1C(p) => p.write(w),
+            Self::Pixi(p) => p.write(w),
+            Self::Ispe(p) => p.write(w),
+            Self::AuxC(p) => p.write(w),
+            Self::Colr(p) => p.write(w),
+        }
+    }
+}
+
+/// Item Property Container box
+#[derive(Debug, Clone)]
+pub struct IpcoBox {
+    props: ArrayVec<IpcoProp, 7>,
+}
+
+impl IpcoBox {
+    pub fn new() -> Self {
+        Self { props: ArrayVec::new() }
+    }
+
+    pub fn push(&mut self, prop: IpcoProp) -> u8 {
+        self.props.push(prop);
+        self.props.len() as u8 // the spec wants them off by one
+    }
+}
+
+impl MpegBox for IpcoBox {
+    #[inline]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE
+            + self.props.iter().map(|a| a.len()).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"ipco")?;
+        for p in self.props.iter() {
+            p.write(&mut b)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct AuxCBox {
+    pub urn: &'static str,
+}
+
+impl AuxCBox {
+    pub fn len(&self) -> usize {
+        FULL_BOX_SIZE + self.urn.len() + 1
+    }
+
+    pub fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"auxC", 0)?;
+        b.push(self.urn.as_bytes())?;
+        b.u8(0)
+    }
+}
+
+/// Pixies, I guess.
+#[derive(Debug, Copy, Clone)]
+pub struct PixiBox {
+    pub depth: u8,
+    pub channels: u8,
+}
+
+impl PixiBox {
+    pub fn len(&self) -> usize {
+        FULL_BOX_SIZE
+            + 1 + self.channels as usize
+    }
+
+    pub fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"pixi", 0)?;
+        b.u8(self.channels)?;
+        for _ in 0..self.channels {
+            b.u8(self.depth)?;
+        }
+        Ok(())
+    }
+}
+
+/// This is HEVC-specific and not for AVIF, but Chrome wants it :(
+#[derive(Debug, Copy, Clone)]
+pub struct IspeBox {
+    pub width: u32,
+    pub height: u32,
+}
+
+impl MpegBox for IspeBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE + 4 + 4
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"ispe", 0)?;
+        b.u32(self.width)?;
+        b.u32(self.height)
+    }
+}
+
+/// Property→image associations
+#[derive(Debug, Clone)]
+pub struct IpmaEntry {
+    pub item_id: u16,
+    pub prop_ids: ArrayVec<u8, 5>,
+}
+
+#[derive(Debug, Clone)]
+pub struct IpmaBox {
+    pub entries: ArrayVec<IpmaEntry, 2>,
+}
+
+impl MpegBox for IpmaBox {
+    #[inline]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE + 4 + self.entries.iter().map(|e| 2 + 1 + e.prop_ids.len()).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"ipma", 0)?;
+        b.u32(self.entries.len() as _)?; // entry count
+
+        for e in &self.entries {
+            b.u16(e.item_id)?;
+            b.u8(e.prop_ids.len() as u8)?; // assoc count
+            for &p in e.prop_ids.iter() {
+                b.u8(p)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+/// Item Reference box
+#[derive(Debug, Copy, Clone)]
+pub struct IrefEntryBox {
+    pub from_id: u16,
+    pub to_id: u16,
+    pub typ: FourCC,
+}
+
+impl MpegBox for IrefEntryBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE
+            + 2 // from
+            + 2 // refcount
+            + 2 // to
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(self.typ.0)?;
+        b.u16(self.from_id)?;
+        b.u16(1)?;
+        b.u16(self.to_id)
+    }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct IrefBox {
+    pub entry: IrefEntryBox,
+}
+
+impl MpegBox for IrefBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE + self.entry.len()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"iref", 0)?;
+        self.entry.write(&mut b)
+    }
+}
+
+/// Auxiliary item (alpha or depth map)
+#[derive(Debug, Copy, Clone)]
+pub struct AuxlBox {}
+
+impl MpegBox for AuxlBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"auxl", 0)
+    }
+}
+
+/// ColourInformationBox
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct ColrBox {
+    pub color_primaries: ColorPrimaries,
+    pub transfer_characteristics: TransferCharacteristics,
+    pub matrix_coefficients: MatrixCoefficients,
+    pub full_range_flag: bool, // u1 + u7
+}
+
+impl Default for ColrBox {
+    fn default() -> Self {
+        Self {
+            color_primaries: ColorPrimaries::Bt709,
+            transfer_characteristics: TransferCharacteristics::Srgb,
+            matrix_coefficients: MatrixCoefficients::Bt601,
+            full_range_flag: true,
+        }
+    }
+}
+
+impl MpegBox for ColrBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE + 4 + 2 + 2 + 2 + 1
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"colr")?;
+        b.u32(u32::from_be_bytes(*b"nclx"))?;
+        b.u16(self.color_primaries as u16)?;
+        b.u16(self.transfer_characteristics as u16)?;
+        b.u16(self.matrix_coefficients as u16)?;
+        b.u8(if self.full_range_flag { 1 << 7 } else { 0 })
+    }
+}
+#[derive(Debug, Copy, Clone)]
+pub struct Av1CBox {
+    pub seq_profile: u8,
+    pub seq_level_idx_0: u8,
+    pub seq_tier_0: bool,
+    pub high_bitdepth: bool,
+    pub twelve_bit: bool,
+    pub monochrome: bool,
+    pub chroma_subsampling_x: bool,
+    pub chroma_subsampling_y: bool,
+    pub chroma_sample_position: u8,
+}
+
+impl MpegBox for Av1CBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE + 4
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"av1C")?;
+        let flags1 =
+            u8::from(self.seq_tier_0) << 7 |
+            u8::from(self.high_bitdepth) << 6 |
+            u8::from(self.twelve_bit) << 5 |
+            u8::from(self.monochrome) << 4 |
+            u8::from(self.chroma_subsampling_x) << 3 |
+            u8::from(self.chroma_subsampling_y) << 2 |
+            self.chroma_sample_position;
+
+        b.push(&[
+            0x81, // marker and version
+            (self.seq_profile << 5) | self.seq_level_idx_0, // x2d == 45
+            flags1,
+            0,
+        ])
+    }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct PitmBox(pub u16);
+
+impl MpegBox for PitmBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE + 2
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"pitm", 0)?;
+        b.u16(self.0)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct IlocBox {
+    pub items: ArrayVec<IlocItem, 2>,
+}
+
+#[derive(Debug, Clone)]
+pub struct IlocItem {
+    pub id: u16,
+    pub extents: ArrayVec<IlocExtent, 1>,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum IlocOffset {
+    Relative(usize),
+    Absolute(u32),
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct IlocExtent {
+    pub offset: IlocOffset,
+    pub len: usize,
+}
+
+impl MpegBox for IlocBox {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        FULL_BOX_SIZE
+        + 1 // offset_size, length_size
+        + 1 // base_offset_size, reserved
+        + 2 // num items
+        + self.items.iter().map(|i| ( // for each item
+            2 // id
+            + 2 // dat ref idx
+            + 0 // base_offset_size
+            + 2 // extent count
+            + i.extents.len() * ( // for each extent
+               4 // extent_offset
+               + 4 // extent_len
+            )
+        )).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.full_box(*b"iloc", 0)?;
+        b.push(&[4 << 4 | 4, 0])?; // offset and length are 4 bytes
+
+        b.u16(self.items.len() as _)?; // num items
+        for item in self.items.iter() {
+            b.u16(item.id)?;
+            b.u16(0)?;
+            b.u16(item.extents.len() as _)?; // num extents
+            for ex in &item.extents {
+                b.u32(match ex.offset {
+                    IlocOffset::Absolute(val) => val,
+                    IlocOffset::Relative(_) => panic!("absolute offset must be set"),
+                })?;
+                b.u32(ex.len as _)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct MdatBox<'data> {
+    pub data_chunks: ArrayVec<&'data [u8], 4>,
+}
+
+impl MpegBox for MdatBox<'_> {
+    #[inline(always)]
+    fn len(&self) -> usize {
+        BASIC_BOX_SIZE + self.data_chunks.iter().map(|c| c.len()).sum::<usize>()
+    }
+
+    fn write<B: WriterBackend>(&self, w: &mut Writer<B>) -> Result<(), B::Error> {
+        let mut b = w.new_box(self.len());
+        b.basic_box(*b"mdat")?;
+        for ch in &self.data_chunks {
+            b.push(ch)?;
+        }
+        Ok(())
+    }
+}
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644 (file)
index 0000000..22428ed
--- /dev/null
@@ -0,0 +1,54 @@
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ColorPrimaries {
+    /// ITU-R BT1361
+    Bt709 = 1,
+    Unspecified = 2,
+    /// ITU-R BT601-6 525
+    Bt601 = 6,
+    /// ITU-R BT2020
+    Bt2020 = 9,
+    /// SMPTE ST 431-2
+    DciP3 = 11,
+    /// SMPTE ST 432-1
+    DisplayP3 = 12,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum TransferCharacteristics {
+    /// ITU-R BT1361
+    Bt709 = 1,
+    Unspecified = 2,
+    /// ITU-R BT601-6 525
+    Bt601 = 6,
+    /// "Linear transfer characteristics"
+    Linear = 8,
+    /// "Logarithmic transfer characteristic (100:1 range)"
+    Log = 9,
+    /// "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
+    LogSqrt = 10,
+    /// sRGB
+    Srgb = 13,
+    /// ITU-R BT2020 for 10-bit system
+    Bt2020_10 = 14,
+    /// ITU-R BT2020 for 12-bit system
+    Bt2020_12 = 15,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum MatrixCoefficients {
+    /// GBR (sRGB)
+    Rgb = 0,
+    /// ITU-R BT1361
+    Bt709 = 1,
+    Unspecified = 2,
+    /// ITU-R BT601-6 525
+    Bt601 = 6,
+    Ycgco = 8,
+    /// ITU-R BT2020 non-constant luminance system
+    Bt2020Ncl = 9,
+    /// ITU-R BT2020 constant luminance system
+    Bt2020Cl = 10,
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..ab0839b
--- /dev/null
@@ -0,0 +1,348 @@
+//! # AVIF image serializer (muxer)
+//!
+//! ## Usage
+//!
+//! 1. Compress pixels using an AV1 encoder, such as [rav1e](//lib.rs/rav1e). [libaom](//lib.rs/libaom-sys) works too.
+//!
+//! 2. Call `avif_serialize::serialize_to_vec(av1_data, None, width, height, 8)`
+//!
+//! See [cavif](https://github.com/kornelski/cavif-rs) for a complete implementation.
+
+mod boxes;
+pub mod constants;
+mod writer;
+
+use crate::boxes::*;
+use arrayvec::ArrayVec;
+use std::io;
+
+/// Config for the serialization (allows setting advanced image properties).
+///
+/// See [`Aviffy::new`].
+pub struct Aviffy {
+    premultiplied_alpha: bool,
+    colr: ColrBox,
+}
+
+/// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](//lib.rs/rav1e))
+///
+/// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
+/// The color image MUST have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
+/// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
+///
+/// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
+/// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
+///
+/// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
+/// `depth_bits` should be 8, 10 or 12, depending on how the image was encoded (typically 8).
+///
+/// Color and alpha must have the same dimensions and depth.
+///
+/// Data is written (streamed) to `into_output`.
+pub fn serialize<W: io::Write>(into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
+    Aviffy::new().write(into_output, color_av1_data, alpha_av1_data, width, height, depth_bits)
+}
+
+impl Aviffy {
+    #[must_use]
+    pub fn new() -> Self {
+        Self {
+            premultiplied_alpha: false,
+            colr: Default::default(),
+        }
+    }
+
+    /// Set whether image's colorspace uses premultiplied alpha, i.e. RGB channels were multiplied by their alpha value,
+    /// so that transparent areas are all black. Image decoders will be instructed to undo the premultiplication.
+    ///
+    /// Premultiplied alpha images usually compress better and tolerate heavier compression, but
+    /// may not be supported correctly by less capable AVIF decoders.
+    ///
+    /// This just sets the configuration property. The pixel data must have already been processed before compression.
+    pub fn premultiplied_alpha(&mut self, is_premultiplied: bool) -> &mut Self {
+        self.premultiplied_alpha = is_premultiplied;
+        self
+    }
+
+    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
+    /// Defaults to BT.601, because that's what Safari assumes when `colr` is missing.
+    /// Other browsers are smart enough to read this from the AV1 payload instead.
+    pub fn matrix_coefficients(&mut self, matrix_coefficients: constants::MatrixCoefficients) -> &mut Self {
+        self.colr.matrix_coefficients = matrix_coefficients;
+        self
+    }
+
+    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
+    /// Defaults to sRGB.
+    pub fn transfer_characteristics(&mut self, transfer_characteristics: constants::TransferCharacteristics) -> &mut Self {
+        self.colr.transfer_characteristics = transfer_characteristics;
+        self
+    }
+
+    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
+    /// Defaults to sRGB/Rec.709.
+    pub fn color_primaries(&mut self, color_primaries: constants::ColorPrimaries) -> &mut Self {
+        self.colr.color_primaries = color_primaries;
+        self
+    }
+
+    /// If set, must match the AV1 color payload, and will result in `colr` box added to AVIF.
+    /// Defaults to full.
+    pub fn full_color_range(&mut self, full_range: bool) -> &mut Self {
+        self.colr.full_range_flag = full_range;
+        self
+    }
+
+    /// Makes an AVIF file given encoded AV1 data (create the data with [`rav1e`](//lib.rs/rav1e))
+    ///
+    /// `color_av1_data` is already-encoded AV1 image data for the color channels (YUV, RGB, etc.).
+    /// The color image MUST have been encoded without chroma subsampling AKA YUV444 (`Cs444` in `rav1e`)
+    /// AV1 handles full-res color so effortlessly, you should never need chroma subsampling ever again.
+    ///
+    /// Optional `alpha_av1_data` is a monochrome image (`rav1e` calls it "YUV400"/`Cs400`) representing transparency.
+    /// Alpha adds a lot of header bloat, so don't specify it unless it's necessary.
+    ///
+    /// `width`/`height` is image size in pixels. It must of course match the size of encoded image data.
+    /// `depth_bits` should be 8, 10 or 12, depending on how the image has been encoded in AV1.
+    ///
+    /// Color and alpha must have the same dimensions and depth.
+    ///
+    /// Data is written (streamed) to `into_output`.
+    pub fn write<W: io::Write>(&self, into_output: W, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> io::Result<()> {
+        self.make_boxes(color_av1_data, alpha_av1_data, width, height, depth_bits).write(into_output)
+    }
+
+    fn make_boxes<'data>(&self, color_av1_data: &'data [u8], alpha_av1_data: Option<&'data [u8]>, width: u32, height: u32, depth_bits: u8) -> AvifFile<'data> {
+        let mut image_items = ArrayVec::new();
+        let mut iloc_items = ArrayVec::new();
+        let mut compatible_brands = ArrayVec::new();
+        let mut ipma_entries = ArrayVec::new();
+        let mut data_chunks = ArrayVec::new();
+        let mut irefs = ArrayVec::new();
+        let mut ipco = IpcoBox::new();
+        let color_image_id = 1;
+        let alpha_image_id = 2;
+        const ESSENTIAL_BIT: u8 = 0x80;
+        let color_depth_bits = depth_bits;
+        let alpha_depth_bits = depth_bits; // Sadly, the spec requires these to match.
+
+        image_items.push(InfeBox {
+            id: color_image_id,
+            typ: FourCC(*b"av01"),
+            name: "",
+        });
+        let ispe_prop = ipco.push(IpcoProp::Ispe(IspeBox { width, height }));
+        // This is redundant, but Chrome wants it, and checks that it matches :(
+        let av1c_color_prop = ipco.push(IpcoProp::Av1C(Av1CBox {
+            seq_profile: if color_depth_bits >= 12 { 2 } else { 1 },
+            seq_level_idx_0: 31,
+            seq_tier_0: false,
+            high_bitdepth: color_depth_bits >= 10,
+            twelve_bit: color_depth_bits >= 12,
+            monochrome: false,
+            chroma_subsampling_x: false,
+            chroma_subsampling_y: false,
+            chroma_sample_position: 0,
+        }));
+        // Useless bloat
+        let pixi_3 = ipco.push(IpcoProp::Pixi(PixiBox {
+            channels: 3,
+            depth: color_depth_bits,
+        }));
+        let mut prop_ids: ArrayVec<u8, 5> = [ispe_prop, av1c_color_prop | ESSENTIAL_BIT, pixi_3].into_iter().collect();
+        // Redundant info, already in AV1
+        if self.colr != Default::default() {
+            let colr_color_prop = ipco.push(IpcoProp::Colr(self.colr));
+            prop_ids.push(colr_color_prop);
+        }
+        ipma_entries.push(IpmaEntry {
+            item_id: color_image_id,
+            prop_ids,
+        });
+
+        if let Some(alpha_data) = alpha_av1_data {
+            image_items.push(InfeBox {
+                id: alpha_image_id,
+                typ: FourCC(*b"av01"),
+                name: "",
+            });
+            let av1c_alpha_prop = ipco.push(boxes::IpcoProp::Av1C(Av1CBox {
+                seq_profile: if alpha_depth_bits >= 12 { 2 } else { 0 },
+                seq_level_idx_0: 31,
+                seq_tier_0: false,
+                high_bitdepth: alpha_depth_bits >= 10,
+                twelve_bit: alpha_depth_bits >= 12,
+                monochrome: true,
+                chroma_subsampling_x: true,
+                chroma_subsampling_y: true,
+                chroma_sample_position: 0,
+            }));
+            // So pointless
+            let pixi_1 = ipco.push(IpcoProp::Pixi(PixiBox {
+                channels: 1,
+                depth: alpha_depth_bits,
+            }));
+
+            // that's a silly way to add 1 bit of information, isn't it?
+            let auxc_prop = ipco.push(IpcoProp::AuxC(AuxCBox {
+                urn: "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha",
+            }));
+            irefs.push(IrefBox {
+                entry: IrefEntryBox {
+                    from_id: alpha_image_id,
+                    to_id: color_image_id,
+                    typ: FourCC(*b"auxl"),
+                },
+            });
+            if self.premultiplied_alpha {
+                irefs.push(IrefBox {
+                    entry: IrefEntryBox {
+                        from_id: color_image_id,
+                        to_id: alpha_image_id,
+                        typ: FourCC(*b"prem"),
+                    },
+                });
+            }
+            ipma_entries.push(IpmaEntry {
+                item_id: alpha_image_id,
+                prop_ids: [ispe_prop, av1c_alpha_prop | ESSENTIAL_BIT, auxc_prop, pixi_1].into_iter().collect(),
+            });
+
+            // Use interleaved color and alpha, with alpha first.
+            // Makes it possible to display partial image.
+            iloc_items.push(IlocItem {
+                id: color_image_id,
+                extents: [
+                    IlocExtent {
+                        offset: IlocOffset::Relative(alpha_data.len()),
+                        len: color_av1_data.len(),
+                    },
+                ].into(),
+            });
+            iloc_items.push(IlocItem {
+                id: alpha_image_id,
+                extents: [
+                    IlocExtent {
+                        offset: IlocOffset::Relative(0),
+                        len: alpha_data.len(),
+                    },
+                ].into(),
+            });
+            data_chunks.push(alpha_data);
+            data_chunks.push(color_av1_data);
+        } else {
+            iloc_items.push(IlocItem {
+                id: color_image_id,
+                extents: [
+                    IlocExtent {
+                        offset: IlocOffset::Relative(0),
+                        len: color_av1_data.len(),
+                    },
+                ].into(),
+            });
+            data_chunks.push(color_av1_data);
+        };
+
+        compatible_brands.push(FourCC(*b"mif1"));
+        compatible_brands.push(FourCC(*b"miaf"));
+        AvifFile {
+            ftyp: FtypBox {
+                major_brand: FourCC(*b"avif"),
+                minor_version: 0,
+                compatible_brands,
+            },
+            meta: MetaBox {
+                hdlr: HdlrBox {},
+                iinf: IinfBox { items: image_items },
+                pitm: PitmBox(color_image_id),
+                iloc: IlocBox { items: iloc_items },
+                iprp: IprpBox {
+                    ipco,
+                    // It's not enough to define these properties,
+                    // they must be assigned to the image
+                    ipma: IpmaBox {
+                        entries: ipma_entries,
+                    },
+                },
+                iref: irefs,
+            },
+            // Here's the actual data. If HEIF wasn't such a kitchen sink, this
+            // would have been the only data this file needs.
+            mdat: MdatBox {
+                data_chunks,
+            },
+        }
+    }
+
+    #[must_use] pub fn to_vec(&self, color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
+        let mut out = Vec::with_capacity(color_av1_data.len() + alpha_av1_data.map_or(0, |a| a.len()) + 410);
+        self.write(&mut out, color_av1_data, alpha_av1_data, width, height, depth_bits).unwrap(); // Vec can't fail
+        out
+    }
+}
+
+/// See [`serialize`] for description. This one makes a `Vec` instead of using `io::Write`.
+#[must_use] pub fn serialize_to_vec(color_av1_data: &[u8], alpha_av1_data: Option<&[u8]>, width: u32, height: u32, depth_bits: u8) -> Vec<u8> {
+    Aviffy::new().to_vec(color_av1_data, alpha_av1_data, width, height, depth_bits)
+}
+
+#[test]
+fn test_roundtrip_parse_mp4() {
+    let test_img = b"av12356abc";
+    let avif = serialize_to_vec(test_img, None, 10, 20, 8);
+
+    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
+
+    assert_eq!(&test_img[..], ctx.primary_item_coded_data());
+}
+
+#[test]
+fn test_roundtrip_parse_mp4_alpha() {
+    let test_img = b"av12356abc";
+    let test_a = b"alpha";
+    let avif = serialize_to_vec(test_img, Some(test_a), 10, 20, 8);
+
+    let ctx = mp4parse::read_avif(&mut avif.as_slice(), mp4parse::ParseStrictness::Normal).unwrap();
+
+    assert_eq!(&test_img[..], ctx.primary_item_coded_data());
+    assert_eq!(&test_a[..], ctx.alpha_item_coded_data());
+}
+
+#[test]
+fn test_roundtrip_parse_avif() {
+    let test_img = [1,2,3,4,5,6];
+    let test_alpha = [77,88,99];
+    let avif = serialize_to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
+
+    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
+
+    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
+    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
+}
+
+#[test]
+fn test_roundtrip_parse_avif_colr() {
+    let test_img = [1,2,3,4,5,6];
+    let test_alpha = [77,88,99];
+    let avif = Aviffy::new()
+        .matrix_coefficients(constants::MatrixCoefficients::Bt709)
+        .to_vec(&test_img, Some(&test_alpha), 10, 20, 8);
+
+    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
+
+    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
+    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
+}
+
+#[test]
+fn premultiplied_flag() {
+    let test_img = [1,2,3,4];
+    let test_alpha = [55,66,77,88,99];
+    let avif = Aviffy::new().premultiplied_alpha(true).to_vec(&test_img, Some(&test_alpha), 5, 5, 8);
+
+    let ctx = avif_parse::read_avif(&mut avif.as_slice()).unwrap();
+
+    assert!(ctx.premultiplied_alpha);
+    assert_eq!(&test_img[..], ctx.primary_item.as_slice());
+    assert_eq!(&test_alpha[..], ctx.alpha_item.as_deref().unwrap());
+}
diff --git a/src/writer.rs b/src/writer.rs
new file mode 100644 (file)
index 0000000..6ec1321
--- /dev/null
@@ -0,0 +1,117 @@
+use std::convert::Infallible;
+use std::convert::TryFrom;
+use std::io;
+
+pub trait WriterBackend {
+    type Error;
+    fn extend_from_slice(&mut self, data: &[u8]) -> Result<(), Self::Error>;
+}
+
+/// `io::Write` generates bloated code (with backtrace for every byte written),
+/// so small boxes are written infallibly.
+impl WriterBackend for Vec<u8> {
+    type Error = Infallible;
+    #[inline(always)]
+    fn extend_from_slice(&mut self, data: &[u8]) -> Result<(), Infallible> {
+        self.extend_from_slice(data);
+        Ok(())
+    }
+}
+
+pub struct IO<W>(pub W);
+
+impl<W: io::Write> WriterBackend for IO<W> {
+    type Error = io::Error;
+    #[inline(always)]
+    fn extend_from_slice(&mut self, data: &[u8]) -> io::Result<()> {
+        self.0.write_all(data)
+    }
+}
+
+pub struct Writer<'p, 'w, B> {
+    parent: Option<&'p mut usize>,
+    left: Option<usize>,
+    out: &'w mut B,
+}
+
+impl<'w, B> Writer<'static, 'w, B> {
+    #[inline]
+    pub fn new(out: &'w mut B) -> Self {
+        Self {
+            parent: None,
+            left: None,
+            out,
+        }
+    }
+}
+
+impl<'p, 'w, B: WriterBackend> Writer<'p, 'w, B> {
+    #[inline]
+    pub fn new_box(&mut self, len: usize) -> Writer<'_, '_, B> {
+        Writer {
+            parent: match &mut self.left {
+                Some(l) => Some(l),
+                None => None,
+            },
+            left: Some(len),
+            out: self.out,
+        }
+    }
+
+    #[inline(always)]
+    pub fn full_box(&mut self, typ: [u8; 4], version: u8) -> Result<(), B::Error> {
+        self.basic_box(typ)?;
+        self.push(&[version, 0, 0, 0])
+    }
+
+    #[inline]
+    pub fn basic_box(&mut self, typ: [u8; 4]) -> Result<(), B::Error> {
+        let len = self.left.unwrap();
+        if let Some(parent) = &mut self.parent {
+            **parent -= len;
+        }
+        match u32::try_from(len) {
+            Ok(len) => self.u32(len)?,
+            Err(_) => {
+                self.u32(1)?;
+                self.u64(len as u64)?;
+            }
+        }
+        self.push(&typ)
+    }
+
+    #[inline(always)]
+    pub fn push(&mut self, data: &[u8]) -> Result<(), B::Error> {
+        *self.left.as_mut().unwrap() -= data.len();
+        self.out.extend_from_slice(data)
+    }
+
+    #[inline(always)]
+    pub fn u8(&mut self, val: u8) -> Result<(), B::Error> {
+        self.push(std::slice::from_ref(&val))
+    }
+
+    #[inline(always)]
+    pub fn u16(&mut self, val: u16) -> Result<(), B::Error> {
+        self.push(&val.to_be_bytes())
+    }
+
+    #[inline(always)]
+    pub fn u32(&mut self, val: u32) -> Result<(), B::Error> {
+        self.push(&val.to_be_bytes())
+    }
+
+    #[inline(always)]
+    pub fn u64(&mut self, val: u64) -> Result<(), B::Error> {
+        self.push(&val.to_be_bytes())
+    }
+}
+
+#[cfg(debug_assertions)]
+impl<B> Drop for Writer<'_, '_, B> {
+    fn drop(&mut self) {
+        if let Some(unwritten_bytes) = self.left {
+            assert_eq!(0, unwritten_bytes);
+        }
+    }
+}