pdfox 0.1.0

A pure-Rust PDF library — create, parse, and render PDF documents with zero C dependencies
Documentation
/// Digital signature support (PDF §12.8).
///
/// Creates a /Sig field with subtype adbe.pkcs7.detached.
/// The actual PKCS#7 cryptographic signature bytes are provided by the caller
/// (e.g. from a HSM, signing service, or openssl/rustls).
///
/// Workflow:
///   1. Create a `SignatureField` describing where the signature box appears.
///   2. Attach it via `Document::signature()`.
///   3. During `build()` pdfox writes a ByteRange placeholder and a zeroed
///      /Contents hex string of `reserved_bytes` size.
///   4. After build, use `SignatureHole` (returned alongside the PDF bytes)
///      to locate the byte ranges and inject the real PKCS#7 DER blob.
///
/// NOTE: Computing the PKCS#7 blob itself requires an external crypto library
/// (e.g. `rcgen`, `p12`, or an HSM).  pdfox only handles the PDF side.

use crate::color::Color;
use crate::font::BuiltinFont;
use crate::object::{ObjRef, PdfDict, PdfObject};
use crate::writer::PdfWriter;

// ── Appearance ────────────────────────────────────────────────────────────────

/// Visual appearance of the signature widget on the page
#[derive(Debug, Clone)]
pub struct SignatureAppearance {
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
    /// Signer name shown inside the box (purely visual)
    pub label: String,
    pub font: BuiltinFont,
    pub font_size: f64,
    pub text_color: Color,
    pub border_color: Color,
    pub bg_color: Color,
    pub page: usize,          // 0-indexed page the widget appears on
}

impl SignatureAppearance {
    pub fn new(x: f64, y: f64, w: f64, h: f64, page: usize) -> Self {
        Self {
            x, y, width: w, height: h, page,
            label: "Digitally signed".into(),
            font: BuiltinFont::Helvetica,
            font_size: 9.0,
            text_color: Color::rgb_u8(20, 40, 100),
            border_color: Color::rgb_u8(20, 40, 100),
            bg_color: Color::rgb_u8(235, 242, 255),
        }
    }
    pub fn label(mut self, l: impl Into<String>) -> Self { self.label = l.into(); self }
    pub fn font_size(mut self, s: f64) -> Self { self.font_size = s; self }
    pub fn colors(mut self, text: Color, border: Color, bg: Color) -> Self {
        self.text_color = text; self.border_color = border; self.bg_color = bg; self
    }
}

// ── Signature field ───────────────────────────────────────────────────────────

/// A PDF digital signature field (invisible or visible)
#[derive(Debug, Clone)]
pub struct SignatureField {
    /// Unique field name
    pub name: String,
    /// Optional visible widget; None = invisible signature
    pub appearance: Option<SignatureAppearance>,
    /// Reason string embedded in the signature dictionary
    pub reason: Option<String>,
    /// Location string embedded in the signature dictionary
    pub location: Option<String>,
    /// Contact info embedded in the signature dictionary
    pub contact: Option<String>,
    /// Reserved space in bytes for the PKCS#7 blob (default 8192)
    pub reserved_bytes: usize,
}

