From d94790d97a1518c231c3b7d5ead2be591a60d08b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:27:07 -0400 Subject: [PATCH 1/9] refactor: replace redundant closures with method paths (SonarCloud S1612) (#1261) Skipped where the receiver type is generic or not evident without a compiler (wrapping_neg, total_cmp, exists, to_vec, as_ref, split_first, Option::into_inner). Co-authored-by: Claude Sonnet 5.5 --- apps/deckcraft-cli/src/main.rs | 2 +- crates/av1/src/cdef.rs | 4 ++-- crates/av1/src/decoder.rs | 6 +++--- crates/av1/src/frame.rs | 6 +++--- crates/av1/src/inter.rs | 4 ++-- crates/engine/src/cmd/insert.rs | 4 ++-- crates/h264/src/deblock.rs | 6 +++--- crates/h264/src/picture.rs | 2 +- crates/h264/src/slicedec.rs | 6 +++--- crates/h264/src/synth_tests.rs | 2 +- crates/h264/src/transform.rs | 4 ++-- crates/hevc/src/inter.rs | 2 +- crates/metafile/src/tests.rs | 2 +- crates/pptx/tests/it/corpus_kinds.rs | 2 +- crates/render/src/morph_text.rs | 2 +- crates/ui-egui/src/backstage.rs | 6 +++--- crates/ui-egui/src/dialogs.rs | 2 +- crates/vp9/src/boolcoder.rs | 2 +- crates/vp9/src/decoder.rs | 2 +- crates/vp9/src/inter.rs | 8 ++++---- crates/vp9/src/transform.rs | 6 +++--- 21 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/deckcraft-cli/src/main.rs b/apps/deckcraft-cli/src/main.rs index c6276efb..a3c03ce2 100644 --- a/apps/deckcraft-cli/src/main.rs +++ b/apps/deckcraft-cli/src/main.rs @@ -407,7 +407,7 @@ fn frame_paths(out: &str, ts: &[f64]) -> Result, String> { return Ok(vec![out.to_string()]); } let p = std::path::Path::new(out); - let stem = p.file_stem().and_then(|x| x.to_str()).unwrap_or("frame"); + let stem = p.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("frame"); let mut paths: Vec = Vec::with_capacity(ts.len()); for t in ts { let path = p.with_file_name(format!("{stem}-t{t:.3}.png")).to_string_lossy().into_owned(); diff --git a/crates/av1/src/cdef.rs b/crates/av1/src/cdef.rs index abae4bc0..9860f65c 100644 --- a/crates/av1/src/cdef.rs +++ b/crates/av1/src/cdef.rs @@ -242,14 +242,14 @@ fn filter_rows( } for i in 0..h { let ci = ((i + 2) * PAD_STRIDE + 2) as isize; - let Some(&x) = b16.get(ci as usize..).and_then(|r| r.first_chunk::()) else { + let Some(&x) = b16.get(ci as usize..).and_then(<[_]>::first_chunk::) else { return; }; let (mut sum, mut max, mut min) = ([0i16; W], x, x); for &(off, weight, thr, adj) in taps { let (weight, thr) = (weight as i16, thr as i16); let start = (ci + off) as usize; - let Some(p) = b16.get(start..).and_then(|r| r.first_chunk::()) else { + let Some(p) = b16.get(start..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { diff --git a/crates/av1/src/decoder.rs b/crates/av1/src/decoder.rs index 06de1589..3e0aa237 100644 --- a/crates/av1/src/decoder.rs +++ b/crates/av1/src/decoder.rs @@ -61,7 +61,7 @@ impl Slot { } fn set(&self, v: Result) { - let mut g = self.value.lock().unwrap_or_else(|e| e.into_inner()); + let mut g = self.value.lock().unwrap_or_else(std::sync::PoisonError::into_inner); *g = Some(v); self.cv.notify_all(); } @@ -71,12 +71,12 @@ impl Slot { } fn wait(&self) -> Result { - let mut g = self.value.lock().unwrap_or_else(|e| e.into_inner()); + let mut g = self.value.lock().unwrap_or_else(std::sync::PoisonError::into_inner); loop { if let Some(v) = g.as_ref() { return v.clone(); } - g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner()); + g = self.cv.wait(g).unwrap_or_else(std::sync::PoisonError::into_inner); } } } diff --git a/crates/av1/src/frame.rs b/crates/av1/src/frame.rs index 3cf4b9f0..f3243551 100644 --- a/crates/av1/src/frame.rs +++ b/crates/av1/src/frame.rs @@ -16,7 +16,7 @@ const POOL_MAX_SAMPLES: usize = 96 << 20; fn zeroed(len: usize) -> Vec { if len >= POOL_MIN_LEN { let found = { - let mut pool = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner()); + let mut pool = PLANE_POOL.lock().unwrap_or_else(std::sync::PoisonError::into_inner); pool.iter().position(|v| v.capacity() >= len && v.capacity() <= 2 * len).map(|i| pool.swap_remove(i)) }; if let Some(mut v) = found { @@ -52,8 +52,8 @@ impl Drop for Plane { fn drop(&mut self) { if self.data.capacity() >= POOL_MIN_LEN { let v = std::mem::take(&mut self.data); - let mut pool = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner()); - let kept: usize = pool.iter().map(|b| b.capacity()).sum(); + let mut pool = PLANE_POOL.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + let kept: usize = pool.iter().map(Vec::capacity).sum(); if kept + v.capacity() <= POOL_MAX_SAMPLES { pool.push(v); } diff --git a/crates/av1/src/inter.rs b/crates/av1/src/inter.rs index 76c0ef1a..d3418a99 100644 --- a/crates/av1/src/inter.rs +++ b/crates/av1/src/inter.rs @@ -678,7 +678,7 @@ fn hpass(src: &[u16], hf: &[i16; 8], out: &mut [i32], half: i32, let mut acc = [half; W]; for t in 0..8 { let f = hf[t] as i32; - let Some(p) = src.get(t..).and_then(|s| s.first_chunk::()) else { + let Some(p) = src.get(t..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { @@ -699,7 +699,7 @@ fn vpass(rows: &[i32], vf: &[i16; 8], out: &mut [i32], half: i32 let mut acc = [half; W]; for t in 0..8 { let f = vf[t] as i32; - let Some(p) = rows.get(t * W..).and_then(|s| s.first_chunk::()) else { + let Some(p) = rows.get(t * W..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 4f28ddef..8bc45380 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -577,7 +577,7 @@ fn commit_album(s: &mut Session, items: Vec, opts: AlbumOpts, replace } fn album_norm(s: &str) -> String { - s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::().to_ascii_lowercase() + s.chars().filter(char::is_ascii_alphanumeric).collect::().to_ascii_lowercase() } fn album_layout(p: &Value) -> Result { @@ -770,7 +770,7 @@ fn image_px(bytes: &[u8]) -> (f64, f64) { } fn file_stem(name: &str) -> String { - let base = std::path::Path::new(name).file_name().and_then(|n| n.to_str()).unwrap_or(name); + let base = std::path::Path::new(name).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(name); let stem = match base.rsplit_once('.') { Some((s, ext)) if !s.is_empty() && !ext.is_empty() => s, _ => base, diff --git a/crates/h264/src/deblock.rs b/crates/h264/src/deblock.rs index ded8396c..0d815aff 100644 --- a/crates/h264/src/deblock.rs +++ b/crates/h264/src/deblock.rs @@ -232,7 +232,7 @@ fn filter_vertical(pix: &mut [u8], stride: usize, x: usize, y: u let mut w = [0u64; N]; for (i, w) in w.iter_mut().enumerate() { let o = (y + i) * stride + x - 4; - let Some(&b) = pix.get(o..).and_then(|p| p.first_chunk::<8>()) else { + let Some(&b) = pix.get(o..).and_then(<[_]>::first_chunk::<8>) else { return; }; *w = u64::from_le_bytes(b); @@ -259,7 +259,7 @@ fn filter_horizontal(pix: &mut [u8], stride: usize, x: usize, y: let base = (y - 4) * stride + x; let mut s = [[0i16; N]; 8]; for (k, row) in s.iter_mut().enumerate() { - let Some(src) = pix.get(base + k * stride..).and_then(|p| p.first_chunk::()) else { + let Some(src) = pix.get(base + k * stride..).and_then(<[_]>::first_chunk::) else { return; }; for i in 0..N { @@ -269,7 +269,7 @@ fn filter_horizontal(pix: &mut [u8], stride: usize, x: usize, y: filter_lanes(&mut s, ep, chroma); let (k0, k1) = if chroma { (3, 5) } else { (1, 7) }; for (k, row) in s.iter().enumerate().take(k1).skip(k0) { - let Some(dst) = pix.get_mut(base + k * stride..).and_then(|p| p.first_chunk_mut::()) else { + let Some(dst) = pix.get_mut(base + k * stride..).and_then(<[_]>::first_chunk_mut::) else { return; }; for i in 0..N { diff --git a/crates/h264/src/picture.rs b/crates/h264/src/picture.rs index 8a9f0a94..d97fa063 100644 --- a/crates/h264/src/picture.rs +++ b/crates/h264/src/picture.rs @@ -301,7 +301,7 @@ fn plane_allocator() -> fn(usize) -> Vec { fn copy_line(line: &[u8], x0: i32, out: &mut [u8], w: usize) { if w <= N && x0 >= 0 - && let Some(src) = line.get(x0 as usize..).and_then(|l| l.first_chunk::()) + && let Some(src) = line.get(x0 as usize..).and_then(<[_]>::first_chunk::) && let Some(dst) = out.first_chunk_mut::() { *dst = *src; diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index 10132d68..d1334808 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -907,7 +907,7 @@ impl<'a> SliceDecoder<'a> { let stride = self.pic.planes.width; let px = self.mb_x * 16 + (raster & 3) * 4; let py = self.mb_y * 16 + (raster >> 2) * 4; - let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(|c| c.first_chunk_mut::<16>()) else { + let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(<[_]>::first_chunk_mut::<16>) else { return; }; transform::idct4_add(blk, &mut self.pic.planes.y[py * stride + px..], stride); @@ -921,7 +921,7 @@ impl<'a> SliceDecoder<'a> { let stride = self.pic.planes.width; let px = self.mb_x * 16 + (b8 & 1) * 8; let py = self.mb_y * 16 + (b8 >> 1) * 8; - let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(|c| c.first_chunk_mut::<64>()) else { + let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(<[_]>::first_chunk_mut::<64>) else { return; }; transform::idct8_add(blk, &mut self.pic.planes.y[py * stride + px..], stride); @@ -940,7 +940,7 @@ impl<'a> SliceDecoder<'a> { } let px = self.mb_x * 8 + (b & 1) * 4; let py = self.mb_y * 8 + (b >> 1) * 4; - let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(|c| c.first_chunk_mut::<16>()) else { + let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(<[_]>::first_chunk_mut::<16>) else { continue; }; let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr }; diff --git a/crates/h264/src/synth_tests.rs b/crates/h264/src/synth_tests.rs index ec5f601d..a0c9da2f 100644 --- a/crates/h264/src/synth_tests.rs +++ b/crates/h264/src/synth_tests.rs @@ -107,7 +107,7 @@ struct Pic { impl Pic { fn is_p(&self) -> bool { - !self.idr && !self.b && self.mbs.iter().any(|m| m.is_none()) + !self.idr && !self.b && self.mbs.iter().any(Option::is_none) } fn inter(&self) -> bool { self.b || self.is_p() diff --git a/crates/h264/src/transform.rs b/crates/h264/src/transform.rs index f87a1c98..131ea98c 100644 --- a/crates/h264/src/transform.rs +++ b/crates/h264/src/transform.rs @@ -117,7 +117,7 @@ pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) { o[3][j] = e - h; } for (i, row) in o.iter().enumerate() { - let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<4>()) else { + let Some(line) = dst.get_mut(i * stride..).and_then(<[_]>::first_chunk_mut::<4>) else { return; }; for (p, &v) in line.iter_mut().zip(row) { @@ -173,7 +173,7 @@ pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) { } } for (i, row) in o.iter().enumerate() { - let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<8>()) else { + let Some(line) = dst.get_mut(i * stride..).and_then(<[_]>::first_chunk_mut::<8>) else { return; }; for (p, &v) in line.iter_mut().zip(row) { diff --git a/crates/hevc/src/inter.rs b/crates/hevc/src/inter.rs index de0f28e7..8d1b6751 100644 --- a/crates/hevc/src/inter.rs +++ b/crates/hevc/src/inter.rs @@ -25,7 +25,7 @@ fn filt_w(src: &[i16], step: usize, f: &[i8; T], let mut acc = [0i32; W]; for k in 0..T { let c = f[k] as i32; - let Some(p) = src.get(k * step..).and_then(|s| s.first_chunk::()) else { + let Some(p) = src.get(k * step..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 09e76413..b54f4ec2 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -1217,7 +1217,7 @@ fn savedc_restores_text_colour_and_font() { fn efont(height: i32, esc: i32, weight: i32, italic: u8, charset: u8, face: &str) -> Vec { let mut v = cat(vec![h32(1), e32(height), e32(0), e32(esc), e32(esc), e32(weight)]); v.extend([italic, 0, 0, charset, 0, 0, 0, 0]); - let mut name: Vec = face.encode_utf16().flat_map(|u| u.to_le_bytes()).collect(); + let mut name: Vec = face.encode_utf16().flat_map(u16::to_le_bytes).collect(); name.resize(64, 0); v.extend(name); erec(82, &v) diff --git a/crates/pptx/tests/it/corpus_kinds.rs b/crates/pptx/tests/it/corpus_kinds.rs index 56fc5adf..b229f3ff 100644 --- a/crates/pptx/tests/it/corpus_kinds.rs +++ b/crates/pptx/tests/it/corpus_kinds.rs @@ -437,7 +437,7 @@ fn next_rid(rels: &str) -> String { let mut max = 0u32; let mut rest = rels; while let Some(i) = rest.find("rId") { - let digits: String = rest[i + 3..].chars().take_while(|c| c.is_ascii_digit()).collect(); + let digits: String = rest[i + 3..].chars().take_while(char::is_ascii_digit).collect(); if let Ok(n) = digits.parse::() { max = max.max(n); } diff --git a/crates/render/src/morph_text.rs b/crates/render/src/morph_text.rs index 74cd4b7f..391ec0ba 100644 --- a/crates/render/src/morph_text.rs +++ b/crates/render/src/morph_text.rs @@ -208,7 +208,7 @@ pub(crate) fn draw( } } let unmatched_b = - tb.iter().enumerate().filter(|(j, _)| pairs.get(*j).is_none_or(|p| p.is_none())).flat_map(|(_, tok)| tok.glyphs.iter().copied()); + tb.iter().enumerate().filter(|(j, _)| pairs.get(*j).is_none_or(Option::is_none)).flat_map(|(_, tok)| tok.glyphs.iter().copied()); for g in unmatched_b.chain(loose_b) { if let Some((run, gid, pos)) = glyph_at(&lb, g) { draw_glyph(ctx, run, gid, place_b * Affine::translate(pos.to_vec2()), t); diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index 999900d3..774bb974 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -572,8 +572,8 @@ fn template_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize) -> /// File name and folder of a recent path, split as text (nothing on disk is read). pub fn split_path(path: &str) -> (String, String) { let p = std::path::Path::new(path); - let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(path).to_string(); - let folder = p.parent().and_then(|f| f.to_str()).unwrap_or("").to_string(); + let name = p.file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(path).to_string(); + let folder = p.parent().and_then(std::path::Path::to_str).unwrap_or("").to_string(); (name, folder) } @@ -827,7 +827,7 @@ fn info(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) { app.set_status("Copied the path"); } #[cfg(not(target_arch = "wasm32"))] - if let Some(dir) = std::path::Path::new(p).parent().and_then(|d| d.to_str()) + if let Some(dir) = std::path::Path::new(p).parent().and_then(std::path::Path::to_str) && ui.button("Open file location").clicked() { app.open_url(&format!("file:///{}", dir.replace('\\', "/").trim_start_matches('/'))); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index efe8a07f..1193efeb 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -272,7 +272,7 @@ fn pick_album_picture(app: &mut SlideApp) -> Option { app.set_status("That picture is larger than 32 MB"); return None; } - let name = std::path::Path::new(&path).file_name().and_then(|n| n.to_str()).filter(|s| !s.is_empty()).unwrap_or("picture").to_string(); + let name = std::path::Path::new(&path).file_name().and_then(std::ffi::OsStr::to_str).filter(|s| !s.is_empty()).unwrap_or("picture").to_string(); Some(json!({"name": name, "data": deckcraft_engine::cmd::base64_encode(&loaded)})) } diff --git a/crates/vp9/src/boolcoder.rs b/crates/vp9/src/boolcoder.rs index de56d5c1..73e0e1c5 100644 --- a/crates/vp9/src/boolcoder.rs +++ b/crates/vp9/src/boolcoder.rs @@ -29,7 +29,7 @@ impl<'a> BoolDecoder<'a> { fn fill(&mut self) { // Room for whole bytes below the top 8 bits and the `count` valid bits. let mut shift = 48 - self.count; - if let Some(&bytes) = self.data.get(self.pos..).and_then(|d| d.first_chunk::<8>()) { + if let Some(&bytes) = self.data.get(self.pos..).and_then(<[_]>::first_chunk::<8>) { let v = u64::from_be_bytes(bytes); let n = ((shift + 8) / 8) as usize; // bytes that fit let n = n.min(7); diff --git a/crates/vp9/src/decoder.rs b/crates/vp9/src/decoder.rs index 28e9d6d2..ce05d4b1 100644 --- a/crates/vp9/src/decoder.rs +++ b/crates/vp9/src/decoder.rs @@ -347,7 +347,7 @@ impl Decoder { let sz = if last { data.len() - pos } else { - let Some(&b) = data.get(pos..).and_then(|d| d.first_chunk::<4>()) else { + let Some(&b) = data.get(pos..).and_then(<[_]>::first_chunk::<4>) else { return Err(Error::Invalid("tile size beyond frame data".into())); }; let s = u32::from_be_bytes(b) as usize; diff --git a/crates/vp9/src/inter.rs b/crates/vp9/src/inter.rs index 14f2ee22..32188181 100644 --- a/crates/vp9/src/inter.rs +++ b/crates/vp9/src/inter.rs @@ -148,7 +148,7 @@ fn filter_h_n(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) { let mut acc = [64u16; W]; for t in 0..8 { let c = f[t] as u16; - let Some(p) = s.get(t..).and_then(|s| s.first_chunk::()) else { + let Some(p) = s.get(t..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { @@ -161,7 +161,7 @@ fn filter_h_n(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) { let mut acc = [64i32; W]; for t in 0..8 { let c = f[t] as i32; - let Some(p) = s.get(t..).and_then(|s| s.first_chunk::()) else { + let Some(p) = s.get(t..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { @@ -212,7 +212,7 @@ fn filter_v_n(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut let mut acc = [64u16; W]; for t in 0..8 { let c = f[t] as u16; - let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::()) else { + let Some(p) = s.get(t * ss..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { @@ -225,7 +225,7 @@ fn filter_v_n(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut let mut acc = [64i32; W]; for t in 0..8 { let c = f[t] as i32; - let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::()) else { + let Some(p) = s.get(t * ss..).and_then(<[_]>::first_chunk::) else { return; }; for j in 0..W { diff --git a/crates/vp9/src/transform.rs b/crates/vp9/src/transform.rs index 30658a9c..4e02ba11 100644 --- a/crates/vp9/src/transform.rs +++ b/crates/vp9/src/transform.rs @@ -161,7 +161,7 @@ pub fn inverse_transform_add( if lossless { let mut t = [0i32; 16]; for i in 0..4 { - let c = coefs.get(i * 4..).and_then(|c| c.first_chunk::<4>()).copied().unwrap_or_default(); + let c = coefs.get(i * 4..).and_then(<[_]>::first_chunk::<4>).copied().unwrap_or_default(); let r = iwht4(c, 2); t[i * 4..i * 4 + 4].copy_from_slice(&r); } @@ -235,7 +235,7 @@ fn itx_narrow(coefs: &[i32], p: &TxParams, dst: &mut [u16], stri add_cols(&y, 0, p, dst, stride); } else { for c0 in (0..N).step_by(8) { - let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i].get(c0..).and_then(|r| r.first_chunk::<8>()).copied().unwrap_or_default())); + let x: [Cols<8>; N] = std::array::from_fn(|i| Cols(t[i].get(c0..).and_then(<[_]>::first_chunk::<8>).copied().unwrap_or_default())); let y = col_tx8(x, p.col_adst); add_cols(&y, c0, p, dst, stride); } @@ -246,7 +246,7 @@ fn itx_narrow(coefs: &[i32], p: &TxParams, dst: &mut [u16], stri #[inline(always)] fn add_cols(y: &[Cols; N], c0: usize, p: &TxParams, dst: &mut [u16], stride: usize) { for (i, v) in y.iter().enumerate() { - let Some(d) = dst.get_mut(i * stride + c0..).and_then(|d| d.first_chunk_mut::()) else { + let Some(d) = dst.get_mut(i * stride + c0..).and_then(<[_]>::first_chunk_mut::) else { return; }; for j in 0..W { From 1e097e3edd62739fe19e21608cd731d4e2f781c9 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:40:26 -0400 Subject: [PATCH 2/9] security: use exclusive temporary directories for macOS cert import and media fixtures (#1263) --- crates/media/tests/fixtures/make.sh | 10 ++++++---- packaging/macos/import-cert.sh | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/media/tests/fixtures/make.sh b/crates/media/tests/fixtures/make.sh index 083284a4..d85419b9 100755 --- a/crates/media/tests/fixtures/make.sh +++ b/crates/media/tests/fixtures/make.sh @@ -28,11 +28,13 @@ for f in range(6): sys.stdout.buffer.write(bytes(v for y in range(h) for x in range(w) for v in ((x * 4 + f * 20) % 256, (y * 5) % 256, (f * 40) % 256))) " > "$out" } -frames 64 48 /tmp/deckcraft-fixture-64x48.rgb -frames 64 64 /tmp/deckcraft-fixture-64x64.rgb -VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i /tmp/deckcraft-fixture-64x48.rgb" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/deckcraft-fixture.XXXXXX")" +trap 'rm -rf "$TMP_DIR"' EXIT +frames 64 48 "$TMP_DIR/deckcraft-fixture-64x48.rgb" +frames 64 64 "$TMP_DIR/deckcraft-fixture-64x64.rgb" +VID="-f rawvideo -pix_fmt rgb24 -s 64x48 -r 10 -i $TMP_DIR/deckcraft-fixture-64x48.rgb" $FF $VID $TONE44 -c:v libx264 -pix_fmt yuv420p -g 5 -bf 2 -c:a aac -b:a 48k -shortest $META clip-h264.mp4 $FF $VID -c:v libx264 -pix_fmt yuv420p -g 5 -bf 0 $META clip-h264.mov $FF $VID -c:v libx265 -pix_fmt yuv420p -x265-params log-level=error:keyint=5 -tag:v hvc1 $META clip-hevc.mp4 $FF $VID $TONE -c:v libvpx-vp9 -pix_fmt yuv420p -g 5 -b:v 100k -c:a libopus -b:a 32k -shortest $META clip-vp9.webm -$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i /tmp/deckcraft-fixture-64x64.rgb -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv +$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i "$TMP_DIR/deckcraft-fixture-64x64.rgb" -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh index 401cc301..901d3095 100755 --- a/packaging/macos/import-cert.sh +++ b/packaging/macos/import-cert.sh @@ -17,12 +17,14 @@ if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then exit 0 fi -TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" -KEYCHAIN="$TMP/deckcraft-signing.keychain-db" -CERT="$TMP/deckcraft-signing.p12" +TMP_BASE="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +TMP_DIR="$(mktemp -d "$TMP_BASE/deckcraft-cert.XXXXXX")" +chmod 0700 "$TMP_DIR" +KEYCHAIN="$TMP_DIR/deckcraft-signing.keychain-db" +CERT="$TMP_DIR/deckcraft-signing.p12" KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}" -trap 'rm -f "$CERT"' EXIT +trap 'rm -rf "$TMP_DIR"' EXIT printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT" security create-keychain -p "$KC_PASS" "$KEYCHAIN" security set-keychain-settings -lut 21600 "$KEYCHAIN" From 9641817a15e87d47e0b0e74ecfcb671dbc95babe Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:40:29 -0400 Subject: [PATCH 3/9] security: clamp alpha_num bullet expansion and sanitize filename logging (#1264) --- apps/deckcraft/src/apple_events.rs | 5 +++-- crates/text/src/lib.rs | 7 +++---- crates/text/src/tests.rs | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/deckcraft/src/apple_events.rs b/apps/deckcraft/src/apple_events.rs index 4b583ad0..172aff44 100644 --- a/apps/deckcraft/src/apple_events.rs +++ b/apps/deckcraft/src/apple_events.rs @@ -41,8 +41,9 @@ pub fn poll(inbox: &Inbox, app: &mut SlideApp, ctx: &egui::Context) { Event::Open(paths) => { for p in paths.iter().filter_map(|p| p.to_str()) { if let Err(e) = app.open_path(p) { - log::error!("{p}: {e}"); - app.set_status(format!("Couldn't open {p}: {e}")); + let safe_p = p.replace(['\r', '\n'], " "); + log::error!("{safe_p}: {e}"); + app.set_status(format!("Couldn't open {safe_p}: {e}")); } } } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 6b17d004..e70c3304 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -572,11 +572,10 @@ fn alpha_num(n: u32, upper: bool) -> String { if n == 0 { return String::new(); } - let mut n = n - 1; + let n = n - 1; let c = (b'a' + (n % 26) as u8) as char; - let reps = n / 26 + 1; - n = reps; - let s: String = std::iter::repeat_n(c, n as usize).collect(); + let reps = (n / 26 + 1).min(50); + let s: String = std::iter::repeat_n(c, reps as usize).collect(); if upper { s.to_uppercase() } else { s } } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index f3c1ccf4..0df5db4e 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -394,6 +394,7 @@ fn autonum_formats() { assert_eq!(autonum("alphaLcParenR", 2), "b)"); assert_eq!(autonum("arabicParenBoth", 1), "(1)"); assert_eq!(autonum("alphaUcPeriod", 27), "AA."); + assert_eq!(autonum("alphaUcPeriod", u32::MAX).len(), 51); // 50 letters + '.' } #[test] From 713f605566bf46a5ce75bfeae1d8be59a83ce24e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:41:23 -0400 Subject: [PATCH 4/9] perf(chart): optimize axis plotted extent scan and cache non-bar error bar stats (#1266) --- crates/model/src/chart.rs | 47 ++++++++++++++++++++++++++------------ crates/render/src/chart.rs | 3 ++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/crates/model/src/chart.rs b/crates/model/src/chart.rs index 98214f88..4492576d 100644 --- a/crates/model/src/chart.rs +++ b/crates/model/src/chart.rs @@ -609,27 +609,44 @@ impl Chart { return (!secondary && self.kind != ChartType::Combo).then_some((0.0, 0.0)); } let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY); - let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0); + let has_stacked_or_percent = ser.iter().any(|s| { + let k = self.series_kind(s); + k.stacked() || k.percent() + }); + let mut percent = false; - for i in 0..n { - let (mut p, mut m, mut stacked) = (0.0, 0.0, false); + if has_stacked_or_percent { + let n = ser.iter().map(|s| s.values.len()).max().unwrap_or(0); + let budget = (ser.len() * n).min(10_000_000); + let n_clamped = if ser.is_empty() { 0 } else { n.min(budget / ser.len()) }; + + for i in 0..n_clamped { + let (mut p, mut m, mut stacked) = (0.0, 0.0, false); + for s in &ser { + let k = self.series_kind(s); + let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue }; + if k.percent() { + percent = true; + } else if k.stacked() { + stacked = true; + if v >= 0.0 { p += v } else { m += v } + } else { + hi = hi.max(v); + lo = lo.min(v); + } + } + if stacked { + hi = hi.max(p); + lo = lo.min(m); + } + } + } else { for s in &ser { - let k = self.series_kind(s); - let Some(v) = s.values.get(i).copied().flatten().filter(|v| v.is_finite()) else { continue }; - if k.percent() { - percent = true; - } else if k.stacked() { - stacked = true; - if v >= 0.0 { p += v } else { m += v } - } else { + for v in s.values.iter().flatten().copied().filter(|v| v.is_finite()) { hi = hi.max(v); lo = lo.min(v); } } - if stacked { - hi = hi.max(p); - lo = lo.min(m); - } } if !percent { for s in &ser { diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index ac185780..3a3f06bf 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -696,6 +696,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, let stacked = kind.stacked(); let area_kind = matches!(kind, ChartType::Area | ChartType::StackedArea | ChartType::FilledRadar); let col = if area_kind { series_fill(rctx, c, si, Some(s)) } else { series_line(rctx, c, si, s) }; + let series_stats = s.error_bars.as_ref().map(|_| deckcraft_model::chart::SeriesStats::of(&s.values)); let mut pts = vec![]; for i in 0..ncat { let Some(v) = s.values.get(i).copied().flatten() else { continue }; @@ -719,7 +720,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, } _ => plot.x0 + band * (slot(i) + 0.5), }; - if let Some((lo, hi)) = s.error_bars.as_ref().and_then(|e| e.range(&s.values, i, v)) { + if let Some((lo, hi)) = s.error_bars.as_ref().and_then(|e| series_stats.as_ref().and_then(|st| e.range_in(st, i, v))) { err_marks.push((x, to_axis(sec, lo), to_axis(sec, hi), base * 0.4, s.error_bars.as_ref().is_some_and(|e| e.end_cap))); } pts.push(Point::new(x, to_axis(sec, v))); From 6189fa8bf602992570ede598f7ee79aa9e0c54ef Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:41:43 -0400 Subject: [PATCH 5/9] perf(engine): bound HTML entity semicolon scan window and formatting stack search (#1267) --- crates/engine/src/cmd/richclip.rs | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/engine/src/cmd/richclip.rs b/crates/engine/src/cmd/richclip.rs index 8b21b4bb..05c3255a 100644 --- a/crates/engine/src/cmd/richclip.rs +++ b/crates/engine/src/cmd/richclip.rs @@ -107,14 +107,16 @@ pub fn html_to_body(html: &str) -> Option { } if closing { let kind = tag_kind(&name); - if !kind.is_empty() - && let Some(pos) = stack.iter().rposition(|e| e.0 == kind) - { - let (_, db, di, du) = stack[pos]; - bold -= db; - italic -= di; - underline -= du; - stack.truncate(pos); + if !kind.is_empty() { + let search_start = stack.len().saturating_sub(64); + if let Some(offset) = stack[search_start..].iter().rposition(|e| e.0 == kind) { + let pos = search_start + offset; + let (_, db, di, du) = stack[pos]; + bold -= db; + italic -= di; + underline -= du; + stack.truncate(pos); + } } if is_block(&name) { new_para(&mut paragraphs); @@ -130,7 +132,7 @@ pub fn html_to_body(html: &str) -> Option { bold += db; italic += di; underline += du; - if !token.ends_with('/') { + if !token.ends_with('/') && stack.len() < 128 { stack.push((tag_kind(&name), db, di, du)); } else { bold -= db; @@ -302,9 +304,11 @@ fn decode_text(raw: &str) -> String { while let Some(at) = rest.find('&') { out.push_str(&collapse_space(&rest[..at])); rest = &rest[at..]; - let Some(end) = rest.find(';') else { - out.push_str(&collapse_space(rest)); - break; + let search_window = rest.len().min(32); + let Some(end) = rest[..search_window].find(';') else { + out.push('&'); + rest = &rest[1..]; + continue; }; let entity = &rest[1..end]; let decoded = match entity { @@ -391,6 +395,10 @@ mod tests { assert_eq!(text("

R&D costs; up

"), "R&D costs; up"); assert_eq!(text("

AT&T; Q&A

"), "AT&T; Q&A"); assert_eq!(text("

A & B & C & D

"), "A & B & C & D"); + + // Verify many bare ampersands do not cause quadratic scan freeze + let hostile = format!("

{}

", "&".repeat(2000)); + assert_eq!(text(&hostile), "&".repeat(2000)); } #[test] From 54962c63891ac25533bb2adde673c71a2244ea43 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:43:36 -0400 Subject: [PATCH 6/9] refactor: explicit imports for table modules (SonarCloud S2208) (#1262) * refactor: replace wildcard table imports with explicit names (SonarCloud S2208) Co-Authored-By: Claude Sonnet 5.5 * style: rustfmt Co-Authored-By: Claude Sonnet 5.5 --------- Co-authored-by: Claude Sonnet 5.5 --- crates/anim/src/transition_fx.rs | 5 ++++- crates/av1/src/cdef.rs | 2 +- crates/av1/src/cdf.rs | 22 +++++++++++++++++++++- crates/av1/src/decoder.rs | 6 +++++- crates/av1/src/header.rs | 9 ++++++++- crates/av1/src/inter.rs | 11 ++++++++++- crates/av1/src/inter_info.rs | 9 ++++++++- crates/av1/src/intra.rs | 6 +++++- crates/av1/src/mvpred.rs | 5 ++++- crates/av1/src/postfilter.rs | 5 ++++- crates/av1/src/restoration.rs | 6 +++++- crates/av1/src/state.rs | 2 +- crates/av1/src/tile.rs | 22 +++++++++++++++++++++- crates/av1/src/transform.rs | 5 ++++- crates/h264/src/cabac_mb.rs | 2 +- crates/h264/src/cavlc.rs | 2 +- crates/h264/src/params.rs | 2 +- crates/h264/src/slicedec.rs | 6 +++--- crates/hevc/src/slicedec.rs | 6 +++++- crates/hevc/src/synth_tests.rs | 2 +- crates/opus/src/celt/bands.rs | 2 +- crates/opus/src/celt/energy.rs | 2 +- crates/opus/src/celt/mod.rs | 2 +- crates/opus/src/celt/rate.rs | 2 +- crates/opus/src/silk/lpc.rs | 5 ++++- crates/opus/src/silk/mod.rs | 9 +++++++-- crates/vp9/src/decoder.rs | 2 +- crates/vp9/src/header.rs | 5 ++++- crates/vp9/src/intra.rs | 2 +- crates/vp9/src/loopfilter.rs | 5 ++++- crates/vp9/src/probs.rs | 9 ++++++++- crates/vp9/src/tile.rs | 9 ++++++++- crates/vp9/src/tile/recon.rs | 5 ++++- 33 files changed, 158 insertions(+), 36 deletions(-) diff --git a/crates/anim/src/transition_fx.rs b/crates/anim/src/transition_fx.rs index 3bbc4a8d..f55866ce 100644 --- a/crates/anim/src/transition_fx.rs +++ b/crates/anim/src/transition_fx.rs @@ -9,7 +9,10 @@ use std::f64::consts::PI; use crate::easing::ease; use crate::transition::{FULL, Layer, R, Source}; -use crate::transition_geom::*; +use crate::transition_geom::{ + Cam, V3, boundary, card, center, clip, dir, edge, ein, eout, facing_shade, fan, field, hash64, moved, offset, poly_layers, pow_inout, q2, + rect_poly, rnd, rot, rot_quad, scale_rect, scaled, soft_rect, ss, v3, win, +}; use Source::{Black, New, NewBack, NewContent, Old, OldBack, OldContent, White}; diff --git a/crates/av1/src/cdef.rs b/crates/av1/src/cdef.rs index 9860f65c..0a919351 100644 --- a/crates/av1/src/cdef.rs +++ b/crates/av1/src/cdef.rs @@ -1,7 +1,7 @@ //! CDEF (7.15). use crate::frame::{FrameBuf, Plane}; -use crate::spec_tables::*; +use crate::spec_tables::{CDEF_DIRECTIONS, CDEF_PRI_TAPS, CDEF_SEC_TAPS, CDEF_UV_DIR, DIV_TABLE}; use crate::state::FrameState; /// Run CDEF on `fs.cur`; returns the CdefFrame, or None when no block is filtered (CdefFrame diff --git a/crates/av1/src/cdf.rs b/crates/av1/src/cdf.rs index a2d5e761..c8dec19c 100644 --- a/crates/av1/src/cdf.rs +++ b/crates/av1/src/cdf.rs @@ -1,6 +1,26 @@ //! The adaptive CDF arrays (spec 6.8.2 init_non_coeff_cdfs / init_coeff_cdfs, load_cdfs). -use crate::spec_tables::*; +use crate::spec_tables::{ + DEFAULT_ANGLE_DELTA_CDF, DEFAULT_CFL_ALPHA_CDF, DEFAULT_CFL_SIGN_CDF, DEFAULT_COEFF_BASE_CDF, DEFAULT_COEFF_BASE_EOB_CDF, DEFAULT_COEFF_BR_CDF, + DEFAULT_COMP_BWD_REF_CDF, DEFAULT_COMP_GROUP_IDX_CDF, DEFAULT_COMP_MODE_CDF, DEFAULT_COMP_REF_CDF, DEFAULT_COMP_REF_TYPE_CDF, + DEFAULT_COMPOUND_IDX_CDF, DEFAULT_COMPOUND_MODE_CDF, DEFAULT_COMPOUND_TYPE_CDF, DEFAULT_DC_SIGN_CDF, DEFAULT_DELTA_LF_CDF, DEFAULT_DELTA_Q_CDF, + DEFAULT_DRL_MODE_CDF, DEFAULT_EOB_EXTRA_CDF, DEFAULT_EOB_PT_16_CDF, DEFAULT_EOB_PT_32_CDF, DEFAULT_EOB_PT_64_CDF, DEFAULT_EOB_PT_128_CDF, + DEFAULT_EOB_PT_256_CDF, DEFAULT_EOB_PT_512_CDF, DEFAULT_EOB_PT_1024_CDF, DEFAULT_FILTER_INTRA_CDF, DEFAULT_FILTER_INTRA_MODE_CDF, + DEFAULT_INTER_INTRA_CDF, DEFAULT_INTER_INTRA_MODE_CDF, DEFAULT_INTER_TX_TYPE_SET1_CDF, DEFAULT_INTER_TX_TYPE_SET2_CDF, + DEFAULT_INTER_TX_TYPE_SET3_CDF, DEFAULT_INTERP_FILTER_CDF, DEFAULT_INTRA_TX_TYPE_SET1_CDF, DEFAULT_INTRA_TX_TYPE_SET2_CDF, DEFAULT_INTRABC_CDF, + DEFAULT_IS_INTER_CDF, DEFAULT_MOTION_MODE_CDF, DEFAULT_MV_BIT_CDF, DEFAULT_MV_CLASS_CDF, DEFAULT_MV_CLASS0_BIT_CDF, DEFAULT_MV_CLASS0_FR_CDF, + DEFAULT_MV_CLASS0_HP_CDF, DEFAULT_MV_FR_CDF, DEFAULT_MV_HP_CDF, DEFAULT_MV_JOINT_CDF, DEFAULT_MV_SIGN_CDF, DEFAULT_NEW_MV_CDF, + DEFAULT_PALETTE_SIZE_2_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_2_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_3_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_3_Y_COLOR_CDF, + DEFAULT_PALETTE_SIZE_4_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_4_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_5_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_5_Y_COLOR_CDF, + DEFAULT_PALETTE_SIZE_6_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_6_Y_COLOR_CDF, DEFAULT_PALETTE_SIZE_7_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_7_Y_COLOR_CDF, + DEFAULT_PALETTE_SIZE_8_UV_COLOR_CDF, DEFAULT_PALETTE_SIZE_8_Y_COLOR_CDF, DEFAULT_PALETTE_UV_MODE_CDF, DEFAULT_PALETTE_UV_SIZE_CDF, + DEFAULT_PALETTE_Y_MODE_CDF, DEFAULT_PALETTE_Y_SIZE_CDF, DEFAULT_PARTITION_W8_CDF, DEFAULT_PARTITION_W16_CDF, DEFAULT_PARTITION_W32_CDF, + DEFAULT_PARTITION_W64_CDF, DEFAULT_PARTITION_W128_CDF, DEFAULT_REF_MV_CDF, DEFAULT_RESTORATION_TYPE_CDF, DEFAULT_SEGMENT_ID_CDF, + DEFAULT_SEGMENT_ID_PREDICTED_CDF, DEFAULT_SINGLE_REF_CDF, DEFAULT_SKIP_CDF, DEFAULT_SKIP_MODE_CDF, DEFAULT_TX_8X8_CDF, DEFAULT_TX_16X16_CDF, + DEFAULT_TX_32X32_CDF, DEFAULT_TX_64X64_CDF, DEFAULT_TXB_SKIP_CDF, DEFAULT_TXFM_SPLIT_CDF, DEFAULT_UNI_COMP_REF_CDF, DEFAULT_USE_OBMC_CDF, + DEFAULT_USE_SGRPROJ_CDF, DEFAULT_USE_WIENER_CDF, DEFAULT_UV_MODE_CFL_ALLOWED_CDF, DEFAULT_UV_MODE_CFL_NOT_ALLOWED_CDF, DEFAULT_WEDGE_INDEX_CDF, + DEFAULT_WEDGE_INTER_INTRA_CDF, DEFAULT_Y_MODE_CDF, DEFAULT_ZERO_MV_CDF, FRAME_LF_COUNT, MV_CONTEXTS, +}; /// Every CDF array listed by init_non_coeff_cdfs and init_coeff_cdfs. Each innermost array holds /// N cumulative values (the last is 32768) plus the adaptation counter. diff --git a/crates/av1/src/decoder.rs b/crates/av1/src/decoder.rs index 3e0aa237..b60f94dd 100644 --- a/crates/av1/src/decoder.rs +++ b/crates/av1/src/decoder.rs @@ -17,7 +17,11 @@ use crate::cdf::Cdfs; use crate::frame::FrameBuf; use crate::header::{FilmGrainParams, FrameHeader, HeaderState, LoopFilterDeltas, RefInfo, SegmentationFeatures, SequenceHeader, default_gm_params}; use crate::par::Pool; -use crate::spec_tables::*; +use crate::spec_tables::{ + ALTREF_FRAME, ALTREF2_FRAME, BWDREF_FRAME, DEFAULT_INTRA_FRAME_Y_MODE_CDF, DIV_MULT, GOLDEN_FRAME, INTRA_FRAME, INTRA_ONLY_FRAME, KEY_FRAME, + LAST_FRAME, LAST2_FRAME, MAX_FRAME_DISTANCE, MAX_OFFSET_HEIGHT, MAX_OFFSET_WIDTH, MFMV_STACK_SIZE, NUM_REF_FRAMES, PRIMARY_REF_NONE, + REFMVS_LIMIT, REFS_PER_FRAME, +}; use crate::state::{FrameShared, FrameState, TileState}; use crate::stats::{DecodeStats, Stage, Timer}; use crate::tile::TileDecoder; diff --git a/crates/av1/src/header.rs b/crates/av1/src/header.rs index 16cccd9e..e89876c7 100644 --- a/crates/av1/src/header.rs +++ b/crates/av1/src/header.rs @@ -1,7 +1,14 @@ //! Sequence header (5.5) and uncompressed frame header (5.9) parsing. use crate::bits::BitReader; -use crate::spec_tables::*; +use crate::spec_tables::{ + AFFINE, ALTREF_FRAME, ALTREF2_FRAME, BWDREF_FRAME, GM_ABS_ALPHA_BITS, GM_ABS_TRANS_BITS, GM_ABS_TRANS_ONLY_BITS, GM_ALPHA_PREC_BITS, + GM_TRANS_ONLY_PREC_BITS, GM_TRANS_PREC_BITS, GOLDEN_FRAME, IDENTITY, INTER_FRAME, INTRA_ONLY_FRAME, KEY_FRAME, LAST_FRAME, MAX_SEGMENTS, + MAX_TILE_AREA, MAX_TILE_COLS, MAX_TILE_ROWS, MAX_TILE_WIDTH, NUM_REF_FRAMES, ONLY_4X4, PRIMARY_REF_NONE, REF_FRAME_LIST, REFS_PER_FRAME, + RESTORATION_TILESIZE_MAX, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_SWITCHABLE, RESTORE_WIENER, ROTZOOM, SEG_LVL_ALT_Q, SEG_LVL_MAX, + SEG_LVL_REF_FRAME, SELECT_INTEGER_MV, SELECT_SCREEN_CONTENT_TOOLS, SUPERRES_DENOM_BITS, SUPERRES_DENOM_MIN, SUPERRES_NUM, SWITCH_FRAME, + TOTAL_REFS_PER_FRAME, TRANSLATION, TX_MODE_LARGEST, TX_MODE_SELECT, WARPEDMODEL_PREC_BITS, +}; use crate::{Error, Result}; pub const EIGHTTAP: u8 = 0; diff --git a/crates/av1/src/inter.rs b/crates/av1/src/inter.rs index d3418a99..f14e59b9 100644 --- a/crates/av1/src/inter.rs +++ b/crates/av1/src/inter.rs @@ -5,7 +5,16 @@ use std::sync::OnceLock; use crate::frame::Plane; use crate::mvpred::round2_signed64; -use crate::spec_tables::*; +use crate::spec_tables::{ + BLOCK_8X8, BLOCK_SIZES, COMPOUND_AVERAGE, COMPOUND_DIFFWTD, COMPOUND_DISTANCE, COMPOUND_INTRA, COMPOUND_WEDGE, DIV_LUT, DIV_LUT_BITS, + DIV_LUT_PREC_BITS, EIGHTTAP, EIGHTTAP_SHARP, EIGHTTAP_SMOOTH, FILTER_BITS, GLOBAL_GLOBALMV, GLOBALMV, II_H_PRED, II_SMOOTH_PRED, II_V_PRED, + II_WEIGHTS_1D, INTRA_FRAME, LAST_FRAME, LOCALWARP, LS_MV_MAX, MASK_MASTER_SIZE, MAX_FRAME_DISTANCE, MAX_SB_SIZE, MI_HEIGHT_LOG2, MI_WIDTH_LOG2, + NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, OBMC, OBMC_MASK_2, OBMC_MASK_4, OBMC_MASK_8, OBMC_MASK_16, OBMC_MASK_32, QUANT_DIST_LOOKUP, + QUANT_DIST_WEIGHT, REF_SCALE_SHIFT, SCALE_SUBPEL_BITS, SUBPEL_BITS, SUBPEL_FILTERS, SUBPEL_MASK, TRANSLATION, WARP_PARAM_REDUCE_BITS, + WARPED_FILTERS, WARPEDDIFF_PREC_BITS, WARPEDMODEL_NONDIAGAFFINE_CLAMP, WARPEDMODEL_PREC_BITS, WARPEDMODEL_TRANS_CLAMP, WARPEDPIXEL_PREC_SHIFTS, + WEDGE_BITS, WEDGE_CODEBOOK, WEDGE_HORIZONTAL, WEDGE_MASTER_OBLIQUE_EVEN, WEDGE_MASTER_OBLIQUE_ODD, WEDGE_MASTER_VERTICAL, WEDGE_OBLIQUE27, + WEDGE_OBLIQUE63, WEDGE_OBLIQUE117, WEDGE_OBLIQUE153, WEDGE_TYPES, WEDGE_VERTICAL, +}; use crate::tile::TileDecoder; const PRED_STRIDE: usize = 128; diff --git a/crates/av1/src/inter_info.rs b/crates/av1/src/inter_info.rs index 6f9b2744..d4b25e8d 100644 --- a/crates/av1/src/inter_info.rs +++ b/crates/av1/src/inter_info.rs @@ -3,7 +3,14 @@ use crate::Result; use crate::header::{EIGHTTAP, SWITCHABLE}; use crate::mvpred::MvP; -use crate::spec_tables::*; +use crate::spec_tables::{ + ALTREF_FRAME, ALTREF2_FRAME, BLOCK_8X8, BLOCK_32X32, BLOCK_64X64, BLOCK_128X128, BWDREF_FRAME, CLASS0_SIZE, COMP_NEWMV_CTXS, COMPOUND_AVERAGE, + COMPOUND_DIFFWTD, COMPOUND_DISTANCE, COMPOUND_INTRA, COMPOUND_MODE_CTX_MAP, COMPOUND_REFERENCE, COMPOUND_WEDGE, GLOBAL_GLOBALMV, GLOBALMV, + GOLDEN_FRAME, INTRA_FRAME, INTRABC_DELAY_PIXELS, LAST_FRAME, LAST2_FRAME, LAST3_FRAME, LOCALWARP, MV_CLASS_0, MV_INTRABC_CONTEXT, + MV_JOINT_HNZVNZ, MV_JOINT_HNZVZ, MV_JOINT_HZVNZ, NEAR_NEARMV, NEAR_NEWMV, NEAREST_NEARESTMV, NEAREST_NEWMV, NEARESTMV, NEARMV, NEW_NEARESTMV, + NEW_NEARMV, NEW_NEWMV, NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, OBMC, REF_SCALE_SHIFT, SEG_LVL_GLOBALMV, SEG_LVL_REF_FRAME, SEG_LVL_SKIP, + SIMPLE, SINGLE_REFERENCE, SIZE_GROUP, TRANSLATION, UNIDIR_COMP_REFERENCE, UV_CFL_PRED, WEDGE_BITS, +}; use crate::tile::TileDecoder; fn block_width(bs: usize) -> usize { diff --git a/crates/av1/src/intra.rs b/crates/av1/src/intra.rs index 893bb86f..e80d611b 100644 --- a/crates/av1/src/intra.rs +++ b/crates/av1/src/intra.rs @@ -1,7 +1,11 @@ //! Intra prediction (spec 7.11.2), palette prediction (7.11.4) and chroma from luma (7.11.5). use crate::frame::Plane; -use crate::spec_tables::*; +use crate::spec_tables::{ + ANGLE_STEP, D67_PRED, DC_PRED, DR_INTRA_DERIVATIVE, INTRA_EDGE_KERNEL, INTRA_EDGE_TAPS, INTRA_FILTER_SCALE_BITS, INTRA_FILTER_TAPS, + MODE_TO_ANGLE, SM_WEIGHTS_TX_4X4, SM_WEIGHTS_TX_8X8, SM_WEIGHTS_TX_16X16, SM_WEIGHTS_TX_32X32, SM_WEIGHTS_TX_64X64, SMOOTH_H_PRED, SMOOTH_PRED, + SMOOTH_V_PRED, V_PRED, +}; use crate::transform::round2; /// Inputs of the intra prediction process that come from the block. diff --git a/crates/av1/src/mvpred.rs b/crates/av1/src/mvpred.rs index d281003d..6339d5ee 100644 --- a/crates/av1/src/mvpred.rs +++ b/crates/av1/src/mvpred.rs @@ -1,7 +1,10 @@ //! Motion vector prediction processes (spec 7.10): find_mv_stack, has_overlappable_candidates //! and find_warp_samples. -use crate::spec_tables::*; +use crate::spec_tables::{ + GLOBAL_GLOBALMV, GLOBALMV, IDENTITY, INTRA_FRAME, LEAST_SQUARES_SAMPLES_MAX, MAX_REF_MV_STACK_SIZE, MV_BORDER, NEAR_NEWMV, NEAREST_NEWMV, + NEW_NEARESTMV, NEW_NEARMV, NEW_NEWMV, NEWMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, REF_CAT_LEVEL, TRANSLATION, WARPEDMODEL_PREC_BITS, +}; use crate::tile::TileDecoder; pub(crate) type MvP = [i32; 2]; diff --git a/crates/av1/src/postfilter.rs b/crates/av1/src/postfilter.rs index ea157de5..4d9f9719 100644 --- a/crates/av1/src/postfilter.rs +++ b/crates/av1/src/postfilter.rs @@ -1,7 +1,10 @@ //! In-loop post filters run by decode_frame_wrapup: loop filter (7.14), CDEF (7.15), //! super-resolution upscaling (7.16) and loop restoration (7.17). -use crate::spec_tables::*; +use crate::spec_tables::{ + GLOBAL_GLOBALMV, GLOBALMV, INTRA_FRAME, MAX_LOOP_FILTER, NEARESTMV, NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, SEG_LVL_ALT_LF_Y_V, TX_HEIGHT, + TX_WIDTH, +}; use crate::state::FrameState; use crate::stats::{DecodeStats, Stage, Timer}; diff --git a/crates/av1/src/restoration.rs b/crates/av1/src/restoration.rs index 693e2728..0b829733 100644 --- a/crates/av1/src/restoration.rs +++ b/crates/av1/src/restoration.rs @@ -1,7 +1,11 @@ //! Super-resolution upscaling (7.16) and loop restoration (7.17). use crate::frame::{FrameBuf, Plane}; -use crate::spec_tables::*; +use crate::spec_tables::{ + FILTER_BITS, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_WIENER, SGR_PARAMS, SGRPROJ_MTABLE_BITS, SGRPROJ_PRJ_BITS, SGRPROJ_RECIP_BITS, + SGRPROJ_RST_BITS, SGRPROJ_SGR_BITS, SUPERRES_EXTRA_BITS, SUPERRES_FILTER_OFFSET, SUPERRES_FILTER_TAPS, SUPERRES_SCALE_BITS, SUPERRES_SCALE_MASK, + UPSCALE_FILTER, +}; use crate::state::FrameState; /// Upscale a frame horizontally from FrameWidth to UpscaledWidth (7.16). diff --git a/crates/av1/src/state.rs b/crates/av1/src/state.rs index df600c6d..5a89c9b4 100644 --- a/crates/av1/src/state.rs +++ b/crates/av1/src/state.rs @@ -9,7 +9,7 @@ use crate::decoder::RefFrame; use crate::frame::{FrameBuf, MiInfo}; use crate::header::{FrameHeader, RefInfo, SequenceHeader}; -use crate::spec_tables::*; +use crate::spec_tables::{DEFAULT_INTRA_FRAME_Y_MODE_CDF, NUM_REF_FRAMES, RESTORE_NONE, SUBSAMPLED_SIZE}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; diff --git a/crates/av1/src/tile.rs b/crates/av1/src/tile.rs index c8446795..3c11e71f 100644 --- a/crates/av1/src/tile.rs +++ b/crates/av1/src/tile.rs @@ -6,7 +6,27 @@ use crate::cdf::Cdfs; use crate::frame::{MiInfo, Mv}; use crate::header::{FrameHeader, NONE, SequenceHeader}; use crate::intra::{IntraParams, is_directional_mode, predict_intra, round2_signed}; -use crate::spec_tables::*; +use crate::spec_tables::{ + AC_QLOOKUP, ADJUSTED_TX_SIZE, ADST_FLIPADST, BLOCK_4X4, BLOCK_8X8, BLOCK_64X64, BLOCK_128X128, BR_CDF_SIZE, CFL_SIGN_NEG, CFL_SIGN_ZERO, + COEFF_BASE_CTX_OFFSET, COEFF_BASE_POS_CTX_OFFSET, COEFF_BASE_RANGE, COMPOUND_AVERAGE, DC_PRED, DC_QLOOKUP, DCT_DCT, DCT_FLIPADST, + DEFAULT_SCAN_4X4, DEFAULT_SCAN_4X8, DEFAULT_SCAN_4X16, DEFAULT_SCAN_8X4, DEFAULT_SCAN_8X8, DEFAULT_SCAN_8X16, DEFAULT_SCAN_8X32, + DEFAULT_SCAN_16X4, DEFAULT_SCAN_16X8, DEFAULT_SCAN_16X16, DEFAULT_SCAN_16X32, DEFAULT_SCAN_32X8, DEFAULT_SCAN_32X16, DEFAULT_SCAN_32X32, + DELTA_LF_SMALL, DELTA_Q_SMALL, FILTER_INTRA_MODE_TO_INTRA_DIR, FLIPADST_ADST, FLIPADST_DCT, FLIPADST_FLIPADST, FRAME_LF_COUNT, H_ADST, H_DCT, + H_FLIPADST, H_PRED, IDTX, II_DC_PRED, II_H_PRED, II_V_PRED, INTRA_FRAME, INTRA_MODE_CONTEXT, MAG_REF_OFFSET_WITH_TX_CLASS, MAX_ANGLE_DELTA, + MAX_LOOP_FILTER, MAX_TX_DEPTH_TABLE, MAX_TX_SIZE_RECT, MAX_VARTX_DEPTH, MCOL_SCAN_4X4, MCOL_SCAN_4X8, MCOL_SCAN_4X16, MCOL_SCAN_8X4, + MCOL_SCAN_8X8, MCOL_SCAN_8X16, MCOL_SCAN_16X4, MCOL_SCAN_16X8, MCOL_SCAN_16X16, MI_HEIGHT_LOG2, MI_WIDTH_LOG2, MODE_TO_TXFM, MROW_SCAN_4X4, + MROW_SCAN_4X8, MROW_SCAN_4X16, MROW_SCAN_8X4, MROW_SCAN_8X8, MROW_SCAN_8X16, MROW_SCAN_16X4, MROW_SCAN_16X8, MROW_SCAN_16X16, + NUM_4X4_BLOCKS_HIGH, NUM_4X4_BLOCKS_WIDE, NUM_BASE_LEVELS, PALETTE_COLOR_CONTEXT, PALETTE_COLOR_HASH_MULTIPLIERS, PALETTE_COLORS, + PALETTE_NUM_NEIGHBORS, PARTITION_HORZ, PARTITION_HORZ_4, PARTITION_HORZ_A, PARTITION_HORZ_B, PARTITION_NONE, PARTITION_SPLIT, PARTITION_SUBSIZE, + PARTITION_VERT, PARTITION_VERT_4, PARTITION_VERT_A, PARTITION_VERT_B, QM_OFFSET, QUANTIZER_MATRIX, RESTORE_NONE, RESTORE_SGRPROJ, RESTORE_WIENER, + SEG_LVL_SKIP, SGR_PARAMS, SGRPROJ_PARAMS_BITS, SGRPROJ_PRJ_BITS, SGRPROJ_PRJ_SUBEXP_K, SIG_REF_DIFF_OFFSET, SIG_REF_DIFF_OFFSET_NUM, SIMPLE, + SMOOTH_H_PRED, SMOOTH_PRED, SMOOTH_V_PRED, SPLIT_TX_SIZE, SUPERRES_NUM, TX_4X4, TX_4X8, TX_4X16, TX_8X4, TX_8X8, TX_8X16, TX_8X32, TX_16X4, + TX_16X8, TX_16X16, TX_16X32, TX_16X64, TX_32X8, TX_32X16, TX_32X32, TX_32X64, TX_64X16, TX_64X32, TX_64X64, TX_CLASS_2D, TX_CLASS_HORIZ, + TX_CLASS_VERT, TX_HEIGHT, TX_HEIGHT_LOG2, TX_MODE_SELECT, TX_SET_DCTONLY, TX_SET_INTER_1, TX_SET_INTER_2, TX_SET_INTER_3, TX_SET_INTRA_1, + TX_SET_INTRA_2, TX_SIZE_SQR, TX_SIZE_SQR_UP, TX_SIZES, TX_SIZES_ALL, TX_TYPE_IN_SET_INTER, TX_TYPE_IN_SET_INTRA, TX_TYPE_INTER_INV_SET1, + TX_TYPE_INTER_INV_SET2, TX_TYPE_INTER_INV_SET3, TX_TYPE_INTRA_INV_SET1, TX_TYPE_INTRA_INV_SET2, TX_WIDTH, TX_WIDTH_LOG2, UV_CFL_PRED, V_ADST, + V_DCT, V_FLIPADST, V_PRED, +}; use crate::state::{FrameShared, TileState}; use crate::symbol::SymbolDecoder; use crate::transform::inverse_transform_2d; diff --git a/crates/av1/src/transform.rs b/crates/av1/src/transform.rs index 626cefe8..afa649a4 100644 --- a/crates/av1/src/transform.rs +++ b/crates/av1/src/transform.rs @@ -1,6 +1,9 @@ //! Inverse transforms (spec 7.13), implemented step by step as specified. -use crate::spec_tables::*; +use crate::spec_tables::{ + ADST_ADST, ADST_DCT, ADST_FLIPADST, COS128_LOOKUP, DCT_ADST, DCT_DCT, DCT_FLIPADST, FLIPADST_ADST, FLIPADST_DCT, FLIPADST_FLIPADST, H_ADST, + H_DCT, H_FLIPADST, TRANSFORM_ROW_SHIFT, TX_HEIGHT_LOG2, TX_WIDTH_LOG2, V_ADST, V_DCT, V_FLIPADST, +}; #[inline(always)] fn round2_64(x: i64, n: u32) -> i32 { diff --git a/crates/h264/src/cabac_mb.rs b/crates/h264/src/cabac_mb.rs index f1bf6f0d..78c257e9 100644 --- a/crates/h264/src/cabac_mb.rs +++ b/crates/h264/src/cabac_mb.rs @@ -3,7 +3,7 @@ use crate::cabac::Cabac; use crate::error::{Result, ensure, invalid}; -use crate::mbtypes::*; +use crate::mbtypes::{MbTypeInfo, Part, SubMbInfo, b_mb_type, b_sub_mb_type, intra_mb_type, p_mb_type, p_sub_mb_type}; use crate::picture::MbKind; use crate::slice::SliceType; use crate::slicedec::{MbCur, SliceDecoder}; diff --git a/crates/h264/src/cavlc.rs b/crates/h264/src/cavlc.rs index 9711d258..a72ff50c 100644 --- a/crates/h264/src/cavlc.rs +++ b/crates/h264/src/cavlc.rs @@ -1,6 +1,6 @@ //! CAVLC residual block parsing (9.2). -use crate::cavlc_tables::*; +use crate::cavlc_tables::{COEFF_TOKEN, RUN_BEFORE, TOTAL_ZEROS_2X2, TOTAL_ZEROS_2X4, TOTAL_ZEROS_4X4}; use crate::error::{Result, ensure}; use deckcraft_bitstream::BitReader; use std::sync::OnceLock; diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs index 461d316b..8b36b3ab 100644 --- a/crates/h264/src/params.rs +++ b/crates/h264/src/params.rs @@ -1,7 +1,7 @@ //! Sequence and picture parameter sets (7.3.2.1, 7.3.2.2) including VUI and scaling lists. use crate::error::{Result, ensure, unsupported}; -use crate::tables::*; +use crate::tables::{DEFAULT_4X4_INTER, DEFAULT_4X4_INTRA, DEFAULT_8X8_INTER, DEFAULT_8X8_INTRA, ZIGZAG4, ZIGZAG8}; use deckcraft_bitstream::BitReader; /// Scaling matrices after applying fall-back rules, stored in raster order (weightScale). diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index d1334808..19fa0615 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -5,11 +5,11 @@ use crate::deblock; use crate::error::{Result, ensure, invalid}; use crate::inter::{self, Weight}; use crate::intra::{self, Avail}; -use crate::mbtypes::*; +use crate::mbtypes::{CBP_ME, MbTypeInfo, Part, SubMbInfo, SubPart, b_mb_type, b_sub_mb_type, intra_mb_type, p_mb_type, p_sub_mb_type}; use crate::params::{Pps, Sps}; -use crate::picture::*; +use crate::picture::{Frame, FrameRef, MbKind, MbState, Planes, RefPic}; use crate::slice::{SliceHeader, SliceType}; -use crate::tables::*; +use crate::tables::{BLK4_XY, QPC_TABLE, RASTER_TO_BLK4, ZIGZAG4, ZIGZAG8}; use crate::transform::{self, LevelScale}; use deckcraft_bitstream::BitReader; diff --git a/crates/hevc/src/slicedec.rs b/crates/hevc/src/slicedec.rs index fb83ccb6..48afc3b5 100644 --- a/crates/hevc/src/slicedec.rs +++ b/crates/hevc/src/slicedec.rs @@ -10,7 +10,11 @@ use crate::intra::{self, Refs}; use crate::params::{Layout, Pps, Sps}; use crate::picture::{FrameRef, RefPic}; use crate::slice::{SliceHeader, SliceType}; -use crate::spec_tables::*; +use crate::spec_tables::{ + ABS_MVD_GT0, ABS_MVD_GT1, CBF_CHROMA, CBF_LUMA, CODED_SUB_BLOCK, CU_QP_DELTA_ABS, CU_SKIP, CU_TRANSQUANT_BYPASS, GT1, GT2, INTER_PRED_IDC, + INTRA_CHROMA, LAST_X_PREFIX, LAST_Y_PREFIX, MERGE_FLAG, MERGE_IDX, MVP_FLAG, PART_MODE, PRED_MODE, PREV_INTRA_LUMA, REF_IDX, RQT_ROOT_CBF, + SAO_MERGE, SAO_TYPE, SIG_COEFF, SPLIT_CU, SPLIT_TRANSFORM, TRANSFORM_SKIP, +}; use crate::tables::{CTX_IDX_MAP, LEVEL_SCALE, SCAN_4, qpc_420, scan_order}; use crate::transform; use std::sync::Arc; diff --git a/crates/hevc/src/synth_tests.rs b/crates/hevc/src/synth_tests.rs index dc225571..4c2f1c96 100644 --- a/crates/hevc/src/synth_tests.rs +++ b/crates/hevc/src/synth_tests.rs @@ -7,7 +7,7 @@ use crate::Decoder; use crate::cabac::{Contexts, NEXT_STATE, RANGE_TAB_LPS, init_contexts}; -use crate::spec_tables::*; +use crate::spec_tables::{CU_SKIP, NUM_CTX, PRED_MODE, SPLIT_CU}; use deckcraft_bitstream::{BitWriter, escape_rbsp}; const W: usize = 96; diff --git a/crates/opus/src/celt/bands.rs b/crates/opus/src/celt/bands.rs index 6d0bd3f0..f4b764d7 100644 --- a/crates/opus/src/celt/bands.rs +++ b/crates/opus/src/celt/bands.rs @@ -2,7 +2,7 @@ //! (RFC 6716 §4.3.4 – §4.3.6). use super::rate::{Mode, QTHETA_OFFSET, QTHETA_OFFSET_TWOPHASE, get_pulses}; -use super::tables::*; +use super::tables::{E_MEANS, EBANDS, NB_EBANDS, ORDERY, SHORT_MDCT}; use crate::range::{BITRES, RangeDecoder, ilog}; pub const SPREAD_NONE: usize = 0; diff --git a/crates/opus/src/celt/energy.rs b/crates/opus/src/celt/energy.rs index 9260a5bf..3ac9453b 100644 --- a/crates/opus/src/celt/energy.rs +++ b/crates/opus/src/celt/energy.rs @@ -2,7 +2,7 @@ //! (RFC 6716 §4.3.2). use super::rate::MAX_FINE_BITS; -use super::tables::*; +use super::tables::{BETA_COEF, BETA_INTRA, E_PROB_MODEL, NB_EBANDS, PRED_COEF, SMALL_ENERGY_ICDF}; use crate::range::RangeDecoder; const LAPLACE_MINP: u32 = 1; diff --git a/crates/opus/src/celt/mod.rs b/crates/opus/src/celt/mod.rs index 3f68641c..0cdb61cb 100644 --- a/crates/opus/src/celt/mod.rs +++ b/crates/opus/src/celt/mod.rs @@ -12,7 +12,7 @@ use bands::{anti_collapse, denormalise_bands, quant_all_bands, renormalise_vecto use energy::{unquant_coarse_energy, unquant_energy_finalise, unquant_fine_energy}; use mdct::ImdctScratch; use rate::{Mode, compute_allocation}; -use tables::*; +use tables::{COMB_GAINS, EBANDS, MAX_LM, NB_EBANDS, OVERLAP, PREEMPH, SHORT_MDCT, SPREAD_ICDF, TAPSET_ICDF, TF_SELECT_TABLE, TRIM_ICDF}; use crate::range::{BITRES, RangeDecoder}; diff --git a/crates/opus/src/celt/rate.rs b/crates/opus/src/celt/rate.rs index f5103faa..c857fc37 100644 --- a/crates/opus/src/celt/rate.rs +++ b/crates/opus/src/celt/rate.rs @@ -1,6 +1,6 @@ //! Pulse cache and bit allocation (RFC 6716 §4.3.3, §4.3.4.1). -use super::tables::*; +use super::tables::{ALLOC, EBANDS, LOG2_FRAC_TABLE, MAX_LM, NB_EBANDS, OVERLAP}; use crate::range::{BITRES, RangeDecoder}; const MAX_PSEUDO: i32 = 40; diff --git a/crates/opus/src/silk/lpc.rs b/crates/opus/src/silk/lpc.rs index 29a1e811..32a6d023 100644 --- a/crates/opus/src/silk/lpc.rs +++ b/crates/opus/src/silk/lpc.rs @@ -1,7 +1,10 @@ //! Fixed-point NLSF decoding, NLSF→LPC conversion and filter stabilisation //! (RFC 6716 §4.2.7.5, with the RFC 8251 §6/§7 overflow fixes). -use super::tables::*; +use super::tables::{ + COS_Q12, LSF_ORDER_NBMB, LSF_ORDER_WB, NLSF_CB_NBMB, NLSF_CB_WB, NLSF_MIN_SPACING_NBMB, NLSF_MIN_SPACING_WB, NLSF_PRED_NBMB, NLSF_PRED_WB, + NLSF_PSEL_NBMB, NLSF_PSEL_WB, +}; pub const MAX_LPC_ORDER: usize = 16; diff --git a/crates/opus/src/silk/mod.rs b/crates/opus/src/silk/mod.rs index 15f5de91..e6e096e6 100644 --- a/crates/opus/src/silk/mod.rs +++ b/crates/opus/src/silk/mod.rs @@ -9,9 +9,14 @@ mod resampler; #[allow(dead_code)] pub(crate) mod tables; -use lpc::*; +use lpc::{MAX_LPC_ORDER, bwexpander, div32_varq, inverse32_varq, nlsf_decode, nlsf2a, rshift_round, sat16, smlawb, smulwb, smulww}; use resampler::Resampler; -use tables::*; +use tables::{ + CB_NB_10, CB_NB_20, CB_WB_10, CB_WB_20, DELTA_GAIN, EXC_LSB, EXC_SIGN, GAIN_MSB, LBRR_FLAGS, LTP_FILTER, LTP_PERIODICITY, LTP_SCALE, LTP_TAPS_0, + LTP_TAPS_1, LTP_TAPS_2, MID_ONLY, NLSF_EXT, NLSF_INTERP, NLSF_SEL_NBMB, NLSF_SEL_WB, NLSF_STAGE1, NLSF_STAGE2, PITCH_CONTOUR, PITCH_DELTA, + PITCH_HIGH, PITCH_LOW, PULSE_COUNT, RATE_LEVEL, SHELL_2, SHELL_4, SHELL_8, SHELL_16, STEREO_PRED_JOINT, STEREO_WEIGHTS_Q13, TYPE_ACTIVE, + TYPE_INACTIVE, UNIFORM3, UNIFORM4, UNIFORM5, UNIFORM8, +}; use crate::range::RangeDecoder; use crate::{Error, Result}; diff --git a/crates/vp9/src/decoder.rs b/crates/vp9/src/decoder.rs index ce05d4b1..9f132244 100644 --- a/crates/vp9/src/decoder.rs +++ b/crates/vp9/src/decoder.rs @@ -8,7 +8,7 @@ use crate::frame::{BAND_PAD, Band, Frame, FrameInfo, MiGrid, MiInfo, Pools}; use crate::header::{FrameHeader, HeaderState, KEY_FRAME, RefInfo, parse_compressed, parse_uncompressed, split_superframe}; use crate::loopfilter::{LfFrame, PlaneView, filter_superblock}; use crate::probs::{Counts, adapt_coef_probs, adapt_noncoef_probs}; -use crate::tables::*; +use crate::tables::{AC_QLOOKUP, DC_QLOOKUP, SEG_LVL_ALT_Q, SWITCHABLE, TX_MODE_SELECT}; use crate::tile::{FrameShared, RefUse, Strip, TileDecoder}; use crate::{ColorInfo, Picture, Plane}; use std::collections::VecDeque; diff --git a/crates/vp9/src/header.rs b/crates/vp9/src/header.rs index afb119b6..82f28e28 100644 --- a/crates/vp9/src/header.rs +++ b/crates/vp9/src/header.rs @@ -4,7 +4,10 @@ use crate::boolcoder::BoolDecoder; use crate::error::{Error, Result, ensure}; use crate::probs::FrameContext; -use crate::tables::*; +use crate::tables::{ + ALTREF_FRAME, GOLDEN_FRAME, INV_MAP_TABLE, LAST_FRAME, LITERAL_TO_TYPE, MAX_SEGMENTS, MAX_TILE_WIDTH_B64, MIN_TILE_WIDTH_B64, + SEGMENTATION_FEATURE_BITS, SEGMENTATION_FEATURE_SIGNED, SWITCHABLE, TX_MODE_SELECT, TX_MODE_TO_BIGGEST_TX_SIZE, +}; use deckcraft_bitstream::BitReader; pub const KEY_FRAME: u8 = 0; diff --git a/crates/vp9/src/intra.rs b/crates/vp9/src/intra.rs index 9c797eea..642a28b1 100644 --- a/crates/vp9/src/intra.rs +++ b/crates/vp9/src/intra.rs @@ -1,6 +1,6 @@ //! Intra prediction process (8.5.1). -use crate::tables::*; +use crate::tables::{D45_PRED, D63_PRED, D117_PRED, D135_PRED, D153_PRED, D207_PRED, H_PRED, TM_PRED, V_PRED}; /// Edge availability and geometry of one intra-predicted transform block. pub struct IntraEdge { diff --git a/crates/vp9/src/loopfilter.rs b/crates/vp9/src/loopfilter.rs index 6d1a240c..fe92c6a4 100644 --- a/crates/vp9/src/loopfilter.rs +++ b/crates/vp9/src/loopfilter.rs @@ -2,7 +2,10 @@ use crate::frame::{MiGrid, MiInfo}; use crate::header::{LoopFilterParams, Segmentation}; -use crate::tables::*; +use crate::tables::{ + BLOCK_8X8, BLOCK_16X16, INTRA_FRAME, MAX_LOOP_FILTER, MAX_TXSIZE, NEARESTMV, NEARMV, NEWMV, NUM_8X8_HIGH, NUM_8X8_WIDE, SEG_LVL_ALT_L, + SS_SIZE_LOOKUP, TX_4X4, TX_8X8, TX_16X16, +}; /// Frame-level loop filter setup (8.8.1) plus the per-level limits of 8.8.4. pub struct LfFrame { diff --git a/crates/vp9/src/probs.rs b/crates/vp9/src/probs.rs index 5ac3a394..70fc888a 100644 --- a/crates/vp9/src/probs.rs +++ b/crates/vp9/src/probs.rs @@ -1,7 +1,14 @@ //! Probability tables (a "frame context"), syntax element counters and the backward adaptation //! processes of section 8.4. -use crate::tables::*; +use crate::tables::{ + BINARY_TREE, COUNT_SAT, DEFAULT_COEF_PROBS, DEFAULT_COMP_MODE_PROB, DEFAULT_COMP_REF_PROB, DEFAULT_INTER_MODE_PROBS, DEFAULT_INTERP_FILTER_PROBS, + DEFAULT_IS_INTER_PROB, DEFAULT_MV_BITS_PROB, DEFAULT_MV_CLASS_PROBS, DEFAULT_MV_CLASS0_BIT_PROB, DEFAULT_MV_CLASS0_FR_PROBS, + DEFAULT_MV_CLASS0_HP_PROB, DEFAULT_MV_FR_PROBS, DEFAULT_MV_HP_PROB, DEFAULT_MV_JOINT_PROBS, DEFAULT_MV_SIGN_PROB, DEFAULT_PARTITION_PROBS, + DEFAULT_SINGLE_REF_PROB, DEFAULT_SKIP_PROB, DEFAULT_TX_PROBS, DEFAULT_UV_MODE_PROBS, DEFAULT_Y_MODE_PROBS, INTER_MODE_TREE, INTERP_FILTER_TREE, + INTRA_MODE_TREE, MAX_UPDATE_FACTOR, MV_CLASS_TREE, MV_FR_TREE, MV_JOINT_TREE, PARTITION_TREE, SMALL_TOKEN_TREE, TX_SIZE_8_TREE, TX_SIZE_16_TREE, + TX_SIZE_32_TREE, +}; /// coef_probs[txSz][plane > 0][is_inter][band][ctx][node]. pub type CoefProbs = [[[[[[u8; 3]; 6]; 6]; 2]; 2]; 4]; diff --git a/crates/vp9/src/tile.rs b/crates/vp9/src/tile.rs index 7d9827d0..0efc93ab 100644 --- a/crates/vp9/src/tile.rs +++ b/crates/vp9/src/tile.rs @@ -6,7 +6,14 @@ use crate::boolcoder::BoolDecoder; use crate::frame::{Frame, MiGrid, MiInfo, Mv}; use crate::header::{FrameHeader, Segmentation}; use crate::probs::{Counts, FrameContext}; -use crate::tables::*; +use crate::tables::{ + ALTREF_FRAME, B_HEIGHT_LOG2, B_WIDTH_LOG2, BLOCK_8X8, BLOCK_64X64, BLOCK_INVALID, BORDERINPIXELS, COMPANDED_MVREF_THRESH, COUNTER_TO_CONTEXT, + DC_PRED, GOLDEN_FRAME, IDX_N_COLUMN_TO_SUBBLOCK, INTER_MODE_TREE, INTERP_EXTEND, INTERP_FILTER_TREE, INTRA_FRAME, INTRA_MODE_TREE, + KF_PARTITION_PROBS, KF_UV_MODE_PROBS, KF_Y_MODE_PROBS, LAST_FRAME, MAX_TXSIZE, MI_WIDTH_LOG2, MODE_2_COUNTER, MV_BORDER, MV_CLASS_TREE, + MV_FR_TREE, MV_JOINT_TREE, MV_REF_BLOCKS, NEARESTMV, NEARMV, NEWMV, NONE, NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, PARTITION_TREE, + SEG_LVL_REF_FRAME, SEG_LVL_SKIP, SEGMENT_TREE, SIZE_GROUP, SS_SIZE_LOOKUP, SUBSIZE_LOOKUP, SWITCHABLE, TX_MODE_SELECT, + TX_MODE_TO_BIGGEST_TX_SIZE, TX_SIZE_8_TREE, TX_SIZE_16_TREE, TX_SIZE_32_TREE, ZEROMV, +}; mod recon; diff --git a/crates/vp9/src/tile/recon.rs b/crates/vp9/src/tile/recon.rs index 99c32280..a38da45c 100644 --- a/crates/vp9/src/tile/recon.rs +++ b/crates/vp9/src/tile/recon.rs @@ -7,7 +7,10 @@ use crate::error::Error; use crate::frame::Mv; use crate::inter::{self, RefPlane}; use crate::intra::{self, IntraEdge}; -use crate::tables::*; +use crate::tables::{ + BLOCK_8X8, CAT_PROBS, COEFBAND_4X4, COEFBAND_8X8PLUS, DCT_DCT, ENERGY_CLASS, EXTRA_BITS, INTERP_EXTEND, INTRA_FRAME, MAX_TXSIZE, MODE2TXFM, + NUM_4X4_HIGH, NUM_4X4_WIDE, NUM_8X8_HIGH, NUM_8X8_WIDE, SS_SIZE_LOOKUP, TX_4X4, TX_32X32, neighbors, pareto_full, scan, +}; use crate::transform; /// round_mv_comp_q2 / q4 (8.5.2.1). From c955407373605c012ed77c86fe9f1316e4715350 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 04:45:48 -0400 Subject: [PATCH 7/9] security(engine): check regular file and size bound on read, preserve permissions and symlinks on save (#1265) * security(engine): check regular file and size bound on read, preserve permissions and symlinks on save * style: format recovery.rs write_atomic --- crates/engine/src/io.rs | 64 ++++++++++++++++++++++++++++++++--- crates/engine/src/recovery.rs | 14 +++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index c19efc0f..7d21bb4b 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -8,6 +8,14 @@ #[cfg(not(target_arch = "wasm32"))] pub fn read_file(path: &str) -> Result, String> { crate::file_access::check_read(path)?; + let meta = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?; + if !meta.is_file() { + return Err(format!("{path}: not a regular file")); + } + const MAX_FILE_SIZE: u64 = 2 << 30; // 2 GiB + if meta.len() > MAX_FILE_SIZE { + return Err(format!("{path}: file size {} exceeds 2 GiB limit", meta.len())); + } std::fs::read(path).map_err(|e| format!("{path}: {e}")) } @@ -28,21 +36,29 @@ pub fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> { static COUNTER: AtomicU64 = AtomicU64::new(0); crate::file_access::check_write(path)?; let err = |e: std::io::Error| format!("{path}: {e}"); - // A uniquely named temporary sibling, created exclusively (never following or reusing an - // existing file or symlink), is renamed over the destination: concurrent saves never share - // temporary bytes. + + let target_path = match std::fs::canonicalize(path) { + Ok(canonical) => canonical, + Err(_) => std::path::PathBuf::from(path), + }; + let target_str = target_path.to_str().unwrap_or(path); + let existing_perms = std::fs::metadata(&target_path).ok().map(|m| m.permissions()); + let mut attempts = 0; let (tmp, mut file) = loop { - let tmp = format!("{path}.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed)); + let tmp = format!("{target_str}.{}.{}.saving", std::process::id(), COUNTER.fetch_add(1, Ordering::Relaxed)); match std::fs::OpenOptions::new().write(true).create_new(true).open(&tmp) { Ok(f) => break (tmp, f), Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists && attempts < 16 => attempts += 1, Err(e) => return Err(err(e)), } }; + if let Some(perms) = existing_perms { + let _ = file.set_permissions(perms); + } let written = file.write_all(bytes).and_then(|()| file.sync_all()); drop(file); - let result = written.and_then(|()| std::fs::rename(&tmp, path)); + let result = written.and_then(|()| std::fs::rename(&tmp, &target_path)); if let Err(e) = result { let _ = std::fs::remove_file(&tmp); return Err(err(e)); @@ -67,3 +83,41 @@ pub fn file_len(path: &str) -> Option { pub fn file_len(_path: &str) -> Option { None } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_file_rejects_directory() { + let dir = std::env::temp_dir(); + let res = read_file(dir.to_str().unwrap()); + assert!(res.is_err()); + } + + #[test] + fn write_file_preserves_permissions_and_writes_content() { + let tmp_dir = std::env::temp_dir().join("deckcraft_io_test"); + let _ = std::fs::create_dir_all(&tmp_dir); + let test_file = tmp_dir.join("test_write.txt"); + let path_str = test_file.to_str().unwrap(); + + assert!(write_file(path_str, b"hello world").is_ok()); + let read = read_file(path_str).unwrap(); + assert_eq!(read, b"hello world"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o640); + std::fs::set_permissions(&test_file, perms).unwrap(); + + assert!(write_file(path_str, b"updated world").is_ok()); + let new_meta = std::fs::metadata(&test_file).unwrap(); + assert_eq!(new_meta.permissions().mode() & 0o777, 0o640); + } + + let _ = std::fs::remove_file(&test_file); + let _ = std::fs::remove_dir(&tmp_dir); + } +} diff --git a/crates/engine/src/recovery.rs b/crates/engine/src/recovery.rs index ac35cb13..6c5c0230 100644 --- a/crates/engine/src/recovery.rs +++ b/crates/engine/src/recovery.rs @@ -85,8 +85,20 @@ fn owner_alive(dir: &Path, owner: u64) -> bool { /// Write `bytes` to `path` through a temporary file and a rename, so a crash never leaves it torn. fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> { + use std::io::Write; let tmp = path.with_extension(format!("{}.tmp", path.extension().and_then(|e| e.to_str()).unwrap_or(""))); - std::fs::write(&tmp, bytes).and_then(|_| std::fs::rename(&tmp, path)).map_err(|e| EngineError::Other(format!("{}: {e}", path.display()))) + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut file = opts.open(&tmp).map_err(|e| EngineError::Other(format!("{}: {e}", tmp.display())))?; + file.write_all(bytes).and_then(|()| file.sync_all()).and_then(|()| std::fs::rename(&tmp, path)).map_err(|e| { + let _ = std::fs::remove_file(&tmp); + EngineError::Other(format!("{}: {e}", path.display())) + }) } /// Presentations reopened per start; the rest stay on disk for the next one, so a folder From c55a9f771e4d403d1e31f67b1fa8331a00378335 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 05:06:04 -0400 Subject: [PATCH 8/9] security(metafile): harden EMF+ parsing, clipping complexity, and memory accounting (#1280) * fix(metafile): harden EMF+ parsing, clipping complexity, and memory accounting - Row 72: Use checked tail slicing in comment header offset calculations - Row 78: Break FillRects and DrawRects loops early when canvas is full - Row 75: Pre-check dimensions in decode_compressed before RGBA pixel allocation - Row 85: Account for EMF+ object table image/path memory against MAX_BYTES - Row 42: Replace O(N^2) prefix copying in wrap_line with linear measurement - Row 43 & 54: Share immutable clip regions Arc across placed items and charge canvas regions - Row 48: Bound text transformation scale and glyph curve geometry in Picture::place and draw_text - Row 52 & 68: Impose work budget bounds on Sutherland-Hodgman polygon clipping and bitmap masking * fix(metafile): collapse nested if statement per clippy --- crates/metafile/src/canvas.rs | 3 +- crates/metafile/src/cliprect.rs | 10 ++++ crates/metafile/src/emfplus.rs | 99 +++++++++++++++++++++++++++------ crates/metafile/src/place.rs | 49 ++++++++++++---- crates/metafile/src/tests.rs | 24 ++++++++ crates/render/src/metafile.rs | 15 +++-- 6 files changed, 168 insertions(+), 32 deletions(-) diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index f286dc61..c6441bca 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -390,6 +390,7 @@ impl Canvas { return; } raw.set_region(self.clip.clone()); + let region_cost = self.clip.as_ref().map_or(0, |c| c.len() * std::mem::size_of::<[f64; 4]>()); let cost = match &raw { Raw::Path { ops, .. } => ops.len() * std::mem::size_of::(), // A tile shared with its brush is counted once (by the brush); one made for this fill @@ -400,7 +401,7 @@ impl Canvas { } Raw::Bitmap { img, .. } => img.px.len(), Raw::Text { text: t, .. } => t.text.len() + t.dx.len() * 4 + t.font.family.len() + 128, - }; + } + region_cost; if self.items.len() >= MAX_ITEMS || self.used.saturating_add(cost) > MAX_BYTES { self.full = true; } else { diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs index 396f4402..fd9dfc14 100644 --- a/crates/metafile/src/cliprect.rs +++ b/crates/metafile/src/cliprect.rs @@ -100,8 +100,14 @@ fn is_rect(clip: &[P]) -> bool { /// Sutherland–Hodgman: `poly` cut by convex `clip`. pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec

{ + if poly.len().saturating_mul(clip.len()) > 1_000_000 { + return Vec::new(); + } let mut poly = poly.to_vec(); for i in 0..clip.len() { + if poly.len().saturating_mul(clip.len() - i) > 1_000_000 { + return Vec::new(); + } let (a, b) = (clip[i], clip[(i + 1) % clip.len()]); let keep = |p: P| cross(a, b, p) >= 0.0; let cut = |p: P, q: P| { @@ -307,6 +313,10 @@ fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> { return None; } let mask = !is_rect(clip); + let total_pixels = (x1 - x0).saturating_mul(y1 - y0); + if mask && total_pixels.saturating_mul(clip.len()) > 5_000_000 { + return None; + } let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4); for row in y0..y1 { // Rows are top-down unless the image says otherwise. diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index 3324f5f9..a4dcd4c2 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -222,6 +222,7 @@ pub(crate) struct Plus { /// Convex device clip polygon; None is unclipped. clip: Area, objects: Vec>, + object_bytes: usize, stack: Vec, partial: Option, /// The picture frame in device units (`[x, y, w, h]`), what Clear paints. @@ -237,6 +238,7 @@ impl Plus { page: 1.0, clip: None, objects: vec![None; MAX_OBJECTS], + object_bytes: 0, stack: Vec::new(), partial: None, frame: None, @@ -247,14 +249,16 @@ impl Plus { pub fn comment(&mut self, cv: &mut Canvas, data: Bytes) { let mut pos = 0usize; while !cv.full() { - let (Some(typ), Some(flags), Some(size), Some(dsize)) = (data.u16(pos), data.u16(pos + 2), data.u32(pos + 4), data.u32(pos + 8)) else { + let Some(tail) = data.0.get(pos..) else { break }; + let tail = Bytes(tail); + let (Some(typ), Some(flags), Some(size), Some(dsize)) = (tail.u16(0), tail.u16(2), tail.u32(4), tail.u32(8)) else { break; }; let (size, dsize) = (size as usize, dsize as usize); if size < 12 || dsize > size - 12 { break; } - let Some(rec) = data.slice(pos + 12, dsize) else { break }; + let Some(rec) = tail.slice(12, dsize) else { break }; let _ = self.record(cv, typ, flags, Bytes(rec)); let Some(next) = pos.checked_add(size) else { break }; pos = next; @@ -290,6 +294,9 @@ impl Plus { let fill = self.brush_of(flags, r.u32(0)?)?; let p = self.painter(); for rc in rects(r, 8, r.u32(4)? as usize, compressed)? { + if cv.full() { + break; + } p.fill(cv, &rect_ops(rc), &fill); } Some(()) @@ -298,6 +305,9 @@ impl Plus { let pen = self.pen(id)?; let p = self.painter(); for rc in rects(r, 4, r.u32(0)? as usize, compressed)? { + if cv.full() { + break; + } p.stroke(cv, &rect_ops(rc), pen); } Some(()) @@ -550,8 +560,28 @@ impl Plus { } fn store(&mut self, id: usize, obj: Option) { - if let Some(slot) = self.objects.get_mut(id) { - *slot = obj; + let Some(slot) = self.objects.get_mut(id) else { return }; + if let Some(old) = slot.take() { + self.object_bytes = self.object_bytes.saturating_sub(Self::obj_cost(&old)); + } + if let Some(new_obj) = obj { + let cost = Self::obj_cost(&new_obj); + if self.object_bytes.saturating_add(cost) <= crate::MAX_BYTES { + self.object_bytes += cost; + *slot = Some(new_obj); + } + } + } + + fn obj_cost(obj: &Obj) -> usize { + match obj { + Obj::Image(img) => match img.as_ref() { + ImageData::Bitmap(rgba) => rgba.px.len(), + ImageData::Meta(pic) => pic.items.len().saturating_mul(128), + }, + Obj::Brush(Fill::Texture { img, .. }) => img.px.len(), + Obj::Path(ops) => ops.len().saturating_mul(std::mem::size_of::()), + _ => 0, } } @@ -1282,11 +1312,18 @@ fn decode_compressed(bytes: &[u8]) -> Option { limits.max_image_height = Some(1 << 15); limits.max_alloc = Some((crate::MAX_PIXELS * 4) as u64); reader.limits(limits); - let img = reader.decode().ok()?.to_rgba8(); - let (w, h) = (img.width() as usize, img.height() as usize); - if w.checked_mul(h)? > crate::MAX_PIXELS { + let (w, h) = reader.into_dimensions().ok()?; + let (w, h) = (w as usize, h as usize); + if w == 0 || h == 0 || w.checked_mul(h)? > crate::MAX_PIXELS { return None; } + let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?; + let mut limits = image::Limits::default(); + limits.max_image_width = Some(1 << 15); + limits.max_image_height = Some(1 << 15); + limits.max_alloc = Some((crate::MAX_PIXELS * 4) as u64); + reader.limits(limits); + let img = reader.decode().ok()?.to_rgba8(); Some(Rgba { w, h, bottom_up: false, px: img.into_raw() }) } @@ -1406,31 +1443,61 @@ fn wrap_line(line: &[u16], width: f64, em: f64) -> Vec> { if em <= 0.0 || !width.is_finite() { return vec![line.to_vec()]; } - let fits = |l: &[u16]| l.iter().map(|u| char_em(*u) * em).sum::() <= width; + let trimmed = |v: &[u16]| v.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); + let measure = |v: &[u16]| v.iter().map(|u| char_em(*u) * em).sum::(); + let mut out: Vec> = Vec::new(); let mut cur: Vec = Vec::new(); + let mut cur_total_w = 0.0; + for word in line.split_inclusive(|u| *u == 0x20) { - let mut cand = cur.clone(); - cand.extend_from_slice(word); - let trimmed = |v: &[u16]| v.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); - if fits(&cand[..trimmed(&cand)]) || cur.is_empty() && fits(&word[..trimmed(word)]) { - cur = cand; + let n_non_space = trimmed(word); + let word_non_space_w = measure(&word[..n_non_space]); + let word_total_w = measure(word); + + if cur.is_empty() { + if word_non_space_w <= width { + cur.extend_from_slice(word); + cur_total_w = word_total_w; + continue; + } + } else if cur_total_w + word_non_space_w <= width { + cur.extend_from_slice(word); + cur_total_w += word_total_w; continue; } + if !cur.is_empty() { let n = trimmed(&cur); cur.truncate(n); out.push(std::mem::take(&mut cur)); } + + if word_non_space_w <= width { + cur.extend_from_slice(word); + cur_total_w = word_total_w; + continue; + } + // A word wider than the line on its own: break it between characters. + let mut cur_w = 0.0; for &u in word { - cur.push(u); - if !fits(&cur[..trimmed(&cur)]) && cur.len() > 1 { - cur.pop(); + let u_w = char_em(u) * em; + if cur.is_empty() { + cur.push(u); + cur_w = u_w; + } else if cur_w + u_w <= width { + cur.push(u); + cur_w += u_w; + } else { + let n = trimmed(&cur); + cur.truncate(n); out.push(std::mem::take(&mut cur)); cur.push(u); + cur_w = u_w; } } + cur_total_w = cur_w; } let n = cur.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); cur.truncate(n); diff --git a/crates/metafile/src/place.rs b/crates/metafile/src/place.rs index d9d3bdf8..be00ea22 100644 --- a/crates/metafile/src/place.rs +++ b/crates/metafile/src/place.rs @@ -1,6 +1,8 @@ //! Placing a picture on a target (a page or a device): the fitted transform, paths as kurbo paths in //! target units and bitmaps as target rectangles. The rasteriser and the PDF writer only paint the result. +use std::sync::Arc; + use kurbo::{Affine, BezPath, Rect, Shape}; use crate::{Item, Picture, Seg}; @@ -11,20 +13,22 @@ pub const MAX_BITMAP_SIDE: u32 = 30_000; const MAX_COORD: f64 = 1e6; /// Smallest placed bitmap side (target units), so the stretch onto it is never singular. const MIN_BITMAP_SIDE: f64 = 1e-6; +/// Maximum aggregate path elements retained in placed output. +const MAX_PLACED_ELEMENTS: usize = 500_000; /// One item of a placed picture, in target units. pub enum PlacedItem { /// A filled and/or stroked path. The stroke width is in target units; 0 is a cosmetic hairline that /// the renderer draws at its minimum width. - Path { path: BezPath, fill: Option<[u8; 4]>, stroke: Option<([u8; 4], f64)>, even_odd: bool, region: Option }, + Path { path: BezPath, fill: Option<[u8; 4]>, stroke: Option<([u8; 4], f64)>, even_odd: bool, region: Option> }, /// `Picture::items[index]`, a bitmap, stretched into `rect`. - Bitmap { index: usize, rect: Rect, region: Option }, + Bitmap { index: usize, rect: Rect, region: Option> }, /// `Picture::items[index]`, a pattern fill: `path` (target units) filled with the item's tile /// repeated; `tile` maps tile pixels to target units. - Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option }, + Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option> }, /// `Picture::items[index]`, a text string: `transform` maps its text space to target units; /// `clip` (target units) bounds what it draws. `region` is the device-context clip. - Text { index: usize, transform: Affine, clip: Option, region: Option }, + Text { index: usize, transform: Affine, clip: Option, region: Option> }, } impl Picture { @@ -53,42 +57,65 @@ impl Picture { let c = tf.as_coeffs(); let k = (c[0].abs() + c[3].abs()) / 2.0; let mut out = Vec::with_capacity(self.items.len()); + let mut total_elements = 0usize; + let mut cached_region: Option<(*const [f32; 4], usize, Option>>)> = None; + let mut get_region = |reg: &Option>| -> Option>> { + let (ptr, len) = match reg { + None => (std::ptr::null(), 0), + Some(v) => (v.as_ptr(), v.len()), + }; + if let Some((c_ptr, c_len, ref res)) = cached_region + && c_ptr == ptr + && c_len == len + { + return res.clone(); + } + let res = placed_region(&tf, reg); + cached_region = Some((ptr, len, res.clone())); + res + }; + for (index, item) in self.items.iter().enumerate() { + if total_elements >= MAX_PLACED_ELEMENTS { + break; + } match item { Item::Path { segs, fill, stroke, even_odd, region } => { let path = tf * bez(segs); - let Some(region) = placed_region(&tf, region) else { continue }; + let Some(region) = get_region(region) else { continue }; if in_range(path.bounding_box()) { + total_elements = total_elements.saturating_add(path.elements().len()); let stroke = stroke.map(|(c, w)| (c, f64::from(w) * k)); out.push(PlacedItem::Path { path, fill: *fill, stroke, even_odd: *even_odd, region }); } } Item::Pattern { segs, even_odd, tile: t, region } => { let path = tf * bez(segs); - let Some(region) = placed_region(&tf, region) else { continue }; + let Some(region) = get_region(region) else { continue }; let tile = tf * Affine::translate((f64::from(t.origin[0]), f64::from(t.origin[1]))) * Affine::scale_non_uniform(f64::from(t.cell[0]), f64::from(t.cell[1])); let det = tile.determinant(); if in_range(path.bounding_box()) && det.is_finite() && det.abs() > 1e-12 { + total_elements = total_elements.saturating_add(path.elements().len()); out.push(PlacedItem::Pattern { index, path, even_odd: *even_odd, tile, region }); } } Item::Text(t) => { let transform = tf * Affine::new(t.transform); - let Some(region) = placed_region(&tf, &t.region) else { continue }; + let Some(region) = get_region(&t.region) else { continue }; let clip = t.clip.map(|[x0, y0, x1, y1]| tf.transform_rect_bbox(Rect::new(f64::from(x0), f64::from(y0), f64::from(x1), f64::from(y1)))); let c = transform.as_coeffs(); let origin = Rect::new(c[4], c[5], c[4], c[5]); - if in_range(origin) && c.iter().all(|v| v.is_finite()) && clip.is_none_or(in_range) { + if in_range(origin) && c.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) && clip.is_none_or(in_range) { out.push(PlacedItem::Text { index, transform, clip, region }); } } Item::Bitmap { rect: [x, y, w, h], region, .. } => { let (x0, y0) = (f64::from(*x), f64::from(*y)); let rect = tf.transform_rect_bbox(Rect::new(x0, y0, x0 + f64::from(*w), y0 + f64::from(*h))); - let Some(region) = placed_region(&tf, region) else { continue }; + let Some(region) = get_region(region) else { continue }; if in_range(rect) && rect.width() > MIN_BITMAP_SIDE && rect.height() > MIN_BITMAP_SIDE { out.push(PlacedItem::Bitmap { index, rect, region }); } @@ -113,7 +140,7 @@ impl Item { /// The device-context clip in target units. `Some(None)` is the default clip. `None` means the /// region does not meet the page, so the item draws nothing. -fn placed_region(tf: &Affine, region: &Option>) -> Option> { +fn placed_region(tf: &Affine, region: &Option>) -> Option>> { let Some(rects) = region else { return Some(None) }; let mut path = BezPath::new(); for [x0, y0, x1, y1] in rects { @@ -122,7 +149,7 @@ fn placed_region(tf: &Affine, region: &Option>) -> Option 10, "wrapped into multiple lines: {text_items}"); +} + +#[test] +fn clip_polygon_work_bound() { + let poly: Vec<(f64, f64)> = (0..2000).map(|i| (i as f64, i as f64)).collect(); + let clip: Vec<(f64, f64)> = (0..2000).map(|i| (i as f64, i as f64)).collect(); + let clipped = crate::cliprect::clip_polygon(&poly, &clip); + assert!(clipped.is_empty(), "excessive polygon clip product rejected"); +} diff --git a/crates/render/src/metafile.rs b/crates/render/src/metafile.rs index bb9da83f..91fa0138 100644 --- a/crates/render/src/metafile.rs +++ b/crates/render/src/metafile.rs @@ -51,7 +51,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { ctx.push_clip_layer(&r.to_path(0.1)); for it in pic.place(r, [0.0; 4]) { match it { - PlacedItem::Path { path, fill, stroke, even_odd, region } => clipped(&mut ctx, region.as_ref(), |ctx| { + PlacedItem::Path { path, fill, stroke, even_odd, region } => clipped(&mut ctx, region.as_deref(), |ctx| { if let Some(f) = fill { ctx.set_paint(rgba(f)); ctx.set_fill_rule(if even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero }); @@ -64,7 +64,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { ctx.stroke_path(&path); } }), - PlacedItem::Bitmap { index, rect, region } => clipped(&mut ctx, region.as_ref(), |ctx| { + PlacedItem::Bitmap { index, rect, region } => clipped(&mut ctx, region.as_deref(), |ctx| { let Some(Some(pm)) = pixmaps.get(index) else { return }; let (bw, bh) = (pm.width().max(1) as f64, pm.height().max(1) as f64); ctx.set_paint(vello_cpu::Image { @@ -75,7 +75,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { ctx.fill_rect(&rect); ctx.reset_paint_transform(); }), - PlacedItem::Pattern { index, path, even_odd, tile, region } => clipped(&mut ctx, region.as_ref(), |ctx| { + PlacedItem::Pattern { index, path, even_odd, tile, region } => clipped(&mut ctx, region.as_deref(), |ctx| { let Some(Item::Pattern { tile: t, .. }) = pic.items.get(index) else { return }; let key = Arc::as_ptr(t); let (pm, avg) = tiles.entry(key).or_insert_with(|| (premul(t.width, t.height, &t.rgba), average(&t.rgba))).clone(); @@ -102,7 +102,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option { } } }), - PlacedItem::Text { index, transform, clip, region } => clipped(&mut ctx, region.as_ref(), |ctx| { + PlacedItem::Text { index, transform, clip, region } => clipped(&mut ctx, region.as_deref(), |ctx| { let Some(Item::Text(t)) = pic.items.get(index) else { return }; if let Some(c) = clip { ctx.push_clip_layer(&c.to_path(0.1)); @@ -281,7 +281,14 @@ fn draw_text(ctx: &mut RenderContext, t: &Text, m: Affine) { let s = em / g.face.upem.max(1.0); let skew = if italic && !g.face.italic { Affine::new([1.0, 0.0, -0.2, 1.0, 0.0, 0.0]) } else { Affine::IDENTITY }; let gm = at * Affine::translate((g.x, g.y)) * Affine::scale_non_uniform(x_scale, 1.0) * skew * Affine::scale(s); + let sc = scale_of(gm); + if !sc.is_finite() || sc > 1e5 { + continue; + } let p: BezPath = gm * path.as_ref().clone(); + if p.elements().len() > 10_000 || p.bounding_box().width() > 1e6 || p.bounding_box().height() > 1e6 { + continue; + } ctx.fill_path(&p); if bold && g.face.weight < 600.0 { ctx.set_stroke(kurbo::Stroke::new(em * 0.025 * scale_of(at)).with_join(kurbo::Join::Round)); From f86928d0d50e6fdee166d846ea1a3477ae2658ce Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 05:16:07 -0400 Subject: [PATCH 9/9] perf(pptx): index export lookups and deduplicate relationship parts (#1281) - Row 31: Use HashMap for comment author lookup in author_slot - Row 37: Index preserved parts by ID before resolving graphic frame relationships - Row 10: Index kept chart parts by normalized path and avoid duplicate payload scans - Row 39: Update existing relationship parts instead of appending duplicates in PackageOut --- crates/pptx/src/opc.rs | 7 ++++- crates/pptx/src/write/chart.rs | 10 +++++-- crates/pptx/src/write/mod.rs | 50 +++++++++++++++++++++------------ crates/pptx/src/write/shapes.rs | 24 +++++++++------- 4 files changed, 60 insertions(+), 31 deletions(-) diff --git a/crates/pptx/src/opc.rs b/crates/pptx/src/opc.rs index b8406dc8..f39b88bf 100644 --- a/crates/pptx/src/opc.rs +++ b/crates/pptx/src/opc.rs @@ -387,7 +387,12 @@ impl PackageOut { if rels.items.is_empty() { return; } - self.files.push((rels_path(part), rels.to_xml(), true)); + let path = rels_path(part); + if let Some(existing) = self.files.iter_mut().find(|(name, ..)| *name == path) { + existing.1 = rels.to_xml(); + } else { + self.files.push((path, rels.to_xml(), true)); + } } /// Add a part with no content type of its own (a relationships part). pub fn raw(&mut self, name: &str, data: Vec) { diff --git a/crates/pptx/src/write/chart.rs b/crates/pptx/src/write/chart.rs index 88917450..5dcbe33b 100644 --- a/crates/pptx/src/write/chart.rs +++ b/crates/pptx/src/write/chart.rs @@ -750,11 +750,17 @@ pub fn write_kept(pkg: &mut crate::opc::PackageOut, chart_name: &str, c: &Chart, } } pkg.rels(chart_name, &rels); + let parts_by_norm: std::collections::HashMap<_, _> = c.kept.parts.iter().map(|p| (norm(&p.name), p)).collect(); + let mut seen_payloads = std::collections::HashSet::new(); // The kept parts' own relationships, pointed at the new names. for p in c.kept.parts.iter().filter(|p| kept_payload(&p.name)) { - let Some(new_source) = names.get(&norm(&p.name)) else { continue }; + let norm_name = norm(&p.name); + if !seen_payloads.insert(norm_name.clone()) { + continue; + } + let Some(new_source) = names.get(&norm_name) else { continue }; let rp = norm(&rels_path(&p.name)); - let Some(rels_part) = c.kept.parts.iter().find(|q| norm(&q.name) == rp) else { continue }; + let Some(rels_part) = parts_by_norm.get(&rp) else { continue }; let mut out = RelsOut::default(); for r in crate::opc::rels_from_bytes(&rels_part.bytes, &p.name).items { if r.external { diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index f96f82c5..5d3e60e9 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -637,6 +637,7 @@ pub fn export(p: &Presentation, main_ct: &str) -> Result, PptxError> { // Comment authors. let mut authors: Vec<(String, String, u32)> = vec![]; + let mut author_map: std::collections::HashMap = std::collections::HashMap::new(); let mut n_comments = 0u32; // Slides. @@ -662,13 +663,13 @@ pub fn export(p: &Presentation, main_ct: &str) -> Result, PptxError> { n_comments += 1; let cname = format!("ppt/comments/comment{n_comments}.xml"); so.rels.add("comments", &opc::relative(&sname, &cname), false); - x.pkg.xml_part(&cname, CT_COMMENTS, comments_xml(&s.comments, &mut authors)); + x.pkg.xml_part(&cname, CT_COMMENTS, comments_xml(&s.comments, &mut authors, &mut author_map)); // The modern threaded part next to the flattened legacy one (issue #70): // PowerPoint 2021+ and DeckCraft read replies and resolved state from it, // older readers keep the legacy part. let mname = format!("ppt/comments/modernComment{n_comments}.xml"); let rid = so.rels.add(RT_COMMENTS_MODERN, &opc::relative(&sname, &mname), false); - x.pkg.xml_part(&mname, CT_COMMENTS_MODERN, modern_comments_xml(&s.comments, &cname, &mut authors)); + x.pkg.xml_part(&mname, CT_COMMENTS_MODERN, modern_comments_xml(&s.comments, &cname, &mut authors, &mut author_map)); comment_rel = Some(rid); } let xml = slide_xml(&mut x, &mut so, s, comment_rel.as_deref()); @@ -828,7 +829,7 @@ fn notes_xml(x: &mut Exp, o: &mut Out, notes: &TextBody) -> Vec { w.finish() } -fn comments_xml(list: &[Comment], authors: &mut Vec<(String, String, u32)>) -> Vec { +fn comments_xml(list: &[Comment], authors: &mut Vec<(String, String, u32)>, author_map: &mut std::collections::HashMap) -> Vec { let mut w = W::new(); root_open(&mut w, "p:cmLst", A::new()); let mut flat: Vec<&Comment> = vec![]; @@ -839,7 +840,7 @@ fn comments_xml(list: &[Comment], authors: &mut Vec<(String, String, u32)>) -> V } } for c in flat { - let ai = author_slot(c, authors); + let ai = author_slot(c, authors, author_map); let idx = match authors.get_mut(ai) { Some(a) => { a.2 += 1; @@ -860,32 +861,45 @@ fn comments_xml(list: &[Comment], authors: &mut Vec<(String, String, u32)>) -> V /// The author's slot in the shared bucket list, adding them on first use. Legacy parts index the /// list by number, the modern authors part gives every slot a GUID. -fn author_slot(c: &Comment, authors: &mut Vec<(String, String, u32)>) -> usize { - let author = if c.author.is_empty() { "Author".to_string() } else { c.author.clone() }; - match authors.iter().position(|(n, _, _)| *n == author) { - Some(i) => i, - None => { - let ini = if c.initials.is_empty() { author.chars().filter(|ch| ch.is_alphabetic()).take(2).collect() } else { c.initials.clone() }; - authors.push((author.clone(), ini, 0)); - authors.len() - 1 - } +fn author_slot(c: &Comment, authors: &mut Vec<(String, String, u32)>, map: &mut std::collections::HashMap) -> usize { + let author = if c.author.is_empty() { "Author" } else { &c.author }; + if let Some(&i) = map.get(author) { + return i; } + let ini = if c.initials.is_empty() { author.chars().filter(|ch| ch.is_alphabetic()).take(2).collect() } else { c.initials.clone() }; + let idx = authors.len(); + authors.push((author.to_string(), ini, 0)); + map.insert(author.to_string(), idx); + idx } /// The modern (p188) threaded-comment part: replies nest and the resolved state is kept, which /// the flattened legacy part cannot express (issue #70). -fn modern_comments_xml(list: &[Comment], part: &str, authors: &mut Vec<(String, String, u32)>) -> Vec { +fn modern_comments_xml( + list: &[Comment], + part: &str, + authors: &mut Vec<(String, String, u32)>, + author_map: &mut std::collections::HashMap, +) -> Vec { let mut w = W::new(); w.open("p188:cmLst", A::new().a("xmlns:p188", NS_P188).a("xmlns:a", NS_A).a("xmlns:r", NS_R)); for (ti, c) in list.iter().take(10_000).enumerate() { - modern_comment(&mut w, c, part, ti, authors, 0); + modern_comment(&mut w, c, part, ti, authors, author_map, 0); } w.close("p188:cmLst"); w.finish() } -fn modern_comment(w: &mut W, c: &Comment, part: &str, ti: usize, authors: &mut Vec<(String, String, u32)>, depth: usize) { - let ai = author_slot(c, authors); +fn modern_comment( + w: &mut W, + c: &Comment, + part: &str, + ti: usize, + authors: &mut Vec<(String, String, u32)>, + author_map: &mut std::collections::HashMap, + depth: usize, +) { + let ai = author_slot(c, authors, author_map); let author = authors.get(ai).map(|(n, _, _)| n.clone()).unwrap_or_default(); let dt = if valid_date(&c.date) { c.date.clone() } else { "2024-01-01T00:00:00.000".into() }; let mut a = A::new().a("id", crate::tables::guid(&format!("deckcraft-comment:{part}:{ti}"))).a("authorId", author_guid(&author)).a("created", dt); @@ -899,7 +913,7 @@ fn modern_comment(w: &mut W, c: &Comment, part: &str, ti: usize, authors: &mut V if depth < 4 && !c.replies.is_empty() { w.open0("p188:replyLst"); for (ri, r) in c.replies.iter().take(1000).enumerate() { - let ri_ai = author_slot(r, authors); + let ri_ai = author_slot(r, authors, author_map); let r_author = authors.get(ri_ai).map(|(n, _, _)| n.clone()).unwrap_or_default(); let rdt = if valid_date(&r.date) { r.date.clone() } else { "2024-01-01T00:00:00.000".into() }; let mut ra = A::new() diff --git a/crates/pptx/src/write/shapes.rs b/crates/pptx/src/write/shapes.rs index 14710cfa..38c71235 100644 --- a/crates/pptx/src/write/shapes.rs +++ b/crates/pptx/src/write/shapes.rs @@ -852,21 +852,25 @@ fn opaque( // parts re-added under this slide's relationship ids. if let Ok(doc) = crate::xml::parse(xml.as_bytes()) && matches!(doc.root.local(), "graphicFrame" | "sp" | "grpSp" | "cxnSp") - && let Some(ids) = crate::read::rel_refs(&doc.root) + { + let part_map: std::collections::HashMap<&str, (&str, deckcraft_model::MediaId)> = + parts.iter().map(|(id, kind, m)| (id.as_str(), (kind.as_str(), *m))).collect(); + if let Some(ids) = crate::read::rel_refs(&doc.root) .iter() .map(|r| { - let (_, kind, m) = parts.iter().find(|(id, ..)| id == r)?; + let (kind, m) = part_map.get(r.as_str())?; Some((r.clone(), x.media_rel(o, *m, kind)?)) }) .collect::>>() - { - let mut root = doc.root; - set_rel_ids(&mut root, &ids, 0); - ole_fallback(&mut root, x, o, preview); - let id = cx.id_for(s); - set_cnvpr_id(&mut root, id); - w.raw(&root.to_xml()); - return; + { + let mut root = doc.root; + set_rel_ids(&mut root, &ids, 0); + ole_fallback(&mut root, x, o, preview); + let id = cx.id_for(s); + set_cnvpr_id(&mut root, id); + w.raw(&root.to_xml()); + return; + } } log::warn!("pptx: object '{label}' written as its preview picture"); let id = cx.id_for(s);