mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-09 11:59:36 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b396f94f0d | ||
|
|
469eede715 |
@@ -433,6 +433,17 @@ pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<deckcraft_model::Shap
|
||||
pub fn xfrm_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
|
||||
let mut x = shape.xfrm.unwrap_or_else(|| ctx_xfrm(doc, sel, shape));
|
||||
if let deckcraft_model::ShapeKind::Table(t) = &shape.kind {
|
||||
// A loaded table may not be normalized yet: measure the bounded grid an edit will
|
||||
// leave, not every cell the file stored.
|
||||
let normalized;
|
||||
let t = if t.is_normalized() {
|
||||
t
|
||||
} else {
|
||||
let mut n = t.clone();
|
||||
n.normalize();
|
||||
normalized = n;
|
||||
&normalized
|
||||
};
|
||||
let drawn: f64 = table_drawn_heights(doc, sel, t).iter().sum();
|
||||
if drawn.is_finite() && drawn > x.h {
|
||||
x.h = drawn;
|
||||
|
||||
@@ -285,6 +285,8 @@ fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> R
|
||||
};
|
||||
let r = edit_shapes(s, &json!({"id": id}), cmd, |sh| match &mut sh.kind {
|
||||
ShapeKind::Table(t) => {
|
||||
// Edits walk the grid: bound it first (a loaded table need not be normalized yet).
|
||||
t.normalize();
|
||||
f(t)?;
|
||||
t.normalize();
|
||||
let (w, h) = (t.width(), t.height());
|
||||
|
||||
@@ -81,9 +81,7 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
|
||||
Some(r) => r,
|
||||
None => contained(|| decode_symphonia(bytes, "mkv")),
|
||||
},
|
||||
Container::Mp4 | Container::Unknown | Container::Mp3 if !crate::mp4guard::probe_fits(bytes) => {
|
||||
Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()))
|
||||
}
|
||||
Container::Mp4 if !crate::mp4guard::tables_fit(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())),
|
||||
c => contained(|| decode_symphonia(bytes, c.name())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,6 @@ pub(crate) fn tables_fit(b: &[u8]) -> bool {
|
||||
boxes_fit(b, b.len() as u64, 0)
|
||||
}
|
||||
|
||||
/// Bytes symphonia's probe scans for a format marker before giving up.
|
||||
const PROBE_SEARCH_LIMIT: usize = 1024 * 1024;
|
||||
|
||||
/// Whether symphonia can be given `b`, whatever the sniffer made of it: its probe takes the first
|
||||
/// `ftyp` within its search window as the start of an MP4 (the size field 4 bytes before it), so
|
||||
/// the table check must start where that parser will, not at offset 0.
|
||||
pub(crate) fn probe_fits(b: &[u8]) -> bool {
|
||||
if crate::Container::sniff(b) == crate::Container::Mp4 && !tables_fit(b) {
|
||||
return false;
|
||||
}
|
||||
// The probe reads leading ID3v2 tags as metadata and searches again after the last one.
|
||||
let mut rest = b;
|
||||
while rest.starts_with(b"ID3") {
|
||||
let size = rest.get(6..10).map(|s| s.iter().fold(0usize, |a, &x| (a << 7) | (x & 0x7f) as usize)).unwrap_or(0);
|
||||
rest = rest.get(10 + size..).unwrap_or(&[]);
|
||||
}
|
||||
let head = &rest[..rest.len().min(PROBE_SEARCH_LIMIT + 16)];
|
||||
match head.windows(4).position(|w| w == b"ftyp") {
|
||||
Some(p) => tables_fit(&rest[p.saturating_sub(4)..]),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> Option<u64> {
|
||||
b.get(at..at.checked_add(4)?).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64)
|
||||
}
|
||||
@@ -97,29 +74,3 @@ fn table_fits(kind: &[u8], body: &[u8], file_len: u64) -> bool {
|
||||
};
|
||||
be32(body, 4).is_some_and(|count| count <= body.len().saturating_sub(8) as u64 / entry)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An MP4 whose `stsc` count asks for far more entries than the file holds.
|
||||
fn mp4_with_huge_stsc() -> Vec<u8> {
|
||||
let stsc = [&24u32.to_be_bytes()[..], b"stsc", &[0, 0, 0, 0], &u32::MAX.to_be_bytes(), &[0; 8]].concat();
|
||||
let stbl = [&(stsc.len() as u32 + 8).to_be_bytes()[..], b"stbl", &stsc].concat();
|
||||
let moov = [&(stbl.len() as u32 + 8).to_be_bytes()[..], b"moov", &stbl].concat();
|
||||
[&16u32.to_be_bytes()[..], b"ftyp", b"isom", &[0; 4], &moov].concat()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefixed_mp4_is_checked_where_symphonia_starts_it() {
|
||||
let mp4 = mp4_with_huge_stsc();
|
||||
assert!(!tables_fit(&mp4));
|
||||
assert!(!probe_fits(&mp4));
|
||||
let prefixed = [&b"junk before the file "[..], &mp4].concat();
|
||||
assert_eq!(crate::Container::sniff(&prefixed), crate::Container::Unknown);
|
||||
assert!(tables_fit(&prefixed), "offset 0 sees no MP4 boxes");
|
||||
assert!(!probe_fits(&prefixed));
|
||||
let far = [&vec![0u8; PROBE_SEARCH_LIMIT + 16][..], &mp4].concat();
|
||||
assert!(probe_fits(&far), "past the probe window symphonia never reads it as an MP4");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,7 @@ pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
};
|
||||
if info.audio.is_none() && info.video.is_none() || info.audio.as_ref().is_some_and(|a| a.sample_rate == 0) {
|
||||
// Plain audio files (and anything our demuxers didn't describe): symphonia's probe.
|
||||
// Symphonia opens an MP4 wherever its probe finds an `ftyp` (after leading ID3v2 tags too); other known containers (a WAV's audio bytes can spell an `ftyp`) skip the check.
|
||||
if matches!(c, Container::Mp4 | Container::Unknown | Container::Mp3) && !crate::mp4guard::probe_fits(bytes) {
|
||||
if c == Container::Mp4 && !crate::mp4guard::tables_fit(bytes) {
|
||||
return Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into()));
|
||||
}
|
||||
let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))
|
||||
|
||||
+98
-12
@@ -77,6 +77,12 @@ impl Default for Table {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cells a table may hold after [`Table::normalize`] (PowerPoint's own limit is 75 x 75).
|
||||
pub const MAX_CELLS: usize = 100_000;
|
||||
/// Cell visits [`Table::merge`] may spend growing its rectangle over existing merges: a few
|
||||
/// passes over the largest grid, where a real table needs one or two.
|
||||
const MAX_MERGE_WORK: usize = 16 * MAX_CELLS;
|
||||
|
||||
impl Table {
|
||||
pub fn new(rows: usize, cols: usize, width: f64, row_height: f64) -> Self {
|
||||
let rows = rows.clamp(1, 1000);
|
||||
@@ -164,8 +170,12 @@ impl Table {
|
||||
true
|
||||
}
|
||||
/// The merged areas: every uncovered cell spanning more than one cell, clipped to the grid.
|
||||
/// Merges of a well-formed table do not overlap, so together they cover at most the grid; a
|
||||
/// file's overlapping spans past that are dropped, so rebuilding them cannot revisit cells
|
||||
/// without bound.
|
||||
fn merges(&self) -> Vec<Merge> {
|
||||
let (nr, nc) = (self.rows.len(), self.cols.len());
|
||||
let mut area = nr.saturating_mul(nc);
|
||||
let mut out = vec![];
|
||||
for (r, row) in self.rows.iter().enumerate() {
|
||||
for (c, cell) in row.cells.iter().enumerate().take(nc) {
|
||||
@@ -174,7 +184,10 @@ impl Table {
|
||||
}
|
||||
let rs = (cell.row_span.max(1) as usize).min(nr - r);
|
||||
let gs = (cell.grid_span.max(1) as usize).min(nc - c);
|
||||
if rs > 1 || gs > 1 {
|
||||
if (rs > 1 || gs > 1)
|
||||
&& let Some(left) = area.checked_sub(rs.saturating_mul(gs))
|
||||
{
|
||||
area = left;
|
||||
out.push(Merge { r, c, rs, gs });
|
||||
}
|
||||
}
|
||||
@@ -227,9 +240,17 @@ impl Table {
|
||||
if r1 >= self.rows.len() || c1 >= self.cols.len() || (r0 == r1 && c0 == c1) {
|
||||
return false;
|
||||
}
|
||||
// Grow the rectangle until no existing merge sticks out of it.
|
||||
// Grow the rectangle until no existing merge sticks out of it. Each pass reads the whole
|
||||
// grid and may grow it by one row, so the passes are charged against
|
||||
// [`MAX_MERGE_WORK`]: past it the merge is refused, before anything changed.
|
||||
let (mut r0, mut c0, mut r1, mut c1) = (r0, c0, r1, c1);
|
||||
let pass = self.rows.len().saturating_mul(self.cols.len()).max(1);
|
||||
let mut work = 0usize;
|
||||
loop {
|
||||
work = work.saturating_add(pass);
|
||||
if work > MAX_MERGE_WORK {
|
||||
return false;
|
||||
}
|
||||
let before = (r0, c0, r1, c1);
|
||||
for (r, row) in self.rows.iter().enumerate() {
|
||||
for (c, cell) in row.cells.iter().enumerate().take(self.cols.len()) {
|
||||
@@ -283,21 +304,31 @@ impl Table {
|
||||
return false;
|
||||
}
|
||||
// Spans come from the file and may reach past the table; only walk the cells that exist.
|
||||
for rr in r..r.saturating_add(rs).min(self.rows.len()) {
|
||||
for cc in c..c.saturating_add(gs).min(self.cols.len()) {
|
||||
if let Some(x) = self.cell_mut(rr, cc) {
|
||||
x.h_merge = false;
|
||||
x.v_merge = false;
|
||||
x.grid_span = 1;
|
||||
x.row_span = 1;
|
||||
}
|
||||
let end = r.saturating_add(rs).min(self.rows.len());
|
||||
let gs = gs.min(self.cols.len().saturating_sub(c));
|
||||
for row in self.rows.get_mut(r..end).unwrap_or_default() {
|
||||
for x in row.cells.iter_mut().skip(c).take(gs) {
|
||||
x.h_merge = false;
|
||||
x.v_merge = false;
|
||||
x.grid_span = 1;
|
||||
x.row_span = 1;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Make every row have exactly `cols.len()` cells (repair after loading).
|
||||
pub fn normalize(&mut self) {
|
||||
/// Whether the grid is as [`Table::normalize`] leaves it: within [`MAX_CELLS`], every row
|
||||
/// exactly `cols.len()` cells wide.
|
||||
pub fn is_normalized(&self) -> bool {
|
||||
let n = self.cols.len();
|
||||
n <= MAX_CELLS && self.rows.len() <= (MAX_CELLS / n.max(1)).max(1) && self.rows.iter().all(|r| r.cells.len() == n)
|
||||
}
|
||||
/// Make every row have exactly `cols.len()` cells (repair after loading). A grid over
|
||||
/// [`MAX_CELLS`] loses its last rows (and columns) first, so a file naming many rows and many
|
||||
/// columns but few cells cannot make this allocate their product.
|
||||
pub fn normalize(&mut self) {
|
||||
self.cols.truncate(MAX_CELLS);
|
||||
let n = self.cols.len();
|
||||
self.rows.truncate((MAX_CELLS / n.max(1)).max(1));
|
||||
for r in &mut self.rows {
|
||||
r.cells.resize_with(n, || Cell::new(""));
|
||||
for c in &mut r.cells {
|
||||
@@ -513,6 +544,61 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each cell spans two rows, uncovered: merging the last two rows would grow the rectangle a
|
||||
/// row per pass over the whole grid, 10,000 passes. The merge is refused at its work bound
|
||||
/// instead, leaving the table as it was; a small merge still works.
|
||||
#[test]
|
||||
fn merge_expansion_stops_at_its_work_bound() {
|
||||
let mut t = Table::new(10_000, 10, 400.0, 30.0);
|
||||
for row in &mut t.rows {
|
||||
for c in &mut row.cells {
|
||||
c.row_span = 2;
|
||||
}
|
||||
}
|
||||
let before = t.clone();
|
||||
assert!(!t.merge(9_998, 0, 9_999, 9));
|
||||
assert_eq!(t, before);
|
||||
let mut small = Table::new(4, 4, 400.0, 30.0);
|
||||
assert!(small.merge(0, 0, 1, 1));
|
||||
assert!(small.is_normalized() && !Table { cols: vec![1.0; 3], rows: vec![Row::default(); 2], ..Default::default() }.is_normalized());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_caps_a_sparse_grid_at_max_cells() {
|
||||
// 1,000 columns and 1,000 empty rows would be a million cells.
|
||||
let mut t = Table { cols: vec![10.0; 1000], rows: vec![Row::default(); 1000], ..Default::default() };
|
||||
t.normalize();
|
||||
assert_eq!(t.cols.len(), 1000);
|
||||
assert_eq!(t.rows.len(), MAX_CELLS / 1000);
|
||||
assert!(t.rows.iter().all(|r| r.cells.len() == 1000));
|
||||
// Columns alone past the cap keep one row of MAX_CELLS cells.
|
||||
let mut t = Table { cols: vec![1.0; MAX_CELLS + 5], rows: vec![Row::default(); 3], ..Default::default() };
|
||||
t.normalize();
|
||||
assert_eq!((t.cols.len(), t.rows.len()), (MAX_CELLS, 1));
|
||||
// A normal table is untouched.
|
||||
let mut t = Table::new(4, 4, 400.0, 30.0);
|
||||
t.normalize();
|
||||
assert_eq!((t.n_rows(), t.n_cols()), (4, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_file_spans_rebuild_in_bounded_work() {
|
||||
// Every cell an uncovered anchor spanning to the far corner: their areas add up to about
|
||||
// n^4 / 4, but only what fits the grid is kept, so an edit's rebuild stays near n^2.
|
||||
let n = 200;
|
||||
let mut t = Table::new(n, n, 400.0, 30.0);
|
||||
for row in &mut t.rows {
|
||||
for c in &mut row.cells {
|
||||
c.grid_span = u32::MAX;
|
||||
c.row_span = u32::MAX;
|
||||
}
|
||||
}
|
||||
assert_eq!(t.merges().len(), 1, "the first anchor already covers the whole grid");
|
||||
t.insert_row(n);
|
||||
assert_eq!(t.n_rows(), n + 1);
|
||||
assert!(t.split(0, 0));
|
||||
}
|
||||
|
||||
fn merged() -> Table {
|
||||
// 4x4 with a 2x2 merge at (1,1) holding "M".
|
||||
let mut t = Table::new(4, 4, 400.0, 30.0);
|
||||
|
||||
Reference in New Issue
Block a user