impl SignatureField {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            appearance: None,
            reason: None,
            location: None,
            contact: None,
            reserved_bytes: 8192,
        }
    }

    pub fn visible(mut self, app: SignatureAppearance) -> Self {
        self.appearance = Some(app); self
    }
    pub fn reason(mut self, r: impl Into<String>) -> Self { self.reason = Some(r.into()); self }
    pub fn location(mut self, l: impl Into<String>) -> Self { self.location = Some(l.into()); self }
    pub fn contact(mut self, c: impl Into<String>) -> Self { self.contact = Some(c.into()); self }
    pub fn reserved_bytes(mut self, n: usize) -> Self { self.reserved_bytes = n; self }

    /// Write the signature field into the PDF.  Returns:
    /// - The widget annotation ObjRef (to add to the page /Annots array)
    /// - The AcroForm field ObjRef (to add to /AcroForm /Fields)
    /// - A `SignaturePlaceholder` describing where the zeroed Contents go
    pub fn write(
        &self,
        writer: &mut PdfWriter,
        page_refs: &[ObjRef],
    ) -> (ObjRef, ObjRef, SignaturePlaceholder) {
        let page_ref = page_refs.get(
            self.appearance.as_ref().map_or(0, |a| a.page)
        ).copied().unwrap_or_else(|| ObjRef::new(1));

        // ── Build appearance stream ───────────────────────────────────────────
        let ap_ref = if let Some(app) = &self.appearance {
            let ap_stream = build_appearance_stream(app);
            Some(writer.add_stream(ap_stream))
        } else {
            None
        };

        // ── /Sig value dictionary (the actual signature object) ───────────────
        let mut sig_dict = PdfDict::new();
        sig_dict.set("Type",    PdfObject::name("Sig"));
        sig_dict.set("Filter",  PdfObject::name("Adobe.PPKLite"));
        sig_dict.set("SubFilter", PdfObject::name("adbe.pkcs7.detached"));
        if let Some(ref r) = self.reason   { sig_dict.set("Reason",      PdfObject::string(r.as_str())); }
        if let Some(ref l) = self.location { sig_dict.set("Location",    PdfObject::string(l.as_str())); }
        if let Some(ref c) = self.contact  { sig_dict.set("ContactInfo", PdfObject::string(c.as_str())); }

        // ByteRange and Contents are placeholders; real values filled post-build
        sig_dict.set("ByteRange", PdfObject::Array(vec![
            PdfObject::Integer(0), PdfObject::Integer(0),
            PdfObject::Integer(0), PdfObject::Integer(0),
        ]));
        // Zeroed hex string placeholder for the PKCS#7 blob
        let zeroed: Vec<u8> = vec![0u8; self.reserved_bytes];
        sig_dict.set("Contents", PdfObject::HexString(zeroed));

        let sig_ref = writer.add_object(PdfObject::Dictionary(sig_dict));

        // ── Widget annotation + field dictionary ──────────────────────────────
        let rect = self.appearance.as_ref().map(|a| {
            PdfObject::Array(vec![
                PdfObject::Real(a.x),
                PdfObject::Real(a.y),
                PdfObject::Real(a.x + a.width),
                PdfObject::Real(a.y + a.height),
            ])
        }).unwrap_or_else(|| PdfObject::Array(vec![
            PdfObject::Integer(0), PdfObject::Integer(0),
            PdfObject::Integer(0), PdfObject::Integer(0),
        ]));

        let mut widget = PdfDict::new();
        widget.set("Type",    PdfObject::name("Annot"));
        widget.set("Subtype", PdfObject::name("Widget"));
        widget.set("FT",      PdfObject::name("Sig"));
        widget.set("T",       PdfObject::string(self.name.as_str()));
        widget.set("V",       PdfObject::Reference(sig_ref));
        widget.set("P",       PdfObject::Reference(page_ref));
        widget.set("Rect",    rect);
        widget.set("F",       PdfObject::Integer(4)); // Print flag

        if let Some(ap_ref) = ap_ref {
            let mut ap = PdfDict::new();
            ap.set("N", PdfObject::Reference(ap_ref));
            widget.set("AP", PdfObject::Dictionary(ap));
        }

        let widget_ref = writer.add_object(PdfObject::Dictionary(widget));

        let placeholder = SignaturePlaceholder {
            sig_obj_ref: sig_ref,
            reserved_bytes: self.reserved_bytes,
        };

        (widget_ref, widget_ref, placeholder)
    }
}

// ── Post-build signature injection ───────────────────────────────────────────

/// Describes where in the final PDF bytes to inject the PKCS#7 signature.
#[derive(Debug, Clone)]
pub struct SignaturePlaceholder {
    pub sig_obj_ref:    ObjRef,
    pub reserved_bytes: usize,
}

impl SignaturePlaceholder {
    /// Locate the zeroed /Contents placeholder in `pdf_bytes` and return
    /// the two byte ranges that should be signed:
    ///   range1 = [0, contents_start)
    ///   range2 = (contents_end, end_of_file]
    ///
    /// Call this AFTER `Document::build()` returns the final PDF bytes.
    pub fn byte_ranges(&self, pdf_bytes: &[u8]) -> Option<[usize; 4]> {
        // Find the /Contents <000...> hex string in the output
        let needle: Vec<u8> = vec![b'0'; self.reserved_bytes * 2];
        let pos = pdf_bytes.windows(needle.len())
            .position(|w| w == needle.as_slice())?;

        // The hex string starts one byte after the '<' delimiter
        let contents_start = pos - 1; // points to '<'
        let contents_end   = pos + needle.len() + 1; // past '>'

        Some([0, contents_start, contents_end, pdf_bytes.len() - contents_end])
    }

