diff options
author | DongHun Kwak <dh0128.kwak@samsung.com> | 2023-04-06 16:21:12 +0900 |
---|---|---|
committer | DongHun Kwak <dh0128.kwak@samsung.com> | 2023-04-06 16:21:12 +0900 |
commit | 28e1baacc2f4528665d47e8b556a26f164b6cdda (patch) | |
tree | 6a3242bf696050f2a1ee0a61f3b7559e47fab502 | |
download | rust-avif-serialize-28e1baacc2f4528665d47e8b556a26f164b6cdda.tar.gz rust-avif-serialize-28e1baacc2f4528665d47e8b556a26f164b6cdda.tar.bz2 rust-avif-serialize-28e1baacc2f4528665d47e8b556a26f164b6cdda.zip |
Import avif-serialize 0.8.1upstream/0.8.1upstream
-rw-r--r-- | .cargo_vcs_info.json | 6 | ||||
-rw-r--r-- | Cargo.lock | 130 | ||||
-rw-r--r-- | Cargo.toml | 47 | ||||
-rw-r--r-- | Cargo.toml.orig | 23 | ||||
-rw-r--r-- | LICENSE | 29 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rw-r--r-- | src/boxes.rs | 645 | ||||
-rw-r--r-- | src/constants.rs | 54 | ||||
-rw-r--r-- | src/lib.rs | 348 | ||||
-rw-r--r-- | src/writer.rs | 117 |
10 files changed, 1420 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..ca33e0f --- /dev/null +++ b/.cargo_vcs_info.json @@ -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 index 0000000..e1ed2a8 --- /dev/null +++ b/Cargo.lock @@ -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 index 0000000..d7aed66 --- /dev/null +++ b/Cargo.toml @@ -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 index 0000000..35c2df0 --- /dev/null +++ b/Cargo.toml.orig @@ -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" } @@ -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 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 index 0000000..9fdb328 --- /dev/null +++ b/src/boxes.rs @@ -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 index 0000000..22428ed --- /dev/null +++ b/src/constants.rs @@ -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 index 0000000..ab0839b --- /dev/null +++ b/src/lib.rs @@ -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 index 0000000..6ec1321 --- /dev/null +++ b/src/writer.rs @@ -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); + } + } +} |