1use crate::error::FormsError;
9use lopdf::{Document, Object, ObjectId, StringFormat};
10use std::collections::HashMap;
11
12pub const F8949_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8949.pdf");
14pub const SCHEDULE_D_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_d.pdf");
16pub const SCHEDULE_SE_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_se.pdf");
18pub const F8283_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8283.pdf");
20pub const F1040_PDF_2025: &[u8] = include_bytes!("../forms/2025/f1040.pdf");
22
23pub const F8949_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8949.pdf");
25pub const SCHEDULE_D_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_d.pdf");
27pub const SCHEDULE_SE_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_se.pdf");
29pub const F8283_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8283.pdf");
31pub const F1040_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040.pdf");
33pub const F8959_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8959.pdf");
35pub const F8960_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8960.pdf");
37pub const F8995_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8995.pdf");
39pub const SCHEDULE_2_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s2.pdf");
41pub const SCHEDULE_3_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s3.pdf");
43pub const SCHEDULE_A_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sa.pdf");
45pub const SCHEDULE_1_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s1.pdf");
47pub const SCHEDULE_C_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sc.pdf");
49pub const SCHEDULE_B_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sb.pdf");
51
52pub const F8949_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8949.pdf");
54pub const SCHEDULE_D_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_d.pdf");
56pub const SCHEDULE_SE_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_se.pdf");
58pub const F8283_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8283.pdf");
60pub const F1040_PDF_2017: &[u8] = include_bytes!("../forms/2017/f1040.pdf");
62
63pub fn f8949_pdf(year: i32) -> Result<&'static [u8], FormsError> {
65 match year {
66 2017 => Ok(F8949_PDF_2017),
67 2024 => Ok(F8949_PDF_2024),
68 2025 => Ok(F8949_PDF_2025),
69 _ => Err(FormsError::UnsupportedYear(year)),
70 }
71}
72
73pub fn schedule_d_pdf(year: i32) -> Result<&'static [u8], FormsError> {
75 match year {
76 2017 => Ok(SCHEDULE_D_PDF_2017),
77 2024 => Ok(SCHEDULE_D_PDF_2024),
78 2025 => Ok(SCHEDULE_D_PDF_2025),
79 _ => Err(FormsError::UnsupportedYear(year)),
80 }
81}
82
83pub fn schedule_se_pdf(year: i32) -> Result<&'static [u8], FormsError> {
85 match year {
86 2017 => Ok(SCHEDULE_SE_PDF_2017),
87 2024 => Ok(SCHEDULE_SE_PDF_2024),
88 2025 => Ok(SCHEDULE_SE_PDF_2025),
89 _ => Err(FormsError::UnsupportedYear(year)),
90 }
91}
92
93pub fn f8959_pdf(year: i32) -> Result<&'static [u8], FormsError> {
95 match year {
96 2024 => Ok(F8959_PDF_2024),
97 _ => Err(FormsError::UnsupportedYear(year)),
98 }
99}
100
101pub fn f8960_pdf(year: i32) -> Result<&'static [u8], FormsError> {
103 match year {
104 2024 => Ok(F8960_PDF_2024),
105 _ => Err(FormsError::UnsupportedYear(year)),
106 }
107}
108
109pub fn f8995_pdf(year: i32) -> Result<&'static [u8], FormsError> {
111 match year {
112 2024 => Ok(F8995_PDF_2024),
113 _ => Err(FormsError::UnsupportedYear(year)),
114 }
115}
116
117pub fn schedule_2_pdf(year: i32) -> Result<&'static [u8], FormsError> {
119 match year {
120 2024 => Ok(SCHEDULE_2_PDF_2024),
121 _ => Err(FormsError::UnsupportedYear(year)),
122 }
123}
124
125pub fn schedule_3_pdf(year: i32) -> Result<&'static [u8], FormsError> {
127 match year {
128 2024 => Ok(SCHEDULE_3_PDF_2024),
129 _ => Err(FormsError::UnsupportedYear(year)),
130 }
131}
132
133pub fn schedule_b_pdf(year: i32) -> Result<&'static [u8], FormsError> {
135 match year {
136 2024 => Ok(SCHEDULE_B_PDF_2024),
137 _ => Err(FormsError::UnsupportedYear(year)),
138 }
139}
140
141pub fn schedule_c_pdf(year: i32) -> Result<&'static [u8], FormsError> {
143 match year {
144 2024 => Ok(SCHEDULE_C_PDF_2024),
145 _ => Err(FormsError::UnsupportedYear(year)),
146 }
147}
148
149pub fn schedule_1_pdf(year: i32) -> Result<&'static [u8], FormsError> {
151 match year {
152 2024 => Ok(SCHEDULE_1_PDF_2024),
153 _ => Err(FormsError::UnsupportedYear(year)),
154 }
155}
156
157pub fn schedule_a_pdf(year: i32) -> Result<&'static [u8], FormsError> {
159 match year {
160 2024 => Ok(SCHEDULE_A_PDF_2024),
161 _ => Err(FormsError::UnsupportedYear(year)),
162 }
163}
164
165pub fn f8283_pdf(year: i32) -> Result<&'static [u8], FormsError> {
167 match year {
168 2017 => Ok(F8283_PDF_2017),
169 2024 => Ok(F8283_PDF_2024),
170 2025 => Ok(F8283_PDF_2025),
171 _ => Err(FormsError::UnsupportedYear(year)),
172 }
173}
174
175pub fn f1040_pdf(year: i32) -> Result<&'static [u8], FormsError> {
177 match year {
178 2017 => Ok(F1040_PDF_2017),
179 2024 => Ok(F1040_PDF_2024),
180 2025 => Ok(F1040_PDF_2025),
181 _ => Err(FormsError::UnsupportedYear(year)),
182 }
183}
184
185#[derive(Debug, Clone)]
188pub struct Field {
189 pub id: ObjectId,
191 pub fqn: String,
193 pub rect: Option<[f32; 4]>,
195 pub is_button: bool,
197 pub max_len: Option<usize>,
205}
206
207impl Field {
208 pub fn cx(&self) -> Option<f32> {
210 self.rect.map(|r| (r[0] + r[2]) / 2.0)
211 }
212 pub fn cy(&self) -> Option<f32> {
214 self.rect.map(|r| (r[1] + r[3]) / 2.0)
215 }
216}
217
218#[derive(Debug, Clone)]
220pub enum FieldValue {
221 Text(String),
223 Check {
225 on: String,
227 },
228}
229
230pub fn load(bytes: &[u8]) -> Result<Document, FormsError> {
232 Ok(Document::load_mem(bytes)?)
233}
234
235fn number(o: &Object) -> Option<f32> {
236 match o {
237 Object::Integer(i) => Some(*i as f32),
238 Object::Real(r) => Some(*r),
239 _ => None,
240 }
241}
242
243fn rect_of(dict: &lopdf::Dictionary) -> Option<[f32; 4]> {
244 let arr = dict.get(b"Rect").ok()?.as_array().ok()?;
245 if arr.len() != 4 {
246 return None;
247 }
248 Some([
249 number(&arr[0])?,
250 number(&arr[1])?,
251 number(&arr[2])?,
252 number(&arr[3])?,
253 ])
254}
255
256fn acroform_id(doc: &Document) -> Result<ObjectId, FormsError> {
258 match doc.catalog()?.get(b"AcroForm") {
259 Ok(Object::Reference(id)) => Ok(*id),
260 Ok(_) => Err(FormsError::Structure(
261 "AcroForm is not an indirect reference".into(),
262 )),
263 Err(_) => Err(FormsError::Structure("catalog has no AcroForm".into())),
264 }
265}
266
267pub fn drop_xfa_and_set_needappearances(doc: &mut Document) -> Result<(), FormsError> {
270 let id = acroform_id(doc)?;
271 let acro = doc.get_dictionary_mut(id)?;
272 acro.remove(b"XFA");
273 acro.set("NeedAppearances", Object::Boolean(true));
274 Ok(())
275}
276
277pub fn collect_fields(doc: &Document) -> Result<Vec<Field>, FormsError> {
279 let acro = doc.get_dictionary(acroform_id(doc)?)?;
280 let mut out = Vec::new();
281 let fields = acro
282 .get(b"Fields")
283 .and_then(|o| o.as_array())
284 .map_err(|_| FormsError::Structure("AcroForm has no /Fields array".into()))?;
285 for f in fields {
286 if let Ok(id) = f.as_reference() {
287 walk(doc, id, "", None, None, &mut out)?;
288 }
289 }
290 Ok(out)
291}
292
293pub(crate) fn decode_pdf_text(b: &[u8]) -> String {
296 if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
297 let units: Vec<u16> = b[2..]
298 .chunks(2)
299 .map(|c| ((c[0] as u16) << 8) | *c.get(1).unwrap_or(&0) as u16)
300 .collect();
301 String::from_utf16_lossy(&units)
302 } else {
303 b.iter().map(|&c| c as char).collect()
304 }
305}
306
307fn field_component_name(dict: &lopdf::Dictionary) -> Option<String> {
308 dict.get(b"T")
309 .ok()
310 .and_then(|o| o.as_str().ok())
311 .map(decode_pdf_text)
312}
313
314fn walk(
315 doc: &Document,
316 id: ObjectId,
317 parent_fqn: &str,
318 inherited_ft: Option<String>,
319 inherited_max_len: Option<usize>,
320 out: &mut Vec<Field>,
321) -> Result<(), FormsError> {
322 let dict = match doc.get_dictionary(id) {
323 Ok(d) => d,
324 Err(_) => return Ok(()), };
326 let name = field_component_name(dict);
327 let fqn = match &name {
328 Some(t) if parent_fqn.is_empty() => t.clone(),
329 Some(t) => format!("{parent_fqn}.{t}"),
330 None => parent_fqn.to_string(),
331 };
332 let ft = dict
333 .get(b"FT")
334 .ok()
335 .and_then(|o| o.as_name().ok())
336 .map(|b| String::from_utf8_lossy(b).into_owned())
337 .or(inherited_ft);
338
339 let max_len = dict
341 .get(b"MaxLen")
342 .ok()
343 .and_then(|o| o.as_i64().ok())
344 .and_then(|n| usize::try_from(n).ok())
345 .or(inherited_max_len);
346
347 let kids: Option<Vec<ObjectId>> = dict
349 .get(b"Kids")
350 .ok()
351 .and_then(|o| o.as_array().ok())
352 .map(|arr| arr.iter().filter_map(|k| k.as_reference().ok()).collect());
353 match kids {
354 Some(kids) if !kids.is_empty() => {
355 for k in kids {
356 walk(doc, k, &fqn, ft.clone(), max_len, out)?;
357 }
358 }
359 _ => {
360 out.push(Field {
361 id,
362 fqn,
363 rect: rect_of(dict),
364 is_button: ft.as_deref() == Some("Btn"),
365 max_len,
366 });
367 }
368 }
369 Ok(())
370}
371
372pub fn index(fields: &[Field]) -> HashMap<String, Field> {
374 fields.iter().map(|f| (f.fqn.clone(), f.clone())).collect()
375}
376
377pub fn apply_writes(
379 doc: &mut Document,
380 index: &HashMap<String, Field>,
381 writes: &[(String, FieldValue)],
382) -> Result<(), FormsError> {
383 for (fqn, value) in writes {
384 let field = index
385 .get(fqn)
386 .ok_or_else(|| FormsError::MapFieldMissing(fqn.clone()))?;
387 let dict = doc.get_dictionary_mut(field.id)?;
388 match value {
389 FieldValue::Text(s) => {
390 dict.set(
391 "V",
392 Object::String(s.clone().into_bytes(), StringFormat::Literal),
393 );
394 }
395 FieldValue::Check { on } => {
396 dict.set("V", Object::Name(on.clone().into_bytes()));
397 dict.set("AS", Object::Name(on.clone().into_bytes()));
398 }
399 }
400 }
401 Ok(())
402}
403
404pub fn strip_nondeterminism(doc: &mut Document) {
408 if let Ok(info) = doc.trailer.get(b"Info").and_then(|o| o.as_reference()) {
409 if let Ok(d) = doc.get_dictionary_mut(info) {
410 d.remove(b"CreationDate");
411 d.remove(b"ModDate");
412 }
413 }
414 doc.trailer.remove(b"ID");
415}
416
417pub fn save(doc: &mut Document) -> Result<Vec<u8>, FormsError> {
419 let mut buf = Vec::new();
420 doc.save_to(&mut buf)?;
421 Ok(buf)
422}
423
424pub fn text_value(doc: &Document, id: ObjectId) -> Option<String> {
426 let v = doc.get_dictionary(id).ok()?.get(b"V").ok()?;
427 match v {
428 Object::String(b, _) => Some(decode_pdf_text(b)),
429 _ => None,
430 }
431}
432
433pub fn checkbox_on(doc: &Document, id: ObjectId) -> Option<String> {
435 let as_ = doc.get_dictionary(id).ok()?.get(b"AS").ok()?;
436 match as_ {
437 Object::Name(b) if b != b"Off" => Some(String::from_utf8_lossy(b).into_owned()),
438 _ => None,
439 }
440}
441
442pub fn button_on_states(doc: &Document, id: ObjectId) -> Vec<String> {
447 let mut out = Vec::new();
448 let Ok(dict) = doc.get_dictionary(id) else {
449 return out;
450 };
451 let ap = match dict.get(b"AP") {
452 Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
453 Ok(Object::Dictionary(d)) => Some(d),
454 _ => None,
455 };
456 let n = ap.and_then(|ap| match ap.get(b"N") {
457 Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
458 Ok(Object::Dictionary(d)) => Some(d),
459 _ => None,
460 });
461 if let Some(n) = n {
462 for (k, _) in n.iter() {
463 if k != b"Off" {
464 out.push(String::from_utf8_lossy(k).into_owned());
465 }
466 }
467 }
468 out.sort();
469 out
470}