mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
Fix bounds and limits in shared vector readers
This commit is contained in:
4 files changed
+98
-25
No files matched your search
+60
-4
@@ -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<Imported, Error> {
|
||||
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<Imported, Error> {
|
||||
unsupported = unsupported.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
elements = elements.saturating_add(converted.iter().map(|path| path.elements().len()).sum::<usize>());
|
||||
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<Imported, Error> {
|
||||
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<i64>) -> f64 {
|
||||
match code {
|
||||
0 => 1.0,
|
||||
@@ -96,7 +151,7 @@ fn units_to_mm(code: i64, _measurement: Option<i64>) -> 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::<f64>().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));
|
||||
|
||||
+4
-10
@@ -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<BezPath>,
|
||||
/// 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<String>,
|
||||
pub preview_fallback: bool,
|
||||
@@ -45,10 +46,7 @@ pub fn import(bytes: &[u8], limits: Limits) -> Result<Imported, Error> {
|
||||
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<Imported, Error> {
|
||||
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());
|
||||
|
||||
+33
-10
@@ -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<Imported, Error> {
|
||||
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<Imported, Error> {
|
||||
import_selected(bytes, limits, Some(page_index))
|
||||
}
|
||||
|
||||
fn import_selected(bytes: &[u8], limits: Limits, selected: Option<usize>) -> Result<Imported, Error> {
|
||||
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<Imported, Error> {
|
||||
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::<usize>() > 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user