Skip to main content

zim_studio/pdf/
mod.rs

1//! PDF export — walk a tree of markdown files and produce a single PDF
2//! with a TOC, sectioned by directory and subsectioned by file.
3
4pub mod assemble;
5pub mod compile;
6pub mod latex;
7pub mod walk;
8
9use crate::zimignore::ZimIgnore;
10use std::error::Error;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14/// Inputs for one PDF generation run.
15pub struct GenerateOptions {
16    pub root: PathBuf,
17    pub output: PathBuf,
18    pub title: String,
19    pub author: Option<String>,
20    pub keep_tex: bool,
21}
22
23/// Result summary returned to the CLI handler.
24pub struct GenerateStats {
25    pub num_files: usize,
26    pub num_dirs: usize,
27    pub output: PathBuf,
28    pub kept_tex: Option<PathBuf>,
29}
30
31/// Walk, render, and compile in one call.
32pub fn generate(opts: &GenerateOptions) -> Result<GenerateStats, Box<dyn Error>> {
33    let zimignore = ZimIgnore::load_for_directory(&opts.root);
34    let tree = walk::walk(&opts.root, &zimignore)?;
35    if tree.is_empty() {
36        return Err(format!("no .md files found under {}", opts.root.display()).into());
37    }
38
39    let num_dirs = tree.len();
40    let num_files: usize = tree.iter().map(|d| d.file_count()).sum();
41
42    let latex_source = assemble::build_latex(
43        &tree,
44        &assemble::PdfOptions {
45            title: opts.title.clone(),
46            author: opts.author.clone(),
47        },
48    )?;
49
50    let work_dir = std::env::temp_dir().join(format!("zim-pdf-{}", std::process::id()));
51    let pdf_path = compile::compile(&latex_source, &work_dir)?;
52
53    if let Some(parent) = opts.output.parent()
54        && !parent.as_os_str().is_empty()
55    {
56        fs::create_dir_all(parent)?;
57    }
58    fs::copy(&pdf_path, &opts.output)?;
59
60    let kept_tex = if opts.keep_tex {
61        let target = sibling(&opts.output, "tex");
62        fs::copy(compile::tex_path(&work_dir), &target)?;
63        Some(target)
64    } else {
65        None
66    };
67
68    let _ = fs::remove_dir_all(&work_dir);
69
70    Ok(GenerateStats {
71        num_files,
72        num_dirs,
73        output: opts.output.clone(),
74        kept_tex,
75    })
76}
77
78fn sibling(p: &Path, ext: &str) -> PathBuf {
79    let mut s = p.to_path_buf();
80    s.set_extension(ext);
81    s
82}