From 2c9f5a6a11ce89aa268bf2baba8e621109f253e4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 00:12:54 -0800 Subject: [PATCH 1/2] HP-GL export: an HP-GL plot past 16-bit coordinates gets a warning Coordinates outside -32,768 to 32,767 plotter units (about 82 cm from the origin) are written as they are, with a warning that older devices may not read them. HP-GL/2 plots are not checked against this range. --- crates/cad/src/hpgl/write.rs | 8 ++++++++ crates/cad/src/hpgl/write/tests.rs | 21 +++++++++++++++++++++ crates/engine/src/cmd/fileio/hpgl.rs | 2 +- docs/mcp.md | 8 +++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/cad/src/hpgl/write.rs b/crates/cad/src/hpgl/write.rs index 9e77495aa2..22773ac68b 100644 --- a/crates/cad/src/hpgl/write.rs +++ b/crates/cad/src/hpgl/write.rs @@ -15,6 +15,8 @@ //! Coordinates are plotter units (1016 to the inch, y up) from the region's bottom-left corner or //! its center, rounded to whole units. Art past ±8,388,607 units (2²³ − 1, the smallest range //! the HP-GL/2 reference asks a device to take for encoded polylines) from the origin is cut off. +//! An HP-GL plot with a coordinate outside −32,768 to 32,767 (16-bit numbers, the range older +//! devices read) gets a warning; HP-GL/2's encoded polylines take the wider range. //! The pens are drawn in turn (pen 1's paths, then pen 2's), each in paint order or, with //! `sort`, each next path the one that starts nearest where the pen stopped. //! @@ -39,6 +41,8 @@ use clip::Region; const PLU_PER_PT: f64 = 1016.0 / 72.0; /// The largest coordinate written, plotter units. pub const MAX_COORD: i64 = (1 << 23) - 1; +/// The coordinates an HP-GL plot keeps to without a warning: 16-bit numbers, plotter units. +const CLASSIC_RANGE: std::ops::RangeInclusive = -32_768..=32_767; /// The curve tolerance a path is flattened to by default, points. pub const DEFAULT_TOLERANCE: f64 = 0.1; /// The smallest and largest curve tolerance, points. @@ -67,6 +71,7 @@ const TOO_MANY_POINTS: &str = "the export is too large: the art past its first 1 const CLIP_WORK: &str = "clipping masks are too complex to apply in full: the art past that point is drawn whole"; const MASK_POINTS: &str = "clipping masks past 10 million points together are left out: the art they clip is drawn unclipped by them"; const OUT_OF_RANGE: &str = "art past 8,388,607 plotter units (about 210 m) from the origin is cut off"; +const PAST_CLASSIC_RANGE: &str = "the plot has coordinates past 32,767 plotter units (about 82 cm) from the origin, which older HP-GL devices may not read: HP-GL/2 takes a wider range"; const MANY_COLORS: &str = "the art has more than 255 colors: the colors past those draw with the pen of the nearest color"; const UNSORTED: &str = "the plot has too many paths to sort in full: the rest are drawn in paint order"; const TOO_DEEP: &str = "symbols, blends, envelopes and repeats nested more than 8 deep were left out"; @@ -261,6 +266,9 @@ pub fn export_regions(doc: &Document, opts: &HpglOptions, regions: &[Rect]) -> R } } } + if opts.version == Version::Hpgl && by_pen.iter().flatten().flatten().any(|(x, y)| !CLASSIC_RANGE.contains(x) || !CLASSIC_RANGE.contains(y)) { + warn(&mut warnings, PAST_CLASSIC_RANGE); + } let bytes = match opts.version { Version::Hpgl => plain(&by_pen), Version::Hpgl2 => encoded(&by_pen, &pens), diff --git a/crates/cad/src/hpgl/write/tests.rs b/crates/cad/src/hpgl/write/tests.rs index 2093fe8b48..7ef5105340 100644 --- a/crates/cad/src/hpgl/write/tests.rs +++ b/crates/cad/src/hpgl/write/tests.rs @@ -454,6 +454,27 @@ fn art_past_the_plotter_range_is_cut_off() { assert_eq!(text(&out), "IN;PU;SP1;PU0,706;PD141,706;PU;SP0;"); } +/// HP-GL past 16-bit coordinates is written as it is, with a warning; HP-GL/2 gets none. +#[test] +fn hpgl_past_16_bit_coordinates_warns() { + let region = Rect::new(-2400.0, 0.0, 2400.0, 100.0); + // Past 32,767 from the bottom-left corner, within the range from the center. + let d = doc_with(|d| vec![stroked(d, line((-2000.0, 50.0), (2000.0, 50.0)), Color::BLACK)]); + let out = plot(&d, &HpglOptions { region, ..HpglOptions::default() }); + assert_eq!(out.warnings, [PAST_CLASSIC_RANGE]); + assert_eq!(text(&out), "IN;PU;SP1;PU5644,706;PD62089,706;PU;SP0;"); + let out = plot(&d, &HpglOptions { region, origin: Origin::Center, ..HpglOptions::default() }); + assert!(out.warnings.is_empty(), "{:?}", out.warnings); + assert_eq!(text(&out), "IN;PU;SP1;PU-28222,0;PD28222,0;PU;SP0;"); + // Past −32,768 and 32,767 from the center. + let d = doc_with(|d| vec![stroked(d, line((-2400.0, 50.0), (2400.0, 50.0)), Color::BLACK)]); + let out = plot(&d, &HpglOptions { region, origin: Origin::Center, ..HpglOptions::default() }); + assert_eq!(out.warnings, [PAST_CLASSIC_RANGE]); + assert_eq!(text(&out), "IN;PU;SP1;PU-33867,0;PD33867,0;PU;SP0;"); + let out = plot(&d, &HpglOptions { region, origin: Origin::Center, version: Version::Hpgl2, ..HpglOptions::default() }); + assert!(out.warnings.is_empty(), "{:?}", out.warnings); +} + #[test] fn a_plot_stops_at_its_point_budget() { let d = doc_with(|d| (0..MAX_POINTS / 4).map(|i| stroked(d, rect(1.0, 1.0, 2.0 + (i % 50) as f64, 3.0), Color::BLACK)).collect()); diff --git a/crates/engine/src/cmd/fileio/hpgl.rs b/crates/engine/src/cmd/fileio/hpgl.rs index 0016afa10e..a7980b5072 100644 --- a/crates/engine/src/cmd/fileio/hpgl.rs +++ b/crates/engine/src/cmd/fileio/hpgl.rs @@ -22,7 +22,7 @@ pub(super) const OPTIONS: &[FormatOption] = &[ name: "hpglVersion", ty: "string", default: "\"hpgl\"", - description: "hpgl (IN, SP, PU and PD only) or hpgl2 (also the pens' colors, NP and PC, and each pen's paths as one encoded polyline, PE)", + description: "hpgl (IN, SP, PU and PD only; a coordinate outside -32,768 to 32,767 gets a warning) or hpgl2 (also the pens' colors, NP and PC, and each pen's paths as one encoded polyline, PE)", }, FormatOption { name: "curveTolerance", diff --git a/docs/mcp.md b/docs/mcp.md index 39ec4c777e..3494b98244 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6419,10 +6419,12 @@ are VectorCraft's own. `hpglVersion: "hpgl"` (the default) writes `IN;`, then for each pen `PU;SP;` and each path `PUx,y;PDx,y,…;`, and ends with `PU;SP0;`. `"hpgl2"` also writes the palette (`NP` and a `PC` color for each pen) and draws each pen's -paths as one encoded polyline (`PE`, base 64), which takes fewer bytes than the same moves as text. `warnings` names +paths as one encoded polyline (`PE`, base 64), which takes fewer bytes than the same moves as text. An `"hpgl"` plot +with a coordinate outside −32,768 to 32,767 units (about 82 cm from the origin, the range of 16-bit numbers) is written +as it is, with a warning that older devices may not read it; this check is VectorCraft's own. `warnings` names type drawn as outlines, inline graphics in type (left out), placed images, opacity masks, raster effects, stroke -looks that are not drawn, art past the plotter's range, colors past 255 pens, and the limits on points, clipping -work, symbols, live art, nesting depth and sorting. +looks that are not drawn, art past the plotter's range, HP-GL coordinates past 16-bit numbers, colors past 255 pens, +and the limits on points, clipping work, symbols, live art, nesting depth and sorting. ```json {"name":"export","arguments":{"path":"/tmp/decal.plt","format":"hpgl"}} From 87ab5b445f27934cd68741e974a3c580a33d2628 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 00:21:44 -0800 Subject: [PATCH 2/2] HP-GL export: the writer refuses a bad curve tolerance or more than 255 pens A tolerance outside 0.01 to 10 points, or one that is not finite, and a pen table of more than 255 colors are errors instead of being clamped or cut short. document.export already refuses both. --- crates/cad/src/hpgl/write.rs | 15 +++++++++++---- crates/cad/src/hpgl/write/tests.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/crates/cad/src/hpgl/write.rs b/crates/cad/src/hpgl/write.rs index 22773ac68b..80ac40f866 100644 --- a/crates/cad/src/hpgl/write.rs +++ b/crates/cad/src/hpgl/write.rs @@ -142,9 +142,11 @@ impl Origin { #[derive(Clone, Debug)] pub struct HpglOptions { pub version: Version, - /// How far a flattened curve may lie from the curve, points ([`TOLERANCE_RANGE`]). + /// How far a flattened curve may lie from the curve, points ([`TOLERANCE_RANGE`]; a value + /// outside it is an error). pub tolerance: f64, - /// The pen table: pen 1's color first. Empty: a pen for each color the art draws in. + /// The pen table: pen 1's color first, up to [`MAX_PENS`] (more is an error). Empty: a pen for + /// each color the art draws in. pub pens: Vec<[u8; 3]>, pub origin: Origin, /// Draw each pen's paths nearest first, instead of in paint order. @@ -184,8 +186,13 @@ pub fn export_regions(doc: &Document, opts: &HpglOptions, regions: &[Rect]) -> R if regions.iter().any(|r| ![r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()) || r.width() <= 0.0 || r.height() <= 0.0) { return Err("the export region is empty".into()); } - let tolerance = - if opts.tolerance.is_finite() { opts.tolerance.clamp(*TOLERANCE_RANGE.start(), *TOLERANCE_RANGE.end()) } else { DEFAULT_TOLERANCE }; + if !TOLERANCE_RANGE.contains(&opts.tolerance) { + return Err("the curve tolerance must be from 0.01 to 10 points".into()); + } + if opts.pens.len() > MAX_PENS { + return Err("the pen table holds more than 255 pens".into()); + } + let tolerance = opts.tolerance; // Layers the plot leaves out go first: baking can wrap one in art that would be drawn (its // crop marks). let shown = plotted(&doc.layers, false).map(|layers| { diff --git a/crates/cad/src/hpgl/write/tests.rs b/crates/cad/src/hpgl/write/tests.rs index 7ef5105340..0917cf02c9 100644 --- a/crates/cad/src/hpgl/write/tests.rs +++ b/crates/cad/src/hpgl/write/tests.rs @@ -441,6 +441,22 @@ fn an_empty_or_zero_length_plot_has_no_moves() { assert!(export(&d, &nan).is_err()); } +/// A tolerance outside its range and a pen table of more than 255 pens are errors, not changed. +#[test] +fn bad_options_are_errors() { + let d = three(); + for tolerance in [0.0, 0.009, 10.01, f64::NAN, f64::INFINITY] { + let o = HpglOptions { tolerance, ..opts(&d) }; + assert_eq!(export(&d, &o).err().as_deref(), Some("the curve tolerance must be from 0.01 to 10 points"), "{tolerance}"); + } + for tolerance in [0.01, 10.0] { + assert!(export(&d, &HpglOptions { tolerance, ..opts(&d) }).is_ok(), "{tolerance}"); + } + let o = HpglOptions { pens: vec![[0, 0, 0]; MAX_PENS + 1], ..opts(&d) }; + assert_eq!(export(&d, &o).err().as_deref(), Some("the pen table holds more than 255 pens")); + assert!(export(&d, &HpglOptions { pens: vec![[0, 0, 0]; MAX_PENS], ..opts(&d) }).is_ok()); +} + #[test] fn art_past_the_plotter_range_is_cut_off() { let d = doc_with(|d| vec![stroked(d, line((0.0, 50.0), (1e9, 50.0)), Color::BLACK)]);