use crate::color::Color;
use crate::font::BuiltinFont;
use crate::object::{ObjRef, PdfDict, PdfObject};
use crate::writer::PdfWriter;
#[derive(Debug, Clone)]
pub struct SignatureAppearance {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
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, }
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
}
}
#[derive(Debug, Clone)]
pub struct SignatureField {
pub name: String,
pub appearance: Option<SignatureAppearance>,
pub reason: Option<String>,
pub location: Option<String>,
pub contact: Option<String>,
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 }
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));
let ap_ref = if let Some(app) = &self.appearance {
let ap_stream = build_appearance_stream(app);
Some(writer.add_stream(ap_stream))
} else {
None
};
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())); }
sig_dict.set("ByteRange", PdfObject::Array(vec![
PdfObject::Integer(0), PdfObject::Integer(0),
PdfObject::Integer(0), PdfObject::Integer(0),
]));
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));
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));
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)
}
}
#[derive(Debug, Clone)]
pub struct SignaturePlaceholder {
pub sig_obj_ref: ObjRef,
pub reserved_bytes: usize,
}
impl SignaturePlaceholder {
pub fn byte_ranges(&self, pdf_bytes: &[u8]) -> Option<[usize; 4]> {
let needle: Vec<u8> = vec![b'0'; self.reserved_bytes * 2];
let pos = pdf_bytes.windows(needle.len())
.position(|w| w == needle.as_slice())?;
let contents_start = pos - 1; let contents_end = pos + needle.len() + 1;
Some([0, contents_start, contents_end, pdf_bytes.len() - contents_end])
}
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")?;
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());
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) {
let end = (rpos + old_range.len()).min(rpos + new_range.len());
let _ = end; 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)
}
}
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),
]));
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());
}
}