mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
CLI, MCP: the raster scale no longer resizes DXF exports
`vectorcraft-cli run/convert --scale` and the MCP export tool's `scale` are raster pixels per point, but they were passed to every export, and DXF reads `scale` as drawing units per unit: a 2x PNG next to a DXF doubled the drawing and left it unitless ($INSUNITS 0). They now go only to raster formats. DXF's own scale stays reachable through the format options.
This commit is contained in:
@@ -79,14 +79,16 @@ USAGE:
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N]
|
||||
Headless batch: open FILE (any readable format) or start a new document, run commands in
|
||||
order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE
|
||||
is a template, written as file.saveAsTemplate writes it). Prints one JSON result per step.
|
||||
is a template, written as file.saveAsTemplate writes it). --scale sizes raster exports
|
||||
(pixels per point, default 1); other formats leave it out. Prints one JSON result per step.
|
||||
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit]
|
||||
Open IN (any readable format) and export OUT in the format its extension picks (see Writable
|
||||
formats). --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
|
||||
formats). --scale sizes a raster OUT (pixels per point, default 1); other formats leave it
|
||||
out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard
|
||||
unless one of them is given, EPS the bounds of the art, the other formats the first artboard.
|
||||
A .ait OUT is a template of every artboard, written as file.saveAsTemplate writes it (not with
|
||||
--artboard or --range).
|
||||
@@ -278,7 +280,8 @@ fn convert(args: &[String]) -> Result<(), String> {
|
||||
} else {
|
||||
// A conversion keeps hidden layers and objects, written hidden, where the format can (SVG,
|
||||
// PSD).
|
||||
let mut params = json!({"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy});
|
||||
let params = json!({"path": output, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy});
|
||||
let mut params = with_pixel_scale(params, &output, scale);
|
||||
if let Some(decimals) = decimals {
|
||||
params["decimals"] = json!(decimals);
|
||||
}
|
||||
@@ -302,6 +305,16 @@ fn is_template(path: &str) -> bool {
|
||||
vectorcraft_engine::cmd::fileio::format_for_name(path).is_some_and(|f| f.id == "ait")
|
||||
}
|
||||
|
||||
/// The export `params` for `path`, with `--scale` (pixels per point) when its format is raster.
|
||||
/// DXF has a `scale` of its own (drawing units per unit), which would resize the drawing and leave
|
||||
/// its units unset.
|
||||
fn with_pixel_scale(mut params: Value, path: &str, scale: f64) -> Value {
|
||||
if vectorcraft_engine::cmd::fileio::writable_format(None, Some(path)).is_ok_and(|f| f.raster) {
|
||||
params["scale"] = json!(scale);
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
fn info(args: &[String]) -> Result<(), String> {
|
||||
let file = args.first().ok_or("info needs a FILE")?;
|
||||
let mut h = Headless::new();
|
||||
@@ -391,7 +404,7 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
let r = if is_template(&path) {
|
||||
h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": path, "format": "ait"}}))
|
||||
} else {
|
||||
h.call("app.export", json!({"path": path, "scale": scale}))
|
||||
h.call("app.export", with_pixel_scale(json!({"path": path}), &path, scale))
|
||||
};
|
||||
let r = r.map_err(|e| format!("export {path}: {e}"))?;
|
||||
emit(json!({"step": "export", "result": r}))?;
|
||||
|
||||
@@ -58,6 +58,28 @@ fn run_batch_exports() {
|
||||
assert_eq!(last["result"]["artboards"][0]["width"], 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_sizes_raster_exports_only() {
|
||||
// --scale is pixels per point. DXF has a `scale` of its own (drawing units per mm) that would
|
||||
// resize the drawing and leave its units unset.
|
||||
let svg = tmp("scale.svg");
|
||||
std::fs::write(&svg, r#"<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50"><rect x="10" y="10" width="30" height="20"/></svg>"#)
|
||||
.unwrap();
|
||||
let svg = svg.to_str().unwrap();
|
||||
let (plain, scaled, png) = (tmp("plain.dxf"), tmp("scaled.dxf"), tmp("scaled.png"));
|
||||
let ok = |args: &[&str]| {
|
||||
let out = Command::new(BIN).args(args).output().unwrap();
|
||||
assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr));
|
||||
};
|
||||
ok(&["convert", svg, plain.to_str().unwrap()]);
|
||||
ok(&["convert", svg, scaled.to_str().unwrap(), "--scale", "2"]);
|
||||
assert!(std::fs::read(&scaled).unwrap() == std::fs::read(&plain).unwrap(), "convert --scale 2 changed the DXF");
|
||||
ok(&["run", "--in", svg, "--export", scaled.to_str().unwrap(), "--export", png.to_str().unwrap(), "--scale", "2"]);
|
||||
assert!(std::fs::read(&scaled).unwrap() == std::fs::read(&plain).unwrap(), "run --scale 2 changed the DXF");
|
||||
// 100 pt at scale 2 → 200 px (IHDR width).
|
||||
assert_eq!(std::fs::read(&png).unwrap()[16..20], 200u32.to_be_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_reports_errors() {
|
||||
let out = Command::new(BIN).args(["run", "--cmd", "nope.nothing"]).output().unwrap();
|
||||
|
||||
@@ -132,6 +132,17 @@ fn export_is_the_same_call_in_the_app_and_headless() {
|
||||
assert_eq!(direct["dataBase64"], r["dataBase64"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_raster_scale_leaves_other_formats_alone() {
|
||||
// DXF's own `scale` (drawing units per mm) comes through `options`; the raster one would resize
|
||||
// the drawing and leave its units unset.
|
||||
let mut h = two_boards();
|
||||
let dxf = |h: &mut Headless, args: Value| text(&call_tool(h, "export", &args))["dataBase64"].clone();
|
||||
let plain = dxf(&mut h, json!({"format": "dxf"}));
|
||||
assert!(dxf(&mut h, json!({"format": "dxf", "scale": 2})) == plain, "the raster scale changed the DXF");
|
||||
assert!(dxf(&mut h, json!({"format": "dxf", "options": {"scale": 2}})) != plain, "the DXF scale was dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exporting_native_keeps_the_document_path() {
|
||||
let mut h = two_boards();
|
||||
|
||||
+16
-4
@@ -2,7 +2,7 @@
|
||||
//! [`Backend`].
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::cmd::fileio::{ARTBOARD_PARAMS, FORMATS, OPEN_EXTS, SAVE_FORMATS, format};
|
||||
use vectorcraft_engine::cmd::fileio::{ARTBOARD_PARAMS, FORMATS, OPEN_EXTS, SAVE_FORMATS, format, writable_format};
|
||||
|
||||
use crate::backend::Backend;
|
||||
|
||||
@@ -1003,9 +1003,9 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
|
||||
params.remove(k);
|
||||
}
|
||||
}
|
||||
for k in ["path", "format", "scale", "artboard", "range", "outlineText"] {
|
||||
if let Some(v) = a.get(k).filter(|v| !v.is_null()) {
|
||||
params.insert(k.into(), v.clone());
|
||||
for k in named_export_params(a) {
|
||||
if let Some(v) = a.get(*k).filter(|v| !v.is_null()) {
|
||||
params.insert((*k).into(), v.clone());
|
||||
}
|
||||
}
|
||||
let cmd = if a.get("selection").and_then(Value::as_bool) == Some(true) { "document.exportSelection" } else { "document.export" };
|
||||
@@ -1054,6 +1054,18 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
|
||||
}
|
||||
}
|
||||
|
||||
/// The named `export` arguments passed on to `document.export`. `scale` is raster pixels per
|
||||
/// point, so only a raster format gets it: DXF's own (drawing units per unit, given in `options`)
|
||||
/// would resize the drawing and leave its units unset.
|
||||
fn named_export_params(a: &Args) -> &'static [&'static str] {
|
||||
let format = writable_format(a.get("format").and_then(Value::as_str), a.get("path").and_then(Value::as_str));
|
||||
if format.is_ok_and(|f| f.raster) {
|
||||
&["path", "format", "scale", "artboard", "range", "outlineText"]
|
||||
} else {
|
||||
&["path", "format", "artboard", "range", "outlineText"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one tool. Failures (unknown tool, bad arguments, command errors) come back as an
|
||||
/// `isError` result so the model can read and correct them.
|
||||
pub fn call_tool(b: &mut dyn Backend, name: &str, args: &Value) -> ToolResult {
|
||||
|
||||
Reference in New Issue
Block a user