Skip to main content

btctax_forms/
pdf.rs

1//! The low-level lopdf fill primitive: parse a bundled IRS PDF, walk its AcroForm field tree,
2//! drop the `/XFA` layer, set `/V` (+ checkbox `/AS`), pin determinism, and serialize.
3//!
4//! These forms are **static XFA hybrids**: the `/AcroForm` carries both a live `/XFA` XML layer
5//! (which Acrobat/Reader PREFER — a `/V`-only fill opens BLANK) AND a complete classic AcroForm.
6//! Removing `/XFA` makes the classic `/V`/`/AS` values authoritative and render everywhere.
7
8use crate::error::FormsError;
9use lopdf::{Document, Object, ObjectId, StringFormat};
10use std::collections::HashMap;
11
12/// The bundled TY2025 Form 8949 (official IRS fillable PDF, US-gov public domain).
13pub const F8949_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8949.pdf");
14/// The bundled TY2025 Schedule D (official IRS fillable PDF, US-gov public domain).
15pub const SCHEDULE_D_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_d.pdf");
16/// The bundled TY2025 Schedule SE (official IRS fillable PDF, US-gov public domain).
17pub const SCHEDULE_SE_PDF_2025: &[u8] = include_bytes!("../forms/2025/schedule_se.pdf");
18/// The bundled Form 8283, Rev. 12-2025 (official IRS fillable PDF, US-gov public domain).
19pub const F8283_PDF_2025: &[u8] = include_bytes!("../forms/2025/f8283.pdf");
20/// The bundled TY2025 Form 1040 (official IRS fillable PDF, US-gov public domain).
21pub const F1040_PDF_2025: &[u8] = include_bytes!("../forms/2025/f1040.pdf");
22
23/// The bundled TY2024 Form 8949 (official IRS fillable PDF, US-gov public domain).
24pub const F8949_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8949.pdf");
25/// The bundled TY2024 Schedule D (official IRS fillable PDF, US-gov public domain).
26pub const SCHEDULE_D_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_d.pdf");
27/// The bundled TY2024 Schedule SE (official IRS fillable PDF, US-gov public domain).
28pub const SCHEDULE_SE_PDF_2024: &[u8] = include_bytes!("../forms/2024/schedule_se.pdf");
29/// The bundled Form 8283, Rev. 12-2023 (TY2024; official IRS fillable PDF, US-gov public domain).
30pub const F8283_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8283.pdf");
31/// The bundled Form 8275, Rev. 10-2024 (official IRS fillable PDF, US-gov public domain). ★ Form 8275
32/// is REVISION-versioned, not tax-year-versioned: this ONE asset is aliased to EVERY `SUPPORTED_YEAR`
33/// by [`f8275_pdf`] — there is no separate `F8275_PDF_2017` / `F8275_PDF_2025`.
34pub const F8275_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8275.pdf");
35/// The bundled TY2024 Form 1040 (official IRS fillable PDF, US-gov public domain).
36pub const F1040_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040.pdf");
37/// The bundled TY2024 Form 8959, Additional Medicare Tax (official IRS fillable PDF, public domain).
38pub const F8959_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8959.pdf");
39/// The bundled TY2024 Form 8960, Net Investment Income Tax (official IRS fillable PDF, public domain).
40pub const F8960_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8960.pdf");
41/// The bundled TY2024 Form 8995, QBI deduction — simplified (official IRS fillable PDF, public domain).
42pub const F8995_PDF_2024: &[u8] = include_bytes!("../forms/2024/f8995.pdf");
43/// The bundled TY2024 Schedule 2, Additional Taxes (official IRS fillable PDF, public domain).
44pub const SCHEDULE_2_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s2.pdf");
45/// The bundled TY2024 Schedule 3, Additional Credits and Payments (official IRS fillable PDF, public domain).
46pub const SCHEDULE_3_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s3.pdf");
47/// The bundled TY2024 Schedule A, Itemized Deductions (official IRS fillable PDF, public domain).
48pub const SCHEDULE_A_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sa.pdf");
49/// The bundled TY2024 Schedule 1, Additional Income and Adjustments (official IRS fillable PDF, public domain).
50pub const SCHEDULE_1_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040s1.pdf");
51/// The bundled TY2024 Schedule C, Profit or Loss From Business (official IRS fillable PDF, public domain).
52pub const SCHEDULE_C_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sc.pdf");
53/// The bundled TY2024 Schedule B, Interest and Ordinary Dividends (official IRS fillable PDF, public domain).
54pub const SCHEDULE_B_PDF_2024: &[u8] = include_bytes!("../forms/2024/f1040sb.pdf");
55
56/// The bundled TY2017 Form 8949 (official IRS fillable PDF, US-gov public domain).
57pub const F8949_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8949.pdf");
58/// The bundled TY2017 Schedule D (official IRS fillable PDF, US-gov public domain).
59pub const SCHEDULE_D_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_d.pdf");
60/// The bundled TY2017 Schedule SE (official IRS fillable PDF, US-gov public domain).
61pub const SCHEDULE_SE_PDF_2017: &[u8] = include_bytes!("../forms/2017/schedule_se.pdf");
62/// The bundled Form 8283, Rev. 12-2014 (TY2017; official IRS fillable PDF, US-gov public domain).
63pub const F8283_PDF_2017: &[u8] = include_bytes!("../forms/2017/f8283.pdf");
64/// The bundled TY2017 Form 1040 (official IRS fillable PDF, US-gov public domain).
65pub const F1040_PDF_2017: &[u8] = include_bytes!("../forms/2017/f1040.pdf");
66
67/// The bundled Form 8949 PDF bytes for a supported tax year (the asset bound to the year's map).
68pub fn f8949_pdf(year: i32) -> Result<&'static [u8], FormsError> {
69    match year {
70        2017 => Ok(F8949_PDF_2017),
71        2024 => Ok(F8949_PDF_2024),
72        2025 => Ok(F8949_PDF_2025),
73        _ => Err(FormsError::UnsupportedYear(year)),
74    }
75}
76
77/// The bundled Schedule D PDF bytes for a supported tax year.
78pub fn schedule_d_pdf(year: i32) -> Result<&'static [u8], FormsError> {
79    match year {
80        2017 => Ok(SCHEDULE_D_PDF_2017),
81        2024 => Ok(SCHEDULE_D_PDF_2024),
82        2025 => Ok(SCHEDULE_D_PDF_2025),
83        _ => Err(FormsError::UnsupportedYear(year)),
84    }
85}
86
87/// The bundled Schedule SE PDF bytes for a supported tax year.
88pub fn schedule_se_pdf(year: i32) -> Result<&'static [u8], FormsError> {
89    match year {
90        2017 => Ok(SCHEDULE_SE_PDF_2017),
91        2024 => Ok(SCHEDULE_SE_PDF_2024),
92        2025 => Ok(SCHEDULE_SE_PDF_2025),
93        _ => Err(FormsError::UnsupportedYear(year)),
94    }
95}
96
97/// The bundled Form 8959 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
98pub fn f8959_pdf(year: i32) -> Result<&'static [u8], FormsError> {
99    match year {
100        2024 => Ok(F8959_PDF_2024),
101        _ => Err(FormsError::UnsupportedYear(year)),
102    }
103}
104
105/// The bundled Form 8960 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
106pub fn f8960_pdf(year: i32) -> Result<&'static [u8], FormsError> {
107    match year {
108        2024 => Ok(F8960_PDF_2024),
109        _ => Err(FormsError::UnsupportedYear(year)),
110    }
111}
112
113/// The bundled Form 8995 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
114pub fn f8995_pdf(year: i32) -> Result<&'static [u8], FormsError> {
115    match year {
116        2024 => Ok(F8995_PDF_2024),
117        _ => Err(FormsError::UnsupportedYear(year)),
118    }
119}
120
121/// The bundled Schedule 2 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
122pub fn schedule_2_pdf(year: i32) -> Result<&'static [u8], FormsError> {
123    match year {
124        2024 => Ok(SCHEDULE_2_PDF_2024),
125        _ => Err(FormsError::UnsupportedYear(year)),
126    }
127}
128
129/// The bundled Schedule 3 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
130pub fn schedule_3_pdf(year: i32) -> Result<&'static [u8], FormsError> {
131    match year {
132        2024 => Ok(SCHEDULE_3_PDF_2024),
133        _ => Err(FormsError::UnsupportedYear(year)),
134    }
135}
136
137/// The bundled Schedule B PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
138pub fn schedule_b_pdf(year: i32) -> Result<&'static [u8], FormsError> {
139    match year {
140        2024 => Ok(SCHEDULE_B_PDF_2024),
141        _ => Err(FormsError::UnsupportedYear(year)),
142    }
143}
144
145/// The bundled Schedule C PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
146pub fn schedule_c_pdf(year: i32) -> Result<&'static [u8], FormsError> {
147    match year {
148        2024 => Ok(SCHEDULE_C_PDF_2024),
149        _ => Err(FormsError::UnsupportedYear(year)),
150    }
151}
152
153/// The bundled Schedule 1 PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
154pub fn schedule_1_pdf(year: i32) -> Result<&'static [u8], FormsError> {
155    match year {
156        2024 => Ok(SCHEDULE_1_PDF_2024),
157        _ => Err(FormsError::UnsupportedYear(year)),
158    }
159}
160
161/// The bundled Schedule A PDF bytes for a supported tax year. Full-return v1 is TY2024-only.
162pub fn schedule_a_pdf(year: i32) -> Result<&'static [u8], FormsError> {
163    match year {
164        2024 => Ok(SCHEDULE_A_PDF_2024),
165        _ => Err(FormsError::UnsupportedYear(year)),
166    }
167}
168
169/// The bundled Form 8283 PDF bytes for a supported tax year (bound by filing-year → revision).
170pub fn f8283_pdf(year: i32) -> Result<&'static [u8], FormsError> {
171    match year {
172        2017 => Ok(F8283_PDF_2017),
173        2024 => Ok(F8283_PDF_2024),
174        2025 => Ok(F8283_PDF_2025),
175        _ => Err(FormsError::UnsupportedYear(year)),
176    }
177}
178
179/// The bundled Form 8275 PDF bytes for a supported tax year. ★ Form 8275 is REVISION-versioned, not
180/// tax-year-versioned: the single bundled Rev. 10-2024 asset is returned for EVERY `SUPPORTED_YEAR`
181/// (2017/2024/2025) — never `UnsupportedYear` for those three, unlike every other form here that
182/// bundles a distinct PDF per year. This is what lets a promoted 2025 (or 2017) disposal attach a real
183/// Form 8275 rather than being permanently refused for want of a "2025 revision" that does not exist.
184pub fn f8275_pdf(year: i32) -> Result<&'static [u8], FormsError> {
185    match year {
186        2017 | 2024 | 2025 => Ok(F8275_PDF_2024),
187        _ => Err(FormsError::UnsupportedYear(year)),
188    }
189}
190
191/// The bundled Form 1040 PDF bytes for a supported tax year.
192pub fn f1040_pdf(year: i32) -> Result<&'static [u8], FormsError> {
193    match year {
194        2017 => Ok(F1040_PDF_2017),
195        2024 => Ok(F1040_PDF_2024),
196        2025 => Ok(F1040_PDF_2025),
197        _ => Err(FormsError::UnsupportedYear(year)),
198    }
199}
200
201/// One terminal (leaf) AcroForm field: its object id, fully-qualified name, widget rectangle, and
202/// whether it is a checkbox (`/FT /Btn`).
203#[derive(Debug, Clone)]
204pub struct Field {
205    /// lopdf object id of the field dictionary.
206    pub id: ObjectId,
207    /// Fully-qualified, bracketed name (`topmostSubform[0].Page1[0]…f1_03[0]`).
208    pub fqn: String,
209    /// Widget rectangle `[x0, y0, x1, y1]` in PDF user space, if present.
210    pub rect: Option<[f32; 4]>,
211    /// `true` iff `/FT` is `/Btn` (a checkbox/radio).
212    pub is_button: bool,
213    /// `/MaxLen` — the cell's character capacity, when the form declares one (inheritable, like `/FT`).
214    ///
215    /// The IRS forms set this on their **comb** cells (the SSN boxes are `/MaxLen 9`, comb-flagged), and
216    /// it is the PRIMARY SOURCE for how a value must be formatted: nine characters means nine bare
217    /// digits, not a hyphenated `123-45-6789`, which is eleven and would be silently truncated by the
218    /// viewer. [`crate::verify::verify_flat`] enforces it on read-back, so an over-long write fails
219    /// closed instead of being quietly mangled.
220    pub max_len: Option<usize>,
221}
222
223impl Field {
224    /// Horizontal center of the widget rectangle.
225    pub fn cx(&self) -> Option<f32> {
226        self.rect.map(|r| (r[0] + r[2]) / 2.0)
227    }
228    /// Vertical center of the widget rectangle.
229    pub fn cy(&self) -> Option<f32> {
230        self.rect.map(|r| (r[1] + r[3]) / 2.0)
231    }
232}
233
234/// What to write into a field.
235#[derive(Debug, Clone)]
236pub enum FieldValue {
237    /// A text value (`/Tx`).
238    Text(String),
239    /// Turn a checkbox on to the given on-state name (without the leading `/`).
240    Check {
241        /// The on-state PDF name, e.g. `"6"` for Box I.
242        on: String,
243    },
244}
245
246/// Parse a bundled PDF into a mutable document.
247pub fn load(bytes: &[u8]) -> Result<Document, FormsError> {
248    Ok(Document::load_mem(bytes)?)
249}
250
251fn number(o: &Object) -> Option<f32> {
252    match o {
253        Object::Integer(i) => Some(*i as f32),
254        Object::Real(r) => Some(*r),
255        _ => None,
256    }
257}
258
259fn rect_of(dict: &lopdf::Dictionary) -> Option<[f32; 4]> {
260    let arr = dict.get(b"Rect").ok()?.as_array().ok()?;
261    if arr.len() != 4 {
262        return None;
263    }
264    Some([
265        number(&arr[0])?,
266        number(&arr[1])?,
267        number(&arr[2])?,
268        number(&arr[3])?,
269    ])
270}
271
272/// The AcroForm dictionary's object id (it must be an indirect reference).
273fn acroform_id(doc: &Document) -> Result<ObjectId, FormsError> {
274    match doc.catalog()?.get(b"AcroForm") {
275        Ok(Object::Reference(id)) => Ok(*id),
276        Ok(_) => Err(FormsError::Structure(
277            "AcroForm is not an indirect reference".into(),
278        )),
279        Err(_) => Err(FormsError::Structure("catalog has no AcroForm".into())),
280    }
281}
282
283/// Remove `/XFA` from the AcroForm and set `/NeedAppearances true` (viewers regenerate the visible
284/// appearance from `/V`). Must run before saving.
285pub fn drop_xfa_and_set_needappearances(doc: &mut Document) -> Result<(), FormsError> {
286    let id = acroform_id(doc)?;
287    let acro = doc.get_dictionary_mut(id)?;
288    acro.remove(b"XFA");
289    acro.set("NeedAppearances", Object::Boolean(true));
290    Ok(())
291}
292
293/// Walk the AcroForm `/Fields` tree and collect every terminal (leaf) field.
294pub fn collect_fields(doc: &Document) -> Result<Vec<Field>, FormsError> {
295    let acro = doc.get_dictionary(acroform_id(doc)?)?;
296    let mut out = Vec::new();
297    let fields = acro
298        .get(b"Fields")
299        .and_then(|o| o.as_array())
300        .map_err(|_| FormsError::Structure("AcroForm has no /Fields array".into()))?;
301    for f in fields {
302        if let Ok(id) = f.as_reference() {
303            walk(doc, id, "", None, None, &mut out)?;
304        }
305    }
306    Ok(out)
307}
308
309/// Decode a PDF text string: UTF-16BE if it carries the `FEFF` BOM (Adobe LiveCycle exports field
310/// names this way), else PDFDocEncoding (treated as Latin-1, which is exact for the ASCII names).
311pub(crate) fn decode_pdf_text(b: &[u8]) -> String {
312    if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
313        let units: Vec<u16> = b[2..]
314            .chunks(2)
315            .map(|c| ((c[0] as u16) << 8) | *c.get(1).unwrap_or(&0) as u16)
316            .collect();
317        String::from_utf16_lossy(&units)
318    } else {
319        b.iter().map(|&c| c as char).collect()
320    }
321}
322
323fn field_component_name(dict: &lopdf::Dictionary) -> Option<String> {
324    dict.get(b"T")
325        .ok()
326        .and_then(|o| o.as_str().ok())
327        .map(decode_pdf_text)
328}
329
330fn walk(
331    doc: &Document,
332    id: ObjectId,
333    parent_fqn: &str,
334    inherited_ft: Option<String>,
335    inherited_max_len: Option<usize>,
336    out: &mut Vec<Field>,
337) -> Result<(), FormsError> {
338    let dict = match doc.get_dictionary(id) {
339        Ok(d) => d,
340        Err(_) => return Ok(()), // dangling ref — skip
341    };
342    let name = field_component_name(dict);
343    let fqn = match &name {
344        Some(t) if parent_fqn.is_empty() => t.clone(),
345        Some(t) => format!("{parent_fqn}.{t}"),
346        None => parent_fqn.to_string(),
347    };
348    let ft = dict
349        .get(b"FT")
350        .ok()
351        .and_then(|o| o.as_name().ok())
352        .map(|b| String::from_utf8_lossy(b).into_owned())
353        .or(inherited_ft);
354
355    // /MaxLen is inheritable down the field tree, exactly like /FT.
356    let max_len = dict
357        .get(b"MaxLen")
358        .ok()
359        .and_then(|o| o.as_i64().ok())
360        .and_then(|n| usize::try_from(n).ok())
361        .or(inherited_max_len);
362
363    // A branch node carries /Kids of further named fields; a leaf is a terminal field.
364    let kids: Option<Vec<ObjectId>> = dict
365        .get(b"Kids")
366        .ok()
367        .and_then(|o| o.as_array().ok())
368        .map(|arr| arr.iter().filter_map(|k| k.as_reference().ok()).collect());
369    match kids {
370        Some(kids) if !kids.is_empty() => {
371            for k in kids {
372                walk(doc, k, &fqn, ft.clone(), max_len, out)?;
373            }
374        }
375        _ => {
376            out.push(Field {
377                id,
378                fqn,
379                rect: rect_of(dict),
380                is_button: ft.as_deref() == Some("Btn"),
381                max_len,
382            });
383        }
384    }
385    Ok(())
386}
387
388/// Index the collected leaf fields by fully-qualified name.
389pub fn index(fields: &[Field]) -> HashMap<String, Field> {
390    fields.iter().map(|f| (f.fqn.clone(), f.clone())).collect()
391}
392
393/// Encode a text value for a PDF string object. Pure ASCII is written as literal bytes (identical to
394/// PDFDocEncoding for that range — every existing byte-golden is plain ASCII, so this preserves them
395/// exactly). Any other content is written as **UTF-16BE with the `FEFF` BOM** — Adobe's own convention
396/// for a Unicode string, and precisely what [`decode_pdf_text`] decodes back on read-back. Writing raw
397/// UTF-8 bytes instead (the naive choice) is neither valid PDFDocEncoding nor valid UTF-16: a viewer
398/// (and our own read-back) would render it as mojibake — silently wrong text on a FILED disclosure is
399/// exactly the defect class this crate refuses to ship (surfaced by Form 8275's `Part1Item.line`, the
400/// first domain string in this crate to carry a non-ASCII character, an em dash).
401fn encode_pdf_text(s: &str) -> Vec<u8> {
402    if s.is_ascii() {
403        return s.as_bytes().to_vec();
404    }
405    let mut out = Vec::with_capacity(2 + s.len() * 2);
406    out.extend_from_slice(&[0xFE, 0xFF]);
407    for unit in s.encode_utf16() {
408        out.push((unit >> 8) as u8);
409        out.push((unit & 0xFF) as u8);
410    }
411    out
412}
413
414/// Apply a batch of writes. Errors (fails closed) if any field name is absent from the PDF.
415pub fn apply_writes(
416    doc: &mut Document,
417    index: &HashMap<String, Field>,
418    writes: &[(String, FieldValue)],
419) -> Result<(), FormsError> {
420    for (fqn, value) in writes {
421        let field = index
422            .get(fqn)
423            .ok_or_else(|| FormsError::MapFieldMissing(fqn.clone()))?;
424        let dict = doc.get_dictionary_mut(field.id)?;
425        match value {
426            FieldValue::Text(s) => {
427                dict.set(
428                    "V",
429                    Object::String(encode_pdf_text(s), StringFormat::Literal),
430                );
431            }
432            FieldValue::Check { on } => {
433                dict.set("V", Object::Name(on.clone().into_bytes()));
434                dict.set("AS", Object::Name(on.clone().into_bytes()));
435            }
436        }
437    }
438    Ok(())
439}
440
441/// Strip clock/RNG-derived bytes so `(data, form) → byte-identical` output: drop the document
442/// `/Info` timestamps and the trailer `/ID`. No other source of nondeterminism exists (lopdf writes
443/// objects in stable id order; no float structure; miniz_oxide deflate is deterministic).
444pub fn strip_nondeterminism(doc: &mut Document) {
445    if let Ok(info) = doc.trailer.get(b"Info").and_then(|o| o.as_reference()) {
446        if let Ok(d) = doc.get_dictionary_mut(info) {
447            d.remove(b"CreationDate");
448            d.remove(b"ModDate");
449        }
450    }
451    doc.trailer.remove(b"ID");
452}
453
454/// Serialize the document to bytes.
455pub fn save(doc: &mut Document) -> Result<Vec<u8>, FormsError> {
456    let mut buf = Vec::new();
457    doc.save_to(&mut buf)?;
458    Ok(buf)
459}
460
461/// Read back a leaf field's `/V` as a string (text value) — used by tests and the no-unmapped scan.
462pub fn text_value(doc: &Document, id: ObjectId) -> Option<String> {
463    let v = doc.get_dictionary(id).ok()?.get(b"V").ok()?;
464    match v {
465        Object::String(b, _) => Some(decode_pdf_text(b)),
466        _ => None,
467    }
468}
469
470/// Read back a checkbox's `/AS` on-state (None if `/Off` or absent).
471pub fn checkbox_on(doc: &Document, id: ObjectId) -> Option<String> {
472    let as_ = doc.get_dictionary(id).ok()?.get(b"AS").ok()?;
473    match as_ {
474        Object::Name(b) if b != b"Off" => Some(String::from_utf8_lossy(b).into_owned()),
475        _ => None,
476    }
477}
478
479/// The possible ON-state name(s) of a button widget — the `/AP` `/N` appearance keys other than
480/// `/Off` (without the leading `/`). Read straight from the bundled PDF, so the SP2 same-y `/Btn`
481/// pair oracle (the 1040 Digital-Asset Yes/No question) is map-INDEPENDENT. Empty when the widget
482/// carries no `/AP`/`/N`.
483pub fn button_on_states(doc: &Document, id: ObjectId) -> Vec<String> {
484    let mut out = Vec::new();
485    let Ok(dict) = doc.get_dictionary(id) else {
486        return out;
487    };
488    let ap = match dict.get(b"AP") {
489        Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
490        Ok(Object::Dictionary(d)) => Some(d),
491        _ => None,
492    };
493    let n = ap.and_then(|ap| match ap.get(b"N") {
494        Ok(Object::Reference(r)) => doc.get_dictionary(*r).ok(),
495        Ok(Object::Dictionary(d)) => Some(d),
496        _ => None,
497    });
498    if let Some(n) = n {
499        for (k, _) in n.iter() {
500            if k != b"Off" {
501                out.push(String::from_utf8_lossy(k).into_owned());
502            }
503        }
504    }
505    out.sort();
506    out
507}