fix(pdf): clamp display rasters and bound the print raster

Bug:
The PDF underlay renderer sized its pixmap from page size x requested DPI
with no ceiling: a large page at a high raster DPI (display or print)
computes a multi-gigabyte image - testing reached a 17 GB pixmap and a
543-second render attempt before the allocator gave up.

Reproduce:
1. Attach a large-page PDF; set a high raster DPI (print above 150 DPI,
   or display with DPI raised).
2. Before: minutes of rendering and a multi-GB allocation, then abort.
3. After: display rasters are clamped so the longest side is at most
   3072 (like DWF/DGN, the effective DPI is stored); print rasters over
   268,435,456 pixels are refused with an error instead of rendered.

Fix:
Display (at or below 150 DPI): the scale is clamped to
RASTER_SIDE/longest before the pixmap is allocated, and the DPI actually
used is stored for downstream math. Print (above 150 DPI): refused when
width x height exceeds MAX_PRINT_RASTER_PIXELS (256 Mpx) or is
non-finite - print DPI is never silently rescaled because the
sheet-mm-from-exact-DPI conversion must stay exact; the user gets an
error instead of a hang. A regression test covers the hostile page x DPI
case (about 1 second, previously 543 seconds and 17 GB).
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent 21c4d6ab41
commit 86642bb639
1 file changed
+61 -2
+61 -2
View File
@@ -18,6 +18,13 @@ pub struct PdfPage {
const RASTER_DPI: f32 = 150.0;
/// Ceiling on the print raster's pixel count (RGBA ⇒ ×4 bytes): an ANSI E
/// sheet at 300 DPI is ~135 M px and stays under it, while a crafted
/// MediaBox cannot ask hayro for a multi-gigabyte pixmap — its `u16`
/// dimensions saturate at 65 535, i.e. a 17 GB allocation (abort or a
/// multi-minute freeze).
const MAX_PRINT_RASTER_PIXELS: f64 = 268_435_456.0;
/// Source path, page, DPI bits and whether the page background is transparent.
type PageKey = (String, String, u32, bool);
@@ -115,7 +122,33 @@ fn rasterize_uncached(path: &str, page: &str, dpi: f32, transparent: bool) -> Op
let pdf = Pdf::new(bytes).ok()?;
let page_no = page.trim().parse::<usize>().unwrap_or(1).max(1);
let page = pdf.pages().get(page_no - 1)?;
let scale = dpi / 72.0;
let mut scale = dpi / 72.0;
let mut effective_dpi = dpi;
// Page size in points, straight from the file's MediaBox/CropBox.
let (page_w, page_h) = page.render_dimensions();
let longest = (page_w as f64).max(page_h as f64);
if longest.is_finite() && longest > 0.0 {
if dpi <= DISPLAY_DPI {
// Screen rasters get the same longest-side clamp the DWF/DGN
// underlays receive (`underlay_vector::RASTER_SIDE`); the page
// maps onto the same world rect regardless of pixel count.
if longest * f64::from(dpi) / 72.0 > super::underlay_vector::RASTER_SIDE {
let clamped = super::underlay_vector::RASTER_SIDE / longest;
scale = clamped as f32;
effective_dpi = (clamped * 72.0) as f32;
}
} else {
// Print keeps the exact DPI — the sheet size in millimetres is
// derived from it (`page.width / dpi`) — so an oversized page
// is refused instead of rescaled (a rescale would print the
// wrong physical size).
let pw = page_w as f64 * f64::from(dpi) / 72.0;
let ph = page_h as f64 * f64::from(dpi) / 72.0;
if !(pw.is_finite() && ph.is_finite()) || pw * ph > MAX_PRINT_RASTER_PIXELS {
return None;
}
}
}
let pixmap = hayro::render(
page,
&RenderCache::new(),
@@ -137,7 +170,7 @@ fn rasterize_uncached(path: &str, page: &str, dpi: f32, transparent: bool) -> Op
pixels: Arc::new(pixels),
width,
height,
dpi,
dpi: effective_dpi,
}))
}
@@ -347,4 +380,30 @@ mod tests {
assert_eq!((page.width, page.height), (150, 150));
assert_eq!(page.pixels.len(), 150 * 150 * 4);
}
/// A 15 m square MediaBox at 150 DPI would be 88 583 px a side: hayro
/// saturates its `u16` dimensions to 65 535 and `Pixmap::new` asks for
/// 65 535² × 4 = **17 GB** — the report's allocation-failure abort.
/// The display raster must clamp to `RASTER_SIDE` instead, and the
/// print raster (exact DPI, physical size derived from it) must be
/// refused rather than rescaled.
#[test]
fn an_enormous_page_is_clamped_or_refused_never_a_17gb_pixmap() {
let mut document = PdfDocument::new("PDF underlay huge-page test");
document
.pages
.push(OutputPage::new(Mm(15_000.0), Mm(15_000.0), Vec::new()));
let bytes = document.save(&PdfSaveOptions::default(), &mut Vec::new());
let path = "memory://pdf-underlay-huge-page.pdf";
register_source(path, Arc::new(bytes));
let page = rasterize_page_display(path, "1").expect("display raster is clamped");
assert_eq!((page.width, page.height), (3072, 3072));
assert!(page.dpi < DISPLAY_DPI, "clamped raster reports its effective DPI");
assert!(
rasterize_page_at_dpi(path, "1", 300.0).is_none(),
"a print raster over the pixel budget is refused, not rescaled"
);
}
}