use crate::error::{AvifError as Error, Result};
use crate::meta::Meta;
pub const ITEM_TYPE_RGAN: [u8; 4] = *b"rgan";
pub const ITEM_TYPE_MSKI: [u8; 4] = *b"mski";
pub const REF_TYPE_DRGN: [u8; 4] = *b"drgn";
#[doc(hidden)]
pub const ITEM_TYPE_IDEN: [u8; 4] = *b"iden";
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RegionGeometry {
Point { x: i32, y: i32 },
Rectangle {
x: i32,
y: i32,
width: u32,
height: u32,
},
Ellipse {
x: i32,
y: i32,
radius_x: u32,
radius_y: u32,
},
Polygon { points: Vec<(i32, i32)> },
Polyline { points: Vec<(i32, i32)> },
ReferencedMask {
x: i32,
y: i32,
width: u32,
height: u32,
mask_ref_idx: Option<u32>,
},
InlineMask {
x: i32,
y: i32,
width: u32,
height: u32,
mask_coding_method: u8,
mask_coding_parameters: Option<u32>,
data: Vec<u8>,
},
}
impl RegionGeometry {
pub fn geometry_type(&self) -> u8 {
match self {
RegionGeometry::Point { .. } => 0,
RegionGeometry::Rectangle { .. } => 1,
RegionGeometry::Ellipse { .. } => 2,
RegionGeometry::Polygon { .. } => 3,
RegionGeometry::ReferencedMask { .. } => 4,
RegionGeometry::InlineMask { .. } => 5,
RegionGeometry::Polyline { .. } => 6,
}
}
pub fn is_mask(&self) -> bool {
matches!(
self,
RegionGeometry::ReferencedMask { .. } | RegionGeometry::InlineMask { .. }
)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegionItem {
pub flags: u8,
pub reference_width: u32,
pub reference_height: u32,
pub regions: Vec<RegionGeometry>,
}
impl RegionItem {
#[allow(clippy::too_many_arguments)]
pub fn new(
flags: u8,
reference_width: u32,
reference_height: u32,
regions: Vec<RegionGeometry>,
) -> Self {
Self {
flags,
reference_width,
reference_height,
regions,
}
}
}
impl RegionItem {
pub const FLAG_LARGE_FIELD_SIZE: u8 = 0x01;
pub fn is_large_field_size(&self) -> bool {
self.flags & Self::FLAG_LARGE_FIELD_SIZE != 0
}
pub fn region_count(&self) -> usize {
self.regions.len()
}
pub fn parse(payload: &[u8]) -> Result<Self> {
let mut cur = Cursor::new(payload);
let version = cur.u8()?;
if version != 0 {
return Err(Error::unsupported(format!(
"avif region item: version {version} != 0"
)));
}
let flags = cur.u8()?;
let large = flags & Self::FLAG_LARGE_FIELD_SIZE != 0;
let reference_width = cur.field_u(large)?;
let reference_height = cur.field_u(large)?;
let region_count = cur.u8()? as usize;
let mut regions = Vec::with_capacity(region_count);
for _ in 0..region_count {
regions.push(parse_geometry(&mut cur, large, false)?);
}
Ok(RegionItem {
flags,
reference_width,
reference_height,
regions,
})
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DerivedRegionItem {
pub derived_item_id: u32,
pub source_region_item_id: Option<u32>,
pub drgn_reference_count: usize,
pub drgn_iref_count: usize,
pub has_item_body: bool,
}
impl DerivedRegionItem {
#[allow(clippy::too_many_arguments)]
pub fn new(
derived_item_id: u32,
source_region_item_id: Option<u32>,
drgn_reference_count: usize,
drgn_iref_count: usize,
has_item_body: bool,
) -> Self {
Self {
derived_item_id,
source_region_item_id,
drgn_reference_count,
drgn_iref_count,
has_item_body,
}
}
}
impl DerivedRegionItem {
pub fn is_compliant(&self) -> bool {
self.drgn_reference_count == 1 && self.drgn_iref_count == 1 && !self.has_item_body
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.drgn_reference_count != 1 {
out.push("drgn-reference-count-eq-1");
}
if self.drgn_iref_count != 1 {
out.push("drgn-iref-count-eq-1");
}
if self.has_item_body {
out.push("no-item-body");
}
out
}
}
pub fn resolve_derived_region_items(meta: &Meta) -> Vec<DerivedRegionItem> {
let mut out = Vec::new();
for item in &meta.items {
if item.item_type != ITEM_TYPE_IDEN {
continue;
}
let drgn_iref_count = meta
.irefs
.iter()
.filter(|e| e.reference_type == REF_TYPE_DRGN && e.from_id == item.id)
.count();
if drgn_iref_count == 0 {
continue;
}
let inputs = meta.iref_targets_of(&REF_TYPE_DRGN, item.id);
let drgn_reference_count = inputs.len();
let source_region_item_id = if drgn_reference_count == 1 {
Some(inputs[0])
} else {
None
};
let has_item_body = meta
.location_by_id(item.id)
.map(|loc| loc.extents.iter().any(|x| x.length > 0))
.unwrap_or(false);
out.push(DerivedRegionItem {
derived_item_id: item.id,
source_region_item_id,
drgn_reference_count,
drgn_iref_count,
has_item_body,
});
}
out
}
fn parse_geometry(
cur: &mut Cursor<'_>,
large: bool,
is_region_track: bool,
) -> Result<RegionGeometry> {
let geometry_type = cur.u8()?;
match geometry_type {
0 => Ok(RegionGeometry::Point {
x: cur.field_i(large)?,
y: cur.field_i(large)?,
}),
1 => Ok(RegionGeometry::Rectangle {
x: cur.field_i(large)?,
y: cur.field_i(large)?,
width: cur.field_u(large)?,
height: cur.field_u(large)?,
}),
2 => Ok(RegionGeometry::Ellipse {
x: cur.field_i(large)?,
y: cur.field_i(large)?,
radius_x: cur.field_u(large)?,
radius_y: cur.field_u(large)?,
}),
3 | 6 => {
let point_count = cur.field_u(large)? as usize;
let mut points = Vec::with_capacity(point_count);
for _ in 0..point_count {
let px = cur.field_i(large)?;
let py = cur.field_i(large)?;
points.push((px, py));
}
if geometry_type == 3 {
Ok(RegionGeometry::Polygon { points })
} else {
Ok(RegionGeometry::Polyline { points })
}
}
4 => {
let x = cur.field_i(large)?;
let y = cur.field_i(large)?;
let width = cur.field_u(large)?;
let height = cur.field_u(large)?;
let mask_ref_idx = if is_region_track {
Some(cur.field_u(large)?)
} else {
None
};
Ok(RegionGeometry::ReferencedMask {
x,
y,
width,
height,
mask_ref_idx,
})
}
5 => {
let x = cur.field_i(large)?;
let y = cur.field_i(large)?;
let width = cur.field_u(large)?;
let height = cur.field_u(large)?;
let mask_coding_method = cur.u8()?;
let mask_coding_parameters = if mask_coding_method != 0 {
Some(cur.u32()?)
} else {
None
};
let data = cur.rest().to_vec();
Ok(RegionGeometry::InlineMask {
x,
y,
width,
height,
mask_coding_method,
mask_coding_parameters,
data,
})
}
other => Err(Error::unsupported(format!(
"avif region geometry: reserved geometry_type {other}"
))),
}
}
struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(buf: &'a [u8]) -> Self {
Cursor { buf, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.pos
.checked_add(n)
.ok_or_else(|| Error::invalid("avif region item: offset overflow"))?;
if end > self.buf.len() {
return Err(Error::invalid(format!(
"avif region item: truncated read ({end} > {})",
self.buf.len()
)));
}
let s = &self.buf[self.pos..end];
self.pos = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
fn u32(&mut self) -> Result<u32> {
let s = self.take(4)?;
Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
}
fn field_u(&mut self, large: bool) -> Result<u32> {
if large {
self.u32()
} else {
let s = self.take(2)?;
Ok(u32::from(u16::from_be_bytes([s[0], s[1]])))
}
}
fn field_i(&mut self, large: bool) -> Result<i32> {
if large {
Ok(self.u32()? as i32)
} else {
let s = self.take(2)?;
Ok(i32::from(i16::from_be_bytes([s[0], s[1]])))
}
}
fn rest(&self) -> &'a [u8] {
&self.buf[self.pos..]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn region_item_body(flags: u8, ref_w: u16, ref_h: u16, geoms: &[Vec<u8>]) -> Vec<u8> {
let mut buf = vec![0u8, flags];
let large = flags & 1 != 0;
let push_u = |buf: &mut Vec<u8>, v: u32| {
if large {
buf.extend_from_slice(&v.to_be_bytes());
} else {
buf.extend_from_slice(&(v as u16).to_be_bytes());
}
};
push_u(&mut buf, ref_w as u32);
push_u(&mut buf, ref_h as u32);
buf.push(geoms.len() as u8);
for g in geoms {
buf.extend_from_slice(g);
}
buf
}
fn i16b(v: i16) -> [u8; 2] {
v.to_be_bytes()
}
fn u16b(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
#[test]
fn parses_point_rectangle_ellipse() {
let mut point = vec![0u8]; point.extend_from_slice(&i16b(-5));
point.extend_from_slice(&i16b(7));
let mut rect = vec![1u8]; rect.extend_from_slice(&i16b(10));
rect.extend_from_slice(&i16b(20));
rect.extend_from_slice(&u16b(30));
rect.extend_from_slice(&u16b(40));
let mut ell = vec![2u8]; ell.extend_from_slice(&i16b(50));
ell.extend_from_slice(&i16b(60));
ell.extend_from_slice(&u16b(8));
ell.extend_from_slice(&u16b(9));
let body = region_item_body(0, 1920, 1080, &[point, rect, ell]);
let ri = RegionItem::parse(&body).unwrap();
assert!(!ri.is_large_field_size());
assert_eq!(ri.reference_width, 1920);
assert_eq!(ri.reference_height, 1080);
assert_eq!(ri.region_count(), 3);
assert_eq!(ri.regions[0], RegionGeometry::Point { x: -5, y: 7 });
assert_eq!(
ri.regions[1],
RegionGeometry::Rectangle {
x: 10,
y: 20,
width: 30,
height: 40
}
);
assert_eq!(
ri.regions[2],
RegionGeometry::Ellipse {
x: 50,
y: 60,
radius_x: 8,
radius_y: 9
}
);
assert_eq!(ri.regions[0].geometry_type(), 0);
assert_eq!(ri.regions[2].geometry_type(), 2);
}
#[test]
fn parses_polygon_and_polyline() {
let mut poly = vec![3u8]; poly.extend_from_slice(&u16b(3)); for (px, py) in [(0i16, 0i16), (10, 0), (5, 8)] {
poly.extend_from_slice(&i16b(px));
poly.extend_from_slice(&i16b(py));
}
let mut line = vec![6u8]; line.extend_from_slice(&u16b(2));
for (px, py) in [(-3i16, -4i16), (3, 4)] {
line.extend_from_slice(&i16b(px));
line.extend_from_slice(&i16b(py));
}
let body = region_item_body(0, 100, 100, &[poly, line]);
let ri = RegionItem::parse(&body).unwrap();
assert_eq!(
ri.regions[0],
RegionGeometry::Polygon {
points: vec![(0, 0), (10, 0), (5, 8)]
}
);
assert_eq!(
ri.regions[1],
RegionGeometry::Polyline {
points: vec![(-3, -4), (3, 4)]
}
);
}
#[test]
fn referenced_mask_has_no_ref_idx_in_region_item() {
let mut mask = vec![4u8]; mask.extend_from_slice(&i16b(1));
mask.extend_from_slice(&i16b(2));
mask.extend_from_slice(&u16b(0)); mask.extend_from_slice(&u16b(0)); let body = region_item_body(0, 64, 64, &[mask]);
let ri = RegionItem::parse(&body).unwrap();
assert_eq!(
ri.regions[0],
RegionGeometry::ReferencedMask {
x: 1,
y: 2,
width: 0,
height: 0,
mask_ref_idx: None,
}
);
assert!(ri.regions[0].is_mask());
}
#[test]
fn inline_mask_uncompressed_takes_trailing_bits() {
let mut mask = vec![5u8]; mask.extend_from_slice(&i16b(0));
mask.extend_from_slice(&i16b(0));
mask.extend_from_slice(&u16b(16)); mask.extend_from_slice(&u16b(2));
mask.push(0); mask.extend_from_slice(&[0xAA, 0x55, 0xF0, 0x0F]); let body = region_item_body(0, 16, 2, &[mask]);
let ri = RegionItem::parse(&body).unwrap();
match &ri.regions[0] {
RegionGeometry::InlineMask {
width,
height,
mask_coding_method,
mask_coding_parameters,
data,
..
} => {
assert_eq!(*width, 16);
assert_eq!(*height, 2);
assert_eq!(*mask_coding_method, 0);
assert_eq!(*mask_coding_parameters, None);
assert_eq!(data, &[0xAA, 0x55, 0xF0, 0x0F]);
}
other => panic!("expected InlineMask, got {other:?}"),
}
}
#[test]
fn inline_mask_deflate_carries_coding_parameters() {
let mut mask = vec![5u8];
mask.extend_from_slice(&i16b(0));
mask.extend_from_slice(&i16b(0));
mask.extend_from_slice(&u16b(8));
mask.extend_from_slice(&u16b(8));
mask.push(1); mask.extend_from_slice(&3u32.to_be_bytes()); mask.extend_from_slice(&[0x01, 0x02, 0x03]);
let body = region_item_body(0, 8, 8, &[mask]);
let ri = RegionItem::parse(&body).unwrap();
match &ri.regions[0] {
RegionGeometry::InlineMask {
mask_coding_method,
mask_coding_parameters,
data,
..
} => {
assert_eq!(*mask_coding_method, 1);
assert_eq!(*mask_coding_parameters, Some(3));
assert_eq!(data, &[0x01, 0x02, 0x03]);
}
other => panic!("expected InlineMask, got {other:?}"),
}
}
#[test]
fn large_field_size_uses_32bit_fields() {
let mut rect = vec![1u8];
rect.extend_from_slice(&(-100000i32).to_be_bytes());
rect.extend_from_slice(&42i32.to_be_bytes());
rect.extend_from_slice(&70000u32.to_be_bytes());
rect.extend_from_slice(&80000u32.to_be_bytes());
let body = region_item_body(1, 65535, 65535, &[rect]);
let ri = RegionItem::parse(&body).unwrap();
assert!(ri.is_large_field_size());
assert_eq!(
ri.regions[0],
RegionGeometry::Rectangle {
x: -100000,
y: 42,
width: 70000,
height: 80000,
}
);
}
#[test]
fn rejects_unknown_version() {
let mut body = region_item_body(0, 10, 10, &[]);
body[0] = 1;
assert!(RegionItem::parse(&body).is_err());
}
#[test]
fn rejects_reserved_geometry_type() {
let body = region_item_body(0, 10, 10, &[vec![7u8]]);
assert!(RegionItem::parse(&body).is_err());
}
#[test]
fn rejects_truncated_region_array() {
let mut body = vec![0u8, 0];
body.extend_from_slice(&u16b(10));
body.extend_from_slice(&u16b(10));
body.push(1); assert!(RegionItem::parse(&body).is_err());
}
#[test]
fn zero_regions_parses_empty() {
let body = region_item_body(0, 320, 240, &[]);
let ri = RegionItem::parse(&body).unwrap();
assert_eq!(ri.region_count(), 0);
assert_eq!(ri.reference_width, 320);
}
use crate::meta::{IrefEntry, ItemInfo, Meta};
fn iden_item(id: u32) -> ItemInfo {
ItemInfo {
id,
item_type: ITEM_TYPE_IDEN,
name: String::new(),
content_type: None,
content_encoding: None,
item_uri_type: None,
flags: 0,
}
}
fn rgan_item(id: u32) -> ItemInfo {
ItemInfo {
id,
item_type: ITEM_TYPE_RGAN,
name: String::new(),
content_type: None,
content_encoding: None,
item_uri_type: None,
flags: 0,
}
}
#[test]
fn derived_region_iden_is_compliant() {
let meta = Meta {
items: vec![iden_item(2), rgan_item(3)],
irefs: vec![IrefEntry {
reference_type: REF_TYPE_DRGN,
from_id: 2,
to_ids: vec![3],
}],
..Meta::default()
};
let derived = resolve_derived_region_items(&meta);
assert_eq!(derived.len(), 1);
assert_eq!(derived[0].derived_item_id, 2);
assert_eq!(derived[0].source_region_item_id, Some(3));
assert_eq!(derived[0].drgn_reference_count, 1);
assert!(derived[0].is_compliant());
assert!(derived[0].missing().is_empty());
}
#[test]
fn derived_region_multi_input_is_non_compliant() {
let meta = Meta {
items: vec![iden_item(2)],
irefs: vec![IrefEntry {
reference_type: REF_TYPE_DRGN,
from_id: 2,
to_ids: vec![3, 4],
}],
..Meta::default()
};
let d = &resolve_derived_region_items(&meta)[0];
assert_eq!(d.drgn_reference_count, 2);
assert_eq!(d.source_region_item_id, None);
assert!(!d.is_compliant());
assert!(d.missing().contains(&"drgn-reference-count-eq-1"));
}
#[test]
fn iden_without_drgn_is_not_a_derived_region() {
let meta = Meta {
items: vec![iden_item(2)],
irefs: vec![IrefEntry {
reference_type: *b"dimg",
from_id: 2,
to_ids: vec![3],
}],
..Meta::default()
};
assert!(resolve_derived_region_items(&meta).is_empty());
}
}