//! fig2dev in the engine (`fig2dev`, a program of its own that //! RustFig depends on, plan B12): //! //! - Every export goes through fig2dev: the document's objects, as fig2dev's //! reader would return them for the saved Fig 3.2 text, go straight to //! the language's driver with fig2dev's options ([`export`]); where the //! reader would print a message or refuse the file, the text is written //! and read ([`export_via_text`], as xfig hands a figure to fig2dev). A //! language is available as soon as `fig2dev::drivers::driver` provides //! it; the combined LaTeX exports (`pstex`, `pdftex`, `pspdftex`) need //! both halves. //! - Every Fig file (1.3 to 3.2) opens through fig2dev's reader, converted by //! `rustfig_format::bridge` (`rustfig_format::read_any`). //! - Pictures as fig2dev reads them (`readpics.c`), where the `image` //! crate can't. //! //! fig2dev's language and option tables are re-exported here //! ([`languages`], [`options`]) for the export dialog and the CLI. pub use fig2dev::util::pdfpic::PdfPicture; pub use fig2dev::util::readpics::LoadedPicture; pub use fig2dev::{languages, options}; use fig2dev::object::Compound; use fig2dev::{read, Ctx, Driver}; use languages::Language; use rustfig_model::Document; use std::path::Path; /// Whether the fig2dev crate writes language `id` (fig2dev's name). pub fn driver_available(id: &str) -> bool { fig2dev::drivers::driver(id).is_some() } /// Whether RustFig can export `lang`: fig2dev's driver for it is ported /// (for the combined languages, the drivers of both halves). pub fn language_available(lang: &Language) -> bool { match lang.id { "pstex" => driver_available("pstex") && driver_available("pstex_t"), "pdftex" => driver_available("pdftex") && driver_available("pdftex_t"), "pspdftex" => driver_available("pstex") && driver_available("pdftex") && driver_available("pstex_t"), "pspdf" => driver_available("eps") && driver_available("pdf"), id => driver_available(id), } } /// Export `doc` with fig2dev's driver for `lang` (fig2dev's `-L` name): /// `options` are fig2dev's option letters and arguments, in order (general /// ones too); `dir` is where relative picture names are found. The output /// and fig2dev's messages (its warnings); fig2dev's errors are `Err`. /// /// The drivers get the objects straight from the document /// ([`rustfig_format::bridge::export_objects`]), the same as through /// [`export_via_text`] (plan D7); where the Fig file would make fig2dev's /// reader say something or read it differently, the text path runs. pub fn export( doc: &Document, lang: &str, options: &[(char, Option)], dir: Option<&Path>, ) -> Result<(Vec, Vec), String> { let (mut dev, mut ctx) = setup(lang, options, dir)?; match rustfig_format::bridge::export_objects(doc, &mut ctx) { Some(objects) => finish(lang, dev.as_mut(), &mut ctx, &objects), None => export_via_text(doc, lang, options, dir), } } /// [`export`] the way xfig does it: the document written as Fig 3.2 text /// and read by fig2dev's reader. The reference for [`export`]. pub fn export_via_text( doc: &Document, lang: &str, options: &[(char, Option)], dir: Option<&Path>, ) -> Result<(Vec, Vec), String> { let (mut dev, mut ctx) = setup(lang, options, dir)?; let fig = rustfig_format::write_fig(doc); let objects = read::readfp_fig(&mut ctx, fig.as_bytes()).map_err(|_| ctx.messages.join("\n"))?; finish(lang, dev.as_mut(), &mut ctx, &objects) } /// The driver and the context with fig2dev's options applied. fn setup(lang: &str, options: &[(char, Option)], dir: Option<&Path>) -> Result<(Box, Ctx), String> { let mut dev = fig2dev::drivers::driver(lang).ok_or_else(|| format!("no fig2dev driver for {lang}"))?; let mut ctx = Ctx { lang: lang.to_string(), dir: dir.map(Path::to_path_buf), ..Ctx::default() }; let l = Some(lang.to_string()); for (c, arg) in std::iter::once(&('L', l)).chain(options) { let arg = arg.as_deref(); if !ctx.general_option(*c, arg)? { dev.option(&mut ctx, *c, arg)?; } } // After the options, so that -m counts (fig2dev's get_args). ctx.font_size *= ctx.fontmag; Ok((dev, ctx)) } fn finish( lang: &str, dev: &mut dyn Driver, ctx: &mut Ctx, objects: &Compound, ) -> Result<(Vec, Vec), String> { let status = fig2dev::convert(ctx, dev, objects)?; let messages = std::mem::take(&mut ctx.messages); if status.exit != 0 && status.output.is_empty() { return Err(if messages.is_empty() { format!("fig2dev's {lang} driver failed") } else { messages.join("\n") }); } Ok((status.output, messages)) } /// Read picture `path` as fig2dev does (`.gz`/`.Z` variants, XBM, XPM, /// PCX, EPS, PDF and the common bitmaps). pub fn load_picture(path: &Path) -> Result { fig2dev::util::readpics::load_picture(&mut Ctx::default(), &path.to_string_lossy()) } /// An EPS file as a PDF page by ghostscript, when it is installed (B15; /// fig2dev's `util::gs`: `gs` on `PATH`, no shell, time and size limits). #[cfg(not(target_arch = "wasm32"))] pub fn gs_eps_to_pdf(eps: &[u8]) -> Result>, String> { fig2dev::util::gs::eps_to_pdf(eps) } /// fig2dev's command line (`fig2dev::cli::run`) on the process's stdin, /// stdout and stderr: what `fig2dev ARGS…` does, reached as /// `rustfig fig2dev ARGS…`, `rustfig-cli fig2dev ARGS…` or by starting /// the program under the name `fig2dev` (plan DD10). Returns the exit /// status. Not on wasm (no process streams). #[cfg(not(target_arch = "wasm32"))] pub fn run_cli(args: Vec) -> u8 { fig2dev::cli::run( args, &mut std::io::stdin().lock(), &mut std::io::stdout().lock(), &mut std::io::stderr().lock(), ) } /// The process arguments after the program name, as fig2dev reads them /// (not UTF-8 is read lossily rather than aborting). #[cfg(not(target_arch = "wasm32"))] pub fn process_args() -> Vec { std::env::args_os() .skip(1) .map(|a| a.to_string_lossy().into_owned()) .collect() } /// True when the program was started under the name `fig2dev` (a link or /// copy of the RustFig binary): `argv[0]`'s file stem is `fig2dev`, so /// `fig2dev.exe` on Windows counts too. #[cfg(not(target_arch = "wasm32"))] pub fn started_as_fig2dev() -> bool { std::env::args_os().next().is_some_and(|a| is_fig2dev_name(&a)) } /// `argv[0]` names fig2dev: its file stem is `fig2dev` (case-insensitive, /// for Windows). pub fn is_fig2dev_name(argv0: &std::ffi::OsStr) -> bool { // Both separators, whatever the platform: argv[0] may come from either. let s = argv0.to_string_lossy(); let base = s.rsplit(['/', '\\']).next().unwrap_or(""); let stem = base .strip_suffix(".exe") .or_else(|| base.strip_suffix(".EXE")) .unwrap_or(base); stem.eq_ignore_ascii_case("fig2dev") } #[cfg(test)] mod name_tests { use super::is_fig2dev_name; use std::ffi::OsStr; #[test] fn program_names() { for yes in ["fig2dev", "/usr/local/bin/fig2dev", "C:\\x\\fig2dev.exe", "fig2dev.EXE"] { assert!(is_fig2dev_name(OsStr::new(yes)), "{yes}"); } for no in [ "rustfig", "rustfig-cli", "fig2dev-cli", "xfig2dev", "", "/a/fig2dev/rustfig", ] { assert!(!is_fig2dev_name(OsStr::new(no)), "{no}"); } } } .