From 4546555126ccdac61af23a681ab1ff121f05fd4b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 05:42:34 -0500 Subject: [PATCH] Fix bounds and limits in shared vector readers --- crates/dxf/src/paths.rs | 64 ++++++++++++++++++++++++++++++++++++++--- crates/eps/src/paths.rs | 14 +++------ crates/pdf/src/paths.rs | 43 ++++++++++++++++++++------- crates/svg/src/write.rs | 2 +- 4 files changed, 98 insertions(+), 25 deletions(-) diff --git a/crates/dxf/src/paths.rs b/crates/dxf/src/paths.rs index 01e555797..6b656d442 100644 --- a/crates/dxf/src/paths.rs +++ b/crates/dxf/src/paths.rs @@ -7,6 +7,8 @@ use kurbo::{BezPath, Point, Rect, Shape as _}; /// Limits on the number of paths returned by [`import`]. Parsing also uses the crate's tag cap. pub const MAX_PATHS: usize = 200_000; +/// Aggregate number of Bézier elements a drawing may generate during conversion. +pub const MAX_PATH_ELEMENTS: usize = 2_000_000; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -35,17 +37,24 @@ pub fn import(bytes: &[u8]) -> Result { let tags = crate::parse_with_limit(bytes, crate::max_tags())?; let drawing = crate::model::Document::read(&tags); let unit_to_mm = units_to_mm(drawing.header.insunits(), drawing.header.measurement()); + let layer_visibility: std::collections::HashMap<_, _> = + drawing.layers().into_iter().map(|layer| (layer.name, layer.on && !layer.frozen)).collect(); let mut paths = Vec::new(); let mut warnings = Vec::new(); let mut unsupported = 0usize; + let mut elements = 0usize; for record in &drawing.entities { let entity = crate::model::Entity::read(record); - if !entity.common.visible || record.paper_space() { + if !entity.common.visible || record.paper_space() || layer_visibility.get(&entity.common.layer) == Some(&false) { continue; } if excessive_sweep(&entity.kind) { return Err(Error::TooManySegments); } + let remaining = MAX_PATH_ELEMENTS.saturating_sub(elements); + if estimated_elements(&entity.kind) > remaining { + return Err(Error::TooManySegments); + } let converted = entity_paths(&entity.kind, entity.common.extrusion); if converted.iter().flat_map(|path| path.elements()).any(|element| { let finite = |p: Point| p.x.is_finite() && p.y.is_finite(); @@ -62,6 +71,10 @@ pub fn import(bytes: &[u8]) -> Result { unsupported = unsupported.saturating_add(1); continue; } + elements = elements.saturating_add(converted.iter().map(|path| path.elements().len()).sum::()); + if elements > MAX_PATH_ELEMENTS { + return Err(Error::TooManySegments); + } if paths.len().saturating_add(converted.len()) > MAX_PATHS { return Err(Error::TooManyPaths); } @@ -77,6 +90,48 @@ pub fn import(bytes: &[u8]) -> Result { Ok(Imported { paths, bounds, unit_to_mm, warnings }) } +fn estimated_elements(kind: &crate::model::EntityKind) -> usize { + use crate::model::EntityKind as E; + let arc_count = |sweep: f64| (sweep.abs() / std::f64::consts::FRAC_PI_2).ceil().max(1.0) as usize; + let bulge_count = |bulge: f64| { + if bulge.is_finite() && bulge.abs() > 1e-12 { arc_count(4.0 * bulge.atan()) } else { 1 } + }; + let polyline_count = |vertices: &[(Point, f64)], closed: bool| { + if vertices.is_empty() { + return 0; + } + let mut count = 1usize; + for pair in vertices.windows(2) { + count = count.saturating_add(bulge_count(pair[0].1)); + } + if closed && vertices.len() > 1 { + count = count.saturating_add(bulge_count(vertices.last().map_or(0.0, |vertex| vertex.1))); + count = count.saturating_add(1); + } + count + }; + match kind { + E::Line { .. } => 2, + E::Circle { .. } => 6, + E::Arc { start_angle, end_angle, .. } => 1usize.saturating_add(arc_count((end_angle - start_angle).to_radians())), + E::LwPolyline(poly) => { + let vertices: Vec<_> = poly.vertices.iter().map(|v| (Point::new(v.x, v.y), v.bulge)).collect(); + polyline_count(&vertices, poly.closed) + } + E::Polyline(poly) if !poly.is_mesh() && !poly.is_polyface() => { + let vertices: Vec<_> = poly + .vertices + .iter() + .filter(|vertex| !vertex.is_face()) + .map(|vertex| (Point::new(vertex.location[0], vertex.location[1]), vertex.bulge)) + .collect(); + polyline_count(&vertices, poly.closed()) + } + E::Ellipse { start, end, .. } => 1usize.saturating_add(arc_count(end - start)), + _ => 0, + } +} + fn units_to_mm(code: i64, _measurement: Option) -> f64 { match code { 0 => 1.0, @@ -96,7 +151,7 @@ fn units_to_mm(code: i64, _measurement: Option) -> f64 { 15 => 10_000.0, 16 => 100_000.0, 17 => 1e9, - 18 => 149_597_870_700.0, + 18 => 149_597_870_700_000.0, 19 => 9.460_730_472_580_8e18, 20 => 3.085_677_581_491_367e19, 21 => 1200.0 / 3937.0 * 1000.0, @@ -138,7 +193,7 @@ fn entity_paths(kind: &crate::model::EntityKind, normal: crate::model::Point) -> fn map_ocs_path(path: BezPath, normal: crate::model::Point, z: f64) -> BezPath { let [x, y, n] = crate::model::ocs_axes(normal); - let transform = kurbo::Affine::new([x[0], y[0], x[1], y[1], n[0] * z, n[1] * z]); + let transform = kurbo::Affine::new([x[0], x[1], y[0], y[1], n[0] * z, n[1] * z]); transform * path } @@ -238,7 +293,8 @@ fn ellipse(center: crate::model::Point, major: crate::model::Point, normal: crat if !(minor_len.is_finite() && minor_len > 0.0) { return BezPath::new(); } - let bx = [minor[0] / minor_len * ratio, minor[1] / minor_len * ratio, minor[2] / minor_len * ratio]; + let major_len = major.iter().map(|v| v * v).sum::().sqrt(); + let bx = [minor[0] / minor_len * major_len * ratio, minor[1] / minor_len * major_len * ratio, minor[2] / minor_len * major_len * ratio]; let point = |t: f64| Point::new(center[0] + major[0] * t.cos() + bx[0] * t.sin(), center[1] + major[1] * t.cos() + bx[1] * t.sin()); let mut path = BezPath::new(); path.move_to(point(start)); diff --git a/crates/eps/src/paths.rs b/crates/eps/src/paths.rs index c4adc0d1b..e77b90b60 100644 --- a/crates/eps/src/paths.rs +++ b/crates/eps/src/paths.rs @@ -29,11 +29,12 @@ pub enum Error { OutputLimit, } -/// First-page path geometry in PostScript points, plus the page size and import notes. +/// First-page path geometry normalized to the top-left origin in PostScript points, plus notes. #[derive(Debug, Clone)] pub struct Imported { pub paths: Vec, - /// Page bounds in source PostScript points, including a nonzero or negative origin. + /// Origin-zero page bounds in PostScript points, using the same accepted DSC box as the + /// interpreter. pub page_bounds: Rect, pub warnings: Vec, pub preview_fallback: bool, @@ -45,10 +46,7 @@ pub fn import(bytes: &[u8], limits: Limits) -> Result { return Err(Error::InputLimit { limit: limits.max_input_bytes }); } let source = vectorcraft_eps::import(bytes).map_err(Error::Parse)?; - let section = crate::sections(bytes).map_err(|e| Error::Parse(e.to_string()))?; - let source_box = crate::Comments::read(section.postscript).page_box().unwrap_or([0.0, 0.0, 612.0, 792.0]); - let [llx, lly, urx, ury] = source_box; - let page_bounds = Rect::new(llx, lly, urx, ury); + let page_bounds = vectorcraft_eps::page(bytes).map_err(Error::Parse)?; let mut paths = Vec::new(); let mut total_elements = 0usize; let mut clipped = false; @@ -56,10 +54,6 @@ pub fn import(bytes: &[u8], limits: Limits) -> Result { for layer in &source.document.layers { collect(layer, &mut paths, &mut total_elements, limits, &mut clipped, &mut omitted)?; } - let inverse_page = kurbo::Affine::new([1.0, 0.0, 0.0, -1.0, llx, ury]); - for path in &mut paths { - *path = inverse_page * path.clone(); - } let mut warnings = source.warnings; if clipped { warnings.push("EPS clipping groups are returned as unclipped artwork".into()); diff --git a/crates/pdf/src/paths.rs b/crates/pdf/src/paths.rs index 9f7126013..bbb93942e 100644 --- a/crates/pdf/src/paths.rs +++ b/crates/pdf/src/paths.rs @@ -1,11 +1,11 @@ //! Bounded extraction of page vector paths from PDF content streams. //! -//! Paths remain in PDF user-space points. Text glyphs and raster images are counted as omitted -//! content; clipping and paint are not applied. Applications own page selection and model mapping. +//! Paths are returned in page-oriented points with `/UserUnit`, rotation and crop origin applied. +//! Text glyphs and raster images are counted as omitted content; clipping and paint are not applied. use hayro_interpret::font::Glyph; use hayro_interpret::{ - BlendMode, ClipPath, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, RectExt, SoftMask, + BlendMode, ClipPath, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, RectExt, SoftMask, TransformExt, }; use kurbo::{Affine, BezPath, Rect}; use std::sync::{ @@ -36,11 +36,13 @@ pub enum Error { Interpret(#[from] crate::read::ExecutionError), #[error("PDF contains more than {limit} pages")] Pages { limit: usize }, + #[error("PDF has no page at index {index}")] + PageIndex { index: usize }, #[error("PDF path output exceeds configured limit")] OutputLimit, } -/// Extracted paths for one PDF page, with its intersected crop box in user-space points. +/// Extracted paths for one PDF page, normalized to an origin-zero page in physical points. #[derive(Debug, Clone)] pub struct Page { pub crop_box: Rect, @@ -59,11 +61,23 @@ pub struct Imported { /// Open and interpret vector content using shared PDF parsing and interpreter APIs. pub fn import(bytes: &[u8], limits: Limits) -> Result { + import_selected(bytes, limits, None) +} + +/// Interpret only the requested page and apply path and element limits to that page's output. +pub fn import_page(bytes: &[u8], page_index: usize, limits: Limits) -> Result { + import_selected(bytes, limits, Some(page_index)) +} + +fn import_selected(bytes: &[u8], limits: Limits, selected: Option) -> Result { let options = crate::read::OpenOptions { max_input_bytes: limits.max_input_bytes, ..Default::default() }; let pdf = crate::read::open_slice_with_options(bytes, None, &options)?; if pdf.pages().len() > limits.max_pages { return Err(Error::Pages { limit: limits.max_pages }); } + if selected.is_some_and(|index| index >= pdf.pages().len()) { + return Err(Error::PageIndex { index: selected.unwrap_or_default() }); + } let cache = InterpreterCache::new(); let truncated = Arc::new(AtomicBool::new(false)); let truncation_flag = truncated.clone(); @@ -78,20 +92,29 @@ pub fn import(bytes: &[u8], limits: Limits) -> Result { let mut pages = Vec::with_capacity(pdf.pages().len()); let mut total_paths = 0usize; let mut total_elements = 0usize; - for page in pdf.pages().iter() { - let crop_box = page.intersected_crop_box().to_kurbo(); - let mut context = hayro_interpret::Context::new(Affine::IDENTITY, crop_box, &cache, pdf.xref(), settings.clone()); + for (page_index, page) in pdf.pages().iter().enumerate() { + if selected.is_some_and(|index| index != page_index) { + continue; + } + let source_crop_box = page.intersected_crop_box().to_kurbo(); + let (width, height) = page.render_dimensions(); + let crop_box = Rect::new(0.0, 0.0, f64::from(width), f64::from(height)); + let initial_transform = page.initial_transform(true).to_kurbo(); + let mut context = hayro_interpret::Context::new(initial_transform, source_crop_box, &cache, pdf.xref(), settings.clone()); let mut device = Collector { - max_paths: limits.max_paths.saturating_sub(total_paths), - max_elements: limits.max_path_elements.saturating_sub(total_elements), + max_paths: if selected.is_some() { limits.max_paths } else { limits.max_paths.saturating_sub(total_paths) }, + max_elements: if selected.is_some() { limits.max_path_elements } else { limits.max_path_elements.saturating_sub(total_elements) }, ..Collector::default() }; crate::read::interpret_page(page, &mut context, &mut device)?; if device.exceeded { return Err(Error::OutputLimit); } + if device.paths.len() > limits.max_paths || device.paths.iter().map(|path| path.elements().len()).sum::() > limits.max_path_elements { + return Err(Error::OutputLimit); + } total_paths = total_paths.saturating_add(device.paths.len()); - total_elements = device.paths.iter().fold(total_elements, |n, path| n.saturating_add(path.elements().len())); + total_elements = device.paths.iter().fold(total_elements, |count, path| count.saturating_add(path.elements().len())); if total_paths > limits.max_paths || total_elements > limits.max_path_elements { return Err(Error::OutputLimit); } diff --git a/crates/svg/src/write.rs b/crates/svg/src/write.rs index b73c8c1bf..469a4f8fa 100644 --- a/crates/svg/src/write.rs +++ b/crates/svg/src/write.rs @@ -182,7 +182,7 @@ fn write_elements(writer: &mut SvgWriter, elements: &[Element], depth: usize) { } fn view_box_data(bounds: [f64; 4]) -> String { - format!("{} {} {} {}", num(bounds[0], 3), num(bounds[1], 3), num(bounds[2], 3), num(bounds[3], 3)) + format!("{} {} {} {}", shortest(bounds[0]), shortest(bounds[1]), shortest(bounds[2]), shortest(bounds[3])) } /// A number written with a fixed count of decimals. A value that is not finite is written as