forked from 1b-agent/opencadstudio-upstream
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:
1 parent
6306359390
commit
2a05479d69
5 files changed
+218
-25
No files matched your search
+10
-1
@@ -305,7 +305,13 @@ fn emit_image(
|
||||
// One shared factor from the denser axis preserves the pixel aspect ratio.
|
||||
let (w, h) = downsample_target(width, height, dpi_x.max(dpi_y), target);
|
||||
// Skip token reductions (< 5%): a Lanczos pass costs more than it saves.
|
||||
if (w != width || h != height) && w as f64 / (width as f64) < 0.95 {
|
||||
let needs_resample = (w != width || h != height) && w as f64 / (width as f64) < 0.95;
|
||||
// The same Arc placed repeatedly (a title block on every layout)
|
||||
// resamples once: an existing XObject at the target dims means
|
||||
// only the draw transform below is new work.
|
||||
let already_embedded =
|
||||
resources.contains_key(&(std::sync::Arc::as_ptr(&image.pixels) as usize, w, h));
|
||||
if needs_resample && !already_embedded {
|
||||
if let Some(resized) = image::RgbaImage::from_raw(
|
||||
width,
|
||||
height,
|
||||
@@ -319,6 +325,9 @@ fn emit_image(
|
||||
height = h;
|
||||
pixels = std::borrow::Cow::Owned(resized);
|
||||
}
|
||||
} else if already_embedded {
|
||||
width = w;
|
||||
height = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,10 +336,15 @@ pub fn open_in_viewer(path: &std::path::Path) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Render a page and show it the way each platform previews: the OS PDF
|
||||
/// viewer on desktop, an in-page rasterised overlay on the web.
|
||||
pub fn preview_page_pdf(page: &crate::io::pdf_export::PdfPageInput) -> Result<(), String> {
|
||||
/// viewer on desktop, an in-page rasterised overlay on the web. `stem` names
|
||||
/// the web download (`{stem}_preview.pdf`); the desktop temp file is unchanged.
|
||||
pub fn preview_page_pdf(
|
||||
stem: &str,
|
||||
page: &crate::io::pdf_export::PdfPageInput,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let _ = stem;
|
||||
let tmp = temp_pdf_path("preview");
|
||||
crate::io::pdf_export::export_pdf(page, &tmp)?;
|
||||
open_in_viewer(&tmp)
|
||||
@@ -347,26 +352,32 @@ pub fn preview_page_pdf(page: &crate::io::pdf_export::PdfPageInput) -> Result<()
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let bytes = crate::io::pdf_export::render_pdf_bytes(std::slice::from_ref(page), None)?;
|
||||
web_preview_overlay(&bytes)
|
||||
web_preview_overlay(stem, &bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Web-only multi-page variant used by print-all: render every layout into
|
||||
/// one PDF and preview the first pages in the overlay.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn preview_pages_pdf(pages: &[crate::io::pdf_export::PdfPageInput]) -> Result<(), String> {
|
||||
pub fn preview_pages_pdf(
|
||||
stem: &str,
|
||||
pages: &[crate::io::pdf_export::PdfPageInput],
|
||||
) -> Result<(), String> {
|
||||
let bytes = crate::io::pdf_export::render_pdf_bytes(pages, None)?;
|
||||
web_preview_overlay(&bytes)
|
||||
web_preview_overlay(stem, &bytes)
|
||||
}
|
||||
|
||||
/// Web preview: rasterise the plotted PDF (the same hayro pipeline that draws
|
||||
/// PDF underlays) and show the pages in an in-page overlay. The browser gets
|
||||
/// plain PNGs, so neither popup permission nor a PDF viewer plugin is needed.
|
||||
/// Rasterisation is bounded by a total pixel budget so large sheets lower the
|
||||
/// preview DPI instead of the tab's memory ceiling.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn web_preview_overlay(bytes: &[u8]) -> Result<(), String> {
|
||||
fn web_preview_overlay(stem: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
use crate::scene::model::pdf_raster;
|
||||
const PREVIEW_DPI: f32 = 150.0;
|
||||
const MAX_PREVIEW_PAGES: usize = 12;
|
||||
let pages = crate::scene::model::pdf_raster::rasterize_bytes_pages(
|
||||
let (pages, total_pages) = pdf_raster::rasterize_preview_pages(
|
||||
std::sync::Arc::new(bytes.to_vec()),
|
||||
PREVIEW_DPI,
|
||||
MAX_PREVIEW_PAGES,
|
||||
@@ -385,7 +396,7 @@ fn web_preview_overlay(bytes: &[u8]) -> Result<(), String> {
|
||||
pngs.push(png);
|
||||
}
|
||||
}
|
||||
crate::sys::show_pdf_preview("plot_preview.pdf", bytes, &pngs);
|
||||
crate::sys::show_pdf_preview(&format!("{stem}_preview.pdf"), bytes, &pngs, total_pages);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user