///|
/// Read primary-item canvas dimensions from AVIF / HEIF without decoding image
/// data. An `ispe` is used only when `ipma` associates it with the `pitm`
/// primary item, so a thumbnail's dimensions cannot be mistaken for the main
/// image's dimensions.
///|
pub fn avif_dimensions(bytes : Bytes) -> (Int32, Int32)? {
let end = bytes.length()
// The first box must be a `ftyp` brand box, otherwise this is not an
// ISO-BMFF-based image.
match box_payload(bytes, 0, end) {
Some(_) => if !box_type_is(bytes, 4, b"ftyp") { return None }
None => return None
}
// `meta` is a FullBox, so its children start after the 4-byte version/flags.
let (meta_start, meta_end) = match find_box(bytes, 0, end, b"meta") {
Some(value) => value
None => return None
}
let primary_item = match avif_primary_item_id(bytes, meta_start, meta_end) {
Some(value) => value
None => return None
}
let (iprp_start, iprp_end) = match
find_child_box(bytes, meta_start, meta_end, b"iprp") {
Some(value) => value
None => return None
}
// `iprp` and `ipco` are plain boxes (no version/flags), so their children
// start right at the payload boundary.
let (ipco_start, ipco_end) = match
find_box(bytes, iprp_start, iprp_end, b"ipco") {
Some(value) => value
None => return None
}
let property_count = match
avif_ipco_property_count(bytes, ipco_start, ipco_end) {
Some(value) => value
None => return None
}
let (ipma_start, ipma_end) = match
find_box(bytes, iprp_start, iprp_end, b"ipma") {
Some(value) => value
None => return None
}
avif_primary_dimensions(
bytes, ipma_start, ipma_end, ipco_start, ipco_end, property_count, primary_item,
)
}
///|
/// Parse the primary item identifier from the `pitm` FullBox.
fn avif_primary_item_id(
bytes : Bytes,
meta_start : Int32,
meta_end : Int32,
) -> Int64? {
let (start, end) = match
find_child_box(bytes, meta_start, meta_end, b"pitm") {
Some(value) => value
None => return None
}
if start + 4 > end ||
bytes[start + 1] != b'\x00' ||
bytes[start + 2] != b'\x00' ||
bytes[start + 3] != b'\x00' {
return None
}
let version = bytes[start].to_int()
if version == 0 {
if start + 6 != end {
return None
}
Some(be_u16(bytes, start + 4))
} else if version == 1 {
if start + 8 != end {
return None
}
Some(be_u32(bytes, start + 4))
} else {
None
}
}
///|
/// Resolve the primary item's `ipma` associations to an `ispe` in `ipco`.
/// Property indexes are one-based and retain their `ipco` ordering.
fn avif_primary_dimensions(
bytes : Bytes,
start : Int32,
end : Int32,
ipco_start : Int32,
ipco_end : Int32,
property_count : Int64,
primary_item : Int64,
) -> (Int32, Int32)? {
if start + 8 > end ||
bytes[start + 1] != b'\x00' ||
bytes[start + 2] != b'\x00' {
return None
}
let version = bytes[start].to_int()
if version > 1 {
return None
}
let flags = bytes[start + 3].to_int()
if (flags & 0xFE) != 0 {
return None
}
let wide_indexes = (flags & 1) != 0
let entry_count = be_u32(bytes, start + 4)
if entry_count > Int32::to_int64(end - start) {
return None
}
let mut offset = start + 8
let mut found : (Int32, Int32)? = None
for _ in 0.. end {
return None
}
let item_id = if version == 0 {
be_u16(bytes, offset)
} else {
be_u32(bytes, offset)
}
offset = offset + item_id_length
let association_count = bytes[offset].to_int()
offset = offset + 1
let association_length = if wide_indexes { 2 } else { 1 }
if offset + association_count * association_length > end {
return None
}
if item_id == primary_item {
for _ in 0.. property_count {
return None
}
if property_index > 0L {
let (is_ispe, width, height) = avif_ipco_dimensions(
bytes, ipco_start, ipco_end, property_index,
)
if is_ispe {
if width <= 0 || height <= 0 || found is Some(_) {
return None
}
found = Some((width, height))
}
}
}
} else {
offset = offset + association_count * association_length
}
}
if offset != end {
return None
}
found
}
///|
/// Validate and count all property boxes. A valid low-index property followed
/// by a malformed box must not let a truncated property container pass.
fn avif_ipco_property_count(
bytes : Bytes,
start : Int32,
end : Int32,
) -> Int64? {
let mut offset = start
let mut count = 0L
while offset < end {
match box_payload(bytes, offset, end) {
Some((_, payload_end)) => {
if payload_end <= offset {
return None
}
offset = payload_end
count = count + 1L
}
None => return None
}
}
Some(count)
}
///|
/// Resolve one one-based `ipco` property index. The Boolean indicates whether
/// the selected property is an `ispe` FullBox.
fn avif_ipco_dimensions(
bytes : Bytes,
start : Int32,
end : Int32,
property_index : Int64,
) -> (Bool, Int32, Int32) {
let mut offset = start
let mut index = 1L
while offset < end {
let property_start = offset
match box_payload(bytes, offset, end) {
Some((payload_start, payload_end)) => {
if index == property_index {
if !box_type_is(bytes, property_start + 4, b"ispe") {
return (false, 0, 0)
}
// `ispe` is version zero with exactly two uint32 payload values.
if payload_start + 12 != payload_end ||
bytes[payload_start] != b'\x00' ||
bytes[payload_start + 1] != b'\x00' ||
bytes[payload_start + 2] != b'\x00' ||
bytes[payload_start + 3] != b'\x00' {
return (true, 0, 0)
}
let width = be_u32(bytes, payload_start + 4)
let height = be_u32(bytes, payload_start + 8)
if width <= 0L ||
height <= 0L ||
width >= 2_147_483_648L ||
height >= 2_147_483_648L {
return (true, 0, 0)
}
return (true, Int64::to_int(width), Int64::to_int(height))
}
offset = payload_end
index = index + 1L
}
None => return (false, 0, 0)
}
}
(false, 0, 0)
}
///|
/// Read a big-endian unsigned 16-bit integer.
fn be_u16(bytes : Bytes, offset : Int32) -> Int64 {
Int32::to_int64(bytes[offset].to_int()) * 256L +
Int32::to_int64(bytes[offset + 1].to_int())
}
///|
async fn read_avif_dimensions(path : String, size : Int64) -> (Int32, Int32)? {
if size < 12L {
return None
}
// The `meta` box and its `ispe` property normally sit near the front of the
// file; bound the read to keep memory use predictable.
let header_length = if size > 4_194_304L {
4_194_304
} else {
Int64::to_int(size)
}
let file = @fs.open(path, mode=@fs.ReadOnly)
defer file.close()
let header = file.read_exactly_at(header_length, position=0L)
avif_dimensions(header)
}
///|
test "reads AVIF dimensions through the primary item's ispe association" {
// pitm identifies item 1; ipma associates property 1 (the ispe) with it.
let ispe : Bytes = b"\x00\x00\x00\x14ispe\x00\x00\x00\x00\x00\x00\x02\x58\x00\x00\x01\x0E"
let ipco : Bytes = b"\x00\x00\x00\x1Cipco" + ispe
let ipma : Bytes = b"\x00\x00\x00\x14ipma\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x01"
let iprp : Bytes = b"\x00\x00\x00\x38iprp" + ipco + ipma
let pitm : Bytes = b"\x00\x00\x00\x0Epitm\x00\x00\x00\x00\x00\x01"
let meta : Bytes = b"\x00\x00\x00\x52meta\x00\x00\x00\x00" + pitm + iprp
let bytes : Bytes = b"\x00\x00\x00\x10ftypavif\x00\x00\x00\x00" + meta
assert_true(avif_dimensions(bytes) == Some((600, 270)))
}
///|
test "selects the primary item's ispe rather than an earlier thumbnail" {
let thumbnail : Bytes = b"\x00\x00\x00\x14ispe\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x20"
let primary : Bytes = b"\x00\x00\x00\x14ispe\x00\x00\x00\x00\x00\x00\x03\x20\x00\x00\x01\x68"
let ipco : Bytes = b"\x00\x00\x00\x30ipco" + thumbnail + primary
let ipma : Bytes = b"\x00\x00\x00\x18ipma\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x01\x01\x00\x02\x01\x02"
let iprp : Bytes = b"\x00\x00\x00\x50iprp" + ipco + ipma
let pitm : Bytes = b"\x00\x00\x00\x0Epitm\x00\x00\x00\x00\x00\x02"
let meta : Bytes = b"\x00\x00\x00\x6Ameta\x00\x00\x00\x00" + pitm + iprp
let bytes : Bytes = b"\x00\x00\x00\x10ftypavif\x00\x00\x00\x00" + meta
assert_true(avif_dimensions(bytes) == Some((800, 360)))
}
///|
test "rejects AVIF files that are not ISO-BMFF or lack the ispe box" {
assert_true(avif_dimensions(b"not an avif") is None)
let no_ispe : Bytes = b"\x00\x00\x00\x10ftypavif\x00\x00\x00\x00" +
b"\x00\x00\x00\x08free"
assert_true(avif_dimensions(no_ispe) is None)
let unassociated : Bytes = b"\x00\x00\x00\x10ftypavif\x00\x00\x00\x00" +
b"\x00\x00\x00\x52meta\x00\x00\x00\x00" +
b"\x00\x00\x00\x0Epitm\x00\x00\x00\x00\x00\x01" +
b"\x00\x00\x00\x38iprp" +
b"\x00\x00\x00\x1Cipco" +
b"\x00\x00\x00\x14ispe\x00\x00\x00\x00\x00\x00\x02\x58\x00\x00\x01\x0E" +
b"\x00\x00\x00\x14ipma\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00"
assert_true(avif_dimensions(unassociated) is None)
let bad_ftyp : Bytes = b"\x00\x00\x00\x08noty\x00\x00\x00\x00"
assert_true(avif_dimensions(bad_ftyp) is None)
}