mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
Merge #475: CLI: --export writes the file type its extension names
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
//! Headless DesignCraft.
|
||||
//!
|
||||
//! ```text
|
||||
//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub] [--all-pages DIR]
|
||||
//! designcraft-cli run [--in FILE.designcraft|FILE.idml | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT.png|.jpg|.pdf|.designcraft|.idml|.epub|.html|.rtf|.txt] [--all-pages DIR]
|
||||
//! # --page, --scale and --pdf-options apply to the exports that follow them
|
||||
//! # .rtf and .txt hold the story being edited or of the selected frame
|
||||
//! designcraft-cli commands [FILTER] # list commands (JSON), optionally only ids/labels/menus containing FILTER
|
||||
//! designcraft-cli describe ID # one command: label, menu, shortcut, parameters
|
||||
//! designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]
|
||||
@@ -186,12 +187,48 @@ fn option_after_last_export(args: &[String]) -> Option<&str> {
|
||||
unused
|
||||
}
|
||||
|
||||
/// What `--export OUT` writes. The extension of `OUT` picks it, in any case.
|
||||
enum Export {
|
||||
/// A page as PNG or JPEG.
|
||||
Image,
|
||||
Pdf,
|
||||
/// The document itself.
|
||||
Document,
|
||||
/// What this command writes to a `path`.
|
||||
Command(&'static str),
|
||||
}
|
||||
|
||||
impl Export {
|
||||
/// A name with another extension is an error: it used to get a PNG of the page. One with no
|
||||
/// extension still does (`--export /dev/stdout`).
|
||||
fn of(out: &str) -> Result<Self, String> {
|
||||
let ext = std::path::Path::new(out).extension().and_then(|e| e.to_str()).map(str::to_lowercase);
|
||||
Ok(match ext.as_deref() {
|
||||
None | Some("png" | "jpg" | "jpeg") => Export::Image,
|
||||
Some("pdf") => Export::Pdf,
|
||||
Some("designcraft") => Export::Document,
|
||||
Some("idml") => Export::Command("file.exportIdml"),
|
||||
Some("epub") => Export::Command("file.exportEpub"),
|
||||
Some("html" | "htm") => Export::Command("file.exportHtml"),
|
||||
// The story being edited or of the selected frame.
|
||||
Some("rtf" | "txt") => Export::Command("file.exportText"),
|
||||
Some(_) => return Err(format!("--export {out}: unknown file type (.png, .jpg, .pdf, .designcraft, .idml, .epub, .html, .rtf, .txt)")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: &[String]) -> Result<(), String> {
|
||||
if let Some(opt) = option_after_last_export(args) {
|
||||
return Err(format!(
|
||||
"{opt} has no --export or --all-pages after it; options apply to the exports that follow them, so put it before its --export"
|
||||
));
|
||||
}
|
||||
// Nothing runs when one of the exports cannot be written.
|
||||
for a in args.windows(2) {
|
||||
if a[0] == "--export" {
|
||||
Export::of(&a[1])?;
|
||||
}
|
||||
}
|
||||
let mut s = Session::new();
|
||||
let mut page = 0usize;
|
||||
let mut scale = 1.0f64;
|
||||
@@ -231,22 +268,32 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
"--scale" => scale = val()?.parse().map_err(|_| "bad --scale")?,
|
||||
"--pdf-options" => {
|
||||
pdf_opts = serde_json::from_str(&val()?).map_err(|e| format!("--pdf-options: {e}"))?;
|
||||
// The export adds its path to them.
|
||||
if !pdf_opts.is_object() {
|
||||
return Err("--pdf-options: expected a JSON object".into());
|
||||
}
|
||||
}
|
||||
"--export" => {
|
||||
let out = val()?;
|
||||
if out.ends_with(".epub") {
|
||||
let r = s.execute("file.exportEpub", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
eprintln!("wrote {out} ({} bytes)", r["bytes"]);
|
||||
} else if out.ends_with(".pdf") {
|
||||
let mut p = pdf_opts.clone();
|
||||
p["path"] = json!(out);
|
||||
let r = s.execute("file.exportPdf", &p).map_err(|e| e.to_string())?;
|
||||
eprintln!("wrote {out} ({} pages, {} bytes)", r["pages"], r["bytes"]);
|
||||
for w in r["warnings"].as_array().into_iter().flatten() {
|
||||
eprintln!("warning: {}", w.as_str().unwrap_or_default());
|
||||
match Export::of(&out)? {
|
||||
Export::Image => export(&mut s, &out, page, scale)?,
|
||||
Export::Pdf => {
|
||||
let mut p = pdf_opts.clone();
|
||||
p["path"] = json!(out);
|
||||
let r = s.execute("file.exportPdf", &p).map_err(|e| e.to_string())?;
|
||||
eprintln!("wrote {out} ({} pages, {} bytes)", r["pages"], r["bytes"]);
|
||||
for w in r["warnings"].as_array().into_iter().flatten() {
|
||||
eprintln!("warning: {}", w.as_str().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Export::Document => {
|
||||
s.execute("file.saveAs", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
eprintln!("saved {out}");
|
||||
}
|
||||
Export::Command(cmd) => {
|
||||
let r = s.execute(cmd, &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
eprintln!("wrote {out} ({} bytes)", r["bytes"]);
|
||||
}
|
||||
} else {
|
||||
export(&mut s, &out, page, scale)?;
|
||||
}
|
||||
}
|
||||
"--all-pages" => {
|
||||
@@ -263,24 +310,16 @@ fn run(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A page as PNG, or as JPEG for `.jpg` and `.jpeg`.
|
||||
fn export(s: &mut Session, out: &str, page: usize, scale: f64) -> Result<(), String> {
|
||||
if out.to_ascii_lowercase().ends_with(".idml") {
|
||||
let r = s.execute("file.exportIdml", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
eprintln!("wrote {out} ({} bytes)", r["bytes"]);
|
||||
return Ok(());
|
||||
}
|
||||
if out.ends_with(".designcraft") {
|
||||
s.execute("file.saveAs", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
eprintln!("saved {out}");
|
||||
return Ok(());
|
||||
}
|
||||
let st = s.doc().map_err(|e| e.to_string())?;
|
||||
let mut r = designcraft_render::Renderer::new();
|
||||
let t = std::time::Instant::now();
|
||||
let img = r
|
||||
.render_page(&st.doc, &s.cache, page, scale, true, &designcraft_render::RenderOptions { printing_only: true, ..Default::default() })
|
||||
.ok_or("no such page")?;
|
||||
let bytes = if out.ends_with(".jpg") || out.ends_with(".jpeg") { img.to_jpeg(90) } else { img.to_png() };
|
||||
let lower = out.to_lowercase();
|
||||
let bytes = if lower.ends_with(".jpg") || lower.ends_with(".jpeg") { img.to_jpeg(90) } else { img.to_png() };
|
||||
std::fs::write(out, bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
eprintln!("wrote {out} ({}×{}, {:.1} ms, {} glyphs)", img.width, img.height, t.elapsed().as_secs_f64() * 1000.0, r.stats.glyphs);
|
||||
Ok(())
|
||||
@@ -319,7 +358,7 @@ fn script(args: &[String]) -> Result<(), String> {
|
||||
let mut setup: Vec<(String, Value)> = Vec::new();
|
||||
let mut connect: Option<String> = None;
|
||||
let mut save: Option<String> = None;
|
||||
let mut export: Vec<String> = Vec::new();
|
||||
let mut export: Vec<(Export, String)> = Vec::new();
|
||||
let mut keep_going = false;
|
||||
let mut it = args.iter();
|
||||
while let Some(a) = it.next() {
|
||||
@@ -333,7 +372,10 @@ fn script(args: &[String]) -> Result<(), String> {
|
||||
"--sample" => setup.push(("file.newSample".into(), json!({}))),
|
||||
"--connect" => connect = Some(val()?),
|
||||
"--save" => save = Some(val()?),
|
||||
"--export" => export.push(val()?),
|
||||
"--export" => {
|
||||
let out = val()?;
|
||||
export.push((Export::of(&out)?, out));
|
||||
}
|
||||
"--keep-going" => keep_going = true,
|
||||
f if !f.starts_with("--") && file.is_none() => file = Some(f.to_string()),
|
||||
other => return Err(format!("unknown script option `{other}`")),
|
||||
@@ -378,19 +420,16 @@ fn script(args: &[String]) -> Result<(), String> {
|
||||
if let Some(out) = &save {
|
||||
exec(b, "file.saveAs", json!({"path": out}))?;
|
||||
}
|
||||
for out in &export {
|
||||
let lower = out.to_lowercase();
|
||||
for (kind, out) in &export {
|
||||
let p = json!({"path": out});
|
||||
if lower.ends_with(".pdf") {
|
||||
exec(b, "file.exportPdf", p)?;
|
||||
} else if lower.ends_with(".idml") {
|
||||
exec(b, "file.exportIdml", p)?;
|
||||
} else if lower.ends_with(".epub") {
|
||||
exec(b, "file.exportEpub", p)?;
|
||||
} else {
|
||||
match kind {
|
||||
// PNG / JPEG of the first page.
|
||||
b.call("app.export", p)?;
|
||||
}
|
||||
Export::Image => b.call("app.export", p)?,
|
||||
Export::Pdf => exec(b, "file.exportPdf", p)?,
|
||||
// A copy: the document in the app stays the file it is (`--save` is Save As).
|
||||
Export::Document => exec(b, "file.saveACopy", p)?,
|
||||
Export::Command(cmd) => exec(b, cmd, p)?,
|
||||
};
|
||||
}
|
||||
}
|
||||
outln!("{}", serde_json::to_string_pretty(&report.to_json()).unwrap_or_default());
|
||||
|
||||
Reference in New Issue
Block a user