    /// Inject the PKCS#7 DER signature bytes into the PDF in-place.
    /// `pdf_bytes` is the output of `Document::build()`.
    /// `sig_bytes` is the DER-encoded PKCS#7 blob (must be <= reserved_bytes).
    ///
    /// Returns the final signed PDF bytes.
    pub fn inject(&self, mut pdf_bytes: Vec<u8>, sig_bytes: &[u8]) -> Result<Vec<u8>, String> {
        if sig_bytes.len() > self.reserved_bytes {
            return Err(format!(
                "Signature bytes ({}) exceed reserved space ({})",
                sig_bytes.len(), self.reserved_bytes
            ));
        }

        let needle: Vec<u8> = vec![b'0'; self.reserved_bytes * 2];
        let pos = pdf_bytes.windows(needle.len())
            .position(|w| w == needle.as_slice())
            .ok_or("Could not find signature placeholder")?;

        // Overwrite with hex-encoded sig_bytes, zero-padded to reserved_bytes
        let mut padded = sig_bytes.to_vec();
        padded.resize(self.reserved_bytes, 0);
        let hex: String = padded.iter().map(|b| format!("{:02x}", b)).collect();

        pdf_bytes[pos..pos + self.reserved_bytes * 2].copy_from_slice(hex.as_bytes());

        // Also update the ByteRange array in the raw bytes (best-effort text patch)
        let ranges = self.byte_ranges(&pdf_bytes);
        if let Some([r0, r1, r2, r3]) = ranges {
            let new_range = format!("[{} {} {} {}]", r0, r1, r2, r3);
            let old_range = b"[0 0 0 0]";
            if let Some(rpos) = pdf_bytes.windows(old_range.len())
                .position(|w| w == old_range) {
                // Overwrite with spaces then new range
                let end = (rpos + old_range.len()).min(rpos + new_range.len());
                let _ = end; // suppress warning
                // Simple in-place patch (zero-padded with spaces to same length is safe)
                let padded_range = format!("{:<width$}", new_range, width = old_range.len());
                let patch = padded_range.as_bytes();
                let patch_len = patch.len().min(old_range.len());
                pdf_bytes[rpos..rpos + patch_len].copy_from_slice(&patch[..patch_len]);
            }
        }

        Ok(pdf_bytes)
    }
}

// ── Appearance stream builder ─────────────────────────────────────────────────

fn build_appearance_stream(app: &SignatureAppearance) -> crate::object::PdfStream {
    let bg = app.bg_color;
    let bc = app.border_color;
    let tc = app.text_color;

    let content = format!(
        "q\n\
         {}\n\
         0 0 {:.4} {:.4} re f\n\
         {}\n\
         0.8 w\n\
         0 0 {:.4} {:.4} re S\n\
         {}\n\
         BT\n\
         /F1 {:.2} Tf\n\
         4 {:.4} Td\n\
         ({}) Tj\n\
         ET\n\
         Q\n",
        app.bg_color.fill_op(),
        app.width, app.height,
        app.border_color.stroke_op(),
        app.width, app.height,
        app.text_color.fill_op(),
        app.font_size,
        app.height / 2.0 - app.font_size / 3.0,
        crate::content::escape_for_stream(&app.label),
    );

    let mut dict = crate::object::PdfDict::new();
    dict.set("Type",    PdfObject::name("XObject"));
    dict.set("Subtype", PdfObject::name("Form"));
    dict.set("BBox",    PdfObject::Array(vec![
        PdfObject::Real(0.0), PdfObject::Real(0.0),
        PdfObject::Real(app.width), PdfObject::Real(app.height),
    ]));
    // Minimal font resource for appearance
    let mut font_dict = PdfDict::new();
    let mut f1 = PdfDict::new();
    f1.set("Type",     PdfObject::name("Font"));
    f1.set("Subtype",  PdfObject::name("Type1"));
    f1.set("BaseFont", PdfObject::name(app.font.pdf_name()));
    font_dict.set("F1", PdfObject::Dictionary(f1));
    let mut res = PdfDict::new();
    res.set("Font", PdfObject::Dictionary(font_dict));
    dict.set("Resources", PdfObject::Dictionary(res));
    dict.set("Length", PdfObject::Integer(content.len() as i64));

    crate::object::PdfStream { dict, data: content.into_bytes() }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_signature_field_builds() {
        let field = SignatureField::new("sig1")
            .reason("Approval")
            .location("Stockholm");
        assert_eq!(field.name, "sig1");
        assert_eq!(field.reason.as_deref(), Some("Approval"));
    }

    #[test]
    fn test_placeholder_inject_too_large() {
        let ph = SignaturePlaceholder { sig_obj_ref: ObjRef::new(1), reserved_bytes: 4 };
        let result = ph.inject(vec![0u8; 100], &[0u8; 100]);
        assert!(result.is_err());
    }
}