web: harden plot preview after adversarial review

Three reviewers (security, correctness, performance) converged on:

- Download PDF died on the second click: Closure::once_into_js throws
  when invoked twice. The handler is now a plain FnMut kept for the
  overlay's lifetime.
- Rasterisation was bounded by page count, not pixels: 12 A0 sheets at
  150 DPI peaked near 1.8 GB of RGBA plus ~1.7 GB of decoded images,
  and a drawing declaring absurd paper sizes made the rasteriser
  saturate its u16 dimensions into a ~17 GB allocation that aborts the
  tab. Preview now computes a uniform DPI under a 64-Mpx total budget
  (floor 24 DPI) from the page tree before allocating, with a per-page
  4096-px edge clamp.
- Overlays stacked when previewing twice and their blob URLs were only
  revoked via Close. The overlay is now single-instance (id-tagged);
  a replacement or Close revokes every URL through a shared registry.
- emit_image resampled before probing the XObject cache, so a title
  block placed on 12 layouts ran 12 Lanczos passes to embed once.
  The cache is now probed with the target dimensions first.
- Preview downloads are named {stem}_preview.pdf instead of a constant
  plot_preview.pdf, and the page label says 'first N of M' when the
  preview is capped while the PDF keeps every page.
This commit is contained in:
Afonsop committed 2026-09-15 21:07:55 +01:00
1 parent 6306359390
commit 2a05479d69
5 files changed
+218 -25

No files matched your search

+20 -4
View File
@@ -3266,8 +3266,9 @@ impl OpenCADStudio {
// PDF and open it in a browser tab for the browser print dialog.
#[cfg(target_arch = "wasm32")]
{
let stem = self.active_stem("_layouts");
let work = move || {
crate::io::print_to_printer::preview_pages_pdf(&pages)
crate::io::print_to_printer::preview_pages_pdf(&stem, &pages)
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
@@ -3523,9 +3524,10 @@ impl OpenCADStudio {
// in a browser tab, where the browser's own print dialog takes over.
#[cfg(target_arch = "wasm32")]
{
let stem = self.active_stem("");
background_task(
move || {
crate::io::print_to_printer::preview_page_pdf(&page)
crate::io::print_to_printer::preview_page_pdf(&stem, &page)
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
},
@@ -3631,6 +3633,18 @@ impl OpenCADStudio {
dir.join(format!("{stem}_{ts}.pdf"))
}
/// File stem of the active drawing for default output names, with an
/// optional suffix (e.g. "_layouts" for print-all). Falls back to
/// "drawing" when the tab has never been saved.
fn active_stem(&self, suffix: &str) -> String {
self.tabs[self.active_tab]
.current_path
.as_deref()
.and_then(|path| path.file_stem())
.map(|stem| format!("{}{}", stem.to_string_lossy(), suffix))
.unwrap_or_else(|| format!("drawing{suffix}"))
}
/// Open the full Plot / Print dialog, seeding its state from the active
/// layout's plot settings and the printers found on the system.
pub(super) fn on_plot_dialog_open(&mut self) -> Task<Message> {
@@ -4454,8 +4468,9 @@ impl OpenCADStudio {
// Preview renders to a temp PDF and opens it — never a save dialog,
// whatever the output target is.
if preview {
let stem = self.active_stem("");
let work = move || {
crate::io::print_to_printer::preview_page_pdf(&page)
crate::io::print_to_printer::preview_page_pdf(&stem, &page)
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};
@@ -4479,8 +4494,9 @@ impl OpenCADStudio {
let page = self.layout_plot_page();
if preview {
let stem = self.active_stem("");
let work = move || {
crate::io::print_to_printer::preview_page_pdf(&page)
crate::io::print_to_printer::preview_page_pdf(&stem, &page)
.map(|_| crate::t!("Opened plot preview.").into_owned())
.map_err(|e| crate::tf!("Preview failed: {e}").into_owned())
};