#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#[allow(unused_imports)]
use crate::axis::{plot::PlotKey, AxisKey};
use crate::axis::{plot::Plot2D, Axis};
use rand::distributions::{Alphanumeric, DistString};
use std::fmt;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use tempfile::NamedTempFile;
use thiserror::Error;
pub mod axis;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Engine {
PdfLatex,
#[cfg(feature = "tectonic")]
Tectonic,
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("io error")]
IoError(#[from] std::io::Error),
#[error("compilation failed with status {status}")]
BadExitCode { status: ExitStatus },
#[cfg(feature = "tectonic")]
#[error("tectonic error")]
TectonicError(#[from] tectonic::errors::Error),
}
#[derive(Debug, Error)]
pub enum ShowPdfError {
#[error("compilation error")]
BadCompilation(#[from] CompileError),
#[error("opening the pdf failed")]
OpenerError(#[from] opener::OpenError),
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum PictureKey {
Custom(String),
}
impl fmt::Display for PictureKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PictureKey::Custom(key) => write!(f, "{key}"),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Picture {
keys: Vec<PictureKey>,
pub axes: Vec<Axis>,
}
impl fmt::Display for Picture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\\begin{{tikzpicture}}")?;
if !self.keys.is_empty() {
writeln!(f, "[")?;
for key in self.keys.iter() {
writeln!(f, "\t{key},")?;
}
write!(f, "]")?;
}
writeln!(f)?;
for axis in self.axes.iter() {
writeln!(f, "{axis}")?;
}
write!(f, "\\end{{tikzpicture}}")?;
Ok(())
}
}
impl From<Axis> for Picture {
fn from(axis: Axis) -> Self {
Self {
keys: Vec::new(),
axes: vec![axis],
}
}
}
impl From<Plot2D> for Picture {
fn from(plot: Plot2D) -> Self {
Picture::from(Axis::from(plot))
}
}
impl Picture {
pub fn new() -> Self {
Default::default()
}
pub fn add_key(&mut self, key: PictureKey) {
match key {
PictureKey::Custom(_) => (),
}
self.keys.push(key);
}
pub fn standalone_string(&self) -> String {
String::from("\\documentclass{standalone}\n")
+ "\\usepackage{pgfplots}\n"
+ "\\begin{document}\n"
+ &self.to_string()
+ "\n\\end{document}"
}
pub fn to_pdf<P, S>(
&self,
working_dir: P,
jobname: S,
engine: Engine,
) -> Result<PathBuf, CompileError>
where
P: AsRef<Path>,
S: AsRef<str>,
{
let mut tex_file = NamedTempFile::new()?;
tex_file.write_all(self.standalone_string().as_bytes())?;
match engine {
Engine::PdfLatex => {
let status = Command::new("pdflatex")
.current_dir(working_dir.as_ref())
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("-interaction=batchmode")
.arg("-halt-on-error")
.arg(String::from("-jobname=") + jobname.as_ref())
.arg(tex_file.path())
.status()?;
if !status.success() {
return Err(CompileError::BadExitCode { status });
}
}
#[cfg(feature = "tectonic")]
Engine::Tectonic => {
let mut status = tectonic::status::NoopStatusBackend::default();
let auto_create_config_file = false;
let config = tectonic::ctry!(tectonic::config::PersistentConfig::open(auto_create_config_file);
"failed to open the default configuration file");
let only_cached = false;
let bundle = tectonic::ctry!(config.default_bundle(only_cached, &mut status);
"failed to load the default resource bundle");
let format_cache_path = tectonic::ctry!(config.format_cache_path();
"failed to set up the format cache");
let mut sb = tectonic::driver::ProcessingSessionBuilder::default();
sb.bundle(bundle)
.primary_input_path(tex_file.path())
.tex_input_name(jobname.as_ref())
.format_name("latex")
.format_cache_path(format_cache_path)
.keep_logs(true)
.keep_intermediates(true)
.print_stdout(false)
.output_format(tectonic::driver::OutputFormat::Pdf)
.output_dir(working_dir.as_ref());
let mut sess = tectonic::ctry!(sb.create(&mut status); "failed to initialize the LaTeX processing session");
tectonic::ctry!(sess.run(&mut status); "the LaTeX engine failed");
}
}
Ok(working_dir
.as_ref()
.join(String::from(jobname.as_ref()) + ".pdf"))
}
pub fn show_pdf(&self, engine: Engine) -> Result<(), ShowPdfError> {
fn random_jobname() -> String {
loop {
let mut jobname = "pgfplots_".to_string();
Alphanumeric.append_string(&mut rand::thread_rng(), &mut jobname, 8);
let pdf_path = std::env::temp_dir().join(jobname.clone() + ".pdf");
let log_path = std::env::temp_dir().join(jobname.clone() + ".log");
let aux_path = std::env::temp_dir().join(jobname.clone() + ".aux");
if !pdf_path.exists() && !log_path.exists() && !aux_path.exists() {
return jobname;
}
}
}
let jobname = random_jobname();
let pdf_path = self.to_pdf(std::env::temp_dir(), &jobname, engine)?;
opener::open(pdf_path)?;
Ok(())
}
}
#[cfg(test)]
mod tests;