///|
priv struct AvifMetadataBuilder {
mut icc : FixedArray[Byte]?
mut exif : FixedArray[Byte]?
mut exif_tiff_offset : Int?
mut xmp : FixedArray[Byte]?
mut aspect : (Int64, Int64)?
transforms : Array[AvifDisplayTransform]
mut remaining : Int
}
///|
fn metadata_builder(limit : Int) -> AvifMetadataBuilder {
{
icc: None,
exif: None,
exif_tiff_offset: None,
xmp: None,
aspect: None,
transforms: [],
remaining: limit,
}
}
///|
fn metadata_finish(
builder : AvifMetadataBuilder,
primary : Int?,
track : Int?,
) -> AvifMetadata {
{
primary_item_id: primary,
track_id: track,
icc: builder.icc,
exif: builder.exif,
exif_tiff_offset: builder.exif_tiff_offset,
xmp: builder.xmp,
pixel_aspect_ratio: builder.aspect,
transforms: FixedArray::from_array(builder.transforms),
}
}
///|
fn metadata_u32(
data : Array[Byte],
at : Int,
end : Int,
) -> Int64 raise AvifError {
let mut value = 0L
for i in 0..<4 {
value = value * 256L + u8(data, at + i, end).to_int64()
}
value
}
///|
fn metadata_blob(
builder : AvifMetadataBuilder,
data : Array[Byte],
start : Int,
end : Int,
) -> FixedArray[Byte] raise AvifError {
if start < 0 || end < start || end > data.length() {
raise AvifError::Invalid
}
if end - start > builder.remaining {
raise AvifError::Diagnostic(
decode_error(
LimitExceeded,
"metadata",
"Combined exposed metadata exceeds max_input_bytes",
),
)
}
builder.remaining -= end - start
FixedArray::makei(end - start, i => data[start + i])
}
///|
fn metadata_property(
builder : AvifMetadataBuilder,
data : Array[Byte],
box : Box,
) -> Unit raise AvifError {
let p = box.payload
let n = box.end - p
if box.typ == t_colr {
let kind = u32(data, p, box.end)
if kind == 0x70726F66 || kind == 0x72494343 { // prof / rICC
if builder.icc is Some(_) || n <= 4 {
raise AvifError::Invalid
}
builder.icc = Some(metadata_blob(builder, data, p + 4, box.end))
}
} else if box.typ == 0x70617370 { // pasp
if n != 8 || builder.aspect is Some(_) {
raise AvifError::Invalid
}
let h = metadata_u32(data, p, box.end)
let v = metadata_u32(data, p + 4, box.end)
if h == 0L || v == 0L {
raise AvifError::Invalid
}
builder.aspect = Some((h, v))
} else if box.typ == 0x69726F74 { // irot
if n != 1 || u8(data, p, box.end) > 3 {
raise AvifError::Invalid
}
builder.transforms.push(Rotate(u8(data, p, box.end)))
} else if box.typ == 0x696D6972 { // imir
if n != 1 || u8(data, p, box.end) > 1 {
raise AvifError::Invalid
}
builder.transforms.push(Mirror(u8(data, p, box.end)))
} else if box.typ == 0x636C6170 { // clap
if n != 32 {
raise AvifError::Invalid
}
let crop : AvifCleanAperture = {
width_n: metadata_u32(data, p, box.end),
width_d: metadata_u32(data, p + 4, box.end),
height_n: metadata_u32(data, p + 8, box.end),
height_d: metadata_u32(data, p + 12, box.end),
horizontal_n: u32(data, p + 16, box.end).to_int64(),
horizontal_d: metadata_u32(data, p + 20, box.end),
vertical_n: u32(data, p + 24, box.end).to_int64(),
vertical_d: metadata_u32(data, p + 28, box.end),
}
if crop.width_n == 0L ||
crop.width_d == 0L ||
crop.height_n == 0L ||
crop.height_d == 0L ||
crop.horizontal_d == 0L ||
crop.vertical_d == 0L {
raise AvifError::Invalid
}
builder.transforms.push(Crop(crop))
}
}
///|
fn metadata_string_end(
data : Array[Byte],
start : Int,
end : Int,
) -> Int raise AvifError {
for p in start.. Bool {
let chars = expected.to_array()
if end - start != chars.length() + 1 {
return false
}
for i in 0.. Array[Byte] raise AvifError {
let output : Array[Byte] = []
for i in 0.. (box.payload, box.end)
None => raise AvifError::Invalid
}
} else {
(0, data.length())
}
let length = item.lens[i]
if length < 0 || length > limit - output.length() {
raise AvifError::Diagnostic(
decode_error(
LimitExceeded,
"metadata",
"Metadata extents exceed the byte budget",
),
)
}
if item.base < 0 ||
item.offs[i] < 0 ||
base > end - length ||
item.base > end - length - base ||
item.offs[i] > end - length - base - item.base {
raise AvifError::Invalid
}
let offset = base + item.base + item.offs[i]
for p in offset..<(offset + length) {
output.push(data[p])
}
}
output
}
///|
fn metadata_descriptions(
data : Array[Byte],
boxes : Array[Box],
meta : Box,
primary : Int,
) -> Array[Int] raise AvifError {
let linked : Array[Int] = []
let iref = match find_box(boxes, idx_box(boxes, meta.start), 0x69726566) {
Some(value) => value
None => return linked
}
let version = u8(data, iref.payload, iref.end)
if version > 1 {
raise AvifError::Unsupported
}
let refs : Array[Box] = []
walk(data, iref.payload + 4, iref.end, -1, refs)
for reference in refs {
if reference.parent != -1 || reference.typ != 0x63647363 {
continue
}
let size = if version == 0 { 2 } else { 4 }
let from = if size == 2 {
u16(data, reference.payload, reference.end)
} else {
u32(data, reference.payload, reference.end)
}
let count = u16(data, reference.payload + size, reference.end)
let start = reference.payload + size + 2
if count > (reference.end - start) / size {
raise AvifError::Invalid
}
for i in 0.. Unit raise AvifError {
let mi = idx_box(boxes, meta.start)
let iinf = match find_box(boxes, mi, t_iinf) {
Some(value) => value
None => if primary is Some(_) { raise AvifError::Invalid } else { return }
}
let ii = idx_box(boxes, iinf.start)
let items : Array[Item] = []
let xmp_ids : Array[Int] = []
for box in boxes {
if box.parent != ii || box.typ != t_infe {
continue
}
let version = u8(data, box.payload, box.end)
if version != 2 && version != 3 {
raise AvifError::Unsupported
}
let size = if version == 2 { 2 } else { 4 }
let id = if size == 2 {
u16(data, box.payload + 4, box.end)
} else {
u32(data, box.payload + 4, box.end)
}
if id <= 0 || items.any(item => item.id == id) {
raise AvifError::Invalid
}
if u16(data, box.payload + 4 + size, box.end) != 0 {
raise AvifError::Unsupported
}
let kind = u32(data, box.payload + 6 + size, box.end)
items.push({
id,
typ: kind,
method_id: 0,
base: 0,
offs: [],
lens: [],
props: [],
})
if kind == 0x6D696D65 {
let content = metadata_string_end(data, box.payload + 10 + size, box.end)
let end = metadata_string_end(data, content, box.end)
if metadata_string_equals(data, content, end, "application/rdf+xml") {
if end < box.end && u8(data, end, box.end) != 0 {
raise AvifError::Unsupported
}
xmp_ids.push(id)
}
}
}
let iloc = match find_box(boxes, mi, t_iloc) {
Some(value) => value
None => if items.is_empty() { return } else { raise AvifError::Invalid }
}
parse_iloc(data, iloc, items)
let iprp = find_box(boxes, mi, t_iprp)
let essential : Array[(Int, Int)] = []
parse_ipma(data, boxes, iprp, items, essential=Some(essential))
if primary is Some(id) {
let item_index = idx_item(items, id)
if item_index < 0 {
raise AvifError::Invalid
}
if iprp is Some(rp) {
if find_box(boxes, idx_box(boxes, rp.start), t_ipco) is Some(co) {
let ci = idx_box(boxes, co.start)
let properties = boxes.filter(box => box.parent == ci)
for entry in essential {
let (owner, index) = entry
if owner != id {
continue
}
if index <= 0 || index > properties.length() {
raise AvifError::Invalid
}
let kind = properties[index - 1].typ
if ![
t_ispe, t_av1c, t_colr, t_auxc, t_a1op, t_lsel, 0x70697869, 0x70617370,
0x636C6170, 0x69726F74, 0x696D6972,
].contains(kind) {
raise AvifError::Diagnostic(
decode_error(
Unsupported,
"metadata.property",
"Unknown essential primary-item property",
offset=Some(properties[index - 1].start),
),
)
}
}
for association in items[item_index].props {
if association <= 0 || association > properties.length() {
raise AvifError::Invalid
}
metadata_property(builder, data, properties[association - 1])
}
}
}
}
let linked = match primary {
Some(id) => metadata_descriptions(data, boxes, meta, id)
None => []
}
let idat = find_box(boxes, mi, t_idat)
for item in items {
if primary is Some(_) && !linked.contains(item.id) {
continue
}
if item.typ != 0x45786966 && !xmp_ids.contains(item.id) {
continue
}
let payload = metadata_item_payload(data, item, idat, builder.remaining)
if payload.is_empty() {
continue
}
if item.typ == 0x45786966 {
if builder.exif is Some(_) {
raise AvifError::Invalid
}
let offset = metadata_u32(payload, 0, payload.length())
if offset > (payload.length() - 4).to_int64() {
raise AvifError::Invalid
}
builder.exif_tiff_offset = Some(offset.to_int())
builder.exif = Some(metadata_blob(builder, payload, 4, payload.length()))
} else {
if builder.xmp is Some(_) {
raise AvifError::Invalid
}
builder.xmp = Some(metadata_blob(builder, payload, 0, payload.length()))
}
}
}
///|
/// Read primary-item metadata without reconstructing pixels. Input and total
/// exposed metadata are bounded by max_input_bytes. Transform metadata remains
/// available even when its crop cannot be represented by an integer raster.
pub fn avif_metadata(
data : Array[Byte],
limits? : DecodeLimits = DecodeLimits::default(),
) -> Result[AvifMetadata, DecodeError] {
match decode_checked_budget(data.length(), limits) {
Err(error) => return Err(error)
Ok(_) => ()
}
try {
let boxes : Array[Box] = []
walk(data, 0, data.length(), -1, boxes)
let meta = match find_box(boxes, -1, t_meta) {
Some(value) => value
None => raise AvifError::Invalid
}
let pitm = match find_box(boxes, idx_box(boxes, meta.start), t_pitm) {
Some(value) => value
None => raise AvifError::Invalid
}
let version = u8(data, pitm.payload, pitm.end)
if version > 1 {
raise AvifError::Unsupported
}
let primary = if version == 0 {
u16(data, pitm.payload + 4, pitm.end)
} else {
u32(data, pitm.payload + 4, pitm.end)
}
let builder = metadata_builder(limits.max_input_bytes)
metadata_meta(builder, data, boxes, meta, Some(primary))
Ok(metadata_finish(builder, Some(primary), None))
} catch {
AvifError::Diagnostic(error) => Err(error)
AvifError::Unsupported =>
Err(
decode_error(
Unsupported,
"metadata",
"Unsupported metadata syntax, protection or content encoding",
),
)
_ =>
Err(
decode_error(
InvalidData,
"metadata",
"Invalid metadata item, association, extent or property",
),
)
}
}
///|
fn avif_track_metadata(
data : Array[Byte],
track : GaAnimationTrack,
limit : Int,
) -> Result[AvifMetadata, DecodeError] {
try {
let builder = metadata_builder(limit)
for property in track.properties {
if property.parent == -1 {
metadata_property(builder, data, {
typ: property.typ,
start: property.start,
payload: property.payload,
end: property.end,
parent: -1,
})
}
}
let tracks : Array[GaBox] = []
ga_walk(data, 0, data.length(), -1, tracks)
if ga_find(tracks, ga_idx(tracks, track.box_start), ga_meta) is Some(meta) {
let boxes : Array[Box] = []
walk(data, meta.start, meta.end, -1, boxes)
metadata_meta(builder, data, boxes, boxes[0], None)
}
Ok(metadata_finish(builder, None, Some(track.id)))
} catch {
AvifError::Diagnostic(error) => Err(error)
AvifError::Unsupported | GridAnimError::Unsupported =>
Err(
decode_error(
Unsupported,
"animation.metadata",
"Unsupported track metadata syntax",
),
)
_ =>
Err(
decode_error(
InvalidData,
"animation.metadata",
"Invalid track metadata",
),
)
}
}
///|
fn avif_metadata_copy(metadata : AvifMetadata) -> AvifMetadata {
{
..metadata,
icc: metadata.icc.map(bytes => bytes.map(byte => byte)),
exif: metadata.exif.map(bytes => bytes.map(byte => byte)),
xmp: metadata.xmp.map(bytes => bytes.map(byte => byte)),
transforms: metadata.transforms.map(transform => transform),
}
}