#![deny(dead_code)]
#![deny(unreachable_patterns)]
#![deny(unused_extern_crates)]
#![deny(unused_imports)]
#![deny(unused_qualifications)]
#![deny(clippy::all)]
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![deny(variant_size_differences)]
use std::borrow::Cow;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
mod num_conversions;
use std::io::Write;
mod isbn;
mod mimetypes;
use aho_corasick::AhoCorasick;
pub use isbn::{display_isbn, validate_isbn};
pub use mimetypes::{GuessMimeType, MimeType, MimeTypeHelper};
mod svg;
use lazy_static::lazy_static;
pub use svg::{convert_svg_file_to_png, convert_svg_to_jpg, convert_svg_to_png, simplify_svg};
use temp_file_name::HashToString;
pub mod fonts;
lazy_static! {
static ref HTML_FINDER: AhoCorasick = AhoCorasick::new(&HTML_TARGET_CHARS);
static ref LATEX_FINDER: AhoCorasick = AhoCorasick::new(&LATEX_TARGET_CHARS);
}
static HTML_TARGET_CHARS: [&str; 4] = ["<", ">", "&", "'"];
static HTML_REPLACEMENTS: [&str; 4] = ["<", ">", "&", "’"];
pub fn escape_to_html<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
let input = input.into();
let input_bytes = input.as_bytes();
if HTML_FINDER.is_match(input_bytes) {
let mut wtr = Vec::with_capacity(input.len());
HTML_FINDER
.stream_replace_all(input_bytes, &mut wtr, &HTML_REPLACEMENTS)
.expect("Aho-Corasick error");
unsafe { Cow::Owned(String::from_utf8_unchecked(wtr)) }
} else {
input
}
}
static LATEX_TARGET_CHARS: [&str; 16] = [
"…", "–", "—", "\u{a0}", "&", "%", "$", "#", "_", "{", "}", "[", "]", "~", "^", "\\",
];
static LATEX_REPLACEMENTS: [&str; 16] = [
"\\ldots{}",
"--",
"---",
"~",
"\\&",
r"\%",
r"\$",
r"\#",
r"\_",
r"\{",
r"\}",
r"{[}",
r"{]}",
r"\textasciitilde{}",
r"\textasciicircum{}",
r"\textbackslash{}",
];
pub fn escape_to_latex<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
let input = input.into();
let input_bytes = input.as_bytes();
if LATEX_FINDER.is_match(input_bytes) {
let mut wtr = Vec::with_capacity(input.len());
LATEX_FINDER
.stream_replace_all(input_bytes, &mut wtr, &LATEX_REPLACEMENTS)
.expect("Aho-Corasick error");
unsafe { Cow::Owned(String::from_utf8_unchecked(wtr)) }
} else {
input
}
}
pub fn call_latex(tex: &str) -> Result<Vec<u8>, std::io::Error> {
_call_latex(tex, false)
}
pub fn call_latex_verbose(tex: &str) -> Result<Vec<u8>, std::io::Error> {
_call_latex(tex, true)
}
fn _call_latex(tex: &str, verbose: bool) -> Result<Vec<u8>, std::io::Error> {
let filename_base = tex.hash_to_string();
let mut outdir = std::env::temp_dir();
outdir = outdir.join("bookbinder");
let tex_fn = format!("{}.tex", &filename_base);
let texpath = outdir.join(tex_fn);
let filename = format!("{}.pdf", &filename_base);
let outpath = outdir.join(&filename);
std::fs::write(&texpath, tex)?;
let odir_arg = format!("-output-directory={}", &outdir.to_string_lossy());
let mut ltx = if !verbose {
Command::new("latexmk")
.args(&[
&odir_arg,
"-xelatex",
"-interaction=batchmode",
"-halt-on-error",
texpath.to_string_lossy().as_ref(),
])
.spawn()?
} else {
Command::new("latexmk")
.args(&[&odir_arg, "-xelatex", texpath.to_string_lossy().as_ref()])
.spawn()?
};
let _ = ltx.wait()?;
if !outpath.exists() {
let mut log = texpath;
let _ = log.set_extension("log");
let log = std::fs::read_to_string(log).unwrap_or_else(|_| {
"Latex error without log generated; perhaps LaTeX is not installed?".to_string()
});
let e = std::io::Error::new(std::io::ErrorKind::Other, log);
return Err(e);
}
let o = std::fs::read(outpath)?;
Ok(o)
}
pub fn epubcheck(p: PathBuf) -> Result<(), String> {
let epubcheck = Command::new("epubcheck")
.arg(p.to_str().unwrap())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|_| "Error launching epubcheck -- is it installed?".to_string())?;
if epubcheck.status.success() {
Ok(())
} else {
let (stdout, stderr) = unsafe {
let stdout = String::from_utf8_unchecked(epubcheck.stdout);
let stderr = String::from_utf8_unchecked(epubcheck.stderr);
(stdout, stderr)
};
let mut msg = String::new();
msg.push_str(&stdout);
msg.push_str(&stderr);
Err(msg)
}
}
pub fn convert_to_jpg<P: AsRef<Path>>(filepath: P) -> Result<Vec<u8>, Box<dyn Error>> {
let p = filepath.as_ref();
let ext = p.extension().map(|o| o.to_str()).flatten();
match ext {
Some("pdf") => {
let data = std::fs::read(p)?;
let svg = convert_pdf_to_svg(&data, None)?;
let jpg = convert_svg_to_jpg(&svg, None)?;
Ok(jpg)
}
Some("svg") => {
let svg = std::fs::read_to_string(p)?;
let jpg = convert_svg_to_jpg(&svg, None)?;
Ok(jpg)
}
_ => {
let mut output = Vec::new();
let dynamic_image = image::open(p)?;
dynamic_image.write_to(&mut output, image::ImageOutputFormat::Jpeg(100))?;
Ok(output)
}
}
}
pub fn convert_pdf_to_svg(pdf: &[u8], dpi: Option<usize>) -> Result<String, Box<dyn Error>> {
let dpi = dpi.unwrap_or(150).to_string();
let mut cv = Command::new("pdftocairo")
.args(&["-svg", "-origpagesizes", "-r", &dpi, "-", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let stdin = cv.stdin.as_mut().unwrap();
stdin.write_all(&pdf)?;
let o = cv.wait_with_output()?;
let mut svg = String::from_utf8(o.stdout)?;
svg = svg.replacen(r#"width="432pt""#, r#"width="100%""#, 1);
svg = svg.replacen(
r#"height="648pt""#,
r#"height="100%" preserveAspectRatio="xMidYMid meet" x="0px" y="0px""#,
1,
);
Ok(svg)
}
pub fn get_current_year() -> String {
let now = time::now_utc();
time::strftime("%Y", &now).unwrap()
}
pub const fn number_to_letter(n: u8) -> Result<char, ()> {
if n > 25 {
Err(())
} else {
let codepoint = 65 + n;
let letter = codepoint as char;
Ok(letter)
}
}
pub const fn number_to_roman(n: u8) -> &'static str {
num_conversions::number_to_roman(n)
}
pub const fn number_to_words(n: u8) -> &'static str {
num_conversions::number_to_words(n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_numbers_to_letter() {
assert_eq!(number_to_letter(0), Ok('A'));
assert_eq!(number_to_letter(25), Ok('Z'));
assert_eq!(number_to_letter(27), Err(()));
}
#[test]
fn test_get_current_year() {
assert_eq!(get_current_year(), "2020".to_string());
}
#[test]
fn test_hash_to_string() {
let s = "Hello world".hash_to_string();
assert_eq!(s, "2216321107127430384");
}
#[test]
fn test_latex_escapes() {
let escapes = [
("&", "\\&"),
("%", "\\%"),
("$", "\\$"),
("#", "\\#"),
("_", "\\_"),
("{Hello}", "\\{Hello\\}"),
("[Hello]", "{[}Hello{]}"),
("~", "\\textasciitilde{}"),
("^", "\\textasciicircum{}"),
("\\", "\\textbackslash{}"),
];
for (input, expected) in escapes.iter() {
let s = input.to_string();
let out = escape_to_latex(&s);
assert_eq!(out.to_string(), *expected);
}
}
#[test]
fn test_numbers_to_word() {
assert_eq!(number_to_words(0), "ZERO");
assert_eq!(number_to_words(5), "FIVE");
assert_eq!(number_to_words(12), "TWELVE");
assert_eq!(number_to_words(25), "TWENTY-FIVE");
assert_eq!(number_to_words(125), "ONE HUNDRED AND TWENTY-FIVE");
}
#[test]
fn test_numbers_to_roman() {
assert_eq!(number_to_roman(0), "");
assert_eq!(number_to_roman(1), "I");
}
}