mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-19 02:10:25 -05:00
HEIF: hold uncropped SPS sizes and a grid's tiles together to max_pixels (#1518)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0da998f24d
commit
8a4d6f2ebf
3 files changed
+69
-13
No files matched your search
@@ -277,6 +277,10 @@ fn decode_item(ctx: &Context<'_>, id: u32, options: &Options, with_alpha: bool)
|
||||
})
|
||||
}
|
||||
|
||||
/// How many times `max_pixels` the coded pictures of one item may hold together: a grid's tiles
|
||||
/// cover its canvas rounded up to whole tiles, so legitimate grids sit near 1.
|
||||
const GRID_BUDGET: u64 = 2;
|
||||
|
||||
/// The decoded pictures of an item: one, or a grid's tiles in `dimg` order.
|
||||
struct Coded<'a> {
|
||||
frames: Vec<Frame>,
|
||||
@@ -297,6 +301,24 @@ impl<'a> Coded<'a> {
|
||||
let first = *ids.first().ok_or_else(|| Error::Malformed("grid lists no tiles".into()))?;
|
||||
let first_props = if first == id { p.clone() } else { ctx.props(first).map_err(err)? };
|
||||
let chroma_format = first_props.hvcc.as_ref().map_or(1, |c| c.chroma_format);
|
||||
// #1518: the decoder allocates each picture at its SPS's uncropped size, which a
|
||||
// conformance window (or a slice picking another SPS) can make far larger than the
|
||||
// declared one, and a grid holds all its tiles at once. Both are refused before decoding.
|
||||
let mut total = 0u64;
|
||||
for t in &ids {
|
||||
let tp = ctx.props(*t).map_err(err)?;
|
||||
let hvcc = tp.hvcc.as_ref().ok_or(heic_rs::Error::MissingBox("hvcC")).map_err(err)?;
|
||||
let sizes = vui::coded_sizes(&hvcc.parameter_sets()).ok_or_else(|| Error::Malformed("unreadable sequence parameter set".into()))?;
|
||||
let mut largest = 0u64;
|
||||
for (w, h) in sizes {
|
||||
heic_rs::image::check_pixels(w, h, limit).map_err(err)?;
|
||||
largest = largest.max(u64::from(w) * u64::from(h));
|
||||
}
|
||||
total = total.saturating_add(largest);
|
||||
}
|
||||
if total > limit.saturating_mul(GRID_BUDGET) {
|
||||
return Err(Error::Limit(format!("the coded pictures hold {total} pixels, over the {limit}-pixel limit")));
|
||||
}
|
||||
let decode_one = |t: &u32| -> Result<Frame, Error> {
|
||||
let tp = ctx.props(*t).map_err(err)?;
|
||||
let hvcc = tp.hvcc.as_ref().ok_or(heic_rs::Error::MissingBox("hvcC")).map_err(err)?;
|
||||
|
||||
@@ -146,6 +146,17 @@ fn limits_are_checked_before_decoding() {
|
||||
assert!(matches!(r, Err(Error::Limit(_))), "{r:?}");
|
||||
}
|
||||
|
||||
/// #1518: a grid's tiles together are held to the pixel budget before any is decoded. A
|
||||
/// 33 × 33 canvas fits 1089 pixels, but its four 32 × 32 tiles hold 4096: over twice the limit.
|
||||
#[test]
|
||||
fn grid_tiles_are_held_to_the_pixel_budget_together() {
|
||||
let bytes = build(&Spec::new(33, 33, &grey));
|
||||
let r = decode(&bytes, &Options { max_pixels: 33 * 33, ..Default::default() });
|
||||
assert!(matches!(r, Err(Error::Limit(_))), "{r:?}");
|
||||
let d = decode(&bytes, &Options { max_pixels: 2 * 32 * 32, ..Default::default() }).unwrap();
|
||||
assert_eq!((d.width, d.height), (33, 33));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_corrupted_files_are_errors_not_panics() {
|
||||
let bytes = build(&Spec { props: vec![FULL_601, Prop::Irot(1)], thumbnail: Some(&grey), ..Spec::new(64, 64, &grey) });
|
||||
|
||||
+36
-13
@@ -33,6 +33,41 @@ pub fn signal(parameter_sets: &[&[u8]]) -> Option<Signal> {
|
||||
parse_sps(&unescape(sps.get(2..)?))
|
||||
}
|
||||
|
||||
/// The uncropped picture size (`pic_width/height_in_luma_samples`) of every SPS among
|
||||
/// `parameter_sets`: what the decoder allocates before it applies the conformance window, so the
|
||||
/// pixel budget is checked against it (heic-rs reports only the first SPS, cropped). `None`: an
|
||||
/// SPS this reader can't follow.
|
||||
pub fn coded_sizes(parameter_sets: &[&[u8]]) -> Option<Vec<(u32, u32)>> {
|
||||
parameter_sets
|
||||
.iter()
|
||||
.filter(|n| n.first().is_some_and(|b| (b >> 1) & 0x3f == SPS_NUT))
|
||||
.map(|sps| {
|
||||
let rbsp = unescape(sps.get(2..)?);
|
||||
let r = &mut Bits { data: &rbsp, pos: 0 };
|
||||
sps_size(r).map(|(w, h, _)| (w, h))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reads an SPS up to and including the picture size, leaving `r` after it: width, height and
|
||||
/// `sps_max_sub_layers_minus1`.
|
||||
fn sps_size(r: &mut Bits) -> Option<(u32, u32, u32)> {
|
||||
r.skip(4)?; // sps_video_parameter_set_id
|
||||
let max_sub_layers_minus1 = r.u(3)?;
|
||||
if max_sub_layers_minus1 > 6 {
|
||||
return None;
|
||||
}
|
||||
r.skip(1)?; // sps_temporal_id_nesting_flag
|
||||
profile_tier_level(r, max_sub_layers_minus1)?;
|
||||
r.ue()?; // sps_seq_parameter_set_id
|
||||
if r.ue()? == 3 {
|
||||
r.skip(1)?; // separate_colour_plane_flag
|
||||
}
|
||||
let width = r.ue()?; // pic_width_in_luma_samples
|
||||
let height = r.ue()?; // pic_height_in_luma_samples
|
||||
Some((width, height, max_sub_layers_minus1))
|
||||
}
|
||||
|
||||
/// Removes emulation prevention bytes (`00 00 03` → `00 00`).
|
||||
fn unescape(nal: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(nal.len());
|
||||
@@ -165,19 +200,7 @@ fn st_ref_pic_set(r: &mut Bits, idx: usize, num_delta_pocs: &[u32]) -> Option<u3
|
||||
|
||||
fn parse_sps(rbsp: &[u8]) -> Option<Signal> {
|
||||
let r = &mut Bits { data: rbsp, pos: 0 };
|
||||
r.skip(4)?; // sps_video_parameter_set_id
|
||||
let max_sub_layers_minus1 = r.u(3)?;
|
||||
if max_sub_layers_minus1 > 6 {
|
||||
return None;
|
||||
}
|
||||
r.skip(1)?; // sps_temporal_id_nesting_flag
|
||||
profile_tier_level(r, max_sub_layers_minus1)?;
|
||||
r.ue()?; // sps_seq_parameter_set_id
|
||||
if r.ue()? == 3 {
|
||||
r.skip(1)?; // separate_colour_plane_flag
|
||||
}
|
||||
r.ue()?; // pic_width_in_luma_samples
|
||||
r.ue()?; // pic_height_in_luma_samples
|
||||
let (_, _, max_sub_layers_minus1) = sps_size(r)?;
|
||||
if r.flag()? {
|
||||
for _ in 0..4 {
|
||||
r.ue()?; // conf_win_*_offset
|
||||
|
||||
Reference in new issue
Block a user