use crate::{error::EncodeError, hevc::NaluStream};
fn w32(buf: &mut Vec<u8>, v: u32) {
buf.extend_from_slice(&v.to_be_bytes());
}
fn w16(buf: &mut Vec<u8>, v: u16) {
buf.extend_from_slice(&v.to_be_bytes());
}
fn write_fullbox(buf: &mut Vec<u8>, cc: &[u8; 4], ver: u8, flags: u32) {
w32(buf, 0); buf.extend_from_slice(cc);
buf.push(ver);
buf.push((flags >> 16) as u8);
buf.push((flags >> 8) as u8);
buf.push(flags as u8);
}
fn write_box(buf: &mut Vec<u8>, cc: &[u8; 4]) {
w32(buf, 0);
buf.extend_from_slice(cc);
}
fn patch(buf: &mut [u8], start: usize) {
let size = (buf.len() - start) as u32;
buf[start..start + 4].copy_from_slice(&size.to_be_bytes());
}
fn write_colr(f: &mut Vec<u8>, color: &crate::color::ColorMetadata) {
use crate::color::ColorMetadata;
let sh = f.len();
write_box(f, b"colr");
match color {
ColorMetadata::Cicp(enc) => f.extend_from_slice(&enc.nclx_payload()),
ColorMetadata::Icc(icc) => {
f.extend_from_slice(b"prof");
f.extend_from_slice(icc);
}
}
patch(f, sh);
}
pub(crate) fn wrap_hevc_image_with_alpha(
color: &NaluStream,
alpha: &NaluStream,
width: u32,
height: u32,
bit_depth: crate::fmt::BitDepth,
color_meta: &crate::color::ColorMetadata,
metadata: &crate::metadata::Metadata,
) -> Result<Vec<u8>, EncodeError> {
let color_sample = color.to_length_prefixed_slices();
let alpha_sample = alpha.to_length_prefixed_slices();
let _color_hvcc = build_hvcc(color, bit_depth.bits())?;
let _alpha_hvcc = build_hvcc(alpha, bit_depth.bits())?;
const ALPHA_URN: &[u8] = b"urn:mpeg:hevc:2015:auxid:1\0";
let mut f: Vec<u8> = Vec::new();
let s = f.len();
write_box(&mut f, b"ftyp");
f.extend_from_slice(b"heic");
w32(&mut f, 0);
f.extend_from_slice(b"heic");
f.extend_from_slice(b"mif1");
f.extend_from_slice(b"miaf");
f.extend_from_slice(b"iso8");
patch(&mut f, s);
let meta_start = f.len();
write_fullbox(&mut f, b"meta", 0, 0);
{
let s = f.len();
write_fullbox(&mut f, b"hdlr", 0, 0);
w32(&mut f, 0);
f.extend_from_slice(b"pict");
w32(&mut f, 0);
w32(&mut f, 0);
w32(&mut f, 0);
f.push(0);
patch(&mut f, s);
}
{
let s = f.len();
write_fullbox(&mut f, b"pitm", 0, 0);
w16(&mut f, 1);
patch(&mut f, s);
}
let color_offset_patch_pos;
let alpha_offset_patch_pos;
{
let s = f.len();
write_fullbox(&mut f, b"iloc", 1, 0);
f.push(0x44); f.push(0x00); w16(&mut f, 2); w16(&mut f, 1);
w16(&mut f, 0); w16(&mut f, 0); w16(&mut f, 1); color_offset_patch_pos = f.len();
w32(&mut f, 0);
w32(&mut f, color_sample.len() as u32);
w16(&mut f, 2);
w16(&mut f, 0);
w16(&mut f, 0);
w16(&mut f, 1);
alpha_offset_patch_pos = f.len();
w32(&mut f, 0);
w32(&mut f, alpha_sample.len() as u32);
patch(&mut f, s);
}
{
let s = f.len();
write_fullbox(&mut f, b"iinf", 0, 0);
w16(&mut f, 2); for id in [1u16, 2u16] {
let si = f.len();
write_fullbox(&mut f, b"infe", 2, 0);
w16(&mut f, id);
w16(&mut f, 0);
f.extend_from_slice(b"hvc1");
f.push(0); patch(&mut f, si);
}
patch(&mut f, s);
}
{
let s = f.len();
write_fullbox(&mut f, b"iref", 0, 0);
{
let sr = f.len();
write_box(&mut f, b"auxl");
w16(&mut f, 2); w16(&mut f, 1); w16(&mut f, 1); patch(&mut f, sr);
}
patch(&mut f, s);
}
{
let s = f.len();
write_box(&mut f, b"iprp");
let mut irot_idx = 0u8;
let mut imir_idx = 0u8;
let mut clli_idx = 0u8;
{
let si = f.len();
write_box(&mut f, b"ipco");
{
let sh = f.len();
write_box(&mut f, b"hvcC");
f.extend_from_slice(&_color_hvcc);
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"ispe", 0, 0);
w32(&mut f, width);
w32(&mut f, height);
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"pixi", 0, 0);
f.push(3);
f.push(bit_depth.bits());
f.push(bit_depth.bits());
f.push(bit_depth.bits());
patch(&mut f, sh);
}
write_colr(&mut f, color_meta);
{
let sh = f.len();
write_box(&mut f, b"hvcC");
f.extend_from_slice(&_alpha_hvcc);
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"pixi", 0, 0);
f.push(1);
f.push(bit_depth.bits());
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"auxC", 0, 0);
f.extend_from_slice(ALPHA_URN); patch(&mut f, sh);
}
let mut next_prop: u8 = 8;
if metadata.orientation.irot_steps() != 0 {
let sh = f.len();
write_box(&mut f, b"irot");
f.push(metadata.orientation.irot_steps() & 0x03);
patch(&mut f, sh);
irot_idx = next_prop;
next_prop += 1;
}
if let Some(horizontal_axis) = metadata.orientation.imir_axis() {
let sh = f.len();
write_box(&mut f, b"imir");
f.push(if horizontal_axis { 1 } else { 0 });
patch(&mut f, sh);
imir_idx = next_prop;
next_prop += 1;
}
if let Some(cll) = metadata.content_light_level {
let sh = f.len();
write_box(&mut f, b"clli");
f.extend_from_slice(&cll.clli_payload());
patch(&mut f, sh);
clli_idx = next_prop;
next_prop += 1;
}
let _ = next_prop;
patch(&mut f, si);
}
{
let si = f.len();
write_fullbox(&mut f, b"ipma", 0, 0);
w32(&mut f, 2); let mut c_assoc: Vec<u8> = vec![0x80 | 1, 2, 3, 4];
if irot_idx != 0 {
c_assoc.push(0x80 | irot_idx);
}
if imir_idx != 0 {
c_assoc.push(0x80 | imir_idx);
}
if clli_idx != 0 {
c_assoc.push(clli_idx);
}
w16(&mut f, 1);
f.push(c_assoc.len() as u8);
f.extend_from_slice(&c_assoc);
w16(&mut f, 2);
f.push(4);
f.push(0x80 | 5);
f.push(2);
f.push(6);
f.push(7);
patch(&mut f, si);
}
patch(&mut f, s);
}
patch(&mut f, meta_start);
let mdat_start = f.len();
write_box(&mut f, b"mdat");
let color_abs = f.len() as u32;
f.extend_from_slice(&color_sample);
let alpha_abs = f.len() as u32;
f.extend_from_slice(&alpha_sample);
patch(&mut f, mdat_start);
f[color_offset_patch_pos..color_offset_patch_pos + 4].copy_from_slice(&color_abs.to_be_bytes());
f[alpha_offset_patch_pos..alpha_offset_patch_pos + 4].copy_from_slice(&alpha_abs.to_be_bytes());
Ok(f)
}
pub(crate) fn wrap_hevc_image(
stream: &NaluStream,
width: u32,
height: u32,
bit_depth: crate::fmt::BitDepth,
color_meta: &crate::color::ColorMetadata,
metadata: &crate::metadata::Metadata,
) -> Result<Vec<u8>, EncodeError> {
let hevc_sample = stream.to_length_prefixed_slices();
let hvcc_data = build_hvcc(stream, bit_depth.bits())?;
let mut f: Vec<u8> = Vec::new();
let s = f.len();
write_box(&mut f, b"ftyp");
f.extend_from_slice(b"heic"); w32(&mut f, 0); f.extend_from_slice(b"heic"); f.extend_from_slice(b"mif1"); f.extend_from_slice(b"miaf"); f.extend_from_slice(b"iso8"); patch(&mut f, s);
let meta_start = f.len();
write_fullbox(&mut f, b"meta", 0, 0);
{
let s = f.len();
write_fullbox(&mut f, b"hdlr", 0, 0);
w32(&mut f, 0); f.extend_from_slice(b"pict"); w32(&mut f, 0);
w32(&mut f, 0);
w32(&mut f, 0); f.push(0); patch(&mut f, s);
}
{
let s = f.len();
write_fullbox(&mut f, b"pitm", 0, 0);
w16(&mut f, 1); patch(&mut f, s);
}
let has_exif = metadata.exif.is_some();
let exif_payload: Vec<u8> = metadata
.exif
.as_ref()
.map(|e| {
let mut p = Vec::with_capacity(e.len() + 4);
p.extend_from_slice(&0u32.to_be_bytes()); p.extend_from_slice(e);
p
})
.unwrap_or_default();
let iloc_offset_patch_pos;
let mut iloc_exif_patch_pos = 0usize;
{
let s = f.len();
write_fullbox(&mut f, b"iloc", 1, 0);
f.push(0x44); f.push(0x00); w16(&mut f, if has_exif { 2 } else { 1 }); w16(&mut f, 1); w16(&mut f, 0); w16(&mut f, 0); w16(&mut f, 1); iloc_offset_patch_pos = f.len();
w32(&mut f, 0); w32(&mut f, hevc_sample.len() as u32); if has_exif {
w16(&mut f, 2); w16(&mut f, 0); w16(&mut f, 0); w16(&mut f, 1); iloc_exif_patch_pos = f.len();
w32(&mut f, 0); w32(&mut f, exif_payload.len() as u32); }
patch(&mut f, s);
}
{
let s = f.len();
write_fullbox(&mut f, b"iinf", 0, 0);
w16(&mut f, if has_exif { 2 } else { 1 }); {
let si = f.len();
write_fullbox(&mut f, b"infe", 2, 0);
w16(&mut f, 1); w16(&mut f, 0); f.extend_from_slice(b"hvc1"); f.push(0); patch(&mut f, si);
}
if has_exif {
let si = f.len();
write_fullbox(&mut f, b"infe", 2, 0);
w16(&mut f, 2); w16(&mut f, 0); f.extend_from_slice(b"Exif"); f.push(0); patch(&mut f, si);
}
patch(&mut f, s);
}
if has_exif {
let s = f.len();
write_fullbox(&mut f, b"iref", 0, 0);
{
let si = f.len();
write_box(&mut f, b"cdsc");
w16(&mut f, 2); w16(&mut f, 1); w16(&mut f, 1); patch(&mut f, si);
}
patch(&mut f, s);
}
{
let extra_props: (u8, u8, u8);
let s = f.len();
write_box(&mut f, b"iprp");
{
let si = f.len();
write_box(&mut f, b"ipco");
{
let sh = f.len();
write_box(&mut f, b"hvcC");
f.extend_from_slice(&hvcc_data);
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"ispe", 0, 0);
w32(&mut f, width);
w32(&mut f, height);
patch(&mut f, sh);
}
{
let sh = f.len();
write_fullbox(&mut f, b"pixi", 0, 0);
f.push(3); f.push(bit_depth.bits()); f.push(bit_depth.bits()); f.push(bit_depth.bits()); patch(&mut f, sh);
}
write_colr(&mut f, color_meta);
let mut next_prop: u8 = 5;
let mut irot_idx = 0u8;
let mut imir_idx = 0u8;
let mut clli_idx = 0u8;
if metadata.orientation.irot_steps() != 0 {
let sh = f.len();
write_box(&mut f, b"irot");
f.push(metadata.orientation.irot_steps() & 0x03);
patch(&mut f, sh);
irot_idx = next_prop;
next_prop += 1;
}
if let Some(horizontal_axis) = metadata.orientation.imir_axis() {
let sh = f.len();
write_box(&mut f, b"imir");
f.push(if horizontal_axis { 1 } else { 0 });
patch(&mut f, sh);
imir_idx = next_prop;
next_prop += 1;
}
if let Some(cll) = metadata.content_light_level {
let sh = f.len();
write_box(&mut f, b"clli");
f.extend_from_slice(&cll.clli_payload());
patch(&mut f, sh);
clli_idx = next_prop;
next_prop += 1;
}
let _ = next_prop;
patch(&mut f, si);
extra_props = (irot_idx, imir_idx, clli_idx);
}
{
let (irot_idx, imir_idx, clli_idx) = extra_props;
let mut assoc: Vec<u8> = vec![0x80 | 1, 2, 3, 4]; if irot_idx != 0 {
assoc.push(0x80 | irot_idx);
}
if imir_idx != 0 {
assoc.push(0x80 | imir_idx);
}
if clli_idx != 0 {
assoc.push(clli_idx);
} let si = f.len();
write_fullbox(&mut f, b"ipma", 0, 0);
w32(&mut f, 1); w16(&mut f, 1); f.push(assoc.len() as u8); f.extend_from_slice(&assoc);
patch(&mut f, si);
}
patch(&mut f, s);
}
patch(&mut f, meta_start);
let mdat_start = f.len();
write_box(&mut f, b"mdat");
let hevc_absolute_offset = f.len() as u32; f.extend_from_slice(&hevc_sample);
let exif_absolute_offset = f.len() as u32; if has_exif {
f.extend_from_slice(&exif_payload);
}
patch(&mut f, mdat_start);
f[iloc_offset_patch_pos..iloc_offset_patch_pos + 4]
.copy_from_slice(&hevc_absolute_offset.to_be_bytes());
if has_exif {
f[iloc_exif_patch_pos..iloc_exif_patch_pos + 4]
.copy_from_slice(&exif_absolute_offset.to_be_bytes());
}
Ok(f)
}
fn sps_chroma_format_idc(sps: Option<&[u8]>) -> Option<u8> {
let s = sps?;
let start_bit = 15 * 8;
let mut pos = start_bit;
let get_bit = |p: usize| -> u32 {
if p / 8 >= s.len() {
return 0;
}
((s[p / 8] >> (7 - (p % 8))) & 1) as u32
};
let read_ue = |pos: &mut usize| -> u32 {
let mut zeros = 0;
while *pos < s.len() * 8 && get_bit(*pos) == 0 {
zeros += 1;
*pos += 1;
}
*pos += 1; if zeros == 0 {
return 0;
}
let mut val = 0u32;
for _ in 0..zeros {
val = (val << 1) | get_bit(*pos);
*pos += 1;
}
(1 << zeros) - 1 + val
};
let _sps_id = read_ue(&mut pos);
let cfi = read_ue(&mut pos);
Some(cfi as u8)
}
fn build_hvcc(stream: &NaluStream, bit_depth: u8) -> Result<Vec<u8>, EncodeError> {
let mut vps: Vec<&[u8]> = Vec::new();
let mut sps: Vec<&[u8]> = Vec::new();
let mut pps: Vec<&[u8]> = Vec::new();
for nalu in &stream.nalus {
match (nalu.data[0] >> 1) & 0x3f {
32 => vps.push(&nalu.data),
33 => sps.push(&nalu.data),
34 => pps.push(&nalu.data),
_ => {}
}
}
let mut r: Vec<u8> = Vec::new();
r.push(1);
let (profile_byte, compat, constraint6, level) = if let Some(s) = sps.first() {
if s.len() >= 15 {
let mut compat = [0u8; 4];
compat.copy_from_slice(&s[4..8]);
let mut c = [0u8; 6];
c.copy_from_slice(&s[8..14]);
(s[3], compat, c, s[14])
} else {
(0b0000_0100, [0x00, 0, 0, 0], [0x90, 0, 0, 0, 0, 0], 93)
}
} else {
(0b0000_0100, [0x00, 0, 0, 0], [0x90, 0, 0, 0, 0, 0], 93)
};
r.push(profile_byte); r.extend_from_slice(&compat); r.extend_from_slice(&constraint6); r.push(level);
r.push(0xF0);
r.push(0x00); r.push(0xFC); let chroma_idc = sps_chroma_format_idc(sps.first().copied()).unwrap_or(1);
r.push(0xFC | (chroma_idc & 0x3));
r.push(0xF8 | (bit_depth - 8)); r.push(0xF8 | (bit_depth - 8)); r.push(0x00);
r.push(0x00); r.push(0b0000_1111);
let arrays: &[(u8, &Vec<&[u8]>)] = &[(32, &vps), (33, &sps), (34, &pps)];
r.push(arrays.len() as u8);
for &(nal_type, list) in arrays {
r.push(nal_type);
r.push((list.len() >> 8) as u8);
r.push(list.len() as u8);
for &d in list {
r.push((d.len() >> 8) as u8);
r.push(d.len() as u8);
r.extend_from_slice(d);
}
}
Ok(r)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hevc::NaluStream;
fn make_test_stream() -> NaluStream {
use crate::hevc::{build_pps, build_sps, build_vps};
NaluStream {
nalus: vec![
build_vps(
16,
16,
crate::fmt::ChromaFormat::Yuv420,
crate::fmt::BitDepth::Eight,
),
build_sps(
16,
16,
crate::fmt::ChromaFormat::Yuv420,
crate::fmt::BitDepth::Eight,
),
build_pps(30),
],
}
}
#[test]
fn ftyp_brand() {
let b = wrap_hevc_image(
&make_test_stream(),
16,
16,
crate::fmt::BitDepth::Eight,
&crate::color::ColorMetadata::default(),
&crate::metadata::Metadata::default(),
)
.unwrap();
assert_eq!(&b[4..8], b"ftyp");
assert_eq!(&b[8..12], b"heic");
}
#[test]
fn box_order_ftyp_meta_mdat() {
let b = wrap_hevc_image(
&make_test_stream(),
16,
16,
crate::fmt::BitDepth::Eight,
&crate::color::ColorMetadata::default(),
&crate::metadata::Metadata::default(),
)
.unwrap();
let ftyp_size = u32::from_be_bytes(b[0..4].try_into().unwrap()) as usize;
assert_eq!(
&b[ftyp_size + 4..ftyp_size + 8],
b"meta",
"meta must follow ftyp"
);
let meta_size =
u32::from_be_bytes(b[ftyp_size..ftyp_size + 4].try_into().unwrap()) as usize;
assert_eq!(
&b[ftyp_size + meta_size + 4..ftyp_size + meta_size + 8],
b"mdat",
"mdat must follow meta"
);
}
#[test]
fn iloc_offset_points_into_mdat() {
let b = wrap_hevc_image(
&make_test_stream(),
16,
16,
crate::fmt::BitDepth::Eight,
&crate::color::ColorMetadata::default(),
&crate::metadata::Metadata::default(),
)
.unwrap();
let mut pos = 0usize;
let mut mdat_payload_offset = 0u32;
while pos + 8 <= b.len() {
let sz = u32::from_be_bytes(b[pos..pos + 4].try_into().unwrap()) as usize;
if &b[pos + 4..pos + 8] == b"mdat" {
mdat_payload_offset = (pos + 8) as u32;
break;
}
pos += sz;
}
assert!(mdat_payload_offset > 0, "mdat not found");
let iloc_box_pos = b.windows(4).position(|w| w == b"iloc").unwrap() - 4;
let offset_pos = iloc_box_pos + 12 + 4 + 8;
let extent_offset = u32::from_be_bytes(b[offset_pos..offset_pos + 4].try_into().unwrap());
assert_eq!(
extent_offset, mdat_payload_offset,
"iloc extent_offset must point to start of mdat payload"
);
}
#[test]
fn alpha_container_structure() {
let color = make_test_stream();
let alpha = make_test_stream();
let b = wrap_hevc_image_with_alpha(
&color,
&alpha,
16,
16,
crate::fmt::BitDepth::Eight,
&crate::color::ColorMetadata::default(),
&crate::metadata::Metadata::default(),
)
.unwrap();
let s = b.as_slice();
assert!(s.windows(4).any(|w| w == b"iref"), "iref box required");
assert!(
s.windows(4).any(|w| w == b"auxl"),
"auxl reference required"
);
assert!(s.windows(4).any(|w| w == b"auxC"), "auxC property required");
assert!(
s.windows(26).any(|w| w == b"urn:mpeg:hevc:2015:auxid:1"),
"alpha URN required"
);
let ipma_pos = s.windows(4).position(|w| w == b"ipma").unwrap();
let entry_count = u32::from_be_bytes(s[ipma_pos + 8..ipma_pos + 12].try_into().unwrap());
assert_eq!(entry_count, 2, "ipma must have 2 entries (color + alpha)");
}
}