Export: an HP-GL plot of the art's bounds measures images by the art their effects add

An image or a gradient mesh with a visible Crop Marks effect, or with a
Vector Halftone effect that makes dots, is measured in a plot of the art's
bounds. A halftone that makes no dots, such as one past the effect's cell
limit, leaves the image out as before.
This commit is contained in:
dotCooCoo
2026-11-14 09:59:27 -08:00
parent 10225d82e5
commit 951f689e8f
5 changed files with 56 additions and 11 deletions
+8 -5
View File
@@ -1027,15 +1027,18 @@ fn overlaps(a: Rect, b: Rect) -> bool {
/// Whether a plot draws nothing of `n` wherever it is, apart from what every export leaves out
/// (hidden objects, guides and template layers): an object at 0% opacity, a non-printing layer, a
/// placed image, a gradient mesh whose every point is at 0% opacity, or a path or compound path
/// without effects whose fills and strokes paint nothing. A clipping path still clips when the
/// plot draws nothing of it.
pub fn plots_nothing(n: &Node) -> bool {
/// without effects whose fills and strokes paint nothing. An image or a mesh in `doc` with a
/// visible Crop Marks effect, or with a Vector Halftone effect that makes dots, is not one of them.
/// A clipping path still clips when the plot draws nothing of it.
pub fn plots_nothing(doc: &Document, n: &Node) -> bool {
let unpainted = || n.appearance.effects.is_empty() && top_stroke(n).is_none() && top_fill(n).is_none();
let dots = || vectorcraft_effects::addons::halftone::halftone_art(doc, n).is_some_and(|g| g.children().is_some_and(|c| !c.is_empty()));
let adds_art = || vectorcraft_effects::has_crop_marks(n) || dots();
n.opacity <= 0.0
|| match &n.kind {
NodeKind::Layer { printable, .. } => !printable,
NodeKind::Image(_) => true,
NodeKind::Mesh(m) => m.points.iter().all(|p| p.opacity <= 0.0),
NodeKind::Image(_) => !adds_art(),
NodeKind::Mesh(m) => !adds_art() && m.points.iter().all(|p| p.opacity <= 0.0),
NodeKind::Path { .. } | NodeKind::Compound { .. } => unpainted(),
_ => false,
}
+22 -4
View File
@@ -183,7 +183,7 @@ fn meshes_in_blends_are_outlines_and_clear_meshes_draw_nothing() {
#[test]
fn plots_nothing_picks_what_a_plot_draws_nothing_of() {
type Make = fn(&mut Document) -> Node;
let cases: [(&str, Make, bool); 6] = [
let cases: [(&str, Make, bool); 7] = [
("stroked", |d| stroked(d, rect(10.0, 10.0, 40.0, 40.0), Color::BLACK), false),
("unpainted", |d| Node::path(d.alloc_id(), rect(10.0, 10.0, 40.0, 40.0), Appearance::basic(Paint::None, Paint::None, 1.0)), true),
(
@@ -211,6 +211,24 @@ fn plots_nothing_picks_what_a_plot_draws_nothing_of() {
},
true,
),
(
"image with Crop Marks",
|d| {
let image = vectorcraft_doc::ImageObject {
key: "raster-1".into(),
width: 4,
height: 2,
xf: Affine::translate((10.0, 20.0)) * Affine::scale(5.0),
link: None,
placement: Default::default(),
pixels: None,
};
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
n.appearance.effects.push(vectorcraft_doc::Effect { id: "cropMarks".into(), params: serde_json::json!({}), visible: true });
n
},
false,
),
("mesh", |d| Node::new(d.alloc_id(), NodeKind::Mesh(mesh_at(10.0))), false),
(
"clear mesh",
@@ -230,16 +248,16 @@ fn plots_nothing_picks_what_a_plot_draws_nothing_of() {
vec![n]
});
let Some(n) = held else { panic!("{name}") };
assert_eq!(plots_nothing(&n), nothing, "{name}");
assert_eq!(plots_nothing(&d, &n), nothing, "{name}");
assert_eq!(text(&plot(&d, &opts(&d))) == "IN;PU;SP0;", nothing, "{name}");
}
let mut d = doc_with(|d| vec![stroked(d, rect(10.0, 10.0, 40.0, 40.0), Color::BLACK)]);
let layer = d.layers[0].id;
assert!(!plots_nothing(&d.layers[0]));
assert!(!plots_nothing(&d, &d.layers[0]));
if let Some(NodeKind::Layer { printable, .. }) = d.node_mut(layer).map(|n| &mut n.kind) {
*printable = false;
}
assert!(plots_nothing(&d.layers[0]));
assert!(plots_nothing(&d, &d.layers[0]));
assert_eq!(text(&plot(&d, &opts(&d))), "IN;PU;SP0;");
}
+2 -2
View File
@@ -444,7 +444,7 @@ pub(super) fn art_page(cmd: &str, doc: &Document, p: &Value, left_out: impl Fn(&
/// when there is none, which such an export refuses.
pub fn export_art_bounds(doc: &Document, format: &str) -> Option<Rect> {
let doc = &*doc.without_edit_modes();
bounds_without(doc, &|n| format == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(n))
bounds_without(doc, &|n| format == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(doc, n))
}
/// The bounds of the visible art of `doc` with the objects that `left_out` picks hidden
@@ -770,7 +770,7 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
{
return Ok(enc);
}
let left_out = |n: &Node| f.id == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(n);
let left_out = |n: &Node| f.id == "hpgl" && vectorcraft_cad::hpgl::write::plots_nothing(doc, n);
if use_artboards == Some(false)
&& let Some((art, q)) = art_page(C, doc, p, left_out)?
{
@@ -254,6 +254,29 @@ fn a_plot_of_the_arts_bounds_leaves_out_images_in_symbol_art() {
}
}
/// A placed image with a Vector Halftone effect is measured when the effect makes dots, and left
/// out when it would take more cells than the effect makes.
#[test]
fn a_plot_of_the_arts_bounds_measures_images_by_their_halftone_dots() {
let mut s = two_boards();
let plot = |s: &mut Session| s.execute("document.export", &json!({"format": "hpgl", "useArtboards": false})).map(|v| bytes(&v));
let printing = s.doc().unwrap().doc.layers[0].id;
s.execute("layer.new", &json!({})).unwrap();
let mut png = vec![];
image::DynamicImage::new_rgb8(100, 100).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
s.execute("file.place", &json!({"name": "dots.png", "dataBase64": b64(&png), "at": [300, 300]})).unwrap();
s.execute("layer.setProps", &json!({"id": printing, "visible": false})).unwrap();
// At 300 lines per inch a 100 pt image takes more cells than the effect makes.
s.execute("effect.apply", &json!({"effect": "addon.color.halftone", "params": {"frequency": 300}})).unwrap();
let e = plot(&mut s).unwrap_err().to_string();
assert!(e.contains("no visible art"), "{e}");
s.execute("effect.apply", &json!({"effect": "addon.color.halftone", "params": {"frequency": 20}})).unwrap();
let out = String::from_utf8(plot(&mut s).unwrap()).unwrap();
assert!(out.contains("PD"), "{out}");
}
#[test]
fn export_options_are_checked() {
let mut s = two_boards();
+1
View File
@@ -1089,6 +1089,7 @@ way. `document.exportPdf {useArtboards: false}` writes that one page as well, an
(unless `includeNonPrinting` or `createLayers` keeps them; a `.ai` file keeps layers by default). An HP-GL plot of the
art's bounds is measured without what the plot leaves out: non-printing layers, objects at 0% opacity, placed images,
gradient meshes whose every point is at 0% opacity, and paths without effects whose fills and strokes paint nothing.
An image or a mesh with a Crop Marks effect, or with a Vector Halftone effect that makes dots, is measured.
Such objects in the art of a symbol that is not dynamic are left out of its instances too, and an instance whose
symbol holds nothing else is left out.
A document whose only visible art is left out is refused, and Export As refuses an HP-GL plot of it before the save