docxide-pdf 0.13.1

Library and CLI for converting DOCX files to PDF, matching Microsoft Word's output as closely as possible
Documentation
use std::collections::HashMap;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::{env, fs};

#[derive(Clone)]
pub(super) struct CachedFace {
    pub(super) family: String,
    pub(super) bold: bool,
    pub(super) italic: bool,
    pub(super) face_index: u32,
}

#[derive(Clone, Default)]
pub(super) struct CachedFile {
    pub(super) faces: Vec<CachedFace>,
}

#[derive(Default)]
pub(super) struct FontCache {
    pub(super) dir_mtimes: HashMap<PathBuf, i64>,
    pub(super) files: HashMap<PathBuf, CachedFile>,
}

pub(super) const CACHE_VERSION: &str = "v2";

pub(super) fn cache_path() -> Option<PathBuf> {
    let dir = if cfg!(target_os = "macos") {
        env::var("HOME")
            .ok()
            .map(|h| PathBuf::from(h).join("Library/Caches/docxide-pdf"))
    } else if cfg!(target_os = "windows") {
        env::var("LOCALAPPDATA")
            .ok()
            .map(|d| PathBuf::from(d).join("docxide-pdf/cache"))
    } else {
        env::var("XDG_CACHE_HOME")
            .ok()
            .map(PathBuf::from)
            .or_else(|| {
                env::var("HOME")
                    .ok()
                    .map(|h| PathBuf::from(h).join(".cache"))
            })
            .map(|d| d.join("docxide-pdf"))
    };
    dir.map(|d| d.join("font-index.tsv"))
}

pub(super) fn load_cache() -> FontCache {
    let mut fc = FontCache::default();
    let Some(path) = cache_path() else {
        return fc;
    };
    let Ok(content) = fs::read_to_string(&path) else {
        return fc;
    };
    let mut lines = content.lines();
    if lines.next() != Some(CACHE_VERSION) {
        return fc;
    }
    for line in lines {
        let parts: Vec<&str> = line.split('\t').collect();
        match parts.first().copied() {
            Some("D") if parts.len() == 3 => {
                let Ok(mtime) = parts[2].parse::<i64>() else {
                    continue;
                };
                fc.dir_mtimes.insert(PathBuf::from(parts[1]), mtime);
            }
            Some("F") if parts.len() == 6 => {
                let Ok(face_index) = parts[5].parse::<u32>() else {
                    continue;
                };
                fc.files
                    .entry(PathBuf::from(parts[1]))
                    .or_default()
                    .faces
                    .push(CachedFace {
                        family: parts[2].to_string(),
                        bold: parts[3] == "1",
                        italic: parts[4] == "1",
                        face_index,
                    });
            }
            Some("F") if parts.len() == 3 && parts[2] == "-" => {
                fc.files.entry(PathBuf::from(parts[1])).or_default();
            }
            _ => {}
        }
    }
    fc
}

pub(super) fn save_cache(cache: &FontCache) {
    let Some(path) = cache_path() else {
        return;
    };
    if let Some(dir) = path.parent() {
        let _ = fs::create_dir_all(dir);
    }
    let mut out = String::from(CACHE_VERSION);
    out.push('\n');
    for (dir_path, mtime) in &cache.dir_mtimes {
        let _ = writeln!(out, "D\t{}\t{mtime}", dir_path.to_string_lossy());
    }
    for (file_path, cached) in &cache.files {
        let path_str = file_path.to_string_lossy();
        if cached.faces.is_empty() {
            let _ = writeln!(out, "F\t{path_str}\t-");
        } else {
            for face in &cached.faces {
                let _ = writeln!(
                    out,
                    "F\t{path_str}\t{}\t{}\t{}\t{}",
                    face.family,
                    u8::from(face.bold),
                    u8::from(face.italic),
                    face.face_index,
                );
            }
        }
    }
    let _ = fs::write(&path, out);
}

pub(super) fn dir_mtime(path: &Path) -> i64 {
    fs::metadata(path)
        .ok()
        .and_then(|m| m.modified().ok())
        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
        .map(|d| d.as_secs() as i64)
        .unwrap_or(0)
}