mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
M4.12: one loader and encoder for every frontend — engine fileio (load/encode/export/save, append-only FORMATS table, OPEN_EXTS, document.formats query, typed per-format options, parse_range); document.open reads native/legacy, SVG/SVGZ, PDF/.ai, .ait (opens untitled like native templates) and PNG/JPEG/GIF/WebP/TIFF/BMP; desktop, web, control channel, headless MCP and the CLI open and encode through it; open dialogs list "All readable files" plus one filter per format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d99853ed2a
commit
4d503c0327
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless]
|
||||
//! vectorcraft-cli run [--in file.vectorcraft|file.svg] [--cmd id [--params '{json}']]... [--export out.svg|.png|.pdf|.jpg|.webp|.vectorcraft]... [--scale 2]
|
||||
//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}']]... [--export out.svg|.png|.pdf|.jpg|.webp|.vectorcraft]... [--scale 2]
|
||||
//! vectorcraft-cli commands
|
||||
//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0] [--outline-text]
|
||||
//! vectorcraft-cli info FILE
|
||||
@@ -28,21 +28,21 @@ USAGE:
|
||||
(vectorcraft --control 7979), falling back to a headless in-process session.
|
||||
|
||||
vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N]
|
||||
Headless batch: open FILE (.vectorcraft/.svg) or start a new document, run commands in
|
||||
Headless batch: open FILE (any readable format) or start a new document, run commands in
|
||||
order, export (.svg, .png, .pdf, .jpg, .webp, .vectorcraft by extension). Prints one JSON result per step.
|
||||
|
||||
vectorcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
|
||||
vectorcraft-cli convert IN OUT [--scale N] [--artboard I] [--outline-text]
|
||||
Open IN (.vectorcraft, .svg, .pdf/.ai, images) and export OUT by extension (.svg, .pdf, .png,
|
||||
.jpg, .webp, .vectorcraft). Live effects are kept; --outline-text writes SVG text as paths.
|
||||
Open IN (any readable format) and export OUT by extension (.svg, .pdf, .png, .jpg, .webp,
|
||||
.vectorcraft). Live effects are kept; --outline-text writes SVG text as paths.
|
||||
|
||||
vectorcraft-cli info FILE
|
||||
Print a JSON summary: title, colour mode, units, artboards, object counts by kind, fonts.
|
||||
|
||||
vectorcraft-cli bench FILE [--size WxH] [--iters N]
|
||||
Render FILE (.vectorcraft/.svg) fitted to WxH (default 2880x1800) and print ms per frame
|
||||
Render FILE (any readable format) fitted to WxH (default 2880x1800) and print ms per frame
|
||||
(warm), multithreaded and single-threaded.
|
||||
|
||||
vectorcraft-cli perf [--paths N]
|
||||
@@ -50,6 +50,11 @@ USAGE:
|
||||
synthetic N-path document (default 50000). Exits non-zero if a budget is exceeded.
|
||||
";
|
||||
|
||||
/// The usage text plus the formats `document.open` reads.
|
||||
fn usage() -> String {
|
||||
format!("{USAGE}\nReadable formats: .{}\n", vectorcraft_engine::cmd::fileio::OPEN_EXTS.join(", ."))
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let r = match args.first().map(String::as_str) {
|
||||
@@ -61,14 +66,14 @@ fn main() -> ExitCode {
|
||||
Some("bench") => bench(&args[1..]),
|
||||
Some("perf") => perf::run(&args[1..]),
|
||||
Some("-h" | "--help" | "help") | None => {
|
||||
print!("{USAGE}");
|
||||
print!("{}", usage());
|
||||
Ok(())
|
||||
}
|
||||
Some("-V" | "--version") => {
|
||||
println!("vectorcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
}
|
||||
Some(other) => Err(format!("unknown subcommand `{other}`\n\n{USAGE}")),
|
||||
Some(other) => Err(format!("unknown subcommand `{other}`\n\n{}", usage())),
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! The CLI opens every readable format (PDF, .ai, images) through the engine's `document.open`.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli");
|
||||
|
||||
fn tmp(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("vectorcraft-cli-fileio-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir.join(name).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
fn ok(args: &[&str]) -> String {
|
||||
let out = Command::new(BIN).args(args).output().unwrap();
|
||||
assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr));
|
||||
String::from_utf8(out.stdout).unwrap()
|
||||
}
|
||||
|
||||
/// `run` a new `width`×`height` document with a rectangle and export it to `out`.
|
||||
fn make(out: &str, width: u32, height: u32) {
|
||||
ok(&[
|
||||
"run",
|
||||
"--cmd",
|
||||
"file.new",
|
||||
"--params",
|
||||
&format!(r#"{{"width":{width},"height":{height},"artboards":2}}"#),
|
||||
"--cmd",
|
||||
"shape.rectangle",
|
||||
"--params",
|
||||
r#"{"x":1,"y":1,"width":2,"height":1}"#,
|
||||
"--export",
|
||||
out,
|
||||
]);
|
||||
}
|
||||
|
||||
fn info(file: &str) -> Value {
|
||||
serde_json::from_str(&ok(&["info", file])).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_pdf_to_svg_and_info_on_ai() {
|
||||
let pdf = tmp("in.pdf");
|
||||
make(&pdf, 120, 80);
|
||||
let svg = tmp("out.svg");
|
||||
ok(&["convert", &pdf, &svg]);
|
||||
assert!(std::fs::read_to_string(&svg).unwrap().contains("<svg"));
|
||||
|
||||
let ai = tmp("in.ai");
|
||||
std::fs::copy(&pdf, &ai).unwrap();
|
||||
let v = info(&ai);
|
||||
assert_eq!(v["artboards"].as_array().unwrap().len(), 2, "{v}");
|
||||
assert_eq!(v["artboards"][0]["rect"][2], 120.0);
|
||||
// `run --in` takes it too.
|
||||
ok(&["run", "--in", &ai, "--cmd", "document.inspect"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn images_open_at_their_pixel_size() {
|
||||
let webp = tmp("tiny.webp");
|
||||
make(&webp, 3, 2);
|
||||
let v = info(&webp);
|
||||
assert_eq!(v["artboards"][0]["rect"], serde_json::json!([0.0, 0.0, 3.0, 2.0]), "{v}");
|
||||
assert_eq!(v["kinds"]["Image"], 1, "{v}");
|
||||
let png = tmp("tiny.png");
|
||||
ok(&["convert", &webp, &png, "--scale", "2"]);
|
||||
assert_eq!(&std::fs::read(&png).unwrap()[16..24], [0, 0, 0, 6, 0, 0, 0, 4], "IHDR 6×4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_lists_readable_formats() {
|
||||
let out = ok(&["--help"]);
|
||||
assert!(out.contains("Readable formats: .vectorcraft") && out.contains(".ait") && out.contains(".webp"), "{out}");
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_engine::cmd::fileio;
|
||||
use vectorcraft_ui_egui::{Services, VectorcraftApp};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
const OPEN_EXTS: &[&str] = &["vectorcraft", "drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"];
|
||||
const CANVAS_ID: &str = "vectorcraft_canvas";
|
||||
const LOADING_ID: &str = "vectorcraft_loading";
|
||||
|
||||
@@ -99,7 +99,8 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||
let inbox = open_inbox.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let Some(file) = rfd::AsyncFileDialog::new().add_filter("All supported", OPEN_EXTS).pick_file().await else {
|
||||
let dialog = fileio::open_filters().fold(rfd::AsyncFileDialog::new(), |d, (name, exts)| d.add_filter(name, exts));
|
||||
let Some(file) = dialog.pick_file().await else {
|
||||
return;
|
||||
};
|
||||
let bytes = file.read().await;
|
||||
@@ -125,7 +126,7 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
let document = window.document().ok_or("no document")?;
|
||||
let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes));
|
||||
let opts = web_sys::BlobPropertyBag::new();
|
||||
opts.set_type(mime_for(&name));
|
||||
opts.set_type(fileio::format_for_name(&name).map_or("application/octet-stream", |f| f.mime));
|
||||
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &opts).map_err(js)?;
|
||||
let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(js)?;
|
||||
let a: web_sys::HtmlAnchorElement = document.create_element("a").map_err(js)?.dyn_into().map_err(|_| "not an anchor")?;
|
||||
@@ -143,14 +144,3 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000).map_err(js)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mime_for(name: &str) -> &'static str {
|
||||
match name.rsplit('.').next().map(str::to_ascii_lowercase).as_deref() {
|
||||
Some("svg") => "image/svg+xml",
|
||||
Some("png") => "image/png",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("pdf") => "application/pdf",
|
||||
Some("vectorcraft" | "drawcraft") => "application/json",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ mod control_server;
|
||||
mod native_menu;
|
||||
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_engine::cmd::fileio;
|
||||
use vectorcraft_ui_egui::{Services, VectorcraftApp};
|
||||
|
||||
struct App(VectorcraftApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>);
|
||||
@@ -98,8 +99,8 @@ fn save_prefs(app: &VectorcraftApp) {
|
||||
fn services() -> Services {
|
||||
Services {
|
||||
pick_open: Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter("All supported", &["vectorcraft", "drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"])
|
||||
fileio::open_filters()
|
||||
.fold(rfd::FileDialog::new(), |d, (name, exts)| d.add_filter(name, exts))
|
||||
.pick_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user