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 F8275_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8275.pdf");
35pub const F1040_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040.pdf");
37pub const F8959_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8959.pdf");
39pub const F8960_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8960.pdf");
41pub const F8995_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8995.pdf");
43pub const F8995A_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8995a.pdf");
46static F6251_PDF_2024: &[u8] = include_bytes!("../forms/2024/f6251.pdf");
48pub const SCHEDULE_2_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s2.pdf");
50pub const SCHEDULE_3_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s3.pdf");
52pub const SCHEDULE_A_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sa.pdf");
54pub const SCHEDULE_1_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s1.pdf");
56pub const SCHEDULE_C_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sc.pdf");
58pub const SCHEDULE_B_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sb.pdf");
60
61pub const F8949_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8949.pdf");
63pub const SCHEDULE_D_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_d.pdf");
65pub const SCHEDULE_SE_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_se.pdf");
67pub const F8283_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8283.pdf");
69pub const F1040_PDF_2017: &[u8] = include_bytes!("../forms/2017/f1040.pdf");
71
72pub fn f8949_pdf(year: i32) -> Result<&'static [u8], FormsError> {
74 match year {
75 2017 => Ok(F8949_PDF_2017),
76 2024 => Ok(F8949_PDF_2024),
77 2025 => Ok(F8949_PDF_2025),
78 _ => Err(FormsError::UnsupportedYear(year)),
79 }
80}
81
82pub fn schedule_d_pdf(year: i32) -> Result<&'static [u8], FormsError> {
84 match year {
85 2017 => Ok(SCHEDULE_D_PDF_2017),
86 2024 => Ok(SCHEDULE_D_PDF_2024),
87 2025 => Ok(SCHEDULE_D_PDF_2025),
88 _ => Err(FormsError::UnsupportedYear(year)),
89 }
90}
91
92pub fn schedule_se_pdf(year: i32) -> Result<&'static [u8], FormsError> {
94 match year {
95 2017 => Ok(SCHEDULE_SE_PDF_2017),
96 2024 => Ok(SCHEDULE_SE_PDF_2024),
97 2025 => Ok(SCHEDULE_SE_PDF_2025),
98 _ => Err(FormsError::UnsupportedYear(year)),
99 }
100}
101
102pub fn f8959_pdf(year: i32) -> Result<&'static [u8], FormsError> {
104 match year {
105 2024 => Ok(F8959_PDF_2024),
106 _ => Err(FormsError::UnsupportedYear(year)),
107 }
108}
109
110pub fn f8960_pdf(year: i32) -> Result<&'static [u8], FormsError> {
112 match year {
113 2024 => Ok(F8960_PDF_2024),
114 _ => Err(FormsError::UnsupportedYear(year)),
115 }
116}
117
118pub fn f8995_pdf(year: i32) -> Result<&'static [u8], FormsError> {
120 match year {
121 2024 => Ok(F8995_PDF_2024),
122 _ => Err(FormsError::UnsupportedYear(year)),
123 }
124}
125
126pub fn f6251_pdf(year: i32) -> Result<&'static [u8], FormsError> {
129 match year {
130 2024 => Ok(F6251_PDF_2024),
131 _ => Err(FormsError::UnsupportedYear(year)),
132 }
133}
134
135pub fn f8995a_pdf(year: i32) -> Result<&'static [u8], FormsError> {
136 match year {
137 2024 => Ok(F8995A_PDF_2024),
138 _ => Err(FormsError::UnsupportedYear(year)),
139 }
140}
141
142pub fn schedule_2_pdf(year: i32) -> Result<&'static [u8], FormsError> {
144 match year {
145 2024 => Ok(SCHEDULE_2_PDF_2024),
146 _ => Err(FormsError::UnsupportedYear(year)),
147 }
148}
149
150pub fn schedule_3_pdf(year: i32) -> Result<&'static [u8], FormsError> {
152 match year {
153 2024 => Ok(SCHEDULE_3_PDF_2024),
154 _ => Err(FormsError::UnsupportedYear(year)),
155 }
156}
157
158pub fn schedule_b_pdf(year: i32) -> Result<&'static [u8], FormsError> {
160 match year {
161 2024 => Ok(SCHEDULE_B_PDF_2024),
162 _ => Err(FormsError::UnsupportedYear(year)),
163 }
164}
165
166pub fn schedule_c_pdf(year: i32) -> Result<&'static [u8], FormsError> {
168 match year {
169 2024 => Ok(SCHEDULE_C_PDF_2024),
170 _ => Err(FormsError::UnsupportedYear(year)),
171 }
172}
173
174pub fn schedule_1_pdf(year: i32) -> Result<&'static [u8], FormsError> {
176 match year {
177 2024 => Ok(SCHEDULE_1_PDF_2024),
178 _ => Err(FormsError::UnsupportedYear(year)),
179 }
180}
181
182pub fn schedule_a_pdf(year: i32) -> Result<&'static [u8], FormsError> {
184 match year {
185 2024 => Ok(SCHEDULE_A_PDF_2024),
186 _ => Err(FormsError::UnsupportedYear(year)),
187 }
188}
189
190pub fn f8283_pdf(year: i32) -> Result<&'static [u8], FormsError> {
192 match year {
193 2017 => Ok(F8283_PDF_2017),
194 2024 => Ok(F8283_PDF_2024),
195 2025 => Ok(F8283_PDF_2025),
196 _ => Err(FormsError::UnsupportedYear(year)),
197 }
198}
199
200pub fn f8275_pdf(year: i32) -> Result<&'static [u8], FormsError> {
206 match year {
207 2017 | 2024 | 2025 => Ok(F8275_PDF_2024),
208 _ => Err(FormsError::UnsupportedYear(year)),
209 }
210}
211
212pub fn f1040_pdf(year: i32) -> Result<&'static [u8], FormsError> {
214 match year {
215 2017 => Ok(F1040_PDF_2017),
216 2024 => Ok(F1040_PDF_2024),
217 2025 => Ok(F1040_PDF_2025),
218 _ => Err(FormsError::UnsupportedYear(year)),
219 }
220}
221
222#[derive(Debug, Clone)]
225pub struct Field {
226 pub id: ObjectId,
228 pub fqn: String,
230 pub rect: Option<[f32; 4]>,
232 pub is_button: bool,
234 pub max_len: Option<usize>,
242}
243
244impl Field {
245 pub fn cx(&self) -> Option<f32> {
247 self.rect.map(|r| (r[0] + r[2]) / 2.0)
248 }
249 pub fn cy(&self) -> Option<f32> {
251 self.rect.map(|r| (r[1] + r[3]) / 2.0)
252 }
253}
254
255#[derive(Debug, Clone)]
257pub enum FieldValue {
258 Text(String),
260 Check {
262 on: String,
264 },
265}
266
267pub fn load(bytes: &[u8]) -> Result<Document, FormsError> {
269 Ok(Document::load_mem(bytes)?)
270}
271
272fn number(o: &Object) -> Option<f32> {
273 match o {
274 Object::Integer(i) => Some(*i as f32),
275 Object::Real(r) => Some(*r),
276 _ => None,
277 }
278}
279
280fn rect_of(dict: &lopdf::Dictionary) -> Option<[f32; 4]> {
281 let arr = dict.get(b"Rect").ok()?.as_array().ok()?;
282 if arr.len() != 4 {
283 return None;
284 }
285 Some([
286 number(&arr[0])?,
287 number(&arr[1])?,
288 number(&arr[2])?,
289 number(&arr[3])?,
290 ])
291}
292
293fn acroform_id(doc: &Document) -> Result<ObjectId, FormsError> {
295 match doc.catalog()?.get(b"AcroForm") {
296 Ok(Object::Reference(id)) => Ok(*id),
297 Ok(_) => Err(FormsError::Structure(
298 "AcroForm is not an indirect reference".into(),
299 )),
300 Err(_) => Err(FormsError::Structure("catalog has no AcroForm".into())),
301 }
302}
303
304pub fn drop_xfa_and_set_needappearances(doc: &mut Document) -> Result<(), FormsError> {
307 let id = acroform_id(doc)?;
308 let acro = doc.get_dictionary_mut(id)?;
309 acro.remove(b"XFA");
310 acro.set("NeedAppearances", Object::Boolean(true));
311 Ok(())
312}
313
314pub fn collect_fields(doc: &Document) -> Result<Vec<Field>, FormsError> {
316 let acro = doc.get_dictionary(acroform_id(doc)?)?;
317 let mut out = Vec::new();
318 let fields = acro
319 .get(b"Fields")
320 .and_then(|o| o.as_array())
321 .map_err(|_| FormsError::Structure("AcroForm has no /Fields array".into()))?;
322 for f in fields {
323 if let Ok(id) = f.as_reference() {
324 walk(doc, id, "", None, None, &mut out)?;
325 }
326 }
327 Ok(out)
328}
329
330pub(crate) fn decode_pdf_text(b: &[u8]) -> String {
333 if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
334 let units: Vec<u16> = b[2..]
335 .chunks(2)
336 .map(|c| ((c[0] as u16) << 8) | *c.get(1).unwrap_or(&0) as u16)
337 .collect();
338 String::from_utf16_lossy(&units)
339 } else {
340 b.iter().map(|&c| c as char).collect()
341 }
342}
343
344fn field_component_name(dict: &lopdf::Dictionary) -> Option<String> {
345 dict.get(b"T")
346 .ok()
347 .and_then(|o| o.as_str().ok())
348 .map(decode_pdf_text)
349}
350
351fn walk(
352 doc: &Document,
353 id: ObjectId,
354 parent_fqn: &str,
355 inherited_ft: Option<String>,
356 inherited_max_len: Option<usize>,
357 out: &mut Vec<Field>,
358) -> Result<(), FormsError> {
359 let dict = match doc.get_dictionary(id) {
360 Ok(d) => d,
361 Err(_) => return Ok(()), };
363 let name = field_component_name(dict);
364 let fqn = match &name {
365 Some(t) if parent_fqn.is_empty() => t.clone(),
366 Some(t) => format!("{parent_fqn}.{t}"),
367 None => parent_fqn.to_string(),
368 };
369 let ft = dict
370 .get(b"FT")
371 .ok()
372 .and_then(|o| o.as_name().ok())
373 .map(|b| String::from_utf8_lossy(b).into_owned())
374 .or(inherited_ft);
375
376 let max_len = dict
378 .get(b"MaxLen")
379 .ok()
380 .and_then(|o| o.as_i64().ok())
381 .and_then(|n| usize::try_from(n).ok())
382 .or(inherited_max_len);
383
384 let kids: Option<Vec<ObjectId>> = dict
386 .get(b"Kids")
387 .ok()
388 .and_then(|o| o.as_array().ok())
389 .map(|arr| arr.iter().filter_map(|k| k.as_reference().ok()).collect());
390 match kids {
391 Some(kids) if !kids.is_empty() => {
392 for k in kids {
393 walk(doc, k, &fqn, ft.clone(), max_len, out)?;
394 }
395 }
396 _ => {
397 out.push(Field {
398 id,
399 fqn,
400 rect: rect_of(dict),
401 is_button: ft.as_deref() == Some("Btn"),
402 max_len,
403 });
404 }
405 }
406 Ok(())
407}
408
409pub fn index(fields: &[Field]) -> HashMap<String, Field> {
411 fields.iter().map(|f| (f.fqn.clone(), f.clone())).collect()
412}
413
414fn encode_pdf_text(s: &str) -> Vec<u8> {
423 if s.is_ascii() {
424 return s.as_bytes().to_vec();
425 }
426 let mut out = Vec::with_capacity(2 + s.len() * 2);
427 out.extend_from_slice(&[0xFE, 0xFF]);
428 for unit in s.encode_utf16() {
429 out.push((unit >> 8) as u8);
430 out.push((unit & 0xFF) as u8);
431 }
432 out
433}
434
435pub fn apply_writes(
437 doc: &mut Document,
438 index: &HashMap<String, Field>,
439 writes: &[(String, FieldValue)],
440) -> Result<(), FormsError> {
441 for (fqn, value) in writes {
442 let field = index
443 .get(fqn)
444 .ok_or_else(|| FormsError::MapFieldMissing(fqn.clone()))?;
445 match value {
446 FieldValue::Text(s) => {
447 doc.get_dictionary_mut(field.id)?.set(
448 "V",
449 Object::String(encode_pdf_text(s), StringFormat::Literal),
450 );
451 }
452 FieldValue::Check { on } => {
453 let states = button_on_states(doc, field.id);
470 if !states.is_empty() && !states.iter().any(|s| s == on) {
471 return Err(FormsError::Geometry(format!(
472 "{fqn}: on-state {on:?} is not one this widget declares ({states:?}) — \
473 writing it would render the box BLANK on the filed form while reading back \
474 as checked. This is what a swapped Yes/No map looks like."
475 )));
476 }
477 let dict = doc.get_dictionary_mut(field.id)?;
478 dict.set("V", Object::Name(on.clone().into_bytes()));
479 dict.set("AS", Object::Name(on.clone().into_bytes()));
480 }
481 }
482 }
483 Ok(())
484}
485
486pub fn strip_nondeterminism(doc: &mut Document) {
490 if let Ok(info) = doc.trailer.get(b"Info").and_then(|o| o.as_reference()) {
491 if let Ok(d) = doc.get_dictionary_mut(info) {
492 d.remove(b"CreationDate");
493 d.remove(b"ModDate");
494 }
495 }
496 doc.trailer.remove(b"ID");
497}
498
499pub fn save(doc: &mut Document) -> Result<Vec<u8>, FormsError> {
501 let mut buf = Vec::new();
502 doc.save_to(&mut buf)?;
503 Ok(buf)
504}
505
506pub fn text_value(doc: &Document, id: ObjectId) -> Option<String> {
508 let v = doc.get_dictionary(id).ok()?.get(b"V").ok()?;
509 match v {
510 Object::String(b, _) => Some(decode_pdf_text(b)),
511 _ => None,
512 }
513}
514
515pub fn checkbox_on(doc: &Document, id: ObjectId) -> Option<String> {
517 let as_ = doc.get_dictionary(id).ok()?.get(b"AS").ok()?;
518 match as_ {
519 Object::Name(b) if b != b"Off" => Some(String::from_utf8_lossy(b).into_owned()),
520 _ => None,
521 }
522}
523
524pub fn button_on_states(doc: &Document, id: ObjectId) -> Vec<String> {
529 let mut out = Vec::new();
530 let Ok(dict) = doc.get_dictionary(id) else {
531 return out;
532 };
533 let ap = match dict.get(b"AP") {
534 Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
535 Ok(Object::Dictionary(d)) => Some(d),
536 _ => None,
537 };
538 let n = ap.and_then(|ap| match ap.get(b"N") {
539 Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
540 Ok(Object::Dictionary(d)) => Some(d),
541 _ => None,
542 });
543 if let Some(n) = n {
544 for (k, _) in n.iter() {
545 if k != b"Off" {
546 out.push(String::from_utf8_lossy(k).into_owned());
547 }
548 }
549 }
550 out.sort();
551 out
552}