vios_app 0.1.1

Small JSON vaults: Argon2id + AES-GCM, with optional AAD binding.
Documentation
use printpdf::*;
use std::fs::File;
use std::io::BufWriter;

/// Super light PDF text writer for `printpdf = "0.7"`.
pub struct SimplePdfWriter {
    doc: PdfDocumentReference,
    page: PdfPageIndex,
    layer: PdfLayerIndex,
    font: IndirectFontRef,

    page_w_mm: f32,
    page_h_mm: f32,
    margin_l_mm: f32,
    margin_r_mm: f32,
    margin_t_mm: f32,
    margin_b_mm: f32,

    font_pt: f32,
    leading_mm: f32,

    cursor_y_mm: f32,
    page_no: usize,
    title: String,
}

impl SimplePdfWriter {
    /// Create an A4 document.
    pub fn new<T: Into<String>>(
        title: T,
        font_pt: f32,
        margins_mm: (f32, f32, f32, f32),
    ) -> Result<Self, String> {
        let title = title.into();
        let (doc, page, layer) = PdfDocument::new(&title, Mm(210.0_f32), Mm(297.0_f32), "Layer 1");
        let font = doc
            .add_builtin_font(BuiltinFont::Courier)
            .map_err(|e| format!("Font error: {e}"))?;

        // line height ~ 1.25x font size in mm
        let pt_to_mm: f32 = 0.35277778;
        let leading_mm = font_pt * 1.25 * pt_to_mm;

        let mut s = Self {
            doc,
            page,
            layer,
            font,
            page_w_mm: 210.0,
            page_h_mm: 297.0,
            margin_l_mm: margins_mm.0,
            margin_r_mm: margins_mm.1,
            margin_t_mm: margins_mm.2,
            margin_b_mm: margins_mm.3,
            font_pt,
            leading_mm,
            cursor_y_mm: 0.0, // set below
            page_no: 1,
            title,
        };

        s.reset_cursor_to_top();
        s.draw_header_on_first_page();
        s.draw_footer(); // page 1 footer
        Ok(s)
    }

    fn reset_cursor_to_top(&mut self) {
        self.cursor_y_mm = self.page_h_mm - self.margin_t_mm;
    }

    fn usable_width_mm(&self) -> f32 {
        self.page_w_mm - self.margin_l_mm - self.margin_r_mm
    }

    fn approx_cols_per_line(&self) -> usize {
        // Courier ~0.6em width per char. Convert pts → mm.
        let pt_to_mm: f32 = 0.35277778;
        let char_w_mm = self.font_pt * 0.6 * pt_to_mm;
        let cols = (self.usable_width_mm() / char_w_mm).floor();
        cols.max(10.0) as usize
    }

    fn needs_page_break(&self) -> bool {
        self.cursor_y_mm <= self.margin_b_mm + self.leading_mm
    }

    fn new_page(&mut self) {
        let (page, layer) = self
            .doc
            .add_page(Mm(self.page_w_mm), Mm(self.page_h_mm), "Layer");
        self.page = page;
        self.layer = layer;
        self.page_no += 1;
        self.reset_cursor_to_top();
        self.draw_footer();
    }

    fn draw_footer(&self) {
        let y = Mm(self.margin_b_mm * 0.6);
        let x = Mm(self.margin_l_mm);
        let layer = self.doc.get_page(self.page).get_layer(self.layer);
        layer.use_text(
            format!("Page {}", self.page_no),
            (self.font_pt - 1.0).max(8.0),
            x,
            y,
            &self.font,
        );
    }

    fn draw_header_on_first_page(&mut self) {
        let layer = self.doc.get_page(self.page).get_layer(self.layer);
        layer.use_text(
            &self.title,
            self.font_pt + 3.0,
            Mm(self.margin_l_mm),
            Mm(self.cursor_y_mm),
            &self.font,
        );
        self.cursor_y_mm -= self.leading_mm * 1.5;
    }

    fn write_line(&mut self, text: &str) {
        if self.needs_page_break() {
            self.new_page();
        }
        let layer = self.doc.get_page(self.page).get_layer(self.layer);
        layer.use_text(
            text,
            self.font_pt,
            Mm(self.margin_l_mm),
            Mm(self.cursor_y_mm),
            &self.font,
        );
        self.cursor_y_mm -= self.leading_mm;
    }

    /// Write text with word wrap and empty-line handling.
    pub fn write_wrapped(&mut self, text: &str) {
        let cols = self.approx_cols_per_line();
        for para in text.split('\n') {
            if para.trim().is_empty() {
                self.write_line(""); // blank line
                continue;
            }
            let mut line = String::new();
            for word in para.split_whitespace() {
                if line.is_empty() {
                    line.push_str(word);
                } else if line.len() + 1 + word.len() <= cols {
                    line.push(' ');
                    line.push_str(word);
                } else {
                    self.write_line(&line);
                    line.clear();
                    line.push_str(word);
                }
            }
            if !line.is_empty() {
                self.write_line(&line);
            }
        }
    }

    pub fn save(self, path: &str) -> Result<(), String> {
        let mut out =
            BufWriter::new(File::create(path).map_err(|e| format!("Create PDF failed: {e}"))?);
        self.doc
            .save(&mut out)
            .map_err(|e| format!("Save failed: {e}"))
    }
}