use crate::error::FormsError;
use lopdf::{Document, Object, ObjectId, StringFormat};
use std::collections::HashMap;
pub const F8949_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8949.pdf");
pub const SCHEDULE_D_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_d.pdf");
pub const SCHEDULE_SE_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_se.pdf");
pub const F8283_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8283.pdf");
pub const F1040_PDF_2025: &[u8] = include_bytes!("../forms/2025/f1040.pdf");
pub const F8949_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8949.pdf");
pub const SCHEDULE_D_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_d.pdf");
pub const SCHEDULE_SE_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_se.pdf");
pub const F8283_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8283.pdf");
pub const F8275_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8275.pdf");
pub const F1040_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040.pdf");
pub const F8959_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8959.pdf");
pub const F8960_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8960.pdf");
pub const F8995_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8995.pdf");
pub const SCHEDULE_2_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s2.pdf");
pub const SCHEDULE_3_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s3.pdf");
pub const SCHEDULE_A_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sa.pdf");
pub const SCHEDULE_1_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s1.pdf");
pub const SCHEDULE_C_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sc.pdf");
pub const SCHEDULE_B_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sb.pdf");
pub const F8949_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8949.pdf");
pub const SCHEDULE_D_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_d.pdf");
pub const SCHEDULE_SE_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_se.pdf");
pub const F8283_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8283.pdf");
pub const F1040_PDF_2017: &[u8] = include_bytes!("../forms/2017/f1040.pdf");
pub fn f8949_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 => Ok(F8949_PDF_2017),
2024 => Ok(F8949_PDF_2024),
2025 => Ok(F8949_PDF_2025),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_d_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 => Ok(SCHEDULE_D_PDF_2017),
2024 => Ok(SCHEDULE_D_PDF_2024),
2025 => Ok(SCHEDULE_D_PDF_2025),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_se_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 => Ok(SCHEDULE_SE_PDF_2017),
2024 => Ok(SCHEDULE_SE_PDF_2024),
2025 => Ok(SCHEDULE_SE_PDF_2025),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f8959_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(F8959_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f8960_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(F8960_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f8995_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(F8995_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_2_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_2_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_3_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_3_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_b_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_B_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_c_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_C_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_1_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_1_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn schedule_a_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2024 => Ok(SCHEDULE_A_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f8283_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 => Ok(F8283_PDF_2017),
2024 => Ok(F8283_PDF_2024),
2025 => Ok(F8283_PDF_2025),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f8275_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 | 2024 | 2025 => Ok(F8275_PDF_2024),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
pub fn f1040_pdf(year: i32) -> Result<&'static [u8], FormsError> {
match year {
2017 => Ok(F1040_PDF_2017),
2024 => Ok(F1040_PDF_2024),
2025 => Ok(F1040_PDF_2025),
_ => Err(FormsError::UnsupportedYear(year)),
}
}
#[derive(Debug, Clone)]
pub struct Field {
pub id: ObjectId,
pub fqn: String,
pub rect: Option<[f32; 4]>,
pub is_button: bool,
pub max_len: Option<usize>,
}
impl Field {
pub fn cx(&self) -> Option<f32> {
self.rect.map(|r| (r[0] + r[2]) / 2.0)
}
pub fn cy(&self) -> Option<f32> {
self.rect.map(|r| (r[1] + r[3]) / 2.0)
}
}
#[derive(Debug, Clone)]
pub enum FieldValue {
Text(String),
Check {
on: String,
},
}
pub fn load(bytes: &[u8]) -> Result<Document, FormsError> {
Ok(Document::load_mem(bytes)?)
}
fn number(o: &Object) -> Option<f32> {
match o {
Object::Integer(i) => Some(*i as f32),
Object::Real(r) => Some(*r),
_ => None,
}
}
fn rect_of(dict: &lopdf::Dictionary) -> Option<[f32; 4]> {
let arr = dict.get(b"Rect").ok()?.as_array().ok()?;
if arr.len() != 4 {
return None;
}
Some([
number(&arr[0])?,
number(&arr[1])?,
number(&arr[2])?,
number(&arr[3])?,
])
}
fn acroform_id(doc: &Document) -> Result<ObjectId, FormsError> {
match doc.catalog()?.get(b"AcroForm") {
Ok(Object::Reference(id)) => Ok(*id),
Ok(_) => Err(FormsError::Structure(
"AcroForm is not an indirect reference".into(),
)),
Err(_) => Err(FormsError::Structure("catalog has no AcroForm".into())),
}
}
pub fn drop_xfa_and_set_needappearances(doc: &mut Document) -> Result<(), FormsError> {
let id = acroform_id(doc)?;
let acro = doc.get_dictionary_mut(id)?;
acro.remove(b"XFA");
acro.set("NeedAppearances", Object::Boolean(true));
Ok(())
}
pub fn collect_fields(doc: &Document) -> Result<Vec<Field>, FormsError> {
let acro = doc.get_dictionary(acroform_id(doc)?)?;
let mut out = Vec::new();
let fields = acro
.get(b"Fields")
.and_then(|o| o.as_array())
.map_err(|_| FormsError::Structure("AcroForm has no /Fields array".into()))?;
for f in fields {
if let Ok(id) = f.as_reference() {
walk(doc, id, "", None, None, &mut out)?;
}
}
Ok(out)
}
pub(crate) fn decode_pdf_text(b: &[u8]) -> String {
if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
let units: Vec<u16> = b[2..]
.chunks(2)
.map(|c| ((c[0] as u16) << 8) | *c.get(1).unwrap_or(&0) as u16)
.collect();
String::from_utf16_lossy(&units)
} else {
b.iter().map(|&c| c as char).collect()
}
}
fn field_component_name(dict: &lopdf::Dictionary) -> Option<String> {
dict.get(b"T")
.ok()
.and_then(|o| o.as_str().ok())
.map(decode_pdf_text)
}
fn walk(
doc: &Document,
id: ObjectId,
parent_fqn: &str,
inherited_ft: Option<String>,
inherited_max_len: Option<usize>,
out: &mut Vec<Field>,
) -> Result<(), FormsError> {
let dict = match doc.get_dictionary(id) {
Ok(d) => d,
Err(_) => return Ok(()), };
let name = field_component_name(dict);
let fqn = match &name {
Some(t) if parent_fqn.is_empty() => t.clone(),
Some(t) => format!("{parent_fqn}.{t}"),
None => parent_fqn.to_string(),
};
let ft = dict
.get(b"FT")
.ok()
.and_then(|o| o.as_name().ok())
.map(|b| String::from_utf8_lossy(b).into_owned())
.or(inherited_ft);
let max_len = dict
.get(b"MaxLen")
.ok()
.and_then(|o| o.as_i64().ok())
.and_then(|n| usize::try_from(n).ok())
.or(inherited_max_len);
let kids: Option<Vec<ObjectId>> = dict
.get(b"Kids")
.ok()
.and_then(|o| o.as_array().ok())
.map(|arr| arr.iter().filter_map(|k| k.as_reference().ok()).collect());
match kids {
Some(kids) if !kids.is_empty() => {
for k in kids {
walk(doc, k, &fqn, ft.clone(), max_len, out)?;
}
}
_ => {
out.push(Field {
id,
fqn,
rect: rect_of(dict),
is_button: ft.as_deref() == Some("Btn"),
max_len,
});
}
}
Ok(())
}
pub fn index(fields: &[Field]) -> HashMap<String, Field> {
fields.iter().map(|f| (f.fqn.clone(), f.clone())).collect()
}
fn encode_pdf_text(s: &str) -> Vec<u8> {
if s.is_ascii() {
return s.as_bytes().to_vec();
}
let mut out = Vec::with_capacity(2 + s.len() * 2);
out.extend_from_slice(&[0xFE, 0xFF]);
for unit in s.encode_utf16() {
out.push((unit >> 8) as u8);
out.push((unit & 0xFF) as u8);
}
out
}
pub fn apply_writes(
doc: &mut Document,
index: &HashMap<String, Field>,
writes: &[(String, FieldValue)],
) -> Result<(), FormsError> {
for (fqn, value) in writes {
let field = index
.get(fqn)
.ok_or_else(|| FormsError::MapFieldMissing(fqn.clone()))?;
let dict = doc.get_dictionary_mut(field.id)?;
match value {
FieldValue::Text(s) => {
dict.set(
"V",
Object::String(encode_pdf_text(s), StringFormat::Literal),
);
}
FieldValue::Check { on } => {
dict.set("V", Object::Name(on.clone().into_bytes()));
dict.set("AS", Object::Name(on.clone().into_bytes()));
}
}
}
Ok(())
}
pub fn strip_nondeterminism(doc: &mut Document) {
if let Ok(info) = doc.trailer.get(b"Info").and_then(|o| o.as_reference()) {
if let Ok(d) = doc.get_dictionary_mut(info) {
d.remove(b"CreationDate");
d.remove(b"ModDate");
}
}
doc.trailer.remove(b"ID");
}
pub fn save(doc: &mut Document) -> Result<Vec<u8>, FormsError> {
let mut buf = Vec::new();
doc.save_to(&mut buf)?;
Ok(buf)
}
pub fn text_value(doc: &Document, id: ObjectId) -> Option<String> {
let v = doc.get_dictionary(id).ok()?.get(b"V").ok()?;
match v {
Object::String(b, _) => Some(decode_pdf_text(b)),
_ => None,
}
}
pub fn checkbox_on(doc: &Document, id: ObjectId) -> Option<String> {
let as_ = doc.get_dictionary(id).ok()?.get(b"AS").ok()?;
match as_ {
Object::Name(b) if b != b"Off" => Some(String::from_utf8_lossy(b).into_owned()),
_ => None,
}
}
pub fn button_on_states(doc: &Document, id: ObjectId) -> Vec<String> {
let mut out = Vec::new();
let Ok(dict) = doc.get_dictionary(id) else {
return out;
};
let ap = match dict.get(b"AP") {
Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
Ok(Object::Dictionary(d)) => Some(d),
_ => None,
};
let n = ap.and_then(|ap| match ap.get(b"N") {
Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
Ok(Object::Dictionary(d)) => Some(d),
_ => None,
});
if let Some(n) = n {
for (k, _) in n.iter() {
if k != b"Off" {
out.push(String::from_utf8_lossy(k).into_owned());
}
}
}
out.sort();
out
}